介绍完了目标文件是怎么链接到可执行程序的,我们不妨进一步学习可执行程序是怎么被加载到内存中并运行的。以及动态链接库是怎么和程序一起被加载的。
可执行目标文件
我们已经学习了链接器是怎么将多个目标文件合并成一个可执行目标文件的。我们的C程序,从一开始的一组ASCII文本文件,被转换成了一个二进制文件。这个二进制文件包含加载程序到内存并运行它所需的所有信息。一个典型的ELF可执行文件有以下内容:

和可重定位目标文件还是有较大的区别。ELF头描述文件的总体格式。它还包括程序的入口点(entryPoint),也就是当程序运行时要执行的第一条指令的地址。.text .rodata .data
节和可重定位目标文件中的节是相似的。此外,还有一个.init
节,这个节中定义了一个小函数,叫做_init
,程序初始化代码时会调用它。同时,因为可执行文件是完全链接的,所以不再需要rel
节
ELF可执行文件被设计的很容易加载到内存中,可执行文件的连续的片被映射到连续的内存段。程序头部表则描述了这种映射关系。我们使用objdump -p
来查看:
1 | 只读代码段 |
其中:
- off:目标文件中的段的第一个节的偏移
- vaddr/paddr:虚拟地址/物理地址(物理地址在现代操作系统中无意义)
- align:指定的对齐要求,使得段能够有效率的传送到内存中
- filesz:目标文件中的段大小
- memsz:内存中的段大小
- flags:运行时的访问权限
我们以读写数据段的加载为例。开始于内存地址0xa4f50
处,总的内存大小为0xb2d8
,于是从目标文件中偏移0xa4f50
处开始的.data
节中的0x5b60
个字节初始化。该段剩下的字节对应于运行时将被初始化为0的.bss
数据。
对于任何段s,链接器必须选择一个起始地址vaddr
,使得:
1 | vaddr mod align = off mod align |
off是段在可执行文件本身的起始位置。根据对齐要求对齐,是为了更好的优化加载的效率。会在虚拟内存中进一步学习。
加载可执行文件
当我们运行一个程序时:
1 | ylin@Ylin:~/Program/test$ ./prog |
由于prog不是一个内置的shell命令,所以shell会将它视作一个可执行目标文件,通过调用驻留在存储器中称为加载器的操作系统代码来运行它。任何Linux程序,都可以通过调用exevce
函数来调用加载器。
记载其将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序。这个将程序到内存并运行的过程叫加载
为了解释加载器的运行,我们还需要认识以下每个Linux程序的内存映像:

在LInux x86_64中,代码段总是从0x400000处开始的,后面是数据段。运行时堆在数据段之后,通过调用用malloc库向上增长。堆之后的区域则是为共享库模块保留的。用户栈是从最大合法与用户地址(248-1)开始的,向低地址处生长。栈上的地址,从248处开始,是为内核中的代码和数据保留的。
不过这只是简图,实际上的内存空间分布略有不同,由于对齐有要求,段之间会有一定的间隙。而且现代编译器使用地址空间布局随机化,使得每次程序运行时这些区域的地址都会改变,但他们的相对位置是不会改变的
当加载器运行时,它会一个内存映像。在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据毒啊。接下来加载器跳转到程序的入口,也就是_start
函数的地址。这个函数在系统目标问及那ctrl.o
中定义。_start
函数调用系统启动函数__libc_start_main
,该函数定义在libc.so中。它负责初始化执行环境,调用用户层的main函数,处理main函数的返回值,并在需要时将控制返回给内核。
加载器的具体工作原理实际上涉及到多个方面:进程、虚拟内存、内存映射。我们之后会回头重新理解分析这个过程。