【Linux】理解链接过程
1.objdump命令
功能:用于显示目标文件(如可执行文件、对象文件、库文件)的详细信息。
objdump [选项] 文件
该命令可以用于反汇编
# 反汇编可执行文件,显示代码段
objdump -d program# 反汇编并显示源代码(需要编译时包含调试信息)
objdump -S program
2. 静态链接
研究静态链接,本质就是研究 .o 是如何链接的。我们通过代码来观察一下:
我们可以看到这里的main函数里用到了run方法,但是run的具体实现在另一个.c文件中,我们把这两个文件编译一下:
编译过程没有什么问题,说明我们在编译过程是没有函数的具体实现方法也可以编过的。此时我们来看一下这两个.o文件的反汇编内容:
这里的两次call就是指我们调用了两次函数(printf 和 run),前面的数字e8其实就是用十六进制代表call命令,后面跟着的一串零其实应该是调用的函数地址(暂时为0),因为此时编译器是不知道函数地址的,和其他的模块链接后才知道函数地址。接下来我们就将这两个.o文件链接起来,在链接过程中也会链接我们包含的C标准库。
我们再来查看myexe中run函数和main函数的反汇编内容:
我们可以看到两个函数的地址都填上了具体的值,我们还可以看到我们call的是printf函数(printf底层调用的就是puts)和run函数。所以我们知道了:链接过程会把我们要调用的函数地址从0重定位到目标函数地址。这个过程就是链接时地址重定位,所以.o/.obj文件叫做可重定位目标文件!所以在链接之前,文件之间都是独立的,甚至不知道对方的存在,链接后才建立的文件之间的关系。
最后main函数成功调用了run函数,所以说明这两个.o文件(以及C标准库)的.text部分(代码区)合并了,并进行了统一的编址。链接过程中会修改.o中没有确定的函数地址,代码合并之后就可以通过call函数地址来进行函数调用。
3. 平坦模式
Linux系统编译形成可执行程序的时候,需要对代码和数据进行编址;当代计算机工作时都采用“平坦模式”进行编址,对ELF编址时也是如此。
平坦模式的核心思想是:让操作系统和应用程序能够访问一个连续的、无段的线性地址空间。也就是说它的编址方法就是从全0到全F进行编址(0000...0000~FFFF...FFFF),这样按照线性地址进行统一编址的。我们仔细看一段myexe的反汇编就可以发现地址是连续的:这种全0到全F的编址方法得到的地址其实就是虚拟地址!虚拟地址不只是进程中的概念,Linux系统中还有很多地方都会用到虚拟地址这个概念。
磁盘可执行文件采用“起始地址+偏移量”的编址方法得到的地址叫做逻辑地址。其实今天我们说的虚拟地址可以理解为逻辑地址的偏移量,或者是起始地址为零的逻辑地址。程序内部互相调用、互相访问时使用的地址是虚拟地址。
4. 程序从加载到执行
一个可执行程序在磁盘里时,其内部代码和数据的布局就已经按照平坦模式编址好了,这个地址被称为逻辑地址。当程序被加载到内存中运行时,操作系统会为它创建一个独立的虚拟地址空间,并将指令和数据放入真实的物理内存中,这样就有了对应的物理地址。此时,操作系统通过填写页表,建立起程序使用的虚拟地址(根据逻辑地址得到)与内存中物理地址之间的映射关系,从而让程序能够正常运行。
在总结代码执行的整个过程前,我们先来大致了解一下程序执行过程中CPU里主要用到的几个模块:CR3寄存器存储着页表起始位置,MMU是负责将虚拟地址转换为物理地址并实施内存访问,EIP寄存器存储着PC指针——CPU接下来要执行的那一条指令地址,IR寄存器对当前指令译码。
所以页表建立好后,代码执行的过程就是:CPU根据CR3寄存器找到当前进程的页表。执行程序时,CPU通过EIP寄存器获取下一条指令的虚拟地址,该地址由MMU单元利用CR3指向的页表即时转换为物理地址,然后从内存中取出指令。取出的指令被送入IR进行译码,同时EIP自动指向后续指令。CPU执行完当前指令后,又继续根据EIP获取下一条指令,如此循环,直到程序正常结束或遇到终止指令。
5. 重新理解虚拟地址空间
ELF在被编译好之后,会把自己程序的入口地址记录在“Entry point address”字段里:
我们可以查到这个地址对应的segment是_start,所以其实在操作系统中程序的入口是_start,它负责搭建C/C++程序运行的初始化环境,可以看到_start里面调用了__libc_start_main函数间接的调用了main函数,所以我们的代码都是从main函数开始执行的:
ELF Segment是磁盘上程序内存布局的"蓝图",它规定了哪些部分需要被加载以及以何种权限加载,我们可以来看一下ELF Segment的内容:
ELF文件中的每个LOAD类型Segment(需要加载的段)在程序加载时,都会在内核中创建一个对应vm_area_struct结构体,该结构体继承了Segment的虚拟地址范围、访问权限和文件偏移等关键信息,成为该段内存在内核中的运行时管理单元。所有这些由Segment初始化的vm_area_struct共同构成了mm_struct所管理的完整虚拟地址空间,而mm_struct又被包含在进程的顶级描述符task_struct中,从而完成了从静态文件到动态进程的内存组织,操作系统正是通过这些vm_area_struct对象来建立虚拟地址到物理内存的映射并管理整个进程地址空间。
这张图非常形象的表现出来了我们总结的内容。