以下主要是参照《深入理解计算机系统》的理解。
以下是在centos 6.3 x86_64上测试的
主要用到的命令有 readelf、objdump,它们是GNU提供的,我这系统自带的有,所以没安装
一个可执行目标文件,从原代码,要经历预处理器、编译器、汇编器和加载器,才会加载到内存中执行。而目标文件,分为可重定位文件和可执行文件,目标中分成不同的节。
节
|
ELF头
|
描述字的大小、生成该文件的系统的字节顺序、帮助链接解析和解释目标文件的信息(ELF头的大小、目标文件类型--可重定位/可执行/共享/机器类型/节头部表的文件偏移/节头部表中表目大小和数量)
|
.text
|
编译完后的机器码
|
.rodata
|
只读数据,如printf中的格式串和switch中的跳转表
|
.data
|
已初始化的全局C变量
|
.bss
|
未初始化的全局C变量
|
.symtab
|
符号表,存放在本文件中被定义和引用的函数和全局变量(此全局变量,包含自己定义全局变量和函数,即使带有static也包含,是在符号后加了个数字,另外还包含本文件中引用的其它文件的全局变量和函数,即使这个函数没有在外面用extern引用--变量不引用则编译报错),不包含局部变量(程序运行时,在栈中生成)
|
.rel.text
|
可重定位的代码,一般是调用的外部函数或者引用全局的变量的指令,引用本地的不需要改
|
.rel.data
|
本模块定义或引用的全局变量
|
.debug
|
调试符号表
|
.line
|
原始C源程序中的等号和.text节中机器指令间的映射
|
.strtab
|
字符串表。包含.symtab和.debug节中的符号表,以及节头部中的节名字
|
描述目标文件
|
节头部表
|
由相同大小的表目组成,每个表目描述上面的一个节
|
1.编写一个test.c
-
#include
-
int x = 3 ;
-
int y ;
-
extern int e_g_z;
-
static int s_g_x = 3 ;
-
static int s_g_y ;
-
int g_f(); //如果些函数,只在这声明,而没在main中被用到,那么在ELF文件的符号表(.symtab)中没有它
-
int n_s_f(){ return 0 ;} //no static
-
static int s_f(){ return 0 ; } // static function
-
-
int main() {
-
int l_x = 4 ;
-
int l_y = 5 ;
-
static int s_l_x = 6 ;
-
static int s_l_y = 7 ;
-
g_f();
-
f() ;
-
e_g_z = 4;
-
printf("%d %d %d %d %d %d %d %d\n",x,y,s_g_x,s_g_y,l_x,l_y,s_l_x,s_l_y);
-
return 0 ;
-
}
复制代码
2.编译生成test.o目标文件
gcc -g -c test.c
3.查看test.o的符号表(.symtab节的内容)
readelf -s test.o //可以用readelf -a test.o看到所有的节
注:
符号表中包含了各个节、源文件名字、非静态本全局变量、静态全局变量、非静态全局函数、静态全局函数、函数代码段内部引用的其它模块函数、被代码段引用的
全局变量、静态局部变量(在.data或.bss中定义空间)。
只是无局部非静态变量,它在栈中管理,链接器对它不管。它的value是地址,但由于是在链接的时候才分配,所以此处全为0,OBJECT表示变量。用
readelf -S test.o,还可以看到有一栏Addr也全为0,它表示还没有给变量和指令分配运行时地址(在链接时分配)。
.symtab这个节是一个包含表目的数组,表目结构如下:
-
typedef struct {
-
int name ; /* 字符串节中,字节的偏移,以null结尾 */
-
int value; /* section offset or VM address,可重定位模块,此符号在它的定义节中的偏移,可执行模块,则为一个绝对运行地址 */
-
int size; /* 目标字节大小 */
-
char type:4, /* 表示此符号是数据、函数、节以及原文件路径名 */
-
binding:4; /*本地,还是全局 */
-
char reserved; /* unused */
-
char section; /* 节索引,表示此符号与哪个节相关联,
复制代码
4.符号解析
链接器解析符号引用的是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号联系起来,对引用和定义在同一模块中的符号,则解析更为简单。
对于多处定义的同名全局符号(即使类型不同),依据强弱来决定到底用哪个(函数和已初始化的全局变量和强符号,未初始化的全局变量为弱符号)。
在Unix链接时,
1、不允许多个强符号出现
2、如果有一个强符号,多个弱符号,则选强符号。
3、如果是多个弱符号,那么从这些弱符号中任意选一个。
如:a.c b.c
int x = 1111; double x ;
int y = 2222; void f() { x = -1 ;}
f();
printf("x = 0x%x \ny = 0x%x\n",x,y);
则输出:x = 0x0,y = 0xbff00000,这是由于int与double据占的字节数不同,导致赋值x=-1时,在x和y的存储区,都被赋了值。
链接静态库:定义三个集合,一个可重定位目标文件集合E,它们最后形成一个可执行文件;未解析的符号(引用了但没定义)集合U,前面输入文件中已定义的符号集合D,最开始三个集合都为空。
输入一个文件,如果为目标文件,则同时更新这三个集合,如果是库文件,则看是否有定义了U集合中的符号,如果有则同时更新这三个集合,否则丢弃它,所有过程之后,如果U是空的,则链接成功,否则失败(与输入文件的顺序有关)。
重定位:将输入目标文件中相同的节合并,如所有的.data节,合并成一个大的、可执行文件的.data节,所有节合并完后,给它们一人执行地址,然后修改符号引用,使其能找到正确的符号定义位置。
查看test.o的重定位表:readelf -r test.o
对比符号表中的全局符号
除了n_s_f和main以外,其它全局符号都是可重定位的。
函数:所有引用的其它模块的函数是都是可重定位的;本模块中实现的函数是不需要重定位的,无论带不带static
变量:本模块定义全局的(不带static)和引用其它模块的,都要重定位,本局部的在运行时的栈中,带static的全局,也只在本模块中用,不需要重定位,但其它模块可以用指针引用到。
重定位节中,每个表目,可以是以下两种格式之一:
每个表目表示一个需要重定位的位置,比如一引用的外部变量 Z ,在代码段用到了三次,则在重定位节中,有至少(因为编译成汇编的时候,有可能是多条指令)三个关于 Z 的重定位表目。
-
typedef struct
-
{
-
Elf32_Addr r_offset; /* Address */
-
Elf32_Word r_info; /* Relocation type and symbol index */
-
} Elf32_Rel;
-
typedef struct
-
{
-
Elf32_Addr r_offset; /* Address */
-
Elf32_Word r_info; /* Relocation type and symbol index */
-
Elf32_Sword r_addend; /* Addend */
-
} Elf32_Rela;
复制代码
这
两种格式唯一的不同是成员r_addend。这个成员一般是个常量,用来辅助计算修订值。如果使用了第一种格式,那么r_addend将被填充在引用外部
符号的地址处,也就是前面所说的留“空”处。具体的体系结构可以选择适合自己的一种格式,或者两种格式都使用,只不过在不同的上下文中使用更合适的格式。
IA32主要使用了前者,但是也在个别的情况下了使用了一点后者。
r_offset为需要重定位的符号在目标文件中的偏移。需要注意的是,对于目标文件与可执行文件或者动态库,这个值是不同的。对于目标文件,r_offset是相对于段的,是段内偏移;而对于可执行文件或者动态库,r_offset是虚拟地址。
r_info中包含重定位类型和此处引用的外部符号在符号表中的索引。根据符号在符号表中的索引,链接器就可以从符号表中解析出符号的地址。因为指令中包含多种不同的寻址方式,并且还要针对不同的情况,所以有多种不同的重定位类型。不同的重定位类型,重定位的方法也不同。
------------------出自:
引自: 链接时,在第一阶段完成后,目标文件已经合并完成,并且已经为符号分配了运行时地址,链接器将进行符号重定位。
模块hello.o中有两处需要重定位,一处是偏移0xb处的变量foo2,另外一处是偏移0x1b处的函数foo2_func。汇编器已经将这两处需要重定位的符号记录在了重定位表中。
root@baisheng:~/demo# readelf -r hello.o
Relocation section '.rel.text' at offset 0x3c8 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
0000000b 00000901 R_386_32 00000000 foo2
0000001b 00000a02 R_386_PC32 00000000 foo2_func
...
符号foo2的重定位类型是R_386_32,ELF标准规定的计算修订值的公式是:
S + A
其中,S表示符号的运行时地址,A就是汇编器填充在引用外部符号处的Addend。
符号foo2_func的重定位类型是R_386_PC32,ELF标准规定的计算修订值的公式是:
S + A - P
其中S、A的意义与前面完全相同,P为修订处的运行时地址或者偏移。对于目标文件,P为修订处在段内的偏移。对于可执行文件和动态库,P为修订处的运行时地址。
首先我们先来确定S。运行时地址在链接时才分配,因此,变量foo2和函数foo2_func的运行时地址在链接后的可执行文件hello的符号表中:
root@baisheng:~/demo# readelf -s hello | grep foo2
38: 00000000 0 FILE LOCAL DEFAULT ABS foo2.c
53: 0804a020 4 OBJECT GLOBAL DEFAULT 24 foo2
68: 08048414 16 FUNC GLOBAL DEFAULT 13 foo2_func
可见,符号foo2的运行时地址为0x0804a020,符号foo2_func的运行时地址是0x08048414。
接下来,我们再来看看汇编器为这两个符号填充的Addend是多少。我们使用工具objdump反汇编hello.o,其中黑体标识的分别是汇编器在引用foo2和foo2_func的地址处填充的Addend:
root@baisheng:~/demo# objdump -d hello.o
hello.o: file format elf32-i386
Disassembly of section .text:
00000000 :
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 e4 f0 and $0xfffffff0,%esp
6: 83 ec 10 sub $0x10,%esp
9: c7 05 00 00 00 00 05 movl $0x5,0x0
10: 00 00 00
13: c7 04 24 32 00 00 00 movl $0x32,(%esp)
1a: e8 fc ff ff ff call 1b
1f: b8 00 00 00 00 mov $0x0,%eax
24: c9 leave
25: c3 ret
根据输出可见,汇编器在引用符号foo2处填充的Addend是0,在引用符号foo2_func处填充的Addend是–4。
于是,可执行文件hello中引用符号foo2的位置的修订值为:
S + A = 0x0804a020 + 0 = 0x0804a020
我们反汇编可执行文件hello,来验证一下引用符号foo2处的值是否修订为我们计算的这个值:
root@baisheng:~/demo# objdump -d hello
hello: file format elf32-i386
...
080483dc :
80483dc: 55 push %ebp
80483dd: 89 e5 mov %esp,%ebp
80483df: 83 e4 f0 and $0xfffffff0,%esp
80483e2: 83 ec 10 sub $0x10,%esp
80483e5: c7 05 20 a0 04 08 05 movl $0x5,0x804a020
80483ec: 00 00 00
80483ef: c7 04 24 32 00 00 00 movl $0x32,(%esp)
80483f6: e8 19 00 00 00 call 8048414
80483fb: b8 00 00 00 00 mov $0x0,%eax
8048400: c9 leave
8048401: c3 ret
8048402: 66 90 xchg %ax,%ax
...
注意偏移0x1b处,确实已经被链接器修订为0x0804a020了。
对于符号foo2_func的修订值,还需要变量P,即引用符号foo2_func处的运行时地址。根据可执行文件hello的反汇编代码可见,引用符号foo2_func的指令的地址是:
0x80483f6 + 1 = 0x80483f7
所以,可执行文件hello中引用符号foo2_func的位置的修订值为:
S + A – P = 0x08048414 + (-4) - 0x80483f7 = 0x19
观察hello的反汇编代码,从地址0x80483f7开始处的4字节,确实也已经被链接器修订为0x19。
这里提醒一下读者,如果foo2_func占据的运行时地址小于main函数,那么这里foo2_func与PC的相对地址将是负数。在机器指令中,使用的是数的补码形式,所以一定要注意,以免造成困惑。
事实上,对于符号foo2使用的重定位类型R_386_32,是绝对地址重定位,链接器只要解析符号foo2的运行时地址替换修订处即可。而对于符号
foo2_func,其使用的重定位类型是R_386_PC32,这是一个PC相对地址重定位。而当执行当前指令时,PC中已经加载了下一条指令的地址,
并不是当前指令的地址,这就是在引用符号foo2_func处填充“–4”的原因。
我们看到,在链接时,链接器在需要重定位的符号所在的偏移处直接进行了编辑修订,所以人们通常也将链接器形象地称为“link editor”。
对于重定位的符号,当新地址确定后,新地址将填充到重定位表目中指定的offset处。
合并文件:
合并文件是在链接时,将所有的目标文件,把相同的节合成一个大节,最终形成的就是一个目标文件,合并完成后,再进行重定位。
但在合并时,由于编译器会加入一些其它的目标文件,所以导致最后可执行文件的某些节,大于原来目标文件对应节的和,这个大于还有一个原因是由于指令等的对齐方式不同,需要填充造成的。
6.静态链接库
将各个目标文件打包成一个 .a 文件,在编译时,将目标文件所需要的目标文件加入到可最后的可执行文件中。
7.动态链接库
动态链接库,在编译可执行文件时,必须能找得到,才能编译成功
在可执行文件加载时,动态链接库文件的目录可以改变位置,但也必须能找得到,可用ldd test找到test需要哪些动态库
readelf -r test 可以查看到动态链接库的重定位节
.rel.dyn 表示全局变量,.rel.plt重定位函数。
执行程序链接到动态库,是因为有两上个表,GOT在.data节,PLT在.text节,前者在第一次动态库中的函数被调用时,由动态链接器获得其地址,
并保存,在以后的调用中,只需要从中读取就可以;后面主要是两条跳转指令,如jmp *0x888888,其中0x888888是GOT中表目的地址,
它保存库函数的地址,这样就可以通过PLT,直接跳转到库函数中。
引自:
我们知道,与静态库不同,动态库不会在可执行文件中有任何副本,那么为什么编译链接时依然需要指定动态库呢?原因包括下面几点:
1)动态加载器需要知道可执行程序依赖的动态库,这样在加载可执行程序时才能加载其依赖的动态库。所以,在链接时,链接器将根据可执行程序引用的动态库中
的符号的情况在dynamic段中记录可执行程序依赖的动态库。我们使用如下命令将foo1.c和foo2.c编译为动态库,并将hello链接到动态库
libfoo.so。
root@baisheng:~/demo# gcc -shared -fPIC foo1.c foo2.c -o libfoo.so
root@baisheng:~/demo# gcc hello.c -o hello -L./ -lfoo
我们来查看hello中的dynamic段:
root@baisheng:~/demo# readelf -d hello | grep Shared
0x00000001 (NEEDED) Shared library: [libfoo.so]
0x00000001 (NEEDED) Shared library: [libc.so.6]
显然,在dynamic段中,记录了hello依赖的动态链接库libfoo.so。
2)链接器需要在重定位表中创建重定位记录(Relocation
Record),这样当动态链接器加载hello时,将依据重定位记录重定位hello引用的这些外部符号。重定位记录存储在ELF文件的重定位段
(Relocation)中,ELF文件中可能有多个段包含需要重定位的符号,所以可能会包含多个重定位段。以hello的重定位段为例:
root@baisheng:~/demo# readelf -r hello
Relocation section '.rel.dyn' at offset 0x3d4 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
08049ffc 00000206 R_386_GLOB_DAT 00000000 __gmon_start__
0804a020 00000905 R_386_COPY 0804a020 foo2
Relocation section '.rel.plt' at offset 0x3e4 contains 3 entries:
Offset Info Type Sym.Value Sym. Name
0804a00c 00000207 R_386_JUMP_SLOT 00000000 __gmon_start__
0804a010 00000307 R_386_JUMP_SLOT 00000000 __libc_start_main
0804a014 00000507 R_386_JUMP_SLOT 00000000 foo2_func
根据输出可见,可执行文件hello包含两个重定位段,“.rel.dyn”段中记录的是加载时需要重定位的变量,“.rel.plt”段中记录的是需要重定位的函数。
因此,虽然编译时不需要链接共享库,但是可执行文件中需要记录其依赖的共享库以及加载/运行时需要重定位的条目,在加载程序时,动态加载器需要这些信息来完成加载时重定位。
最后我们再来关注一下在hello中的全局符号foo2和foo2_func。
root@baisheng:~/demo# nm hello | grep foo
0804a020 B foo2
U foo2_func
在符号表中,我们看到,foo2_func是Undefined的,这没错,因为其确实不在hello中定义。但是注意变量foo2,理论上它也应该是
Undefined的,但是我们看到其在hello中是有定义的,而且其还在BSS段中。换句话说,虽然我们在hello中没有定义一个未初始化的全局变
量,但是链接器却偷偷在hello中定义了一个未初始化的变量foo2。那么,这个foo2与libfoo.so中的全局变量foo2是什么关系呢?为什
么编译器要这样做?这也是和重定位有关的,事实上,这种重定位方式称为“Copy
relocation”,后面我们在讨论用户进程的加载时将会进一步介绍。
8.动态加载
静态库是在程序编译时,添加到可执行文件中,与目标文件一起形成可执行文件,与之相关的节,如
.rela.text等动态链接库,则是加载器加载可执行文件时,根据可执行文件的.dym中的库信息,把库函数加到内在中,这样不必每个调用都添加到可
执行文件中,且便于库的更新。
动态加载,它是在执行文件在执行的过程中,在代码中动态加载库,它在编译时用到 -rdynamic,linux下,用到函数dlopen,dlsym,dlclose。
比较有用的命令:
gcc -g -c test.c 编译成目标文件,反汇编的时候,能看到对应的代码及机器码
readelf -a test.o 查看所有节
readelf -s test.o 查看符号节
readelf -S test.o 查看符号节,及其地址
readelf -r test.o查看重定位表
objdump -d test.o反汇编
ar -r test.a test1.o test2.o将test1o,test2.o打包成一个静态链接库
gcc -shared -fPIC test1.c test2.c -o libtest.so 生成动态链接库 libtest.so
gcc test.c -ltest -L. -o test
(编译test.c文件,与库libtest.so相联,此处-ltest是指 libtest.so库 -L指名库所在的目录;找不到库,也可以用
gcc -o test test.c ./libtest.so编译。则可采用下面两个办法中的一个1、可以把当前路径加入 /etc/ld.so.conf中然后运行ldconfig,或者以当前路径为参数运行ldconfig(要有root权限才行)。2、把当前路径加入环境变量LD_LIBRARY_PATH中
file程序是用来判断文件类型的,在file命令下,所有文件都会原形毕露,不受后缀名的影响。
ldd是用来打印目标程序(由命令行参数指定)所链接的所有动态库的信息的,如果目标程序没有链接动态库,则打印“not a dynamic executable”,ldd的用法请参考manpage。另外,还可以借助程序ldd实用程序来判断。
命令
ar 创建静态库,插入、删除、列出和提取成员
strings 列出一个目标文件中所有可打印的字符串
nm 列出一个目标文件中的符号表中定义的符号
size 列出目标文件中节的名字和大小
readelf 显示一个目标文件的完整结构,包括ELF头中编码的所有信息,包含 size 和 nm 的功能
objdump 所有二进制工具之母。能够显示一个目标文件中所有的信息。它最有用的功能是反汇编 .text 节中的二进制指令。
ldd 列出一个可执行文件在运行时所需要的共享库(其它目标文件在编译时,其实并不知道自己需要的引用,是来自静态库,还是动态库)。
里面很多可能不是很正确,希望各位路过,看过的朋友,留下意见,谢谢!
(38.46 KB, 下载次数: 11)
阅读(1407) | 评论(0) | 转发(0) |