在网上或其他地方有关此主题的信息很少。可用资源中最重要的是官方的x64 ABI,您可以在此处下载(以下称为“ ABI”)。一些信息也可以在
man
-pages 上找到gcc
。本文的目的是为一个主题提供可访问的建议,讨论相关问题,并通过工作中使用的良好代码来演示一些概念。
重要说明:本文不适合初学者使用。在相识之前,建议您对C和汇编器有很好的了解,并且对x64体系结构有基本的了解。
另请参阅我们先前有关相关主题的文章:x86_x64如何寻址内存
代码模型。动机部分
在x64体系结构中,代码和数据均通过相对命令(或使用x64行话,相对于RIP)地址模型进行引用。在这些命令中,从RIP的移位仅限于32位,但是在某些情况下,当命令尝试寻址部分内存或数据时,根本没有足够的32位移位,例如在处理超过2 GB的程序时。
解决此问题的一种方法是完全放弃RIP相对寻址模式,而对所有数据和代码引用进行完整的64位移位。但是,此步骤将非常昂贵:要覆盖(非常罕见的)程序和库非常大的情况,即使是整个代码中最简单的操作也将需要比平时更多的命令。
因此,代码模型成为一种折衷方案。[1]代码模型是程序员与编译器之间的正式协议,程序员在其中声明他对当前编译的目标模块将属于的预期程序的大小的意图。[2]需要代码模型,以便程序员可以告诉编译器:“不用担心,该对象模块只会进入小型程序,因此您可以使用相对于RIP的快速寻址模式。” 另一方面,它可能会告诉编译器以下内容:“我们将把这个模块链接到大型程序中,所以请使用具有完整64位移位的悠闲和安全的绝对寻址模式。”
本文将要讨论的内容
我们将讨论上述两种情况,一个小代码模型和一个大代码模型:第一个模型告诉编译器,对于对象模块中所有对代码和数据的引用,32位相对移位应该足够;第二点坚持认为编译器使用绝对64位寻址模式。此外,还有一个中间版本,即所谓的中间代码模型。
这些代码模型中的每一个都以独立的PIC和非PIC变体形式提供,我们将讨论这六个模型。
C语言的原始例子
为了演示本文讨论的概念,我将使用以下C程序并将其与各种代码模型一起编译。如您所见,该函数
main
访问四个不同的全局数组和一个全局函数。数组有两个参数:大小和可见性。大小对于解释平均代码模型很重要,在使用大小模型时都不需要大小。可见性对于PIC代码模型的操作很重要,并且是静态的(仅在源文件中可见)或全局的(对于链接到程序中的所有对象均可见)。
int global_arr[100] = {2, 3};
static int static_arr[100] = {9, 7};
int global_arr_big[50000] = {5, 6};
static int static_arr_big[50000] = {10, 20};
int global_func(int param)
{
return param * 10;
}
int main(int argc, const char* argv[])
{
int t = global_func(argc);
t += global_arr[7];
t += static_arr[7];
t += global_arr_big[7];
t += static_arr_big[7];
return t;
}
gcc
使用代码模型作为选项值-mcmodel
。另外,该标志-fpic
可用于设置PIC编译。
使用PIC通过大型代码模型编译为目标模块的示例:
> gcc -g -O0 -c codemodel1.c -fpic -mcmodel=large -o codemodel1_large_pic.o
小代码模型
man gcc在小代码模型上的报价翻译:
-mcmodel = small
为小模型生成代码:程序及其符号必须布置在地址空间的低2 GB中。指针的大小为64位。程序可以静态或动态链接。这是基本的代码模型。
换句话说,编译器可以安全地假定可以通过代码中任何命令的32位RIP相对偏移来访问代码和数据。让我们看一下通过非PIC小代码模型编译的C程序的分解示例:
> objdump -dS codemodel1_small.o
[...]
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 48 83 ec 20 sub $0x20,%rsp
1d: 89 7d ec mov %edi,-0x14(%rbp)
20: 48 89 75 e0 mov %rsi,-0x20(%rbp)
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
t += global_arr[7];
36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
3c: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr[7];
3f: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
45: 01 45 fc add %eax,-0x4(%rbp)
t += global_arr_big[7];
48: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
4e: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr_big[7];
51: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
57: 01 45 fc add %eax,-0x4(%rbp)
return t;
5a: 8b 45 fc mov -0x4(%rbp),%eax
}
5d: c9 leaveq
5e: c3 retq
如您所见,使用RIP相对移位,对所有阵列的访问以相同的方式组织。但是,在代码中,移位为0,因为编译器不知道数据段的放置位置,因此对于每次这样的访问,它都会创建一个重定位:
> readelf -r codemodel1_small.o
Relocation section '.rela.text' at offset 0x62bd8 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000002f 001500000002 R_X86_64_PC32 0000000000000000 global_func - 4
000000000038 001100000002 R_X86_64_PC32 0000000000000000 global_arr + 18
000000000041 000300000002 R_X86_64_PC32 0000000000000000 .data + 1b8
00000000004a 001200000002 R_X86_64_PC32 0000000000000340 global_arr_big + 18
000000000053 000300000002 R_X86_64_PC32 0000000000000000 .data + 31098
让我们完全解码对的访问
global_arr
。我们感兴趣的分解部分:
t += global_arr[7];
36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
3c: 01 45 fc add %eax,-0x4(%rbp)
RIP相对寻址是相对于下一个命令的,因此移位必须修补到该命令,
mov
以便它对应于0x3s。我们对第二次重定位很感兴趣,R_X86_64_PC32
它指向mov
地址处的操作数,0x38
并表示以下含义:我们取符号的值,加上术语,然后减去重定位指示的移位。如果正确计算了所有内容,您将看到结果如何在下一个命令和global_arr
,之间加上一个相对移位01
。由于它的01
意思是“数组中的第七个整数”(在x64体系结构中,每个整数的大小int
为4个字节),因此我们需要这种相对移位。因此,使用相对RIP寻址,该命令正确引用global_arr[7]
。
还要注意以下几点:即使访问命令
static_arr
在此处相似,但其重定向使用不同的字符,从而指向节而不是特定字符.data
。这是由于链接程序的作用,它将静态数组放置在该节中的已知位置,因此该数组不能与其他共享库一起使用。结果,链接器将通过此重定位解决问题。另一方面,由于它global_arr
可以被另一个共享库使用(或覆盖),因此已经动态的加载器将不得不处理到的链接global_arr
。 [3]
最后,让我们看一下对
global_func
:
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
由于操作数
callq
也是相对于RIP的,因此R_X86_64_PC32
此处的重定位与将实际相对偏移量放置到操作数中的global_func的方式相同。
总之,我们注意到由于代码模型小,编译器将未来程序的所有数据和代码视为可通过32位移位访问的,从而创建了简单有效的代码来访问各种对象。
大代码模型
man
gcc
大型代码模型中
报价的翻译:
-mcmodel = large
为大型模型生成代码:此模型不对地址和节大小做任何假设。
main
使用非PIC大型模型编译
的反汇编代码示例:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 48 83 ec 20 sub $0x20,%rsp
1d: 89 7d ec mov %edi,-0x14(%rbp)
20: 48 89 75 e0 mov %rsi,-0x20(%rbp)
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: 48 ba 00 00 00 00 00 movabs $0x0,%rdx
35: 00 00 00
38: ff d2 callq *%rdx
3a: 89 45 fc mov %eax,-0x4(%rbp)
t += global_arr[7];
3d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
44: 00 00 00
47: 8b 40 1c mov 0x1c(%rax),%eax
4a: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr[7];
4d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
54: 00 00 00
57: 8b 40 1c mov 0x1c(%rax),%eax
5a: 01 45 fc add %eax,-0x4(%rbp)
t += global_arr_big[7];
5d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
64: 00 00 00
67: 8b 40 1c mov 0x1c(%rax),%eax
6a: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr_big[7];
6d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
74: 00 00 00
77: 8b 40 1c mov 0x1c(%rax),%eax
7a: 01 45 fc add %eax,-0x4(%rbp)
return t;
7d: 8b 45 fc mov -0x4(%rbp),%eax
}
80: c9 leaveq
81: c3 retq
同样,查看重定位很有用:
Relocation section '.rela.text' at offset 0x62c18 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000030 001500000001 R_X86_64_64 0000000000000000 global_func + 0
00000000003f 001100000001 R_X86_64_64 0000000000000000 global_arr + 0
00000000004f 000300000001 R_X86_64_64 0000000000000000 .data + 1a0
00000000005f 001200000001 R_X86_64_64 0000000000000340 global_arr_big + 0
00000000006f 000300000001 R_X86_64_64 0000000000000000 .data + 31080
由于无需假设代码和数据段的大小,因此大型代码模型相当统一,并以相同的方式定义了对所有数据的访问。让我们再看一下
global_arr
:
t += global_arr[7];
3d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
44: 00 00 00
47: 8b 40 1c mov 0x1c(%rax),%eax
4a: 01 45 fc add %eax,-0x4(%rbp)
两个团队需要从数组中获得所需的值。第一个命令将绝对的64位地址放入
rax
其中,正如我们将很快看到的那样,该地址将是地址global_arr
,而第二个命令将单词from (rax) + 01
装入eax
。
因此,让我们专注于团队
0x3d
,movabs
绝对的64位版本mov
的x64体系结构。它可以将完整的64位常量直接抛出到寄存器中,并且由于在我们的反汇编代码中,此常量的值等于0,因此我们将不得不转向重定位表以寻求答案。在其中,我们将R_X86_64_64
在地址处找到操作数的绝对重定位,其0x3f
值为以下值:将符号的值加上加号返回到移位中。换一种说法,rax
将包含一个绝对地址global_arr
。
通话功能呢?
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: 48 ba 00 00 00 00 00 movabs $0x0,%rdx
35: 00 00 00
38: ff d2 callq *%rdx
3a: 89 45 fc mov %eax,-0x4(%rbp)
我们已经知道
movabs
的一个命令后面是一个命令call
,该命令在中的地址处调用一个函数rdx
。查看相应的重定位就足以了解它与数据访问的相似程度。
如您所见,大型代码模型没有对代码和数据段的大小以及字符的最终位置做任何假设,它只是通过绝对的64位步骤(一种“安全路径”)来指代字符。但是,请注意,与小代码模型相比,大模型如何强制每个字符使用额外的命令。这是安全的代价。
因此,我们遇到了两个完全相反的模型:小代码模型假定所有内容都适合底部的2 GB内存,而大模型假定所有内容都不可能,并且任何字符都可以位于整个64-位地址空间。两种模型之间的权衡是中间代码模型。
平均代码模型
和以前一样,让我们看一下引号的翻译
man
gcc
:
-mcmodel=medium
: . . , -mlarge-data-threshold, bss . , .
与小代码模型相似,中间模型假定整个代码以两个较低的千兆字节排列。但是,数据被划分为“小数据”的底部两个千兆字节,而在“大数据”的存储空间中则不受限制。当数据超过定义的上限(等于64 KB)时,数据属于大类。
还需要注意的是,当使用大数据的平均代码模型时,通过与section
.data
和相似,.bss
将创建特殊的部分:.ldata
和.lbss
。在当前文章的主题中,这并不是那么重要,但是我将稍有偏离。有关此问题的更多详细信息,请参见ABI。
现在清楚了为什么这些数组出现在示例中了。
_big
:中间模型需要它们来解释它们的“大数据”,每个大小为200 KB。在下面您可以看到反汇编的结果:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 48 83 ec 20 sub $0x20,%rsp
1d: 89 7d ec mov %edi,-0x14(%rbp)
20: 48 89 75 e0 mov %rsi,-0x20(%rbp)
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
t += global_arr[7];
36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
3c: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr[7];
3f: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
45: 01 45 fc add %eax,-0x4(%rbp)
t += global_arr_big[7];
48: 48 b8 00 00 00 00 00 movabs $0x0,%rax
4f: 00 00 00
52: 8b 40 1c mov 0x1c(%rax),%eax
55: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr_big[7];
58: 48 b8 00 00 00 00 00 movabs $0x0,%rax
5f: 00 00 00
62: 8b 40 1c mov 0x1c(%rax),%eax
65: 01 45 fc add %eax,-0x4(%rbp)
return t;
68: 8b 45 fc mov -0x4(%rbp),%eax
}
6b: c9 leaveq
6c: c3 retq
注意如何访问数组:
_big
通过大代码模型的方法访问数组,而其余数组通过小模型的方法访问。该函数也使用小代码模型方法来调用,并且重定位与前面的示例非常相似,我什至不会对其进行演示。
中等代码模型是大型模型与小型模型之间的一种熟练的折衷。程序代码不太可能会变得太大[4],因此有可能将静态排列的大块数据移动到超过2 GB的限制内,这可能是某些庞大的表搜索的一部分。由于中间代码模型可以过滤出如此大的数据块并以特殊方式处理它们,因此函数代码和小符号的调用将与小代码模型一样高效。类似于大型模型,只有访问大型符号才需要代码使用大型模型的完整64位方法。
小型PIC代码模型
现在,让我们看一下PIC代码模型选项,像以前一样,我们将从一个小模型开始。[5]您可以在下面看到通过小型PIC模型编译的代码示例:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 48 83 ec 20 sub $0x20,%rsp
1d: 89 7d ec mov %edi,-0x14(%rbp)
20: 48 89 75 e0 mov %rsi,-0x20(%rbp)
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
t += global_arr[7];
36: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
3d: 8b 40 1c mov 0x1c(%rax),%eax
40: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr[7];
43: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
49: 01 45 fc add %eax,-0x4(%rbp)
t += global_arr_big[7];
4c: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
53: 8b 40 1c mov 0x1c(%rax),%eax
56: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr_big[7];
59: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
5f: 01 45 fc add %eax,-0x4(%rbp)
return t;
62: 8b 45 fc mov -0x4(%rbp),%eax
}
65: c9 leaveq
66: c3 retq
搬迁:
Relocation section '.rela.text' at offset 0x62ce8 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000002f 001600000004 R_X86_64_PLT32 0000000000000000 global_func - 4
000000000039 001100000009 R_X86_64_GOTPCREL 0000000000000000 global_arr - 4
000000000045 000300000002 R_X86_64_PC32 0000000000000000 .data + 1b8
00000000004f 001200000009 R_X86_64_GOTPCREL 0000000000000340 global_arr_big - 4
00000000005b 000300000002 R_X86_64_PC32 0000000000000000 .data + 31098
由于大数据和小数据之间的差异在小代码模型中不起作用,因此,在通过PIC生成代码时,我们将重点关注本地符号(静态符号)与全局符号之间的差异。
如您所见,为静态数组生成的代码与非PIC情况下的代码没有区别。这是x64体系结构的优点之一:由于IP相对于数据的访问,我们获得了PIC作为奖励,至少在需要外部访问字符之前。所有命令和重定位均保持不变,因此您无需再次处理它们。
有趣的是要注意全局数组:值得回顾的是,在PIC中,全局数据必须通过GOT,因为在某些时候它们可以存储或由共享库使用[6]。您可以在下面看到要访问的代码
global_arr
:
t += global_arr[7];
36: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
3d: 8b 40 1c mov 0x1c(%rax),%eax
40: 01 45 fc add %eax,-0x4(%rbp)
我们感兴趣的重定位是
R_X86_64_GOTPCREL
:符号进入GOT的位置加上术语,再减去应用重定位的偏移。换句话说,该命令正在修补RIP(下一条指令)与为global_arr
GOT 保留的插槽之间的相对偏移。因此,实际地址rax
位于命令中0x36
的address处global_arr
。执行此步骤后,将链接重置为地址,global_arr
再将其转换为中的第七个元素eax
。
现在让我们看一下函数调用:
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
它具有操作数的搬迁
callq
地址0x2e
,R_X86_64_PLT32
:对于搬迁的应用程序的符号加上长期的负面转变PLT入口地址。换句话说,它callq
应该正确地调用PLT跳板global_func
。
注意编译器做出的隐式假设:可以通过RIP相对寻址访问GOT和PLT。在将该模型与其他PIC代码模型变体进行比较时,这一点很重要。
大型PIC代码模型
拆卸:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 53 push %rbx
1a: 48 83 ec 28 sub $0x28,%rsp
1e: 48 8d 1d f9 ff ff ff lea -0x7(%rip),%rbx
25: 49 bb 00 00 00 00 00 movabs $0x0,%r11
2c: 00 00 00
2f: 4c 01 db add %r11,%rbx
32: 89 7d dc mov %edi,-0x24(%rbp)
35: 48 89 75 d0 mov %rsi,-0x30(%rbp)
int t = global_func(argc);
39: 8b 45 dc mov -0x24(%rbp),%eax
3c: 89 c7 mov %eax,%edi
3e: b8 00 00 00 00 mov $0x0,%eax
43: 48 ba 00 00 00 00 00 movabs $0x0,%rdx
4a: 00 00 00
4d: 48 01 da add %rbx,%rdx
50: ff d2 callq *%rdx
52: 89 45 ec mov %eax,-0x14(%rbp)
t += global_arr[7];
55: 48 b8 00 00 00 00 00 movabs $0x0,%rax
5c: 00 00 00
5f: 48 8b 04 03 mov (%rbx,%rax,1),%rax
63: 8b 40 1c mov 0x1c(%rax),%eax
66: 01 45 ec add %eax,-0x14(%rbp)
t += static_arr[7];
69: 48 b8 00 00 00 00 00 movabs $0x0,%rax
70: 00 00 00
73: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
77: 01 45 ec add %eax,-0x14(%rbp)
t += global_arr_big[7];
7a: 48 b8 00 00 00 00 00 movabs $0x0,%rax
81: 00 00 00
84: 48 8b 04 03 mov (%rbx,%rax,1),%rax
88: 8b 40 1c mov 0x1c(%rax),%eax
8b: 01 45 ec add %eax,-0x14(%rbp)
t += static_arr_big[7];
8e: 48 b8 00 00 00 00 00 movabs $0x0,%rax
95: 00 00 00
98: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
9c: 01 45 ec add %eax,-0x14(%rbp)
return t;
9f: 8b 45 ec mov -0x14(%rbp),%eax
}
a2: 48 83 c4 28 add $0x28,%rsp
a6: 5b pop %rbx
a7: c9 leaveq
a8: c3 retq
重定位: 这次,大小数据之间的差异仍然无关紧要,因此我们将重点放在和上。但是首先,您需要注意这段代码中的序言,我们之前从未遇到过这样的事情:
Relocation section '.rela.text' at offset 0x62c70 contains 6 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000027 00150000001d R_X86_64_GOTPC64 0000000000000000 _GLOBAL_OFFSET_TABLE_ + 9
000000000045 00160000001f R_X86_64_PLTOFF64 0000000000000000 global_func + 0
000000000057 00110000001b R_X86_64_GOT64 0000000000000000 global_arr + 0
00000000006b 000800000019 R_X86_64_GOTOFF64 00000000000001a0 static_arr + 0
00000000007c 00120000001b R_X86_64_GOT64 0000000000000340 global_arr_big + 0
000000000090 000900000019 R_X86_64_GOTOFF64 0000000000031080 static_arr_big + 0
static_arr
global_arr
1e: 48 8d 1d f9 ff ff ff lea -0x7(%rip),%rbx
25: 49 bb 00 00 00 00 00 movabs $0x0,%r11
2c: 00 00 00
2f: 4c 01 db add %r11,%rbx
您可以在下面阅读ABI相关报价的翻译:
( GOT) AMD64 IP- . GOT . GOT , AMD64 ISA 32 .
让我们看一下上述序言如何计算GOT地址。首先,该地址处的命令
0x1e
将其自身的地址加载到中rbx
。然后,连同重定位一起,在R_X86_64_GOTPC64
中执行绝对64位步骤r11
。这种重定位的含义如下:取GOT的地址,减去移位后的值,然后加上项。最后,位于address的命令0x2f
将两个结果加在一起。结果是GOT的绝对地址rbx
。 [7]
为什么要计算GOT地址呢?首先,如引言中所述,在大型代码模型中,我们不能假设32位RIP相对移位足以满足GOT寻址要求,这就是为什么我们需要完整的64位地址的原因。其次,我们仍然希望使用PIC变体,因此我们不能简单地将绝对地址放入寄存器中。相反,地址本身必须相对于RIP计算。这就是序言的目的:它执行相对于RIP的64位计算。
无论如何,由于我们
rbx
现在有一个GOT地址,因此让我们看一下如何访问static_arr
:
t += static_arr[7];
69: 48 b8 00 00 00 00 00 movabs $0x0,%rax
70: 00 00 00
73: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
77: 01 45 ec add %eax,-0x14(%rbp)
第一个命令的重定位是
R_X86_64_GOTOFF64
:符号加术语减GOT。在我们的情况下,这是地址static_arr
和GOT地址之间的相对偏移。以下指令将结果添加到rbx
(绝对GOT地址)并通过引用重置偏移量0x1c
。为了简化这种计算的可视化,您可以在下面看到伪C示例:
// char* static_arr
// char* GOT
rax = static_arr + 0 - GOT; // rax now contains an offset
eax = *(rbx + rax + 0x1c); // rbx == GOT, so eax now contains
// *(GOT + static_arr - GOT + 0x1c) or
// *(static_arr + 0x1c)
请注意一个有趣的地方:GOT地址用作的绑定
static_arr
。通常,GOT不包含符号地址,并且由于它static_arr
不是外部符号,因此没有理由将其存储在GOT中。但是,在这种情况下,GOT用作对数据段的相对符号地址的绑定。除其他外,该地址与位置无关,可以通过完整的64位移位找到。链接器能够处理此重定位,因此无需在加载时修改代码部分。
但是呢
global_arr
?
t += global_arr[7];
55: 48 b8 00 00 00 00 00 movabs $0x0,%rax
5c: 00 00 00
5f: 48 8b 04 03 mov (%rbx,%rax,1),%rax
63: 8b 40 1c mov 0x1c(%rax),%eax
66: 01 45 ec add %eax,-0x14(%rbp)
该代码稍长,并且重定位与通常的代码不同。实际上,GOT以更传统的方式使用:重定位仅
R_X86_64_GOT64
用于movabs
告知函数将移位放置在rax
地址所在的GOT中global_arr
。地址处的命令从GOT 0x5f
获取地址global_arr
并将其放入rax
。以下命令将引用重置为global_arr[7]
,并将值放入eax
。
现在,让我们看一下的代码链接
global_func
。回想一下,在大型代码模型中,我们无法假设代码段的大小,因此我们应该假设即使访问PLT,我们也需要一个绝对的64位地址:
int t = global_func(argc);
39: 8b 45 dc mov -0x24(%rbp),%eax
3c: 89 c7 mov %eax,%edi
3e: b8 00 00 00 00 mov $0x0,%eax
43: 48 ba 00 00 00 00 00 movabs $0x0,%rdx
4a: 00 00 00
4d: 48 01 da add %rbx,%rdx
50: ff d2 callq *%rdx
52: 89 45 ec mov %eax,-0x14(%rbp)
我们感兴趣的重定位是
R_X86_64_PLTOFF64
:输入的PLT地址global_func
减去GOT地址。结果放置在中rdx
,然后放置在其中rbx
(GOT的绝对地址)。其结果是,我们得到了PLT条目的地址global_func
的rdx
。
请注意,再次将GOT用作绑定,这次是为输入PLT的移位提供地址独立的引用。
平均PIC代码模型
最后,我们将分解平均PIC模型的生成代码:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 53 push %rbx
1a: 48 83 ec 28 sub $0x28,%rsp
1e: 48 8d 1d 00 00 00 00 lea 0x0(%rip),%rbx
25: 89 7d dc mov %edi,-0x24(%rbp)
28: 48 89 75 d0 mov %rsi,-0x30(%rbp)
int t = global_func(argc);
2c: 8b 45 dc mov -0x24(%rbp),%eax
2f: 89 c7 mov %eax,%edi
31: b8 00 00 00 00 mov $0x0,%eax
36: e8 00 00 00 00 callq 3b <main+0x26>
3b: 89 45 ec mov %eax,-0x14(%rbp)
t += global_arr[7];
3e: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
45: 8b 40 1c mov 0x1c(%rax),%eax
48: 01 45 ec add %eax,-0x14(%rbp)
t += static_arr[7];
4b: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
51: 01 45 ec add %eax,-0x14(%rbp)
t += global_arr_big[7];
54: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
5b: 8b 40 1c mov 0x1c(%rax),%eax
5e: 01 45 ec add %eax,-0x14(%rbp)
t += static_arr_big[7];
61: 48 b8 00 00 00 00 00 movabs $0x0,%rax
68: 00 00 00
6b: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
6f: 01 45 ec add %eax,-0x14(%rbp)
return t;
72: 8b 45 ec mov -0x14(%rbp),%eax
}
75: 48 83 c4 28 add $0x28,%rsp
79: 5b pop %rbx
7a: c9 leaveq
7b: c3 retq
搬迁:
Relocation section '.rela.text' at offset 0x62d60 contains 6 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000021 00160000001a R_X86_64_GOTPC32 0000000000000000 _GLOBAL_OFFSET_TABLE_ - 4
000000000037 001700000004 R_X86_64_PLT32 0000000000000000 global_func - 4
000000000041 001200000009 R_X86_64_GOTPCREL 0000000000000000 global_arr - 4
00000000004d 000300000002 R_X86_64_PC32 0000000000000000 .data + 1b8
000000000057 001300000009 R_X86_64_GOTPCREL 0000000000000000 global_arr_big - 4
000000000063 000a00000019 R_X86_64_GOTOFF64 0000000000030d40 static_arr_big + 0
首先,让我们删除函数调用。与小型模型类似,在中间模型中,我们假设代码引用未超过32位RIP移位的限制,因此,用于调用
global_func
的代码与小型PIC模型中的相同代码以及小型数据数组static_arr
和的情况完全相似global_arr
。因此,我们将专注于大数据阵列,但首先让我们谈一下序言:这里它不同于大数据模型的序言。
1e: 48 8d 1d 00 00 00 00 lea 0x0(%rip),%rbx
这就是整个序幕:仅用一个命令就可以重新定位
R_X86_64_GOTPC32
GOT地址rbx
(相比之下,大型模型中只有三个)。有什么区别?关键是,由于在中间模型中GOT并非“大数据分区”的一部分,因此我们假设其可用性在32位移位内。在大型模型中,我们无法做出这样的假设,因此不得不使用完整的64位移位。
有趣的是,访问代码
global_arr_big
类似于小型PIC模型中的相同代码。发生这种情况的原因相同,即中间模型的序言比大型模型的序言短:我们将GOT的可用性视为32位RIP相对寻址的一部分。确实,global_arr_big
您无法获得这种访问权限,但是这种情况仍然涵盖了GOT,因为实际上global_arr_big
它以完整的64位地址的形式位于其中。
但是,以下情况不同
static_arr_big
:
t += static_arr_big[7];
61: 48 b8 00 00 00 00 00 movabs $0x0,%rax
68: 00 00 00
6b: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
6f: 01 45 ec add %eax,-0x14(%rbp)
这种情况类似于大型PIC代码模型,因为在这里我们仍然获得符号的绝对地址,该地址不在GOT本身中。由于这是一个大字符,不能假定其位于较低的两个千兆字节中,因此,与大型模型一样,我们需要64位PIC移位。
笔记:
[1]不要将代码模型与64位数据模型和Intel内存模型混淆,它们都是不同的主题。
[2]重要的是要记住:实际的命令是由编译器创建的,并且寻址模式在此步骤中已固定。编译器无法知道对象模块将属于哪些程序或共享库,有些可能很小,而有些却很大。链接器知道最终程序的大小,但是为时已晚:链接器只能通过重定位来修补命令的移位,而不能更改命令本身。因此,代码模型的“协议”必须由程序员在编译阶段“签署”。
[3]如果仍然不清楚,请查看下一篇文章。
[4]但是,数量正在逐渐增加。当我上次检查Clang的Debug + Asserts版本时,它几乎达到了1 GB,这在很大程度上要归功于自动生成的代码。
[5]如果您仍然不知道PIC的工作原理(一般而言,尤其是对于x64体系结构),那么该是时候熟悉以下有关该主题的文章了:一次和两次。
[6]因此,链接器无法自行解析链接,而必须将GOT处理转移到动态加载器。
[7] 0x25-0x7 + GOT-0x27 + 0x9 = GOT