关于 ELF 文件格式的笔记(二)
我在 关于 ELF 格式文件的笔记(一) 中讲了基本的 C
编译过程和基本的 ELF
文件格式,本篇文章是 ELF
相关文章的第二篇,没有看过的同学,推荐先看第一篇。
我在第一篇的文章中说过在 ELF
文件中最最最最重要的就是节 (Section
),本篇内容就是讲如何通过各种 Section
中的信息来调用本地的方法。(非 .so
库中的方法)
在开始之前先列出一下本篇文章会用到的 Section
:
- .text :可谓是最重要的部分,存放
CPU
执行的机器码。 - .rela.text :记录
.text
Section
中的重定向地址,简单来说就是.text
中的某些使用到的地址是不可用的,需要借助.rela.text
重新计算地址。当别的Section
也需要重新计算地址时也会有一个对应的.rela.xxx
的Section
,例如.rela.plt
。 - .symtab:符号表,描述了我们定义的方法和全局变量等等,我们可以通过符号表中的信息定位到这些符号所对应的地址。
- .shstrtab / .strtab :字符表,描述程序中用到的字符,比如代码中用到的方法名,变量名等等。它的记录方法很简单就是一个字符串相对于该表的偏移量,每一个字符串结束都用
\0
表示。 - .data:存放已经初始化后的全局变量。
- .bss:存放没有初始化的全局变量。
- .rodata:存放常量。
虽然你可能看了上面的 Section
的描述现在还有很多的疑问,开始时我也和你一样,当你读完本篇文章后,你就会对上面的 Section
有全新的认识。
准备工作
Tips:我是基于 Linux
x86_64
的环境测试的。
写一段测试的代码:
C
int add5(int num) {
return num + 5;
}
int add10(int num) {
int n1 = add5(num);
return add5(n1);
}
char* getStr() {
return "Hello, World!";
}
int var = 3;
void setVar(int n) {
var = n;
}
int getVar() {
return var;
}
上面的代码应该不需要解释吧,哈哈😄。
通过 gcc -c [source file]
编译上面的代码,最后会生成一个 .o
的文件。
通过 readelf -a [ELF file]
命令来查看上面的 .o
文件的信息:
text
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1136 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 14
Section header string table index: 13
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
0000000000000073 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000308
0000000000000078 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 000000b4
0000000000000004 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 000000b8
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 000000b8
000000000000000e 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 000000c6
000000000000002e 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000f4
0000000000000000 0000000000000000 0 0 1
[ 8] .note.gnu.propert NOTE 0000000000000000 000000f8
0000000000000020 0000000000000000 A 0 0 8
[ 9] .eh_frame PROGBITS 0000000000000000 00000118
00000000000000b8 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 00000380
0000000000000078 0000000000000018 I 11 9 8
[11] .symtab SYMTAB 0000000000000000 000001d0
00000000000000f0 0000000000000018 12 4 8
[12] .strtab STRTAB 0000000000000000 000002c0
0000000000000045 0000000000000000 0 0 1
[13] .shstrtab STRTAB 0000000000000000 000003f8
0000000000000074 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
There are no section groups in this file.
There are no program headers in this file.
Relocation section '.rela.text' at offset 0x308 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000028 000400000004 R_X86_64_PLT32 0000000000000000 _Z4add5i - 4
000000000035 000400000004 R_X86_64_PLT32 0000000000000000 _Z4add5i - 4
000000000046 000300000002 R_X86_64_PC32 0000000000000000 .rodata - 4
00000000005c 000700000002 R_X86_64_PC32 0000000000000000 var - 4
00000000006d 000700000002 R_X86_64_PC32 0000000000000000 var - 4
Relocation section '.rela.eh_frame' at offset 0x380 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
000000000040 000200000002 R_X86_64_PC32 0000000000000000 .text + 13
000000000060 000200000002 R_X86_64_PC32 0000000000000000 .text + 3b
000000000080 000200000002 R_X86_64_PC32 0000000000000000 .text + 4c
0000000000a0 000200000002 R_X86_64_PC32 0000000000000000 .text + 63
The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported.
Symbol table '.symtab' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS My_Math.cpp
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 5
4: 0000000000000000 19 FUNC GLOBAL DEFAULT 1 _Z4add5i
5: 0000000000000013 40 FUNC GLOBAL DEFAULT 1 _Z5add10i
6: 000000000000003b 17 FUNC GLOBAL DEFAULT 1 _Z6getStrv
7: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 var
8: 000000000000004c 23 FUNC GLOBAL DEFAULT 1 _Z6setVari
9: 0000000000000063 16 FUNC GLOBAL DEFAULT 1 _Z6getVarv
No version information found in this file.
Displaying notes found at file offset 0x000000f8 with length 0x00000020:
Owner Data size Description
GNU 0x00000010 Unknown note type: (0x00000005)
通过 objdump --disassemble --section=.text [ELF file]
命令来反编译 .text
节机器码,反编译后的源码是汇编(不要害怕,本篇文章不会涉及到汇编的指令使用):
text
My_Math.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z4add5i>:
0: f3 0f 1e fa endbr64
4: 55 pushq %rbp
5: 48 89 e5 movq %rsp, %rbp
8: 89 7d fc movl %edi, -4(%rbp)
b: 8b 45 fc movl -4(%rbp), %eax
e: 83 c0 05 addl $5, %eax
11: 5d popq %rbp
12: c3 retq
0000000000000013 <_Z5add10i>:
13: f3 0f 1e fa endbr64
17: 55 pushq %rbp
18: 48 89 e5 movq %rsp, %rbp
1b: 48 83 ec 18 subq $24, %rsp
1f: 89 7d ec movl %edi, -20(%rbp)
22: 8b 45 ec movl -20(%rbp), %eax
25: 89 c7 movl %eax, %edi
27: e8 00 00 00 00 callq 0x2c <_Z5add10i+0x19>
2c: 89 45 fc movl %eax, -4(%rbp)
2f: 8b 45 fc movl -4(%rbp), %eax
32: 89 c7 movl %eax, %edi
34: e8 00 00 00 00 callq 0x39 <_Z5add10i+0x26>
39: c9 leave
3a: c3 retq
000000000000003b <_Z6getStrv>:
3b: f3 0f 1e fa endbr64
3f: 55 pushq %rbp
40: 48 89 e5 movq %rsp, %rbp
43: 48 8d 05 00 00 00 00 leaq (%rip), %rax # 4a <_Z6getStrv+0xf>
4a: 5d popq %rbp
4b: c3 retq
000000000000004c <_Z6setVari>:
4c: f3 0f 1e fa endbr64
50: 55 pushq %rbp
51: 48 89 e5 movq %rsp, %rbp
54: 89 7d fc movl %edi, -4(%rbp)
57: 8b 45 fc movl -4(%rbp), %eax
5a: 89 05 00 00 00 00 movl %eax, (%rip) # 60 <_Z6setVari+0x14>
60: 90 nop
61: 5d popq %rbp
62: c3 retq
0000000000000063 <_Z6getVarv>:
63: f3 0f 1e fa endbr64
67: 55 pushq %rbp
68: 48 89 e5 movq %rsp, %rbp
6b: 8b 05 00 00 00 00 movl (%rip), %eax # 71 <_Z6getVarv+0xe>
71: 5d popq %rbp
72: c3 retq
这里看到我们自己定义的方法全部都加上了一个前缀和后缀,这个不重要跳过。
我这里假如 .text
,.data
,.rodata
加载到内存中的排列如下:
这里没有考虑其他 Section
,图中也忽略了段 (Segment
),也没有考虑内存对齐,加载的地址也不可能从 0x00
开始,大家都忽略上面的问题,这个图只是为了让你理解 ELF
文件中的一些原理。
如何手动执行特定方法
通常我们的可执行文件都是都是有一个入口地址(通常是我们的 main()
函数),假如我们现在就是要绕过入口地址去执行一个特定的函数,比如我们定义的 add5()
和 add10()
方法。
我们前面提到符号表中展示了定义的方法和全局变量等等,我们就是从它入手:
text
Symbol table '.symtab' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
// ...
4: 0000000000000000 19 FUNC GLOBAL DEFAULT 1 _Z4add5i
5: 0000000000000013 40 FUNC GLOBAL DEFAULT 1 _Z5add10i
// ...
我这里来描述一下他们的意思:
Name
:就是符号的名字,它是存放在.shstrtab
中的,需要通过一个offset
去查找的,上面为了方便阅读,readelf
直接打印出来了。Ndx
:表示该符号属于的Section
编号,上面的为1
,正好就是.text
。Type
:符号对应的类型,我们的都是FUNC
。Size
:符号对应实现的大小。Value
:对应符号在Section
中的位置,它和Ndx
一起就能够找到对应符号的位置。
看看我们的 add5()
和 add10()
对应的就是 .text
Section
,位置分别为 0000000000000000
和 0000000000000013
。
我们再看看反编译后的汇编代码:
text
// ...
0000000000000000 <_Z4add5i>:
0: f3 0f 1e fa endbr64
4: 55 pushq %rbp
5: 48 89 e5 movq %rsp, %rbp
8: 89 7d fc movl %edi, -4(%rbp)
b: 8b 45 fc movl -4(%rbp), %eax
e: 83 c0 05 addl $5, %eax
11: 5d popq %rbp
12: c3 retq
0000000000000013 <_Z5add10i>:
13: f3 0f 1e fa endbr64
17: 55 pushq %rbp
18: 48 89 e5 movq %rsp, %rbp
1b: 48 83 ec 18 subq $24, %rsp
1f: 89 7d ec movl %edi, -20(%rbp)
22: 8b 45 ec movl -20(%rbp), %eax
25: 89 c7 movl %eax, %edi
27: e8 00 00 00 00 callq 0x2c <_Z5add10i+0x19>
2c: 89 45 fc movl %eax, -4(%rbp)
2f: 8b 45 fc movl -4(%rbp), %eax
32: 89 c7 movl %eax, %edi
34: e8 00 00 00 00 callq 0x39 <_Z5add10i+0x26>
39: c9 leave
3a: c3 retq
// ...
哈哈巧了不是,add5()
正好在 0000000000000000
,add10()
正好在 0000000000000013
。
按照我们上面加载到内存中的地址就可以得出 add5()
方法在内存中的地址是 0x00
+ 0x0000000000000000
,add10()
方法在内存中的地址是 0x00
+ 0x0000000000000013
。得到他们的地址和长度后我们就可以在内存中执行了,实际上 add5()
是可以正常执行的,但是 add10()
不能正常执行,原因我们继续看下一节。
重定向 .text 中的方法调用地址
add5()
方法只是做了简单的加运输,而没有做响应的方法调用跳转,所以在上面的代码中是可以正常运行的,但是 add10()
方法中调用了两次 add5()
方法来实现操作,但是它调用 add5()
方法的地址是错的:
text
// ...
0000000000000000 <_Z4add5i>:
0: f3 0f 1e fa endbr64
4: 55 pushq %rbp
5: 48 89 e5 movq %rsp, %rbp
8: 89 7d fc movl %edi, -4(%rbp)
b: 8b 45 fc movl -4(%rbp), %eax
e: 83 c0 05 addl $5, %eax
11: 5d popq %rbp
12: c3 retq
0000000000000013 <_Z5add10i>:
13: f3 0f 1e fa endbr64
17: 55 pushq %rbp
18: 48 89 e5 movq %rsp, %rbp
1b: 48 83 ec 18 subq $24, %rsp
1f: 89 7d ec movl %edi, -20(%rbp)
22: 8b 45 ec movl -20(%rbp), %eax
25: 89 c7 movl %eax, %edi
27: e8 00 00 00 00 callq 0x2c <_Z5add10i+0x19>
2c: 89 45 fc movl %eax, -4(%rbp)
2f: 8b 45 fc movl -4(%rbp), %eax
32: 89 c7 movl %eax, %edi
34: e8 00 00 00 00 callq 0x39 <_Z5add10i+0x26>
39: c9 leave
3a: c3 retq
// ...
我们注意 0x27
和 0x34
位置,他们的值都是 e8 00 00 00 00
,其中 e8
就表示 callq
调用方法的指令,后面四个字节表示方法的相对当前位置的距离( x86_64
是这样的),所以 0x27
那里的地址就应该是 -0x2c
,而 0x34
那里的地址是 -0x39
,他们表示都指向 add5()
方法,具体参考以下图(图中的地址和我的例子不一样,注意下):
因为在计算机中表示数据要用补码 (不懂的同学去别的地方查一下),-0x2c
的补码就是 0xffffffd4
,-0x39
的补码就是 ffffffc7
,在这里的数据表示是小端(little endian
,不懂的同学去网上查一下),所以 0x27
位置就应该是 e8 d4 ff ff ff
,而 0x34
位置就应该是 e8 c7 ff ff ff
。当我们把值替换成我们计算后的值后 add10()
方法就可以正常运行了。
emmm,因为我们是有源码,知道调用的是哪个方法,但是 CPU
它不知道啊,所以就引出了这一节要讲的东西,我们需要根据 .rela.text
Section
中的数据来重新计算这个地址。
text
Relocation section '.rela.text' at offset 0x308 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000028 000400000004 R_X86_64_PLT32 0000000000000000 _Z4add5i - 4
000000000035 000400000004 R_X86_64_PLT32 0000000000000000 _Z4add5i - 4
// ...
Offset
:需要修改的地址的位置,我们的callq
指令刚好是0x27
和0x34
,但是指令还要占一个字节,所以对应的位置就是0x28
和0x35
。Info
:它是一个符合结构,高32
位表示对应的字符表中的位置,我们这里也就是_Z4add5i
,低32
位表示类型,我们这里也就是R_X86_64_PLT32
。Type
:表示重定位的类型。Sysm. Value
:不同的类型有不同的表示,大部分情况下都是和符号表中Value
对应的,也就是在目标Section
中的位置,我们的add5()
方法在.text
中的位置就是0x00
。Name + Addend
:符合的名字和对应的偏移量,这里的偏移量是-4
,后续计算地址时会用到。
不同的重定位类型有不同的计算方式,参照这个。我们的这个类型是 R_X86_64_PLT32
,它的计算公式就是 L + A - P
L
:对应的符号所在的内存地址,在我们这里就是add5()
方法0x00
(内存基准地址) +0x00
(add5()
方法在.text
中的相对偏移)。A
:也就是上面表中提到的Addend
,我们这里是-4
。P
:执行的指令的偏移量,也就是Offset
的值加上内存地址,所以我们这里就是0x00
(内存基准地址) +0x28
(或者0x35
)。
所以 0x28
的位置的地址通过以上方法后计算后得到的结果是 0x00 + 0x00 - 0x04 - 0x28(0x35) = -0x2c(-0x39)
,哈哈哈哈,和我们自己计算的结果是一样的,对应的补码就是 0xffffffd4
(ffffffc7
)。
重定向 .text 中的常量和全局变量调用地址
我们再来看看剩下的三个方法,他们涉及到常量和全局变量的调用。
通过 readelf -x .data [elf file]
查看变量表中的数据:
text
Hex dump of section '.data':
0x00000000 03000000
比较短,一共就 4 Bytes。
通过 readelf -x .rodata [elf file]
查看常量池中的数据:
text
Hex dump of section '.rodata':
0x00000000 48656c6c 6f2c2057 6f726c64 2100 Hello, World!.
一共 14 Bytes,也就是一个字符串 Hello, World!
。
我们接着看看符号表中的信息:
text
Symbol table '.symtab' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
// ...
7: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 var
// ...
这里的 var
就是我们定义的全局变量,Ndx
为 3 表示对应的 Section
为 .data
,Value
为 0x00
表示符号在 .data
中的偏移值为 0
。
这里看看对应的 .text
反编译后的汇编代码:
text
// ...
000000000000003b <_Z6getStrv>:
3b: f3 0f 1e fa endbr64
3f: 55 pushq %rbp
40: 48 89 e5 movq %rsp, %rbp
43: 48 8d 05 00 00 00 00 leaq (%rip), %rax # 4a <_Z6getStrv+0xf>
4a: 5d popq %rbp
4b: c3 retq
000000000000004c <_Z6setVari>:
4c: f3 0f 1e fa endbr64
50: 55 pushq %rbp
51: 48 89 e5 movq %rsp, %rbp
54: 89 7d fc movl %edi, -4(%rbp)
57: 8b 45 fc movl -4(%rbp), %eax
5a: 89 05 00 00 00 00 movl %eax, (%rip) # 60 <_Z6setVari+0x14>
60: 90 nop
61: 5d popq %rbp
62: c3 retq
0000000000000063 <_Z6getVarv>:
63: f3 0f 1e fa endbr64
67: 55 pushq %rbp
68: 48 89 e5 movq %rsp, %rbp
6b: 8b 05 00 00 00 00 movl (%rip), %eax # 71 <_Z6getVarv+0xe>
71: 5d popq %rbp
72: c3 retq
我们再来看看 .rela.text
中的数据:
.text
Relocation section '.rela.text' at offset 0x308 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
// ...
000000000046 000300000002 R_X86_64_PC32 0000000000000000 .rodata - 4
00000000005c 000700000002 R_X86_64_PC32 0000000000000000 var - 4
00000000006d 000700000002 R_X86_64_PC32 0000000000000000 var - 4
我们看到重定向常量和全局变量都是 R_X86_64_PC32
类型,它的重定向计算的公式是 S + A - P
,其中这个 S
和 R_X86_64_PLT32
中的 L
是一样的。
getStr() 方法中的重定向
S
: 符号所在的内存地址(.rodata
):0x00
(基准地址) +0x74
(.text
占用的偏移)+0x04
(.data
占用的偏移) +0x00
(对应常量值在.rodata
中的偏移)A
:-0x04
P
:0x00
(基准地址) +0x46
。
所以最后计算出来的结果是 0x2e
,所以 0x43
地方的值就应该由 48 8d 05 00 00 00 00
修改为 48 8d 05 2e 00 00 00
setVar() 方法中的重定向
S
:0x00
(基准地址) +0x74
(.text
占用偏移) +0x00
(var
全局变量在.data
中的偏移)A
:-0x04
P
:0x00
(基准地址) +0x5c
计算出来的结果是 0x14
,所以 0x5a
地方的值就因该由 89 05 00 00 00 00
修改为 89 05 14 00 00 00
getVar() 方法中的重定向
S
:0x00
(基准地址) +0x74
(.text
占用偏移) +0x00
(var
全局变量在.data
中的偏移)A
:-0x04
P
:0x00
(基准地址) +0x6d
计算出来的结果是 0x03
,所以 0x6b
地方的值就因该由 8b 05 00 00 00 00
修改为 8b 05 03 00 00 00
总结
到这里相信你应该知道 ELF
文件中是如何调用内部的方法,常量和全体变量了,如果没有看懂就多看几遍,我自己都不知道看了多少遍,下一篇文章介绍如何调用 .so
中的方法。