深入理解计算机系统
编译
gcc -v main.c swap.c -o b&& ./b
编译器
main.c -> /tmp/ccVSPuud.s
汇编器
ccVSPuud.s -> cc8urR1R.o
链接器
cc8urR1R.o (main.c)
ccxPHKDw.o (swap.c)
-> b
Collect2在编译器安装目录中,当作ld。
- 符号解析:文件间变量名对应
- 重定位:把链接文件的相对地址变成绝对地址
- 可重定位目标文件:ccVSPuud.s cc8urR1R.o
- 可执行目标文件:b
ELF
Acronyms relevant to Executable and Linkable Format (ELF)
readelf main.o -h
readelf main.o -S
ELF头0x34字节,故[1]节头偏移0x34
[9]节尾偏移量 0x22f,故table从0x230开始
.symtab
readelf main.o -s
main函数在[1].text,data在[3].data偏移0,a在[3]偏移8
main.c不被重定位,swap引用外部
符号解析
链接器
- Rule 1: Multiple strong symbols are not allowed.
- Rule 2: Given a strong symbol and multiple weak symbols, choose the strong symbol.
- Rule 3: Given multiple weak symbols, choose any of the weak symbols.
静态库
- 把addvec.o和multvec.o打包成libvector.a,ibvector.a称为静态库
ar rcs libvector.a addvec.o multvec.o - 链接静态库要放在尾部
gcc -static main.o ./libvector.a - 链接器会从左到右扫描,先main,再到libvector,维持E D U集合
E: 需要用到的模块集
D: 已经定义的符号集
U: 未定义的符号集
一直扫描 libvector至EUD不再变化 - 结束时U非空就报错,否则链接E
- 库内可以相互依赖,因为一直扫描,库间依赖要重复库
gcc -static main.o x.a y.a x.a - p依赖x,x依赖y,y依赖x,x依赖a
gcc p.o libx.a liby.a libx.a
尾部无需再链p,因为p是个块,而xy是集合。块是全入,集合是按需入。
EDU是根据新来的 块 来更新,即使y依赖a,但a已定义的符号集早已全部入D
而x仅仅把被p依赖的块的符号集入D,并不能确保y依赖的那个块有没有入
重定位
objdump -D main.o
objdump -D b
节和符号
同类节合并,比如main.o和swap.o的[1].text节。 合并后就有新的符号表,也定下[1].text存储地址,根据新符号表的main的偏移量就能确定其存储地址。
同样.data节也会合并再确定存储地址
节中符号
main.o地址0x11处
11: e8 fc ff ff ff call 12 <main+0x12>
这条指令执行后PC会指向0x16,加上0xfffffffc就是0x12,也就是CPU会去执行fc ff ff ff。
fc ff ff ff显然这不对,因为main.o没有链接swap.o,这里fc ff ff ff仅是汇编器临时设的,方便日后链接器操作。
实际上,main中的swap在.rel.text而并.text,objdump为了方便才拼在一起。
swap就是节中符号
重定位条目
readelf main.o -R 2
“.rel.text”节的十六进制输出: 0x00000000 12000000 020b0000 17000000 01030000 …
从Section Header Table中可以看出“.rel.text”的size为16
0x00000000是节内偏移,也就是节内地址。
“.rel.text”节实际为0x12000000 020b0000 17000000 01030000
main+0x12中的0x12就是第一个字段
链接器根据swap存储地址就能算出call后面具体要跳多远。
但为什么是-4呢?很容易想到这里地址长度是4字节,32位,如果是64位,应该是-8。
取指call后,pc就变了,指明-4有利于链接器判断PC+4还是+8
swap.c中
int * bufp0 = &data[0];
从swap.o的.symbol表中看出bufp0在.data中,偏移0。
查看下swap.o的.data节: readelf swap.o -R 3
“.data”节的十六进制输出:
0x00000000 00000000 ….
bufp0的值就是00000000
如果int * bufp0 = &data[1];
“.data”节的十六进制输出:
0x00000000 40000000 ….
链接器合并.data节后找到data地址,根据swap.o的.data的偏移给bufp0值
前面swap的重定位是.text中,是相对定位
可执行目标文件
readelf b -l
LOAD第二段size为0x0011c,实际内存大小0x00124,多出来的是给.bss留的。
实际上.bss为空,其中的变量仅在.symtab中保留着。
动态链接
共享库
gcc -shared -fPIC -o libvector.so addvec.c multvec.c -m32
加载时链接
gcc -o p main.c ./libvector.so -m32
加载时(也就是刚执行P时)遇到.interp节会去调用动态链接器,完成以下操作:
- Relocating the text and data of libc.so into some memory segment.
- Relocating the text and data of libvector.so into another memory segment.
- Relocating any references in p to symbols defined by libc.so and libvector.so.
运行时链接
gcc -rdynamic -O2 -o p3 dll.c -ldl -m32
全局变量偏移表(Global Offset Table, GOT),位于.data前的.got.plt,纯数据
过程链接表(Procedure Linkage Table, PLT),位于.text前的.plt,实际为代码块
运行(到某个地方)时,调用某个外部函数,这些函数在plt中有记录。
第一次会先调用_dl_runtime_resolve进行重定位,把重定位的地址写到got中。
第二次运行到此时就直接用got中的记录。
延迟绑定
动态链接器根据push的两个标识确定实际位置,改got并执行函数。