概述
书写和阅读此贴,希望你掌握节的概念,重点理解重定位节rela.text, 还有符号节.symtab, 字符串节.strtab
c 语言的函数地址, 变量甚至指针,结构都会被抽象到一个符号中,用一个符号来表达, 符号是与外部模块连接的接口, 而且读符号表, 你还会发现有许多额外的符号,它们是构成c语言运行所必需的.
符号最重要的概念是它的名称和值,其它还有大小,类型,属性等特征.
rela.text 记录了.text段中需要重定位的地址及对应的符号. rela存在的意义是要把符号的值填写到地址所指的地方,那个地方未重定位前填写的是0. 重定位是运行程序所必需的, 因为它修正了变量或函数的地址, 这些地址是与符号的数值相关联的.
后面有详细分析和结构定义.
本贴并不是解剖麻雀,而是蚂蚁搬家,要你知道点什么,日积月累,自然天成.
参考文献:
- elf 手册页
- elf.h 文件
链接式elf文件,储存有重定位信息和符号表,供将来连接器使用.
连接器会根据重定位表,找到重定位的地址,根据重定位的类型和符号的值,
对重定位地址处内容进行修正.
这些变化,通过反编译程序可以看得一目了然.
下面的示例程序说明了这个问题.
喜欢这个简单的源文件,借用一下.
1.创建测试文件
创建文件common.c
int val = 1;
int func(void)
{
return (val+10);
}
创建文件test.c
extern int val;
extern int func(void);
int main()
{
val = 10;
func();
return 0;
}
2.编译
Makefile 如下:
all:
test
test: test.o common.o
gcc -o $@ $^
%.o:%.c
gcc -c -o $@ $<
clean:
rm *.o test
生成的test.o和common.o属于RELOCATE类型
来分析一下编译后生成的REL文件
3.test.o分析
-
查看符号表
$ readelf -s test.o
Symbol table ‘.symtab’ contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 6
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 5
8: 0000000000000000 26 FUNC GLOBAL DEFAULT 1 main
9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND val
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND func
先解释一下符号表, 它是.symtab 表达的内容, 每个符号如前述包含了名称,值,还有大小,类型bu,属性包括是全局的还是局部的,是否可见,Ndx 说明它属于那个节. 下面是符号的结构定义.
typedef struct {
uint32_t
st_name;
unsigned char st_info;
unsigned char st_other;
uint16_t
st_shndx;
Elf64_Addr
st_value;
uint64_t
st_size;
} Elf64_Sym;
st_name 需要借助于.strtab存储名字,无名符号为0
st_size 若符号无大小也为0.
st_info, 指明了符号的类型和bind 属性
类型包括:无类型,数据类型,函数类型,文件类型,节类型及自定义类型.
bind 包括局部,全局,弱bind 及自定义.
st_other 指明了符号的可见性,一般都是Default,除非是内部的或者隐藏的.
因为val和func不是在test.c中定义的,所以这两个符号的Ndx(符号所在section的index)为UND。为了能让程序顺利执行,我们希望在未来链接的过程中可以从其他文件中找到val和func这两个符号,并确定这两个符号的地址,确定未定义符号的地址的过程即是“重定位”(relocation)。
刚才提到文件中的符号节,字符串节,重定位节,那么如何看一个文件有那些节, 这只需要看文件的节头部就可以了.
-
查看section header table
$ readelf -S test.o
There are 12 section headers, starting at offset 0x128:Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000001a 0000000000000000 AX 0 0 4
[ 2] .rela.text RELA 0000000000000000 00000548
0000000000000030 0000000000000018 10 1 8
[ 3] .data PROGBITS 0000000000000000 0000005c
0000000000000000 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 0000005c
0000000000000000 0000000000000000 WA 0 0 4
[ 5] .comment PROGBITS 0000000000000000 0000005c
000000000000002d 0000000000000001 MS 0 0 1
[ 6] .note.GNU-stack PROGBITS 0000000000000000 00000089
0000000000000000 0000000000000000 0 0 1
[ 7] .eh_frame PROGBITS 0000000000000000 00000090
0000000000000038 0000000000000000 A 0 0 8
[ 8] .rela.eh_frame RELA 0000000000000000 00000578
0000000000000018 0000000000000018 10 7 8
[ 9] .shstrtab STRTAB 0000000000000000 000000c8
0000000000000059 0000000000000000 0 0 1
[10] .symtab SYMTAB 0000000000000000 00000428
0000000000000108 0000000000000018 11 8 8
[11] .strtab STRTAB 0000000000000000 00000530
0000000000000016 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
我们看见了符号表,也看见了字符串表,前面已略作分析.
下面重点关注rela.text section。
可以看到 type=RELA,link=10,info=1,RELA类型的link表示被重定位的符号所在的符号表的section index,info表示需要被重定位的section的index,
可以通俗的理解为, link 节是该节的上家, info节是该节的下家.
就是说, rela.txt 节会从 link节 第10节(.symtab)拿符号,取到地址(连接已完成,地址已确定), 为info节第1节(.text). 相关地址重新定位,
那么这个rela.txt 节到底都包含那些信息呢?
-
查看relocate section里的详细信息。
$ readelf -r test.o
010editor 本来挺能帮助分析的,但它目前版本分析不到重定位信息表,可以依靠readelf 来进行.Relocation section ‘.rela.text’ at offset 0x548 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000006 000900000002 R_X86_64_PC32 0000000000000000 val - 8
00000000000f 000a00000002 R_X86_64_PC32 0000000000000000 func - 4Relocation section ‘.rela.eh_frame’ at offset 0x578 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
可以看到,重定位项的关键语素一个是符号,一个是目标地址.也有自己的类型,属性信息,下面是严格的结构定义:
typedef struct {
Elf64_Addr r_offset;
uint64_t
r_info;
int64_t
r_addend;
} Elf64_Rela;
offset表示目标section中的地址偏移量,
info的高4个字节表示该符号在link节中的index,低4字节表示重定位的类型,不同的类型计算目标地址的方法不一样。
R_X86_64_PC32,跳转地址是PC+偏移量。
r_addend是对符号value值进行必要的调整数值.
val - 8: -8是addend, 这个-8是与特定的cpu指令集相关联的,对x86_64是-8, 代表了本指令执行完后,指针pc向下偏移8字节, 所以要用-8来补回
func-4: -4是addend, 也是同理.
再小结一下:
符号val的重定位地址是在.text的偏移为6处,将来的链接过程中,连接器要将val的地址写到这个位置上来,val在.symtab中的index为9。
func的重定位地址是在.text的偏移为f处,将来的链接过程中,连接器要将func的地址写到这个位置上来,func在.symtab中的index为a。
我们可以反汇编.o文件, 看看目前的机器码是什么样子.
- 查看汇编文件
$ objdump -S test.o
将源码和反汇编混合显示. (源码插入到反汇编代码中)
test.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0:
55
push
%rbp
1:
48 89 e5
mov
%rsp,%rbp
4:
c7 05 00 00 00 00 0a
movl
$0xa,0x0(%rip)
# e <main+0xe>
b:
00 00 00
e:
e8 00 00 00 00
callq
13 <main+0x13>
13:
b8 00 00 00 00
mov
$0x0,%eax
18:
c9
leaveq
19:
c3
retq
可以看到.text中偏移6处四个字节(val的地址)为全0,偏移f处四个字节(func的地址)为全0。这两处就是需要重新定位的地方.
4.common.o分析
- 查看的符号表
…
8: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 val
9: 0000000000000000 15 FUNC GLOBAL DEFAULT 1 func
可以看到val定义在index为3的.data里,func定义在index为1的.text里,这两个符号都是在common.c文件内部定义的。连接程序将来要把这两个符号地址连入引用的地方.
-
查看section header table
…
[ 2] .rela.text RELA 0000000000000000 00000528
0000000000000018 0000000000000018 10 1 8
…
存在一个重定位的节,专门用来重定位.text 节,从.symtab中可取到符号, 这个节详细描述了重定位信息. -
查看重定位详细信息
Relocation section ‘.rela.text’ at offset 0x528 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000006 000800000002 R_X86_64_PC32 0000000000000000 val – 4
…
以上信息可以得出,需要被重定位的地址是.text段的地址6, 重定位符号是.symtab中的index为8,重定位类型为 R_X86_64_PC32,addend 是-4, 这个符号的名字叫val.
虽然val是在本文件定义的,但.o阶段对数据的访问都统统用重定位的方式来定义, 因为将来数据会被合并成一个大段来重新定址.
- 查看汇编文件:
......
0000000000000000 <func>:
0:
55
push
%rbp
1:
48 89 e5
mov
%rsp,%rbp
4:
8b 05 00 00 00 00
mov
0x0(%rip),%eax
# a <func+0xa>
a:
83 c0 0a
add
$0xa,%eax
d:
c9
leaveq
e:
c3
retq
可以看到偏移为6处的四个字节(val的地址)全为0,需要在链接的时候写入val的地址。
5.链接
将两个.o文件链接生成的test为EXEC类型.:
- 第一步:合并各段并统一进行地址分配
扫描所有的输入目标文件,获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步中,连接器将能获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系。
- 第二步:符号解析与重定位
使用上面第一步中收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。
总之链接器的功能就是合并段,造表, 查表,重定位地址.
来看看重定位之后的test文件
6. test 文件分析
可执行文件,我们关心的是program 段的分配.
-
查看test进程在内存中的映像分布:
readelf -l test
…
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000006f4 0x00000000000006f4 R E 200000
LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x000000000000022c 0x0000000000000230 RW 200000
…
可以看到text segment被映射到虚拟地址0x400000处,data segment被映射到虚拟地址0x600e10处。
-
查看符号表
…
51: 0000000000601038 4 OBJECT GLOBAL DEFAULT 24 val
…
57: 0000000000400490 15 FUNC GLOBAL DEFAULT 13 func
…
62: 00000000004004ed 26 FUNC GLOBAL DEFAULT 13 main
… -
反汇编
objdump -S test > test.S
......
00000000004004ed <main>:
4004ed: 55
push
%rbp
4004ee: 48 89 e5
mov
%rsp,%rbp
4004f1: c7 05 3d 0b 20 00 0a
movl
$0xa,0x200b3d(%rip)
# 601038 <val>
4004f8: 00 00 00
4004fb: e8 07 00 00 00
callq
400507 <func>
400500: b8 00 00 00 00
mov
$0x0,%eax
400505: 5d
pop
%rbp
400506: c3
retq
0000000000400507 <func>:
400507: 55
push
%rbp
400508: 48 89 e5
mov
%rsp,%rbp
40050b: 8b 05 27 0b 20 00
mov
0x200b27(%rip),%eax
# 601038 <val>
400511: 83 c0 0a
add
$0xa,%eax
400514: 5d
pop
%rbp
400515: c3
retq
......
main函数中
地址0x4004f1处:
4004f1: c7 05 3d 0b 20 00 0a
movl
$0xa,0x200b3d(%rip)
# 601038 <val>
%rip+0x200b3d=0x4004fb+0x200b3d=0x601038=val的地址
地址0x4004fb处:
4004fb: e8 07 00 00 00
callq
400507 <func>
该条指令的下一条指令地址为0x400500,0x400500+0x07=0x400507=func的地址
func中
地址0x40050b处:
40050b: 8b 05 27 0b 20 00
mov
0x200b27(%rip),%eax
# 601038 <val>
%rip+ 0x200b27= 0x400511+0x200b27=0x601038=val的地址
由此可见这三处重定位的地址都与符号地址相符合,验证了我们推算的正确性。
我们从test 的符号表中,还看到了许多其它符号, 好在它们有的是局部的, 有的虽是全局的,但名字以_开头,容易区分.
链接程序(这里是gcc)将所有.o文件连接成一个大的.o文件,再根据符号对函数,变量进行重新定位建立起可执行文件, 建完之后,那些符号对可执行文件不是必需的,可以删去,以减小可执行文件的大小,这就是strip工具所做的.
对比发现, 被strip 后的可执行文件test, 已经没有了.symtab, .strtab, 其它都没有动.
通过readelf -e test, 我们发现我们距离掌握elf 格式,还有相当的距离,还要继续学习!
最后
以上就是酷炫摩托为你收集整理的elf静态链接实例的全部内容,希望文章能够帮你解决elf静态链接实例所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复