谈谈程序在内存中的分布

原文地址: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 plain copy
  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 plain copy
  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 plain copy
  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 plain copy
  1. movl    $2, -8(%rbp)  

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

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

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

3. 使用objdump再深入分析

使用命令objdump -d address.o显示程序的汇编内容,因为这个命令的输出是在是太详细,所以不能把结果都贴出来,但是并不影响大家理解。大家也可以在自己电脑上试试,再和后面内容对应起来看。如果还是有疑问,就给我留言吧:)
先把objdump输出的开头部分贴出来:
[plain]  view plain copy
  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 plain copy
  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 plain copy
  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 plain copy
  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 plain copy
  1. (gdb) x/8xb main  
  2. 0x4005f4 <main>:  0x55    0x48    0x89    0xe5    0x48    0x83    0xec    0x30  

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

再比如x/2xw main,检测main开始的内存,输出格式为16进制(x),每次4个字节,检测2次(2),输出如下:
[plain]  view plain copy
  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 plain copy
  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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值