在shell提示符下运行可执行文件时,实际上相当于调用了execve()函数。此时操作系统会调用程序装载器。程序装载器会首先读取可执行文件并生成一个进程,并初始化对应的命令、数据和进程栈页表以及寄存器,然后执行一条跳转命令跳转到程序的第一条命令或程序入口点(_start符号的地址)开始执行Prog程序。
在32位Linux系统下,可执行文件装载进内存运行的镜像如图2.15所示。
图2.15 在32位Linux系统下可执行文件装载进内存运行的镜像
说明:
(1)可执行文件被装载的代码段起始地址。在默认情况下,32位Linux系统下可执行文件被装载的代码段起始地址为0x8048000,64位的Linux系统的起始地址为0x400000。可以使用如下命令查看:
如果要改变可执行文件被装载的代码段起始地址,如设置为0x80000,可以在链接阶段增加如下选项进行修改:
.bss和.data的选项依次为-Tbss org和-Tdata org。其中org是一个以十六进制数字表示的地址。
(2).data和.bss为静态数据区,当可执行文件被调度运行时,该文件会被复制到内存区域。堆和栈为动态数据区,其中,栈会根据函数的调用与返回动态变化,堆会根据用户的malloc()、free()函数动态变化。
(3).data以及其他段的地址。.data的起始地址是从代码段后的下一个4 KB对齐的地址。在运行可执行文件时,堆从接下来的下一个4 KB对齐的地址开始。堆之后有一个专门为共享库预留的段。在运行可执行文件时,用户栈从高地址往低地址增长。栈上的预留段供操作系统内核使用。
可执行文件在被调度运行时需要读取ELF文件的头,操作系统内核只关心头中的三种类型条目:
(1)第一种条目是PT_LOAD,用于描述可执行文件被装载程序装载到内存后的运行区域,包括可执行文件的.text和.data以及.bss的大小,.bss将用0填充(因此只需将其长度存储在可执行文件中)。
(2)第二种条目是PT_INTERP,用于标识链接完整程序所需的运行时链接器名字,即动态链接器的名字。
(3)第三种条目是PT_GNU_STACK,如果该条目存在,则操作系统内核可以从该条目获取一个信息位,该信息位用于指示程序的栈是否可执行。
在Linux下可执行文件被调度运行的过程如图2.16所示:
第一步将可执行文件的PT_LOAD段装载进内存,创建程序的内存镜像,将.bss全部用0填充。
第二步检索头的PT_INTERP以及PT_GNU_STACK,使用PT_INTERP标识的动态链接器,如/lib64/ld-linux-x86-64.so.2,读取可执行文件依赖的所有库信息,在磁盘上搜索这些库,并将它们装载到内存中。
第三步执行重定位。需要执行重定位操作的有两个:
①共享库刚开始被装载到不确定的地址,需要重定位以确定其绝对地址。
图2.16 在Linux下可执行文件被调度运行的过程
②在多模块的工程中,一个目标文件对其他目标文件的引用也需要进行重定位,以确定其地址。
第四步调用注册在.preinit_array、.init、.init_array中的共享库初始化函数。
第五步将控制权传递给原始二进制文件的入口点,使用户感觉二进制文件是直接从exec传递过来的。
进程的虚拟地址空间(见图2.17)和体系结构有关。64位Linux系统的进程的虚拟地址空间大小为2 64 B。
图2.17 64位Linux的进程的虚拟地址空间
在Linux下,用户可以使用proc文件系统查看进程的虚拟地址空间。proc文件系统是一个伪文件系统,提供了访问内核数据结构的一个接口。
(1)/proc/[pid]/mem:进程的内存镜像,可以通过open()、read()和lseek()函数对该文件进行打开、读和查找操作。
(2)/proc/[pid]/maps包含进程的当前内存镜像以及各个区的访问权限。
下面举例说明proc文件系统的使用,被测的应用源码如Listing 2.22所示。
Listing 2.22 进程虚拟地址空间测试的应用源码loop.c
(1)访问进程空间的脚本文件:其功能是在进程的堆中搜索要查找的字符串,若找到,则用另一字符串进行替换,代码如Listing 2.23所示。
Listing 2.23 读取进程虚拟地址空间的脚本,read-write-heap.py
(2)脚本文件的运行,命令如下:
上述命令的作用是在进程4618的虚拟内存空间中查找字符串“Holberton”,找到后使用“Fun w vm!”进行替换。