Linux 进程虚拟地址空间
先回忆一下ELF文件的组织结构,可以看这篇文章:Linux 链接与ELF文件。程序执行后进程地址空间布局则和操作系统密切相关。在将应用程序加载到内存空间执行时,操作系统负责代码段与数据段的加载,并在内存中为这些段分配空间。Linux的进程地址空间大致如下:
栈 (stack)
函数调用借助的就是栈。Linux中ulimit -s命令可查看和设置堆栈最大值,调高堆栈容量可能会增加内存开销和启动时间。
内存映射段 (mmmap)
内核将硬盘文件的内容直接映射到内存,借助Linux的mmap()系统调用。这里也用来映射ELF文件用到的动态链接库。
堆 (heap)
分配的堆内存是经过字节对齐的空间,以适合原子操作。堆管理器通过链表管理每个申请的内存,由于堆申请和释放是无序的,最终会产生内存碎片。
堆的末端由break指针标识,当堆管理器需要更多内存时,可通过系统调用brk()和sbrk()来移动break指针以扩张堆,一般由系统自动调用。
数据段 (bss + data)
主要用来放全局变量和静态局部变量,地址编译期确定。
代码段 (text)
放的函数指令,地址编译期确定。
Linux 函数调用栈帧结构
根据反汇编可知,每个函数第一个指令都是push $rbp。当caller者调callee时,指令callq 把callee返回后的下一个指令地址压入栈帧,然后保存更新rbp寄存器,这样层层调用构成函数调用栈,每个栈帧开始就是存的上一个栈帧的rbp, 结尾就是调用callee后的下一个执行令地址。函数调用详细介绍可以看这篇文章从汇编看Linux C函数的调用约定和参数传递的细节。调用栈的结构图大致如下:
下面用一个例子来说明栈帧结构的细节。
int fun3(int a3)
{
int a = 3;
int re = a3;
while(1);
return re;
}
int fun2(int a2)
{
int a = 2;
int re = fun3(a2);
return re;
}
int fun1(int a1)
{
int a = 1;
int re = fun2(a1);
return re;
}
int main()
{
int a = 7;
int b = fun1(a);
return 0;
}
利用gdb对上面里进行详细分析,编译gcc -g main.c, 执行./a.out, 然后新开一个shell窗口ps afxu | grep a.out查找进程PID,gdb - 进行调试。可以很容易分析出函数的栈帧结构组织关系,详细如下:
fun3 (a3=7) at main.c:5
5 while(1);
(gdb) bt
#0 fun3 (a3=7) at main.c:5
#1 0x000000000040051f in fun2 (a2=7) at main.c:12
#2 0x0000000000400543 in fun1 (a1=7) at main.c:19
#3 0x0000000000400564 in main () at main.c:26
(gdb) p $rbp // 当前栈帧0 rbp
$1 = (void *) 0x7fff1c4d0ce0
(gdb) x/i *(long*)($1 + 8) // 栈帧1 返回地址
0x40051f : mov %eax,-0x4(%rbp)
(gdb) p/x *(unsigned long*)$rbp // 栈帧1 rbp地址
$2 = 0x7fff1c4d0d08
(gdb) x/i *(long*)($2 + 8) // 栈帧2 返回地址
0x400543 : mov %eax,-0x4(%rbp)
(gdb) p/x *(unsigned long*)$2 // 栈帧3 rbp
$3 = 0x7fff1c4d0d30
(gdb) x/i *(long*)($3 + 8) // 栈帧3 返回地址
0x400564 : mov %eax,-0x4(%rbp)
(gdb) x/24x $rsp // 从rsp开始24 DWORD的栈内容
0x7fff1c4d0ce0: 0x1c4d0d08 0x00007fff // rbp
0x0040051f 0x00000000 // return addr
0x7fff1c4d0cf0: 0xa68271a8 0x00000007 0xa6dfe4c0 0x00007fe4
0x7fff1c4d0d00: 0x00000002 0x00007fe4
0x1c4d0d30 0x00007fff // rbp
0x7fff1c4d0d10: 0x00400543 0x00000000 // return addr
0x004005bd 0x00000007
0x7fff1c4d0d20: 0x1c4d0d50 0x00007fff 0x00000001 0x00000000
0x7fff1c4d0d30: 0x1c4d0d50 0x00007fff // rbp
0x00400564 0x00000000 // return addr
还可以用pmap命令看布局:
root@ubuntu:~# pmap -p 23620
23620: ./a.out
0000000000400000 4K r-x-- /root/a.out
0000000000600000 4K r---- /root/a.out
0000000000601000 4K rw--- /root/a.out
00007fe4a6817000 1776K r-x-- /lib/x86_64-linux-gnu/libc-2.19.so
00007fe4a69d3000 2044K ----- /lib/x86_64-linux-gnu/libc-2.19.so
00007fe4a6bd2000 16K r---- /lib/x86_64-linux-gnu/libc-2.19.so
00007fe4a6bd6000 8K rw--- /lib/x86_64-linux-gnu/libc-2.19.so
00007fe4a6bd8000 20K rw--- [ anon ]
00007fe4a6bdd000 140K r-x-- /lib/x86_64-linux-gnu/ld-2.19.so
00007fe4a6de6000 12K rw--- [ anon ]
00007fe4a6dfd000 8K rw--- [ anon ]
00007fe4a6dff000 4K r---- /lib/x86_64-linux-gnu/ld-2.19.so
00007fe4a6e00000 4K rw--- /lib/x86_64-linux-gnu/ld-2.19.so
00007fe4a6e01000 4K rw--- [ anon ]
00007fff1c4b2000 132K rw--- [ stack ]
00007fff1c5dd000 8K r-x-- [ anon ]
ffffffffff600000 4K r-x-- [ anon ]
更多有意思的细节可以看后面参考文献。
Reference