原文地址: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(栈)用来进行函数调用,保存函数参数,临时变量,返回地址等。
下面是测试用的程序,比较简单,用来输出各个变量的地址。
- #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
2.使用进程maps文件深入分析
在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),保存局部变量并没有压栈出栈等栈的操作,完全是两码事,比如我们看一下汇编代码,可以发现这局部变量是这样赋值的:
只和基地址有关(rbp)。我个人觉得局部变量地址和编译器有关,但是没有测试,提出来算个想法吧 :)
stack后面还有两个段:vdso,不知道是什么;vsyscall,内核的代码,每个程序都少不了。
顺便再说下,上面的测试还可以看出全局变量没初始化会默认赋值为0,而局部变量不会,所以局部变量使用前一定要初始化,否则会出现不知道的结果。
3. 使用objdump再深入分析
使用命令objdump -d address.o显示程序的汇编内容,因为这个命令的输出是在是太详细,所以不能把结果都贴出来,但是并不影响大家理解。大家也可以在自己电脑上试试,再和后面内容对应起来看。如果还是有疑问,就给我留言吧:)
先把objdump输出的开头部分贴出来:
- 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
首先说我的文件格式是elf64-x86-64的,然后是.init的反汇编(从机器码生成汇编码),后面还有很多程序开始运行后main函数调用之前的很多初始化工作,这些都是编译器和操作系统加的,不要以为程序开始运行后就直接开始执行main哦。不过这里关心的还是main,main的反汇编部分如下:
- 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>
4. 使用gdb再再(装b)深入分析
深入到这个份上,其实已经没有什么好分析的了,只是顺便说说gdb下怎么检测变量和内存。
检查变量就是print了,比如:
- (gdb) print main
- $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),输出如下:
- (gdb) x/8xb main
- 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),输出如下:
- (gdb) x/2xw main
- 0x4005f4 <main>: 0xe5894855 0x30ec8348
输出貌似和上面按字节输出有些不同了,4个4个倒序了,因为我的处理器是intel的,intel采用小头编码方式(little-endian),低地址的字节排在低位(十位、个位),高地址的字节排在高位(千位,万位),所以上面的0x55在低位,按4个字节输出就在最后面了(低位)。
我再换种方式输出可能会更清楚:x/1xg main,同样检测main开始的内存,输出格式还是16进制(x),不同的是每次8个字节(g),只检测一次:
- (gdb) x/1xg main
- 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