PowerPC上ELF可执行文件的符号解析(一)
一. 前言
符号解析是Linux系统导入二进制可执行文件的重要过程,它完成的工作包括将一个符号定位到实际的内存地址,并且要保证可以正确引用这些符号。按解析对象的不同它可以分为变量符号解析和函数符号解析;按解析方式的不同可以分为静态解析和动态解析。
对于静态解析的符号,它们的地址在文件生成时就由link editor(在Linux下通常是ld)已经确定下来了;对于动态解析的符号,他们的地址在程序运行时才由dynamic linker(动态链接器,32位Linux平台下通常是/lib/ld.so.1)确定下来。我们可以这么认为,如果一个符号在共享库中定义,那么当其他可执行文件或共享库引用这个符号时,就需要对它作动态解析。
变量符号的动态解析过程比较简单,系统在载入程序过程中将变量symbol地址存入到GOT(Global Offset Table)中,引用变量symbol时首先计算出GOT表的实际地址,然后以它作为基址加上(变量symbol在GOT表中的偏移量)就可以从GOT表中取得该symbol的实际地址。下面以SUSE Linux Enterprise Server 8.1 for IBM pSeries为例,主要讲述和演示32位PowerPC Linux下函数符号的动态解析过程。
二. 概念
在讲述解析过程之前,先介绍一下在解析过程中要用到的基本概念。 1. ELF(Executable and Linkable Format)文件
ELF是Linux缺省采用的可执行文件(包括共享库,object文件)的格式,具体规范参见参考文献[1]、[2]。这里需要提一下的是section这个概念:section是ELF文件中一段互相联系信息,它可以是一段数据,也可以是一段代码。比如可执行代码信息就放在.text section中,被用户初始化的变量会放在.data section中,没有被用户初始化的变量会放在.bss
section(bss是below stack segment的缩写)中。还有其他的一些 section: .debug、 .hash、 .symtab、 .dynsym、 .plt、 .rel.plt 等等。 .dynsym(动态符号表)、 .plt(过程链接表)和.rel.plt(重定位表)和我们的话题有关。
2. 符号表(symbol table)
符号表记录了程序中符号的定义信息和引用信息,它是一个结构数组,数组中的每个元素对应一个符号所有的信息。我们可以在glibc的源代码glibc-2.2/elf/elf.h中看到这个结构的c语言定义:
typedef struct{
Elf32_Word st_name; /* 符号名 (.string表的索引) */ Elf32_Addr st_value; /* 符号值(Symbol value) */ Elf32_Word st_size; unsigned char st_info; unsigned char st_other; Elf32_Section st_shndx; } Elf32_Sym;
对于可执行文件和共享库而言,符号值记录了该符号的内存地址。可执行文件知道运行时刻
他们的地址,所以他们内部的引用符号在编译时候就已经确定了;共享库symbol的符号值 (symbol value)就要等到共享库被载入到内存中才确定下来。
我们可以用\-s 文件名\来查看elf文件的symbol值,一般会有两个symbol表:.symtab(包含静态符号和动态符号)和.dynsym(仅包含动态符号)表。下面是我自己机器上的一个输出:
cj@bluesky:~/program/GOT> readelf -s test32 Symbol table '.dynsym' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 10010894 488 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 ……………………
Symbol table '.symtab' contains 228 entries:
Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 10000114 0 SECTION LOCAL DEFAULT 1 ……………………
3.过程链接表(Procedure Linkage Table,PLT)
静态解析函数符号很简单,因为引用是在内部进行的,只要用下面的命令就可实现: bl resolved_symbol //resolved_symbol是被引用函数的入口点
动态解析符号则不然,由于link editor不能在编译时就确定被引用函数的入口点,所以它只能将控制权交给第三方,再由这个第三方来完成确定被引用函数入口点的任务,这个第三方
就是过程链接表。
过程链接表的格式如下:
?PLT表开头的18个字(72字节)为dynamic linker保留 ?如果可执行文件或共享库需要N个.PLTi入口,那么紧跟着这18个字,link editor就会保留3*N个字(12*N字节),开头的2*N个字就是所有的.PLT入口,对于第i个引用符号,它的.PLT
入口是(72+(i-1)*8)(1<=i<=N),剩下的N个字留给dynamic linker使用。
过程链接表虽然存在于文件当中,但它的初始化是由dynamic linker在装载可执行文件和共享库时完成的。下面是一个可能的被初始化的PLT表的内容: .PLT: .PLTresolve:
addis r12,r0,dynamic_linker@ha
addi r12,r12,dynamic_linker@l
mtctr r12 //到此为此,r12和ctr中是dynamic linker的地址 addis r12,r0,symtab_addr@ha addi r12,r12,symtab_addr@l bctr //此时r12中是共享库symbol表的地址 .PLTcall:
addis r11,r11,.PLTtable@ha lwz r11,.PLTtable@l(r11) mtctr r11 bctr
8 个nop指令 .PLT1:
addi r11,r0,4*0 b .PLTresolve . . . .PLTi:
addi r11,r0,4*(i-1) b .PLTresolve . . . .PLTN:
addi r11,r0,4*(N-1) b .PLTresolve .PLTtable:
4. Relocation表
Relocation表总是和PLT表紧紧联系在一起,也就是说,PLT表有多少个入口,Relocation表就有多少个入口,他们是一对一的关系。下面是Relocation结构的c语言定义:
typedef struct {
Elf32_Addr r_offset; //.PLTi的地址 Elf32_Word r_info; //包含symbol index信息 } Elf32_Rel;
在上述过程链接表的例子中r11是Relocation表和PLT表的index,dynamic linker需要r11来找到对应于这次解析的Relocation表的元素,然后得到r_info,由于r_info中包含了symbol
index信息,我们就可确定是寻找哪个symbol的地址;得到symbol的地址后还需要r11来确定将这个地址正确重定位到那个PLT入口(r_offset保存了.PLTi的地址)。