elf 符号处理详情

通过源码文本文件得到 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.outbss段,这也就是著名的 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 即可。

猜你喜欢

转载自blog.csdn.net/zhouguoqionghai/article/details/121488840