进程的虚拟地址空间
每个进程运行会有自己的虚拟地址空间,以x86的32位程序为例:
.rodata
数据段存储常量,包括字符串字面量,const常量等
.data
数据段保存初始化且不为0的变量,.bss
保存为初始化或初始化为0的变量,包括(静态和非静态)全局变量和静态局部变量
需要注意的是,这里的数据段(.rodata
、.data
和.bss
)不包含函数中的非静态局部变量,他是存储在栈上的,并通过指令初始化
不同进程都有自己的虚拟地址空间,其中用户空间是私有的,内核空间是公共的(因此,如果想要跨进程通信,则需要通过内核空间,例如匿名管道通信)
虚拟地址空间和虚拟内存是计算机操作系统中的两个相关但不同的概念,以下是它们的区别和联系:
- 虚拟地址空间 (Virtual Address Space)
定义:虚拟地址空间是进程可以访问的地址范围,是操作系统为每个进程分配的地址视图。
作用:
- 为每个进程提供一个独立的、连续的地址空间,隔离进程之间的内存访问,确保安全性。
- 使得进程的地址访问与物理内存无关。
特点:
- 每个进程的虚拟地址空间是独立的,互不干扰。
- 其大小由系统的架构和操作系统决定。例如,32位系统通常提供4GB的虚拟地址空间。
例子:一个程序访问内存地址
0x00400000
,该地址属于其虚拟地址空间,与实际的物理地址无关。
- 虚拟内存 (Virtual Memory)
定义:虚拟内存是一种内存管理技术,它允许程序使用比实际物理内存更大的地址空间。
作用:
- 提供了内存扩展功能,使程序可以运行在物理内存不足的情况下。
- 通过页表机制将虚拟地址映射到物理地址。
- 支持内存分页(Page)和换页(Swapping),提高内存利用率。
特点:
- 物理内存不足时,操作系统会将部分内存数据交换到磁盘(如交换文件或页面文件)。
- 使得多个程序可以共享有限的物理内存,同时仍然有各自的虚拟地址空间。
例子:一个程序需要1GB的内存,但系统只有512MB物理内存,通过虚拟内存机制,程序仍然可以运行,部分数据被交换到磁盘。
区别
属性 虚拟地址空间 虚拟内存 定义 为每个进程提供的独立地址范围 用于管理内存和磁盘之间的交换机制 作用 提供独立的逻辑地址访问视图 扩展物理内存,提高内存利用率 是否依赖物理内存 不依赖物理内存 依赖于物理内存和磁盘之间的映射 关键技术 虚拟地址映射到物理地址 页表管理、分页与换页机制 范围 与CPU架构相关(如32位或64位系统) 实际大小取决于磁盘和物理内存的总容量
联系
- 虚拟地址空间是虚拟内存管理的一部分,操作系统通过虚拟内存技术将虚拟地址空间中的地址映射到物理内存或磁盘。
- 虚拟地址空间为进程提供逻辑上的地址独立性,而虚拟内存则确保在物理内存不足时,程序仍然可以运行。
总结来说,虚拟地址空间是逻辑上的概念,而虚拟内存是实现虚拟地址空间和物理内存交互的底层技术。
函数调用时的栈变化
以下面的简单程序为例:
int sum(int a,int b){
int tmp = 0;
tmp = a + b;
return tmp;
}
int main(){
int a = 1;
int b = 2;
int c = sum(a,b);
return 0;
}
编译,并查看汇编代码:
g++ main.cpp -g
objdump -S a.out
得到的汇编代码如下:
0000000000001129 <_Z3sumii>:
int sum(int a,int b){
1129: f3 0f 1e fa endbr64
112d: 55 push %rbp
112e: 48 89 e5 mov %rsp,%rbp
1131: 89 7d ec mov %edi,-0x14(%rbp)
1134: 89 75 e8 mov %esi,-0x18(%rbp)
int tmp = 0;
1137: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
tmp = a + b;
113e: 8b 55 ec mov -0x14(%rbp),%edx
1141: 8b 45 e8 mov -0x18(%rbp),%eax
1144: 01 d0 add %edx,%eax
1146: 89 45 fc mov %eax,-0x4(%rbp)
return tmp;
1149: 8b 45 fc mov -0x4(%rbp),%eax
}
114c: 5d pop %rbp
114d: c3 ret
000000000000114e <main>:
int main(){
114e: f3 0f 1e fa endbr64
1152: 55 push %rbp
1153: 48 89 e5 mov %rsp,%rbp
1156: 48 83 ec 10 sub $0x10,%rsp
int a = 1;
115a: c7 45 f4 01 00 00 00 movl $0x1,-0xc(%rbp)
int b = 2;
1161: c7 45 f8 02 00 00 00 movl $0x2,-0x8(%rbp)
int c = sum(a,b);
1168: 8b 55 f8 mov -0x8(%rbp),%edx
116b: 8b 45 f4 mov -0xc(%rbp),%eax
116e: 89 d6 mov %edx,%esi
1170: 89 c7 mov %eax,%edi
1172: e8 b2 ff ff ff call 1129 <_Z3sumii>
1177: 89 45 fc mov %eax,-0x4(%rbp)
return 0;
117a: b8 00 00 00 00 mov $0x0,%eax
117f: c9 leave
1180: c3 ret
栈帧示例图:
C++代码的编译和链接原理
预处理 | 编译 | 汇编 | 链接 | |
---|---|---|---|---|
解释 | 处理# 开头的预处理指令和宏(包括:宏定义、头文件展开、条件编译、删除注释) |
生成汇编代码 | 生成可重定向的目标文件(二进制机器码) 此时,指令中符号的地址都是0 |
将多个可重定向目标文件和静态库链接,包括: 符号解析:符号定义的位置 符号重定位:给符号分配虚拟地址,并替换之前代码中对符号的引用地址 静态链接:将静态库代码复制到可执行文件中 |
指令 | g++ -E xxx.cpp -o xxx.i |
g++ -S xxx.cpp -o xxx.s |
g++ -c xxx.s -o xxx.o |
g++ xxx1.o xxx2.o ... -o xxx |
对于这份代码
// main.cpp
extern int sum(int a, int b);
int i = 0;
extern int j;
int main(){
int a = i;
int b = j;
int c = sum(a,b);
return 0;
}
// sum.cpp
int j = 0;
int sum(int a,int b){
int tmp = 0;
tmp = a + b;
return tmp;
}
通过下面命令,我们可以观察符号表:
bingo@bingo:~/Code/learn$ g++ -c main.cpp -o main.o
bingo@bingo:~/Code/learn$ objdump -t main.o
main.o: file format elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 main.cpp
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 g O .bss 0000000000000004 i # global 全局符号
0000000000000000 g F .text 0000000000000037 main
0000000000000000 *UND* 0000000000000000 j # *UND* 表示在当前文件中未找到引用
0000000000000000 *UND* 0000000000000000 _Z3sumii
bingo@bingo:~/Code/learn$ g++ -c sum.cpp -o sum.o
bingo@bingo:~/Code/learn$ objdump -t sum.o
sum.o: file format elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 sum.cpp
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 g O .bss 0000000000000004 j
0000000000000000 g F .text 0000000000000025 _Z3sumii
bingo@bingo:~/Code/learn$ g++ main.o sum.o -o my_program
bingo@bingo:~/Code/learn$ objdump -t my_program
my_program: file format elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 Scrt1.o
000000000000038c l O .note.ABI-tag 0000000000000020 __abi_tag
0000000000000000 l df *ABS* 0000000000000000 crtstuff.c
0000000000001070 l F .text 0000000000000000 deregister_tm_clones
00000000000010a0 l F .text 0000000000000000 register_tm_clones
00000000000010e0 l F .text 0000000000000000 __do_global_dtors_aux
0000000000004010 l O .bss 0000000000000001 completed.0
0000000000003df8 l O .fini_array 0000000000000000 __do_global_dtors_aux_fini_array_entry
0000000000001120 l F .text 0000000000000000 frame_dummy
0000000000003df0 l O .init_array 0000000000000000 __frame_dummy_init_array_entry
0000000000000000 l df *ABS* 0000000000000000 main.cpp
0000000000000000 l df *ABS* 0000000000000000 sum.cpp
0000000000000000 l df *ABS* 0000000000000000 crtstuff.c
00000000000020e8 l O .eh_frame 0000000000000000 __FRAME_END__
0000000000000000 l df *ABS* 0000000000000000
0000000000003e00 l O .dynamic 0000000000000000 _DYNAMIC
0000000000002004 l .eh_frame_hdr 0000000000000000 __GNU_EH_FRAME_HDR
0000000000003fc0 l O .got 0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000000 F *UND* 0000000000000000 __libc_start_main@GLIBC_2.34
0000000000000000 w *UND* 0000000000000000 _ITM_deregisterTMCloneTable
0000000000004000 w .data 0000000000000000 data_start
0000000000001160 g F .text 0000000000000025 _Z3sumii
0000000000004018 g O .bss 0000000000000004 j
0000000000004010 g .data 0000000000000000 _edata
0000000000001188 g F .fini 0000000000000000 .hidden _fini
0000000000004000 g .data 0000000000000000 __data_start
0000000000000000 w *UND* 0000000000000000 __gmon_start__
0000000000004008 g O .data 0000000000000000 .hidden __dso_handle
0000000000002000 g O .rodata 0000000000000004 _IO_stdin_used
0000000000004020 g .bss 0000000000000000 _end
0000000000001040 g F .text 0000000000000026 _start
0000000000004014 g O .bss 0000000000000004 i
0000000000004010 g .bss 0000000000000000 __bss_start
0000000000001129 g F .text 0000000000000037 main
0000000000004010 g O .data 0000000000000000 .hidden __TMC_END__
0000000000000000 w *UND* 0000000000000000 _ITM_registerTMCloneTable
0000000000000000 w F *UND* 0000000000000000 __cxa_finalize@GLIBC_2.2.5
0000000000001000 g F .init 0000000000000000 .hidden _init
在链接之前,符号表的地址全部为0,链接后为符号分配虚拟地址,并替换指令中对符号的引用
链接过程详解:
-
符号解析
-
所有对符号的引用,都要找到定义
-
可能的问题:
- 符号未定义
- 符号重定义
-
-
-
重定向
- 给符号分配虚拟地址(在进程的虚拟地址空间中)
- 替换指令中对符号的引用地址
可重定向目标文件的结构
一个可重定向的二进制文件的结构,可能如下图所示:
bingo@bingo:~/Code/learn$ readelf -S main.o
There are 13 section headers, starting at offset 0x290:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000037 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 000001c0
0000000000000048 0000000000000018 I 10 1 8
[ 3] .data PROGBITS 0000000000000000 00000077
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 00000078
0000000000000004 0000000000000000 WA 0 0 4
[ 5] .comment PROGBITS 0000000000000000 00000078
0000000000000027 0000000000000001 MS 0 0 1
[ 6] .note.GNU-stack PROGBITS 0000000000000000 0000009f
0000000000000000 0000000000000000 0 0 1
[ 7] .note.gnu.pr[...] NOTE 0000000000000000 000000a0
0000000000000020 0000000000000000 A 0 0 8
[ 8] .eh_frame PROGBITS 0000000000000000 000000c0
0000000000000038 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 00000208
0000000000000018 0000000000000018 I 10 8 8
[10] .symtab SYMTAB 0000000000000000 000000f8
00000000000000a8 0000000000000018 11 3 8
[11] .strtab STRTAB 0000000000000000 000001a0
000000000000001a 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 00000220
000000000000006c 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
可重定向目标文件和可执行文件的对比
bingo@bingo:~/Code/learn$ readelf -h main.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 656 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 13
Section header string table index: 12
bingo@bingo:~/Code/learn$ readelf -h my_program
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1040
Start of program headers: 64 (bytes into file)
Start of section headers: 14032 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 29
Section header string table index: 28
- 可重定向目标文件入口地址为0,而可执行文件的入口地址是有效值
本文介绍了进程的虚拟地址空间和函数调用时的栈变化。首先,讨论了x86架构下32位程序的虚拟地址空间结构,包括代码段、数据段(`.rodata`、`.data`、`.bss`)、堆和栈的布局。接着,通过一个简单的C++程序示例,展示了函数调用过程中栈的变化。文章详细描述了汇编代码中栈帧的创建、参数传递、局部变量的存储以及返回值的处理过程。最后,强调了虚拟地址空间和虚拟内存的区别与联系,指出虚拟地址空间是逻辑上的概念,而虚拟内存是实现虚拟地址空间和物理内存交互的底层技术。