通过源码文本文件得到 elf 二进制文件,包含预处理,编译成汇编文件,汇编成目标文件,链接这几个过程。
链接有编译时链接,加载时链接(动态链接库),运行时链接(dlopen
的那一套)。
动态库当中定义的全局符号
无论是变量还是函数,都是间接访问,即使是定义在同一个源文件当中。访问本动态库中的全局变量,一样的使用 .got
; 调用本动态库当中的函数,一样的使用 plt
技术。
myprintf.s 文件
.section .data
.type age,@object
.global age
.size age, 4
age:
.int 18
.global prestr
#.hidden prestr
prestr:
.ascii "age ss %d \0"
.section .text
.global addage
.type addage,@function
#.hidden addage
addage:
add $1,%rdi
mov %rdi,%rax
ret
.global myprintf
.type myprintf, @function
myprintf:
push %rbp
mov %rsp,%rbp
push %rdi
and $-16,%rsp #对齐
mov age@GOTPCREL(%rip),%rax
mov (%rax),%rdi
call addage
mov %rax,%rsi
#mov $prestr,%rdi
#lea prestr(%rip),%rdi
mov prestr@GOTPCREL(%rip),%rdi
#call *printf@GOTPCREL(%rip)
call printf@plt
mov -8(%rbp),%rdi
call printf
leave
ret
main.s 文件
.globl main
.section .data
output:
.ascii "hello\n\0"
.section .text
main:
enter $0, $0
lea output(%rip), %rdi
#call myprintf
#lea myprintf(%rip),%rax
#call *%rax
call *myprintf@GOTPCREL(%rip)
#mov age(%rip),%rax
mov age@GOTPCREL(%rip),%rax
mov (%rax),%rax
leave
ret
编译并执行
gcc -fpic myprintf.s -shared -o libmyprintf.so && gcc main.s ./libmyprintf.so -o main.out && ./main.out
符号对外部模块可见,函数通过 plt
调用,变量通过 .got
访问。
使用 objedump 结合 awk 反编译,并查看动态库当中 .data 段和 .got 段的内容。
objdump -d libmyprintf.so | awk '/<myprintf>/,/^$/' ; readelf -x .data libmyprintf.so ; readelf -x .got libmyprintf.so
符号对外部模块不可见(这里只 hidden addage
函数 和 prestr
变量),函数通过模块内绝对地址调用,变量直接访问 .data
段。
未导出符号
那么,无论是下边哪种ip
相对寻址方式,在 so
当中,都是直接访问 .data
段的数据。
age(%rip)
age@GOTPCREL(%rip)
导出符号
那么只能使用 age@GOTPCREL(%rip)
才能通过编译。位置无关代码是汇编语言决定的,gcc -fpic -S
可以编译得到C语言对应的位置无关的汇编,里边可以看到 GOTPCREL
符号的使用。
如果 main.out
当中未引用该符号,那么lib.so
当中的 .got
段访问的是lib.so
当中的 .data
段。
如果 main.out
有引用该符号,又分两种情况。
C语言标准方式
main.out
当中通过 age(%rip)
来引用,此时在程序加载时,main
函数运行之前,运行时会将 lib.so
当中的变量初始值复制到 main.out
的bss
段,这也就是著名的 copy relocation
,并且将lib.so
中的.got
的间接引用,都填充为指向此 .bss
段。main.out
当中自然是直接访问该 .bss
段来获取数据。
额外的汇编方式
main.out
当中通过 age@GOTPCREL(%rip)
来引用,此时在加载时,最终使用的数据为 lib.so
当中的 .data
当中的数据,所有的访问都是通过自己的.got
来间接访问,包括 main.out
当中的代码。
链接器Bsymbolic
选项
如果编译时,有指令链接器选项,
gcc -Wl,-Bsymbolic
那么,访问本模块的函数,直接通过绝对地址而不是 plt
(动态库文件中的绝对地址,加载时修正为运行时的绝对地址);访问本模块的全局变量,也直接通过绝对地址,而不需要 .got
表。
符号决议
符号的加载顺序:主程序 main.out
, 然后是主程序依赖的动态库的符号,最后间接依赖的其他符号。即依赖的符号最后加载,相同的符号出现多次以第一次出现的那个为准。这样导致主程序main.out
当中所有的符号覆盖动态库当中的,先加载的动态库覆盖后加载的动态库中的符号。这也是环境变量 LD_PRELOAD
的原理,先加载 LD_PRELOAD
环境变量当中指定的动态库,然后再加载主程序依赖的其他的动态库。
所以一个动态库如果自身的符号面临被覆盖的问题,有两种方法。
将符号设置为 hidden,外部不可访问
static
将可见性限制在源码文件,这里是将可见性限制在本动态库。
#编译选项
-fvisibility
//符号属性
__attribute__((visibility("hidden")))
#汇编指令
.hidden
#链接器选项,链接器版本脚本
-Wl,--version-script,version.map
version.map 的内容如下
ver{
global:
printage;
local:
*;
};
无论是通过编译选项,还是设置代码中设置符号的属性,最终都是生成 .hidden
指令(编译选项都可以使用来直接生成汇编)。链接器版本脚本当然对汇编没有影响,作用的顺序由上到下,后边的规则会覆盖前边的规则。
使用链接器 -Wl,-Bsymbolic
选项强制使用本模块符号
总结
ip
相对寻址。一般使用一个立即数,比如 $100(%rip) 表示偏移当前 ip
指针 100 进行寻址;如果使用的是一个符号, name(%rip)
表示访问符号name
,让汇编器自行计算偏移。name@GOTPCREL(%rip)
则表示如果 name
是本模块的一个对外可见符号,或者是一个外部符号,那么强制使用 .got
来访问符号,这里name
可以是变量,也可以是函数。在动态库当中,访问自己的对外可见变量,只能采用name@GOTPCREL(%rip)
来生成位置无关的代码。如果name
是本模块的对外不可见符号,当然所有的对外不可见的符号都是直接访问。
对外可见,访问自己的符号都是间接访问,所以存在被覆盖的问题。这由符号的加载顺序决定。main.out
当中的符号,main.out
依赖的库当中的符号,库按readelf -d
的顺序。所以main.out
当中存在的符号优先,其他则是先加载的库当中的符号。a
依赖 b
,如果b
之前没有被加载,那么 a
当中的符号优先于 b
.
main.out
访问任意符号,都可通过 name(%rip)
或者 name@GOTPCREL(%rip)
,只是如果 name
是一个库当中的符号,name@GOTPCREL(%rip)
强制使用 .got
来访问。
当然,动态链接库又要导出符号,又想直接访问本模块的符号,那么使用链接器选项 -Wl,-Bsymbolic
即可。