Linux ELF 运行时内存详解 - 黑客防线官方站

  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

Linux ELF 运行时内存详解

4/22/2012

前一段时间做ROP (return-oriented programming )的东西,想要系统的了解Linux 中程序的内存格式(memory layout ),网上有很多文章,却没有一个深入完整的介绍。所以花了些时间做深入的了解,不放过一个细节。由于最初写的是英文文档,所以文中的图都是用英文标识的,不过应该不影响阅读。

本文详细解释了Linux ELF 文件的虚拟地址空间。另外本文也大概介绍了ASLR

(Address Space Layout Randomization)技术对ELF 虚拟地址空间的影响。作者的测试系统是Linux Ubuntu 2.6.32-24和Vmware Workstation 7。另外所有的分析都基于Intel x86架构。

虚拟地址空间

当代的操作系统中每个进程都有自己的独立虚拟地址空间。在32位系统上,该虚拟地址空间有4G 大小。为了将虚拟地址转换为物理地址,Linux 内核使用了一个两级(事实上是三级,但是中间一级没有任何实质操作)分页机制,即页目录表和页表。分页机制与MMU (Memory Management Unit )合作将虚拟地址转换为物理地址。当操作系统引入虚拟地址后,所有的用户操作系统和内核线程(事实上Linux 只有进程概念而没有线程概念,Linux 通过页表机制来模拟实现内核线程)都将运行于虚拟地址模式。

另外Linux (以及Windows )使用了CPU 提供的权限机制。内核代码将运行于ring 0而用户程序运行于ring 3。 因此为了适应该分级机制以及适应多任务机制,Linux 的虚拟地址空间被分为两部分,如图1所示:

0xffff ffff

0x0Linux Virtual

Address Split

0xffff ffff

0x0

Windows Virtual Address Split

图1. Linux/Windows 虚拟地址空间的内核部分和用户部分。

Linux 中,内核空间为0xc0000000到0xffffffff 的地址,因此内核代码将被映射到区域。而在Windows 中,默认的分割方式为内核与用户各占2GB 。本文仅详细分析Linux 的地址空间而不再涉及Windows 。下面分两部分介绍Linux 地址空间,首先是内核地址空间然后再介绍用户地址空间。

1. 内核地址空间

客防线 a c k

e r .c o m .c

n

明出处

内核地址空间属于ring 0,因此用户程序无法读取或修改该地址空间(除非通过特殊手段,如果系统调用)。如果用户程序强制涉及内核空间的话,系统将产生一个段错误(该错误对于Linux 程序员是再熟悉不过了)。另外,内核地址空间常驻于内存并且所有的进程共享相同的内核空间,然而用户地址空间随着进程的切换而改变。内核地址空间的详细格式如图2所示。

图2. 内核地址空间。

内核空间的起始地址有PAGE_OFFSET 定义,对于32位x86系统,该值为0xc0000000。PAGE_OFFSET 和VMALLOC_OFFSET 之间的区域是直接内存映射。VMALLOC_OFFSET 是一个8M 的空隙,用来防止越界。

PKMAP_BASE 开始的一段内存提供给kmap()使用。

某些设备需要在编译时就知道虚拟地址如APIC ,FIXADDR_START 和FIXADDR_TOP 之间的内存就是提供给这些设备使用的。

最后一个页是vsyscall 页。在2.4内核中,该页是空白页,即该页不可用。在2.6中,该页提供了一种心的从用户层进入内核层中的方法。现在,用户程序可以使用”call 0xfffff000”来代替”int 0x80”来进入内核层。

2.

用户地址空间

用户空间可以进一步被分为以下几个部分:栈,mmap 段,堆,BSS 段,数据段和代码段。其分布如图2所示。需要注意的是该分布格式是我的测试系统的结果,在其他系统上可能略有差别,如mmap 段可能被置于堆和栈之间。下面将详细解释每个段。

栈向下增长并且栈的大小受参数RLIMIT_STACK 限制。因此当程序向一个未映射的内存区写入数据时,如果该内存区位于RLIMIT_STACK 内,那么将不会产生段错误而只会调用函数expand_stack()来动态增长栈大小。另外,需要注意的是在内核地址空间与栈的起始地址之间有一个空白区。该空白区有ASLR 生成。本文将在第二节介绍ASLR 。

客防线 w w w .h a c k

e r .c o m .c

n

转载请注明出处

random stack offset

RLIMIT_STACK

random mmap offset

random brk offset

0xc0000000 == TASK_SIZE

图2. 用户地址空间分布。

图 3. ELF 刚载入内存时栈的内容。

客防w w w .o .c

n

关于栈的一个很重要的点是,当ELF 文件刚被载入内存时,栈不是空的(参考源代码fs/binfmt_elf.c/create_elf_tables())。至少,很明显main()函数的参数需要被存于栈上。事实上栈上还存储了更多的信息,具体如图3所示。如果程序的参数为字符串,那么字符串本身存储与栈底(main()函数的参数和环境变量)。PRNG (Pseudo Random Noise

Generation)是一个16字节的伪随机数种子。接下来,是一个叫auxiliary vector 的数组。该数组的内容定义在源代码的include/linux/auxvec.h 和arch/x86/include/asm/auxvec.h 文件中。为了得到某个程序的auxiliary vector 的内容,可以使用类似于“LD_SHOW_AUXV=true cat /proc/self/maps ”的命令。图4是本文使用的测试系统上的一个例子。注意其中的AT_SYSINFO_EHDR ,其值为0x242000。事实上该值是VDSO (Virtual Dynamic Shared Object )的起始地址。真是巧了,在系统使用ASLR 后我们就无法预先知道VDSO 的地址了,这个东西正好动态地告诉了我们。事实上,这正是系统告诉库函数VDSO 地址的机制。紧接着auxiliary vector 的是一组环境变量。最后,main()函数的参数被压入栈中并且位于当前栈顶的是argc (即参数数量)。

图 4. 一个auxiliary vector 的例子。

紧接着栈的堆。堆用于动态内存分配。对于C 程序员,该段可以使用mallco()/free()函数来管理。当程序员请求一块内存时,如果在堆中有足够的空闲块,那么库函数就可以直接满足该请求而不需要与内核交互。否则的话,将调用系统调用brk()来增加堆的大小。

接下来我们可以看到3个连续的段:BSS, 数据段和代码段。ELF 文件格式中也含有同名的3个段。BSS 段和数据段含有C 语言中的静态或全局变量。不同之处在于BSS 段含有未初始化的值而数据段含有初始化后的值。在对象文件中(即命令gcc –c xxx.c 的输出文件),只有未初始化的静态变量存储于.bss 段(ELF 文件中而非内存)而未初始化的全局变量放于COMMON 块(ELF 文件格式)。例如,某个C 程序中有两个未初始化的全局变量gCount 和gName ,它们将被存储在BSS 段中,如图5所示。可以看到,BSS 段被初始化为0。这也就是为什么相关的C 语言教材告诉我们不需要初始化全局变量。它们将被编译器初始化为0或NULL 。

另一方面,例如该C 程序还有一个全局变量gVersion 被初始化为1.0。该变量将被存于数据段,如图5所示。gInfo 是指向一个字符串的指针。由于该字符串的地址为

0x08049062并位于代码段中。为什么字符串被放于代码段中?因为字符串是只读的,而数据段和BSS 段均是可写的,只有代码段是只读的。

客防线 w w w .h a c k

e r .c o m .c

n

转载请注明出处

相关文档
最新文档