计算机系统基础II之:链接
Weiquan Huang2-14 linking
链接这个概念其实我们都有所耳闻,一写代码太长了,需要写到不同的源文件,这个时候就需要link来将其连接起来,才能发挥头文件#include
的作用
关于文件从代码文件到可执行文件,分为诸多步骤,gcc
指令的不同也会生成不同阶段的文件,比如-E -S -Og
等;对于一个尚未链接的模块,其机器码会存在一些空位等待连接后去填写
同时,在汇编代码中,会对一些symbol
进行标记,比如这边会标记bufp0
为.globl
全局标记等;同时,在汇编代码中,定义一个变量和引用一个变量不太一样,这里引用的变量buf
实际上并没有在单独的swap.c
里面被定义——直接使用会出错
对于一个可执行程序在内存中保存的形式:程序分为很多个区域,包括代码区域(可读可执行)、只读数据区域、数据区域(可读写);乃至于局部变量和全局变量也是在不同的区域(局部变量更有可能直接在寄存器里面)
链接一下看看!这里数据和代码(指令)区隔得比较远,可以看到%rip
需要再加上较多的偏移量才可以到达数据区;这里其实相当于将两个文件的指令整合在一起,将两个文件的数据整合在一起,数据和指令倒还是分开的
可执行可链接模式文件(ELF)
ELF Header:标识一些当前文件的基本情况,0-3标识为ELF文件,被机器识别为可执行文件;4标识位数;5标识大端小端法等
其他一些部位,比较重要一些的:
2-15 symbol
在ELF文件中,有很多的symbol,用来标识一些变量、函数的情况(局部变量一般不会被标识,因为其一般是以寄存器的形式存在,没有内存地址这一概念)
比如像定义、引用、本地静态变量就可以是symbol
在ELF文件中有symbol tables,用于记录这些symbol
比如像一些不是在本模块被定义的东西就会被设置为Type: NOTYPE, Ndx: UND
,像Ndx: COMMON
这种则表示未初始化的对象
重名全局Symbol
强:函数/已初始化变量;弱:未初始化变量
强强冲突会报错;强弱冲突取强者地址;弱弱冲突优先取本地的变量,但并未做死规定
与静态library做链接
可以将多个.o
文件打包到一个.a
文件中,这样就可以在查找所有库和程序员选择一个库来链接之间找到一个平衡
打包的命令可以是这样的:
在链接.a
.o
文件的时候,会统计当前所有的E U D
,然后将U
按照.a
文件的信息填入;链接就是要消除这些undefined的东西,这个时候顺序是十分关键的,因为前面出现的U
会被后面的文件所消除,所以一般是.c
文件写前面,.a
文件写后面;如果有互相依赖的则需要重复列出
2-16 relocation
重定向的细节是如何的呢?
以这个为例:此时缺一个相对于%rip
的函数地址,所以在链接之前用4 byte占位;这里0a
是开始占位的地址;swap
是获取最终值的变量、函数名;64_PC32
表示当前采用的是32位的地址;-4
为增减量,是一个比较tricky的地方
比如来分析一下这个:此时00的开头的地址为refaddr = 0x4004e0
,swap
函数的地址为0x4004eb
,而后续用于call
的%rip
应该是refaddr + 4 = 0x4004e4
,所以这里面应该填入的内容是addr(swap) - %rip= 0x4004eb - 0x4004e4 = addr(swap) - refaddr + addend = 0x7
这个看着简单点:
比较高级的例子是这个:
ELF format是啥?感觉不是很重要,了解下应该就行
从可执行文件(已链接)中把代码、数据段加载到内存中,其并不是在某个段的开始copy的
动态链接
如果每次都是将库函数打包成.a
文件然后与relocatable文件进行静态链接,那么如果公有库函数做了修改,那么之前生成的可执行文件都需要重新编译、链接才行,不利于开发;因此我们需要一种在静态链接时只连接局部的信息,在执行的时候再去链接
那么这些共有类函数会在哪里呢?ld-linux.so & execve()
会将动态库函数的信息复制到可执行文件在运行时的状态;Memory mapped region for shared libraries
也可以通过调用一些库来进行运行时链接
调用dl函数将库内的函数地址链接到函数指针;这里可以拓展一下,在平时写代码的时候,IDE会检查cc文件的函数调用和函数定义是否具有相同类型的参数;但是如果是汇编码的状态的话,他就只是单纯的mov ...,%rdi
,没有做检查的功能;而这里因为是动态链接,在cc文件无法检查出参数的问题,因此如果实际调用的时候参数写的不对,那么可能不会报错,但是就是会把有问题的字段当成调用函数预期的读取方式进行读取,那就会出一些奇奇怪怪的问题
PIC数据引用
数据、函数地址会存放在GOT里面,根据其与PC的相对地址,对GOT表内的数据字段做调用
解析一下如何管理库函数的函数地址:如果是每个callq
都去填一次目标函数的代码,那有点浪费空间了;这里使用的方案,是每个ref
都有一个在PLT表对应入口,这个表可以帮助程序,在第一次调用库函数的时候,将库函数的地址写到GOT表里面,再次调用的时候就可以直接去向这个GOT表内存储的地址了。具体细节如下:
1,第一次调用该函数callq
,实际上跳到PLT中的0x4005c0
这个位置,然后执行jmpq
操作,如果传入的地址并不是实际函数地址,而是当前指令的下一个指令,说明没有调用过,开启首次调用填充地址的操作:将id = 1, reloc entries
压栈,调用dynamic linker
,通过一系列操作将函数地址写入到对应的表数据块GOT[4]
内
2,这样在下一步就可以直接(也不是直接,比直接多了一步)跳到目标函数了