十分复杂,入坑需谨慎。。。。。
demo
本次学习用的demo
简述一下和静态链接的区别:静态链接在链接过程中就已经完成了重定位工作,因此可执行文件巨大,动态链接就是为了解决这一问题,找到了一种模块化的实现方法,就是可执行文件并不把所有库内段合并过来,而是在runtime的时候完成symbols的relocate操作,并且不同的进程也可以共享动态链接库也就是.so。
gcc -fPIC -shared -o Lib.so lib.c gcc -o program2 program2.c ./Lib.so gcc -o program1 program1.c ./Lib.so #include "lib.h" int main () { foobar(1 ); return 0 ; } #include "lib.h" int main () { foobar(2 ); return 0 ; } #include <stdio.h> void foobar (int i) { printf ("Printing from Lib.so: %d\n" , i); } #ifndef LIB_H #define LIB_H void foobar (int i) ;#endif
PIC
为了了解可执行文件(动态链接库)是如何在runtime进行relocate的需要理解什么是position-independent code,也就是地址无关代码。静态链接库是没有这个概念的,其加载地址在链接中已经确定了,所谓PIC就是解决动态共享库在加载时的模块地址冲突问题的,使其可以加载到任何位置,也就是说运行时地址自动分配,但是也会导致问题,同一个模块的代码装载到不同的位置,指令不能发生变化,否则无法使用其他共享模块。
需要注意一点的是,动态链接库实现代码重用,但是每一个进程是有一份数据部分的副本的。
如何实现
基本思想就是把指令中需要修改的部分提取出来跟数据放一块
所谓需要修改的无非就是外部模块的地址引用
地址引用可以分为以下四种:
因为每一个模块都有副本,因此不太需要关于共享这个问题,直接重定位即可。
关于PLT 所谓延迟绑定,就是当函数第一次使用时才进行重定位操作(符号绑定),也就是说PIC代码不直接指向got.plt表,而是指向plt表,在第一次调用时,先进行dl-resolve函数操作,之后进行符号绑定。但是貌似好多已经优化掉了,这一步感觉好多elf已经不存在了,就直接跳到got表,程序执行的时候就进行装载。。。。
pwndbg> disassemble 0x555555555050 ,+30 Dump of assembler code from 0x555555555050 to 0x55555555506e : 0x0000555555555050 <printf @plt+0 >: endbr64 0x0000555555555054 <printf @plt+4 >: bnd jmp QWORD PTR [rip+0x2f75 ] # 0x555555557fd0 <printf @got.plt> 0x000055555555505b <printf @plt+11 >: nop DWORD PTR [rax+rax*1 +0x0 ] 0x0000555555555060 <_start+0 >: endbr64 0x0000555555555064 <_start+4 >: xor ebp,ebp 0x0000555555555066 <_start+6 >: mov r9,rdx 0x0000555555555069 <_start+9 >: pop rsi 0x000055555555506a <_start+10 >: mov rdx,rsp 0x000055555555506d <_start+13 >: and rsp,0xfffffffffffffff0 End of assembler dump. pwndbg> x/20 gx 0x555555557fd0 0x555555557fd0 <printf @got.plt>: 0x00007ffff7de96f0 0x00007ffff7db2dc0 0x555555557fe0 : 0x0000000000000000 0x0000000000000000 0x555555557ff0 : 0x0000000000000000 0x00007ffff7dce9a0 0x555555558000 : 0x0000000000000000 0x0000555555558008 0x555555558010 <completed.0 >: 0x0000000000000000 0x0000000000000000 0x555555558020 : 0x0000000000000000 0x0000000000000000 0x555555558030 : 0x0000000000000000 0x0000000000000000 0x555555558040 : 0x0000000000000000 0x0000000000000000 0x555555558050 : 0x0000000000000000 0x0000000000000000 0x555555558060 : 0x0000000000000000 0x0000000000000000
ELF文件相关
在了解了动态链接的基本思想之后,再来从ELF文件的角度细节理解一下
总体概览是:1.读取program_header,os来分配页表映射关系;(和静态链接一样)
2.同样加载动态链接器,os将控制权交给动态链接器
3.动态链接器经过一些列操作,如bootstrap等等,对可执行文件进行链接操作,然后将控制权交给可执行文件。
4.程序执行。
.interp节
描述了动态链接器的存储路径,一个字符数组
root@L:/home/l/c++/dynamiclink# readelf -x 1 hh Hex dump of section '.interp' : 0x00000318 2f 6c6962 36342f 6c 642 d6c69 6e75782 d /lib64/ld-linux- 0x00000328 7838362 d 36342e73 6f 2e3200 x86-64. so.2 .
.dynamic节
和符号表类似,也是一个结构体数组,这大概就是描述一个可执行文件重定位信息的一个集合体。
类似于重定位头,跟节表头差不多
其中有动态符号表的文件偏移。。。。
typedef int64_t Elf64_Sxword;typedef uint64_t Elf64_Xword;typedef uint64_t Elf64_Addr;typedef struct { Elf64_Sxword d_tag; union { Elf64_Xword d_val; Elf64_Addr d_ptr; } d_un; } Elf64_Dyn;
.dynsym节
结构基本symtab一样,只是把需要动态绑定的拿出来,组成的一张表
root@L:/home/l/c++/dynamiclink# readelf -s hh Symbol table '.dynsym' contains 7 entries: Num: Value Size Type Bind Vis Ndx Name 0 : 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1 : 0000000000000000 0 FUNC GLOBAL DEFAULT UND _[...]@GLIBC_2.34 (2 ) 2 : 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterT[...] 3 : 0000000000000000 0 FUNC GLOBAL DEFAULT UND [...]@GLIBC_2.2 .5 (3 ) 4 : 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 5 : 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMC[...] 6 : 0000000000000000 0 FUNC WEAK DEFAULT UND [...]@GLIBC_2.2 .5 (3 )
关于重定位节
好像是一个搬运工的身份,其连接了动态符号表以及got表,通过检索动态符号表来将重定位后的值填入到got或got.plt
rela.dyn和rela.dyn的区分个人理解上比较模糊,貌似是通过重定位的类型来区分的,但是不能确定。
关于重定位类型,常见的有:R_X86_64_RELATIVE,R_X86_64_GLOB_DAT,R_X86_64_JUMP_SLO,R_X86_64_RELATIVE代表基址重置,其余两个貌似可以猜出来要干啥,说实话这两个名字的处理方式是一样的,知识跳的位置不一样罢了,其实基址重置也好理解,就是到最后目标位置的值得是装载地址加上addend。
root@L:/home/l/c++/dynamiclink# readelf -r pic.so Relocation section '.rela.dyn' at offset 0x458 contains 8 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000003e48 000000000008 R_X86_64_RELATIVE 1130 000000003e50 000000000008 R_X86_64_RELATIVE 10f0 000000004028 000000000008 R_X86_64_RELATIVE 4028 000000003fd8 000200000006 R_X86_64_GLOB_DAT 0000000000000000 b + 0 000000003fe0 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize + 0 000000003fe8 000400000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCl[...] + 0 000000003ff0 000500000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTM[...] + 0 000000003ff8 000600000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0 Relocation section '.rela.plt' at offset 0x518 contains 2 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000004018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 ext + 0 000000004020 000700000007 R_X86_64_JUMP_SLO 0000000000001139 bar + 0
.rela.dyn and .rela.plt
结构都是一样的,都是下面这种类型。
typedef struct { Elf64_Addr r_offset; Elf64_Xword r_info; Elf64_Sxword r_addend; } Elf64_Rela;
runtime辅助信息
上面讲的都是比较静态的,也就是还未分配页表等映射信息,当os加载程序之后,会初始化一些进程的堆栈信息,这样os转交给链接器的时候,链接器才能知道如何重定位,也就是说加载器负责将模块中的关键信息加载到进程堆栈,之后会调用解释器来进行重定位操作。
typedef struct { uint32_t a_type; union { uint32_t a_val; } a_un; } Elf32_auxv_t; typedef struct { uint64_t a_type; union { uint64_t a_val; } a_un; } Elf64_auxv_t;
动态链接步骤
数据结构了解之后来了解一下algorithm,没有代码,比较高视角(粗糙)。
第一步:链接器的自举 链接器也是可执行文件,但是链接器的符号表的装载,got表的重定位也是需要链接器来完成,但是完成自身这一过程不能使用全局变量以及函数,因此叫做bootstrap,类似于自己生自己,中文叫自举。
第二步:依赖模块的装载 链接器通过阅读可执行文件的.dynamic等字段,将所有所需的模块分配虚拟内存,然后符号表会被合并为全局符号表,具体算法不是很清楚,但是理论上应该是可行的。
Dynamic section at offset 0x2db8 contains 28 entries: Tag Type Name/Value 0x0000000000000001 (NEEDED) Shared library: [./Lib.so] 0x0000000000000001 (NEEDED) Shared library: [libc.so.6 ] 0x000000000000000c (INIT) 0x1000 0x000000000000000d (FINI) 0x1164 0x0000000000000019 (INIT_ARRAY) 0x3da8 0x000000000000001b (INIT_ARRAYSZ) 8 (bytes) 0x000000000000001a (FINI_ARRAY) 0x3db0 0x000000000000001c (FINI_ARRAYSZ) 8 (bytes) 0x000000006ffffef5 (GNU_HASH) 0x3b0 0x0000000000000005 (STRTAB) 0x480 0x0000000000000006 (SYMTAB) 0x3d8 0x000000000000000a (STRSZ) 152 (bytes) 0x000000000000000b (SYMENT) 24 (bytes) 0x0000000000000015 (DEBUG) 0x0 0x0000000000000003 (PLTGOT) 0x3fb8 0x0000000000000002 (PLTRELSZ) 24 (bytes) 0x0000000000000014 (PLTREL) RELA 0x0000000000000017 (JMPREL) 0x618 0x0000000000000007 (RELA) 0x558 0x0000000000000008 (RELASZ) 192 (bytes) 0x0000000000000009 (RELAENT) 24 (bytes) 0x000000000000001e (FLAGS) BIND_NOW 0x000000006ffffffb (FLAGS_1) Flags: NOW PIE 0x000000006ffffffe (VERNEED) 0x528 0x000000006fffffff (VERNEEDNUM) 1 0x000000006ffffff0 (VERSYM) 0x518 0x000000006ffffff9 (RELACOUNT) 3 0x0000000000000000 (NULL ) 0x0
这一部分还涉及到global symbol interpose的问题,就是符号冲突的话,那么第二个加载的符号会被忽略,这也解答了之前非常奇怪的问题,就是为什么模块内函数也要重定位到got表,这就是为了防止外部符号覆盖。
第三步:重定位和初始化 就是通过重定位段来进行got和got.plt表的装载。重定位之后,若库中有.ini段,那么会执行其中代码,来进行全局对象的构造和初始化,退出时也会执行.fini中的代码,尽心全局对象的销毁操作。在这之后就可以把控制权交给程序入口了。