谈谈程序在内存中的分布

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

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),保存局部变量并没有压栈出栈等栈的操作,完全是两码事,比如我们看一下汇编代码,可以发现这局部变量是这样赋值的:

	movl	$2, -8(%rbp)

只和基地址有关(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

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值