作为一个菜鸟,这个题目有点大,所以这篇博客缺点是可能不够深入,但应该还是很详细的,希望能对大家有所帮助。
在linux系统中,程序在内存中的分布如下所示:
低地址 | .text | .data | .bss | heap(堆) --> | unused | <-- stack(栈) | env | 高地址 |
其中 :
.text 部分是编译后程序的主体,也就是程序的机器指令。
.data 和 .bss 保存了程序的全局变量,.data保存有初始化的全局变量,.bss保存只有声明没有初始化的全局变量。
heap(堆)中保存程序中动态分配的内存,比如C的malloc申请的内存,或者C++中new申请的内存。堆向高地址方向增长。
stack(栈)用来进行函数调用,保存函数参数,临时变量,返回地址等。
下面是测试用的程序,比较简单,用来输出各个变量的地址。
#include <stdio.h> #include <stdlib.h> int ug; int dg = 1; void func(int); void func2(int); int main(int argc, char ** argv){ int ul; int dl = 2; int *pi = (int *)malloc(sizeof(int)); *pi = 4; int *pi2 = (int *)malloc(sizeof(int)); *pi2 = 8; printf("address of main: %x\n", main); printf("undefined global %d: %x\n", ug, &ug); printf("defined global %d: %x\n", dg, &dg); printf("undefined local %d: %x\n", ul, &ul); printf("defined local %d: %x\n", dl, &dl); printf("address of func: %x\n", func); func(32); printf("dynamic alloc %d: %x\n", *pi, pi); printf("dynamic alloc %d: %x\n", *pi2, pi2); free(pi); free(pi2); int a; scanf("%d", &a); return 0; } void func(int arg){ int uloc; int dloc = 16; printf("address of argument %d: %x\n", arg, &arg); printf("undefined func local %d: %x\n", uloc, &uloc); printf("defined func local %d: %x\n", dloc, &dloc); func2(); } void func2(){ int loc = 64; printf("local of func2 %d: %x\n", loc, &loc); }
address of main: 4005f4 undefined global 0: 601050 defined global 1: 601038 undefined local 32767: c2b96484 defined local 2: c2b96488 address of func: 40075a address of argument 32: c2b9643c undefined func local -451161400: c2b96448 defined func local 16: c2b9644c local of func2 64: c2b9641c dynamic alloc 4: 16b1010 dynamic alloc 8: 16b1030
在linux下,可以查看进程的maps文件了解程序在内存中的分布,上面那个程序运行后的进程的maps文件内容如下:
cat /proc/2506/maps 00400000-00401000 r-xp 00000000 08:03 4080116 /home/yuduo/Workspace/C/sandbox/address.o 00600000-00601000 r--p 00000000 08:03 4080116 /home/yuduo/Workspace/C/sandbox/address.o 00601000-00602000 rw-p 00001000 08:03 4080116 /home/yuduo/Workspace/C/sandbox/address.o 016b1000-016d2000 rw-p 00000000 00:00 0 [heap] 7fc8e4bfc000-7fc8e4d91000 r-xp 00000000 08:03 4460234 /lib/x86_64-linux-gnu/libc-2.13.so 7fc8e4d91000-7fc8e4f90000 ---p 00195000 08:03 4460234 /lib/x86_64-linux-gnu/libc-2.13.so 7fc8e4f90000-7fc8e4f94000 r--p 00194000 08:03 4460234 /lib/x86_64-linux-gnu/libc-2.13.so 7fc8e4f94000-7fc8e4f95000 rw-p 00198000 08:03 4460234 /lib/x86_64-linux-gnu/libc-2.13.so 7fc8e4f95000-7fc8e4f9b000 rw-p 00000000 00:00 0 7fc8e4f9b000-7fc8e4fbc000 r-xp 00000000 08:03 4460221 /lib/x86_64-linux-gnu/ld-2.13.so 7fc8e5199000-7fc8e519c000 rw-p 00000000 00:00 0 7fc8e51b7000-7fc8e51bb000 rw-p 00000000 00:00 0 7fc8e51bb000-7fc8e51bc000 r--p 00020000 08:03 4460221 /lib/x86_64-linux-gnu/ld-2.13.so 7fc8e51bc000-7fc8e51be000 rw-p 00021000 08:03 4460221 /lib/x86_64-linux-gnu/ld-2.13.so 7fffc2b76000-7fffc2b97000 rw-p 00000000 00:00 0 [stack] 7fffc2bbe000-7fffc2bbf000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
可以看到程序的.text的内存是:00400000-00401000,main函数和func函数的地址都在这个范围内(4005f4、40075a),可以看到这部分内存权限是可执行(r-xp),这里面的代码也确实是需要执行的。写这篇博客时我又发现这段内存刚好一个页面大小(4K),有趣。
.data的内存是:00601000-00602000,因为两个全局变量全在这里(601050、601038),从权限也可以看出来(rw-p),这里w代表可写,上面那部分内存(00600000-00601000)权限是 r--p,估计是用来保存常量(const)的。
然后就是堆(heap)了,地址范围是016b1000-016d2000,两个动态分配的变量刚好在这个范围里面:16b1010、16b1030,从他们的地址可以看出来他们是向高地址增长的。
堆后面直接就是高地址了,首先是一些动态链接库,动态链接库在内存中的位置在每个系统上都不一样,有些系统放在.text前面,这个无所谓了,不关心。
然后就是栈(stack)了,地址范围7fffc2b76000-7fffc2b97000。例子里面很多变量都在这个范围内,main的两个局部变量(c2b96484、c2b96488),func的参数和两个局部变量(c2b9643c、c2b96448、c2b9644c),func2的局部变量(c2b9641c)。从这里也可以看出栈是向低地址增长的,因为我们确定函数调用顺序是main->func->func2,所以压栈顺序也一定是这个,从每个函数中找个代表出来按压栈顺序排列,c2b96484->c2b9643c->c2b9641c,发现地址越来越小了,所以栈向低地址增长没有问题。
还又个问题,我们看到main里面两个局部变量,先声明的地址小(c2b96484),后声明的地址大(c2b96488),其实这并不违背栈向低地址增长,因为在main函数这个栈帧里面(stack frame),保存局部变量并没有压栈出栈等栈的操作,完全是两码事,比如我们看一下汇编代码,可以发现这局部变量是这样赋值的:
movl $2, -8(%rbp)
stack后面还有两个段:vdso,不知道是什么;vsyscall,内核的代码,每个程序都少不了。
顺便再说下,上面的测试还可以看出全局变量没初始化会默认赋值为0,而局部变量不会,所以局部变量使用前一定要初始化,否则会出现不知道的结果。
objdump -d address.o address.o: file format elf64-x86-64 Disassembly of section .init: 0000000000400498 <_init>: 400498: 48 83 ec 08 sub $0x8,%rsp 40049c: e8 9b 00 00 00 callq 40053c <call_gmon_start> 4004a1: e8 2a 01 00 00 callq 4005d0 <frame_dummy> 4004a6: e8 f5 03 00 00 callq 4008a0 <__do_global_ctors_aux> 4004ab: 48 83 c4 08 add $0x8,%rsp 4004af: c3 retq
00000000004005f4 <main>: 4005f4: 55 push %rbp 4005f5: 48 89 e5 mov %rsp,%rbp 4005f8: 48 83 ec 30 sub $0x30,%rsp 4005fc: 89 7d dc mov %edi,-0x24(%rbp) 4005ff: 48 89 75 d0 mov %rsi,-0x30(%rbp) 400603: c7 45 f8 02 00 00 00 movl $0x2,-0x8(%rbp) 40060a: bf 04 00 00 00 mov $0x4,%edi 40060f: e8 dc fe ff ff callq 4004f0 <malloc@plt>
可以看到main的起始地址是4005f4,和第一部分里面结果一样。
关于地址后面的内容我再解释以下吧,55是机器码,对应汇编码push %rbp,因为55只有一个字节,所以后面的地址是4005f5,下一个机器码是48 89 e5,对应的汇编是:mov %rsp, %rbp,然后就以此类推了。不同的汇编对应的机器码的字节数是不同的,所以不要惊讶机器码为什么参差不齐的。再贴一部分func的反汇编(beautiful)吧:
000000000040075a <func>: 40075a: 55 push %rbp 40075b: 48 89 e5 mov %rsp,%rbp 40075e: 48 83 ec 20 sub $0x20,%rsp 400762: 89 7d ec mov %edi,-0x14(%rbp) 400765: c7 45 fc 10 00 00 00 movl $0x10,-0x4(%rbp) 40076c: 8b 4d ec mov -0x14(%rbp),%ecx 40076f: b8 9e 09 40 00 mov $0x40099e,%eax 400774: 48 8d 55 ec lea -0x14(%rbp),%rdx 400778: 89 ce mov %ecx,%esi 40077a: 48 89 c7 mov %rax,%rdi 40077d: b8 00 00 00 00 mov $0x0,%eax 400782: e8 49 fd ff ff callq 4004d0 <printf@plt>
(gdb) print main $1 = {int (int, char **)} 0x4005f4 <main>
(gdb) x/8xb main 0x4005f4 <main>: 0x55 0x48 0x89 0xe5 0x48 0x83 0xec 0x30
(gdb) x/2xw main 0x4005f4 <main>: 0xe5894855 0x30ec8348
(gdb) x/1xg main 0x4005f4 <main>: 0x30ec8348e5894855