Linux进程地址空间详解

来源:互联网 发布:查看centos防火墙命令 编辑:程序博客网 时间:2024/05/29 15:28

http://blog.chinaunix.net/uid-14735472-id-3400847.html

之前写的一篇文章《a.out分段及运行时内存结构》简要介绍了Linux下的可执行文件格式和运行时的内存布局,这篇文章将更为详细得讨论Linux下进程的虚拟地址空间的布局。如下所述的内容都是基于32位系统的。

Linux传统内存布局


进程的线性地址空间分为两部分:

1、从0×00000000到0xbfffffff的线性地址(0~3G),无论进程运行在用户态还是内核态都可以寻址。

2、从0xc0000000到0xffffffff的线性地址(3~4G),只有内核态的进程才能寻址。

进程的栈从地址0xc0000000(3G)向低地址发展,同时内存映射区域从0×40000000(1G)向高地址发展。因为栈所用内存相对较小(通常小于100MB),因此约有2GB左右的映射空间;

可执行文件的正文段(存放CPU可执行的机器指令和常量数据)从0×8048000(128M)开始,然后依次是数据段(存放初始化过的全局变量或静态变量),BSS段(存放程序中未初始化的全局变量静态变量)

进程堆的起始点大于BSS段的结束点,并向高地址发展,因为0×40000000以上已用作内存映射用,因此堆的大小只有约1G,这有点太小了。

为了验证传统内存布局方式,使用以下测试程序,测试环境为Slackware 13.37 (Kernel 2.6.37)。

#include <stdio.h>#include <stdlib.h> int main(int argc, char* argv[]){    int  first = 0;    int* p0 = malloc(1024);    int* p1 = malloc(1024 * 1024);    int* p2 = malloc(512 * 1024 * 1024 );    int* p3 = malloc(1024 * 1024 * 1024 );    printf("main=%p print=%p\n", main, printf);    printf("first=%p\n", &first);    printf("p0=%p p1=%p p2=%p p3=%p\n", p0, p1, p2, p3);     sleep(10);     return 0;}


为了使内核切换到传统内存布局,执行命令#sysctl -w vm.legacy_va_layout=1(因为Linux 2.6.7及以后版本内核已经默认使用新的内存布局方式了)。

运行此程序,查看其输出及相应/proc/pid/maps内容:

#root@slack:~#./a.out &[6] 9528main=0x80483e4 print=0x8048300first=0xbfd00b9cp0=0x804a008 p1=0x4018d008 p2=0x4028e008 p3=0x6028f008 #root@slack:~#cat /proc/9528/maps08048000-08049000 r-xp 00000000 08:01 140878     /root/a.out08049000-0804a000 rw-p 00000000 08:01 140878     /root/a.out0804a000-0806b000 rw-p 00000000 00:00 0          [heap]40000000-4001d000 r-xp 00000000 08:01 931801     /lib/ld-2.13.so4001d000-4001e000 r--p 0001c000 08:01 931801     /lib/ld-2.13.so4001e000-4001f000 rw-p 0001d000 08:01 931801     /lib/ld-2.13.so4001f000-40022000 rw-p 00000000 00:00 0 40029000-40185000 r-xp 00000000 08:01 931779     /lib/libc-2.13.so40185000-40186000 ---p 0015c000 08:01 931779     /lib/libc-2.13.so40186000-40188000 r--p 0015c000 08:01 931779     /lib/libc-2.13.so40188000-40189000 rw-p 0015e000 08:01 931779     /lib/libc-2.13.so40189000-a0290000 rw-p 00000000 00:00 0 bf841000-bf862000 rw-p 00000000 00:00 0          [stack]ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]


首先分析程序的输出:

1、因为main()函数和printf()函数位于代码段中,而代码段是从0×08048000开始的,所以符合表中所述。

2、first是第一个临时变量,由于在first之前还有一些环境变量,它的值并非0xbfffffff,而是0xbfcd1264,这是正常的。

3、p0是在堆中分配的,其地址小于0×4000 0000,这也是正常的。

4、但p1和p2也是在堆中分配的,而其地址竟大于0×4000 0000,与表一描述不符。

原因在于:运行时堆的位置与内存管理算法相关,也就是与malloc的实现相关。

关于内存管理算法的问题,我们在后继文章中有详细描述,这里只作简要说明。在glibc实现的内存管理算法中,malloc小块内存是在小于0×4000 0000的内存中分配的,通过brk/sbrk不断向上扩展,而分配大块内存,malloc直接通过系统调用mmap实现,分配得到的地址在文件映射区,所以其地址大于0×40000000。

所以,p0是在堆上分配的,而p1~p3则是通过mmap实现的,这表现在maps文件的倒数第三行(40189000-a0290000)。

然后分析maps文件内容:

08048000-08049000 代码段

08049000-0804a000 数据段

(注意本程序无BSS段)

0804a000-0806b000 堆

40000000-40189000 内存映射区,本程序映射了ld和libc动态链接库

40189000-a0290000 内存映射区,malloc用其为p1~p3分配内存

bf841000-bf862000 栈

ffffe000-fffff000 内核,为我们映射的系统调用入口代码

Linux最新内存布局

鉴于以上传统内存布局的限制,Linux 2.6.7及以后版本已经默认使用另一种新的内存布局方式,如下图所示:


从上图可以看到,mmap 映射区域至顶向下扩展, mmap 映射区域和堆相对扩展,直至耗尽虚拟地址空间中的剩余区域,弥补了经典内存布局方式的不足。

为了使用此新的内存布局,执行命令#sysctl -w vm.legacy_va_layout=0,然后重新编译运行程序并查看其输出及maps文件内容:

#root@slack:~#./a.out &[6] 9529main=0x80483e4 print=0x8048300first=0xbff18e6cp0=0x804a008 p1=0xb7554008 p2=0x97553008 p3=0x57552008 #root@slack:~#cat /proc/9529/maps08048000-08049000 r-xp 00000000 08:01 140882     /root/a.out08049000-0804a000 rw-p 00000000 08:01 140882     /root/a.out0804a000-0806b000 rw-p 00000000 00:00 0          [heap]575c8000-b76cc000 rw-p 00000000 00:00 0 b76cc000-b7828000 r-xp 00000000 08:01 931779     /lib/libc-2.13.sob7828000-b7829000 ---p 0015c000 08:01 931779     /lib/libc-2.13.sob7829000-b782b000 r--p 0015c000 08:01 931779     /lib/libc-2.13.sob782b000-b782c000 rw-p 0015e000 08:01 931779     /lib/libc-2.13.sob782c000-b782f000 rw-p 00000000 00:00 0 b7837000-b7839000 rw-p 00000000 00:00 0 b7839000-b7856000 r-xp 00000000 08:01 931801     /lib/ld-2.13.sob7856000-b7857000 r--p 0001c000 08:01 931801     /lib/ld-2.13.sob7857000-b7858000 rw-p 0001d000 08:01 931801     /lib/ld-2.13.sobff4e000-bff6f000 rw-p 00000000 00:00 0          [stack]ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]


对比上一次maps文件内容,发现现在的内存映射区域已经从b7857000开始了,并且向下发展。p1~p3现在位于575c8000-b76cc000区域内。

新的内存布局图还显示:栈底和Kernel Space之间、栈顶和映射区域之间、堆和BSS段之间都有一个随机的offset,每次运行程序时的值都不一样,这样会使得使用缓冲区溢出进行攻击更加困难。如果需要,当然也可以让程序的栈和映射区域从一个固定位置开始,只需要设置全局变量randomize_va_space值为0即可(默认值为 1)
不明白offset的作用?让我们再执行一次程序,然后观察其输出和maps文件:

#root@slack:~#./a.out &[6] 9564main=0x80483e4 print=0x8048300first=0xbfec32ccp0=0x804a008 p1=0xb7654008 p2=0x97653008 p3=0x57652008 #root@slack:~#cat /proc/9564/maps08048000-08049000 r-xp 00000000 08:01 140882     /root/a.out08049000-0804a000 rw-p 00000000 08:01 140882     /root/a.out0804a000-0806b000 rw-p 00000000 00:00 0          [heap]574ea000-b75ee000 rw-p 00000000 00:00 0 b75ee000-b774a000 r-xp 00000000 08:01 931779     /lib/libc-2.13.sob774a000-b774b000 ---p 0015c000 08:01 931779     /lib/libc-2.13.sob774b000-b774d000 r--p 0015c000 08:01 931779     /lib/libc-2.13.sob774d000-b774e000 rw-p 0015e000 08:01 931779     /lib/libc-2.13.sob774e000-b7751000 rw-p 00000000 00:00 0 b7759000-b775b000 rw-p 00000000 00:00 0 b775b000-b7778000 r-xp 00000000 08:01 931801     /lib/ld-2.13.sob7778000-b7779000 r--p 0001c000 08:01 931801     /lib/ld-2.13.sob7779000-b777a000 rw-p 0001d000 08:01 931801     /lib/ld-2.13.sobfa0c000-bfa2d000 rw-p 00000000 00:00 0          [stack]ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]


和之前一次执行的输出相比较,发现程序输出中first变量的地址已经变了,而且maps文件中栈和内存映射区域的地址也变了。

那么,禁用offset会怎么样呢?执行命令#sysctl -w kernel.randomize_va_space=0,运行程序两次,再观察其输出:
第一次的输出:

#root@slack:~#./a.out &[6] 9573main=0x80483e4 print=0x8048300first=0xbffff4acp0=0x804a008 p1=0xb7d72008 p2=0x97d71008 p3=0x57d7008 #root@slack:~#cat /proc/9573/maps08048000-08049000 r-xp 00000000 08:01 140882     /root/a.out08049000-0804a000 rw-p 00000000 08:01 140882     /root/a.out0804a000-0806b000 rw-p 00000000 00:00 0          [heap]57d70000-b7e74000 rw-p 00000000 00:00 0 b7e74000-b7fd0000 r-xp 00000000 08:01 931779     /lib/libc-2.13.sob7fd0000-b7fd1000 ---p 0015c000 08:01 931779     /lib/libc-2.13.sob7fd1000-b7fd3000 r--p 0015c000 08:01 931779     /lib/libc-2.13.sob7fd3000-b7fd4000 rw-p 0015e000 08:01 931779     /lib/libc-2.13.sob7fd4000-b7fd7000 rw-p 00000000 00:00 0 b7fdf000-b7fe1000 rw-p 00000000 00:00 0 b7fe1000-b7ffe000 r-xp 00000000 08:01 931801     /lib/ld-2.13.sob7ffe000-b7fff000 r--p 0001c000 08:01 931801     /lib/ld-2.13.sob7fff000-b8000000 rw-p 0001d000 08:01 931801     /lib/ld-2.13.sobffdf000-c0000000 rw-p 00000000 00:00 0          [stack]ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]


第二次的输出:

#root@slack:~#./a.out &[6] 9575main=0x80483e4 print=0x8048300first=0xbffff4acp0=0x804a008 p1=0xb7d72008 p2=0x97d71008 p3=0x57d7008 #root@slack:~#cat /proc/9575/maps08048000-08049000 r-xp 00000000 08:01 140882     /root/a.out08049000-0804a000 rw-p 00000000 08:01 140882     /root/a.out0804a000-0806b000 rw-p 00000000 00:00 0          [heap]57d70000-b7e74000 rw-p 00000000 00:00 0 b7e74000-b7fd0000 r-xp 00000000 08:01 931779     /lib/libc-2.13.sob7fd0000-b7fd1000 ---p 0015c000 08:01 931779     /lib/libc-2.13.sob7fd1000-b7fd3000 r--p 0015c000 08:01 931779     /lib/libc-2.13.sob7fd3000-b7fd4000 rw-p 0015e000 08:01 931779     /lib/libc-2.13.sob7fd4000-b7fd7000 rw-p 00000000 00:00 0 b7fdf000-b7fe1000 rw-p 00000000 00:00 0 b7fe1000-b7ffe000 r-xp 00000000 08:01 931801     /lib/ld-2.13.sob7ffe000-b7fff000 r--p 0001c000 08:01 931801     /lib/ld-2.13.sob7fff000-b8000000 rw-p 0001d000 08:01 931801     /lib/ld-2.13.sobffdf000-c0000000 rw-p 00000000 00:00 0          [stack]ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]


正如所预料的,这两次运行的输出一模一样,其实再多执行几次也是一样的。

多线程内存空间布局

以上所述的都是只有主线程情况下的内存布局,那多线程情况下的布局是怎样的呢?
这里也使用一个程序来测试:

#include <stdio.h>#include <stdlib.h>#include <pthread.h> void* thread_proc(void* param){    int  first = 0;    int* p0 = malloc(1024);    int* p1 = malloc(1024 * 1024);     printf("(0x%x): first=%p/n",    pthread_self(), &first);    printf("(0x%x): p0=%p p1=%p /n", pthread_self(), p0, p1);     return 0;} #define N 5int main(int argc, char* argv[]){    int first = 0;    int i= 0;    void* ret = NULL;    pthread_t tid[N] = {0};     printf("first=%p/n", &first);    for(i = 0; i < N; i++)    {        pthread_create(tid+i, NULL, thread_proc, NULL);    }     for(i = 0; i < N; i++)    {        pthread_join(tid[i], &ret);    }     return 0;}


编译此程序gcc thread.c -lpthread,运行后其输出如下:

first=0xbfbaf648(0xb671db70): first=0xb671d384(0xb671db70): p0=0x804a248 p1=0xb4e1d008(0xb6f1db70): first=0xb6f1d384(0xb6f1db70): p0=0x804a650 p1=0xb49fd008(0xb771db70): first=0xb771d384(0xb771db70): p0=0xb4d00468 p1=0xb4bff008(0xb5f1db70): first=0xb5f1d384(0xb5f1db70): p0=0xb4d00900 p1=0xb4afe008(0xb571db70): first=0xb571d384(0xb571db70): p0=0x804aa58 p1=0xb48fc008


主线程与第一个线程的之间的距离:0xbfbaf648 – 0xb771d384 = 132M
第一个线程与第二个线程的之间的距离:0xb771d384 – 0xb6f1d384= 8M
其它几个线程的栈之间距离均为8M。
也就是说,主线程的栈空间最大为132M,而普通线程的栈空间仅为8M,超这个范围就会造成栈溢出(后果很严重)。


原创粉丝点击