谈谈程序在内存中的分布

来源:互联网 发布:淘宝被差评有什么影响 编辑:程序博客网 时间:2024/04/30 09:18

原文地址:http://blog.csdn.net/high_high/article/details/7202233


作为一个菜鸟,这个题目有点大,所以这篇博客缺点是可能不够深入,但应该还是很详细的,希望能对大家有所帮助。

1.简介加初步分析

在linux系统中,程序在内存中的分布如下所示:

低地址.text.data.bss            heap(堆)      -->      unused   <--      stack(栈)      env高地址

其中 :

.text 部分是编译后程序的主体,也就是程序的机器指令。

.data 和 .bss 保存了程序的全局变量,.data保存有初始化的全局变量,.bss保存只有声明没有初始化的全局变量。

heap(堆)中保存程序中动态分配的内存,比如C的malloc申请的内存,或者C++中new申请的内存。堆向高地址方向增长。

stack(栈)用来进行函数调用,保存函数参数,临时变量,返回地址等。

下面是测试用的程序,比较简单,用来输出各个变量的地址。

[cpp] view plaincopy
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3.   
  4. int ug;  
  5. int dg = 1;  
  6.   
  7. void func(int);  
  8. void func2(int);  
  9. int main(int argc, char ** argv){  
  10.   int ul;  
  11.   int dl = 2;  
  12.   int *pi = (int *)malloc(sizeof(int));  
  13.   *pi = 4;  
  14.   int *pi2 = (int *)malloc(sizeof(int));  
  15.   *pi2 = 8;  
  16.   
  17.   printf("address of main:     %x\n", main);  
  18.   printf("undefined global %d: %x\n", ug, &ug);  
  19.   printf("defined global %d:   %x\n", dg, &dg);  
  20.   printf("undefined local %d:  %x\n", ul, &ul);  
  21.   printf("defined local %d:    %x\n", dl, &dl);  
  22.   printf("address of func:     %x\n", func);  
  23.   func(32);  
  24.   printf("dynamic alloc %d:    %x\n", *pi, pi);  
  25.   printf("dynamic alloc %d:    %x\n", *pi2, pi2);  
  26.   
  27.   free(pi);  
  28.   free(pi2);  
  29.   
  30.   int a;  
  31.   scanf("%d", &a);  
  32.   
  33.   return 0;  
  34. }  
  35.   
  36. void func(int arg){  
  37.   int uloc;  
  38.   int dloc = 16;  
  39.   printf("address of argument %d: %x\n", arg, &arg);  
  40.   printf("undefined func local %d: %x\n", uloc, &uloc);  
  41.   printf("defined func local %d: %x\n", dloc, &dloc);  
  42.   func2();  
  43. }  
  44.   
  45. void func2(){  
  46.   int loc = 64;  
  47.   printf("local of func2 %d: %x\n", loc, &loc);  
  48. }  

程序输出如下:

[plain] view plaincopy
  1. address of main:     4005f4  
  2. undefined global 0: 601050  
  3. defined global 1:   601038  
  4. undefined local 32767:  c2b96484  
  5. defined local 2:    c2b96488  
  6. address of func:     40075a  
  7. address of argument 32: c2b9643c  
  8. undefined func local -451161400: c2b96448  
  9. defined func local 16: c2b9644c  
  10. local of func2 64: c2b9641c  
  11. dynamic alloc 4:    16b1010  
  12. dynamic alloc 8:    16b1030  

2.使用进程maps文件深入分析

在linux下,可以查看进程的maps文件了解程序在内存中的分布,上面那个程序运行后的进程的maps文件内容如下:

[plain] view plaincopy
  1. cat /proc/2506/maps   
  2. 00400000-00401000 r-xp 00000000 08:03 4080116                            /home/yuduo/Workspace/C/sandbox/address.o  
  3. 00600000-00601000 r--p 00000000 08:03 4080116                            /home/yuduo/Workspace/C/sandbox/address.o  
  4. 00601000-00602000 rw-p 00001000 08:03 4080116                            /home/yuduo/Workspace/C/sandbox/address.o  
  5. 016b1000-016d2000 rw-p 00000000 00:00 0                                  [heap]  
  6. 7fc8e4bfc000-7fc8e4d91000 r-xp 00000000 08:03 4460234                    /lib/x86_64-linux-gnu/libc-2.13.so  
  7. 7fc8e4d91000-7fc8e4f90000 ---p 00195000 08:03 4460234                    /lib/x86_64-linux-gnu/libc-2.13.so  
  8. 7fc8e4f90000-7fc8e4f94000 r--p 00194000 08:03 4460234                    /lib/x86_64-linux-gnu/libc-2.13.so  
  9. 7fc8e4f94000-7fc8e4f95000 rw-p 00198000 08:03 4460234                    /lib/x86_64-linux-gnu/libc-2.13.so  
  10. 7fc8e4f95000-7fc8e4f9b000 rw-p 00000000 00:00 0   
  11. 7fc8e4f9b000-7fc8e4fbc000 r-xp 00000000 08:03 4460221                    /lib/x86_64-linux-gnu/ld-2.13.so  
  12. 7fc8e5199000-7fc8e519c000 rw-p 00000000 00:00 0   
  13. 7fc8e51b7000-7fc8e51bb000 rw-p 00000000 00:00 0   
  14. 7fc8e51bb000-7fc8e51bc000 r--p 00020000 08:03 4460221                    /lib/x86_64-linux-gnu/ld-2.13.so  
  15. 7fc8e51bc000-7fc8e51be000 rw-p 00021000 08:03 4460221                    /lib/x86_64-linux-gnu/ld-2.13.so  
  16. 7fffc2b76000-7fffc2b97000 rw-p 00000000 00:00 0                          [stack]  
  17. 7fffc2bbe000-7fffc2bbf000 r-xp 00000000 00:00 0                          [vdso]  
  18. 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),保存局部变量并没有压栈出栈等栈的操作,完全是两码事,比如我们看一下汇编代码,可以发现这局部变量是这样赋值的:

[plain] view plaincopy
  1. movl    $2, -8(%rbp)  

只和基地址有关(rbp)。我个人觉得局部变量地址和编译器有关,但是没有测试,提出来算个想法吧 :)

stack后面还有两个段:vdso,不知道是什么;vsyscall,内核的代码,每个程序都少不了。

顺便再说下,上面的测试还可以看出全局变量没初始化会默认赋值为0,而局部变量不会,所以局部变量使用前一定要初始化,否则会出现不知道的结果。

3. 使用objdump再深入分析

使用命令objdump -d address.o显示程序的汇编内容,因为这个命令的输出是在是太详细,所以不能把结果都贴出来,但是并不影响大家理解。大家也可以在自己电脑上试试,再和后面内容对应起来看。如果还是有疑问,就给我留言吧:)
先把objdump输出的开头部分贴出来:
[plain] view plaincopy
  1. objdump -d address.o  
  2.   
  3. address.o:     file format elf64-x86-64  
  4.   
  5.   
  6. Disassembly of section .init:  
  7.   
  8. 0000000000400498 <_init>:  
  9.   400498:   48 83 ec 08             sub    $0x8,%rsp  
  10.   40049c:   e8 9b 00 00 00          callq  40053c <call_gmon_start>  
  11.   4004a1:   e8 2a 01 00 00          callq  4005d0 <frame_dummy>  
  12.   4004a6:   e8 f5 03 00 00          callq  4008a0 <__do_global_ctors_aux>  
  13.   4004ab:   48 83 c4 08             add    $0x8,%rsp  
  14.   4004af:   c3                      retq     

首先说我的文件格式是elf64-x86-64的,然后是.init的反汇编(从机器码生成汇编码),后面还有很多程序开始运行后main函数调用之前的很多初始化工作,这些都是编译器和操作系统加的,不要以为程序开始运行后就直接开始执行main哦。不过这里关心的还是main,main的反汇编部分如下:
[cpp] view plaincopy
  1. 00000000004005f4 <main>:  
  2.   4005f4:   55                      push   %rbp  
  3.   4005f5:   48 89 e5                mov    %rsp,%rbp  
  4.   4005f8:   48 83 ec 30             sub    $0x30,%rsp  
  5.   4005fc:   89 7d dc                mov    %edi,-0x24(%rbp)  
  6.   4005ff:   48 89 75 d0             mov    %rsi,-0x30(%rbp)  
  7.   400603:   c7 45 f8 02 00 00 00    movl   $0x2,-0x8(%rbp)  
  8.   40060a:   bf 04 00 00 00          mov    $0x4,%edi  
  9.   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)吧:

[plain] view plaincopy
  1. 000000000040075a <func>:  
  2.   40075a:   55                      push   %rbp  
  3.   40075b:   48 89 e5                mov    %rsp,%rbp  
  4.   40075e:   48 83 ec 20             sub    $0x20,%rsp  
  5.   400762:   89 7d ec                mov    %edi,-0x14(%rbp)  
  6.   400765:   c7 45 fc 10 00 00 00    movl   $0x10,-0x4(%rbp)  
  7.   40076c:   8b 4d ec                mov    -0x14(%rbp),%ecx  
  8.   40076f:   b8 9e 09 40 00          mov    $0x40099e,%eax  
  9.   400774:   48 8d 55 ec             lea    -0x14(%rbp),%rdx  
  10.   400778:   89 ce                   mov    %ecx,%esi  
  11.   40077a:   48 89 c7                mov    %rax,%rdi  
  12.   40077d:   b8 00 00 00 00          mov    $0x0,%eax  
  13.   400782:   e8 49 fd ff ff          callq  4004d0 <printf@plt>  

4. 使用gdb再再(装b)深入分析

深入到这个份上,其实已经没有什么好分析的了,只是顺便说说gdb下怎么检测变量和内存。
检查变量就是print了,比如:
[plain] view plaincopy
  1. (gdb) print main  
  2. $1 = {int (int, char **)} 0x4005f4 <main>  

可以看出main是个函数,起始地址0x4005f4,没有什么问题。下面重点介绍怎么检测内存。
gdb使用x检测内存,使用格式是x/FMT ADDRESS,其中FMT是想要重复的次数+格式化字符(format letter)+ 大小字符(size letter),ADDRESS不用说就是想要检测的地址了。
其中格式化字符有:o(octal   8进制), x(hex   16进制), d(decimal   10进制), u(unsigned decimal    无符号10进制), t(binary    2进制), f(float   浮点数), a(address    地址), i(instruction    指令), c(char    字符) 和 s(string   字符串).
大小字符有:b(byte   1个字节), h(halfword   2个字节), w(word   4个字节), g(giant, 8个字节)。
实际使用中格式化字符和大小字符位置貌似可以调换,我用的时候也不太在意。下面用三种方法检测从地址main(0x4005f4)开始的8个字节:
比如x/8xb main表示检测mian开始的内存,输出格式为16进制(x),每次一个字节(b),检测8次(8),输出如下:
[plain] view plaincopy
  1. (gdb) x/8xb main  
  2. 0x4005f4 <main>:  0x55    0x48    0x89    0xe5    0x48    0x83    0xec    0x30  

main的地址还是0x4005f4,然后可以回头看看第三部分里面main的反汇编,前8个字节就是 0x550x480x890xe50x480x830xec0x30。
使用x/8bx main效果是一样的,不过话说回来gcc的一套工具大多参数位置可以随便摆放。

再比如x/2xw main,检测main开始的内存,输出格式为16进制(x),每次4个字节,检测2次(2),输出如下:
[plain] view plaincopy
  1. (gdb) x/2xw main  
  2. 0x4005f4 <main>:  0xe5894855  0x30ec8348  

输出貌似和上面按字节输出有些不同了,4个4个倒序了,因为我的处理器是intel的,intel采用小头编码方式(little-endian),低地址的字节排在低位(十位、个位),高地址的字节排在高位(千位,万位),所以上面的0x55在低位,按4个字节输出就在最后面了(低位)。
我再换种方式输出可能会更清楚:x/1xg main,同样检测main开始的内存,输出格式还是16进制(x),不同的是每次8个字节(g),只检测一次:
[plain] view plaincopy
  1. (gdb) x/1xg main  
  2. 0x4005f4 <main>:  0x30ec8348e5894855  

这个分析就留给读者当小练习吧,如果不懂还是那句话,给我留言吧:)


参考文献:
Linux assembly language programming. Bob Neveln. 2000
The art of debugging with gdb, ddd, and eclipse. Norman Matloff, Peter Jay Salzman. 2008
Professional Linux kernel architecture. Wolfgang Mauerer. 2008
原创粉丝点击