了解x64架构代码模型

“我应该使用哪种代码模型?” -为x64体系结构编写代码时,经常出现但很少处理的问题。但是,这是一个非常有趣的问题,了解代码模型以了解编译器生成的x64机器代码很有用。另外,对于那些担心最小指令性能的人来说,代码模型的选择也会影响优化。



在网上或其他地方有关此主题的信息很少。可用资源中最重要的是官方的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



因此,让我们专注于团队0x3dmovabs绝对的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_arrGOT 保留的插槽之间的相对偏移因此,实际地址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地址0x2eR_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_arrglobal_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_funcrdx



请注意,再次将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_GOTPC32GOT地址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









All Articles