理解x64代码模型

来源:互联网 发布:电脑鼠标点击软件 编辑:程序博客网 时间:2024/05/22 00:47

原作者:Eli Bendersky

http://eli.thegreenplace.net/2012/01/03/understanding-the-x64-code-models

在编写x64架构代码时一个有趣的问题是使用哪个代码模型。这可能是一个不广为人知的议题,但如果想理解编译器生成的x64机器代码,熟悉代码模型是有教育意义的。这与优化及对哪些真正关心性能,从哪怕最小的指令也要抠性能的人有密切关系。

关于这个议题网上或别处的资料很少。目前为止最重要的资源是官方的x64 ABI,你可以从x86-64.org网页获取(下面我将其简称为ABI)。在gccman-page上也有一点信息。本文的目的是提供一个通俗易懂的参考,给出该议题的一些讨论,及展示真实代码中的概念的具体例子。

一项重要的免责声明:这不是给初学者的教程。要求深刻理解C及汇编语言,加上对x64架构的基本了解。

代码模型——动机

在x64上对代码及数据的访问通过指令相对取址模型(x64的说法是RIP-相对)完成。在这些指令里RIP的相对偏移限制在32比特。那么在32比特不够用时我们该怎么做?如果程序超过2GB该怎么办?当指令尝试访问某些代码(或数据),但不能通过RIP的32比特偏移做到时,就会出现问题。

这个问题的一个解决办法是放弃RIP相对取址模式,对所有的代码及数据的引用使用64位绝对偏移。但这个办法代价很高——执行最简单的操作需要更多的指令。这是为了(极少见的)极其庞大的程序或库,而在所有代码中支付高昂的代价。

因此,折中的办法是代码模式[1]。代码模式是程序员与编译器间的一个正式的协议,其中程序员陈述他对最终程序将进入的当前正在编译的目标文件大小的意愿[2]

代码模型就是为了让程序员告诉编译器:不要担心,这个对象只会进入不那么大的程序,因此你可以使用快速的RIP相对取址模式。相反,他可以告诉编译器:这个对象期望链接进巨大的程序,因此请使用慢但安全的,带有完整64位偏移的绝对取址模式。

这里会谈什么

上面谈论的两个场景有名字:向编译器承诺,在编译出的对象中32位相对偏移对所有的代码及对象访问都够用的小代码模型(thesmall code model)。另一方面,大代码模型(The large code model)告诉编译器不要进行任何假设,使用64位绝对取址模型访问代码及数据。更有趣的是,还有中间的道路,称为中等代码模型(the mediumcode model)。

非PIC及PIC代码都分别存在这些代码模型。本文准备讨论所有6种变形。

C例子源码

我将使用下面的C程序,以不同的代码模型,来展示本文中讨论的概念。在这个代码里,main函数访问4个不同的全局数组及一个全局函数。数组有两个不同的参数:大小及可见性。大小对解释中等代码模型是重要的,对小的及大的模型将不起作用。可见性是static(仅在源文件中可见)或全局(对链接到该程序的所有其他对象可见)。这个区别对PIC代码模型是重要的。

int global_arr[100] = {2,3};

staticint static_arr[100] = {9,7};

int global_arr_big[50000] = {5,6};

staticint static_arr_big[50000] = {10,20};

 

intglobal_func(int param)

{

    return param *10;

}

 

intmain(int argc,constchar* 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 -ocodemodel1_large_pic.o

小代码模型

man gcc这样谈到小代码模型:

-mcmodel=small

为小代码模型生成代码:必须在地址空间的低2GB链接程序及其符号。指针是64位的。可以静态或动态链接程序。这是缺省的代码模型。

换而言之,编译器假定所有的代码及数据可以从代码的任何指令处以32位RIP相对偏移访问。让我们看一下例子C程序以非PIC小代码模型编译的汇编:

> 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 0000       mov    0x0(%rip),%eax

  3c: 01 45 fc                add    %eax,-0x4(%rbp)

    t += static_arr[7];

  3f: 8b 05 00 00 0000       mov    0x0(%rip),%eax

  45: 01 45 fc                add    %eax,-0x4(%rbp)

    t +=global_arr_big[7];

  48: 8b 05 00 00 0000       mov    0x0(%rip),%eax

  4e: 01 45 fc                add    %eax,-0x4(%rbp)

    t +=static_arr_big[7];

  51: 8b 05 00 00 0000       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 5entries:

  Offset          Info           Type           Sym. Value    Sym. Name + Addend

00000000002f  001500000002R_X86_64_PC32     0000000000000000global_func - 4

000000000038  001100000002R_X86_64_PC32     0000000000000000global_arr + 18

000000000041  000300000002R_X86_64_PC32     0000000000000000 .data+ 1b8

00000000004a  001200000002R_X86_64_PC32     0000000000000340 global_arr_big+ 18

000000000053  000300000002R_X86_64_PC32     0000000000000000 .data+ 31098

让我们完整地解码对global_arr的访问作为一个例子。这里是相关的汇编部分:

  t += global_arr[7];

36:       8b 05 00 00 0000       mov    0x0(%rip),%eax

3c:       01 45 fc                add    %eax,-0x4(%rbp)

RIP相对取址是相对于下一条指令的。因此填充到mov指令的偏移应该相对于0x3c。相关的重定位是第二个。指向0x38处mov的操作数。它是R_X86_64_PC32,意即:获取符号值,加上Addend,减去这个重定位指向的偏移。如果计算,你会发现这最终会置入下一条指令与global_arr之间的相对偏移,加上0x1c。这个相对偏移正是我们需要的,因为0x1c就是表示“数组的第7个int”(x64的每个int是4字节)。因此使用RIP相对取址,指令正确地访问global_arr[7]。

这里要注意的另一件有趣的事是,尽管访问static_arr的指令是类似的,它的重定位有不同的符号,指向.data段而不是特定的符号。这是因为静态数组由链接器放在.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 0000          mov    $0x0,%eax

2e:       e8 00 00 0000          callq  33 <main+0x1e>

33:       89 45 fc                mov    %eax,-0x4(%rbp)

Callq的操作数也是RIP相对的,一次这里的R_X86_64_PC32重定位的工作类似于向操作数放置到global_func的实际相对偏移。

总结,因为小代码模型向编译器承诺在最终程序中所有代码及数据可以使用32位RIP相对偏移访问,编译器可以为访问这些类型的对象生成简单且高效的代码。

大代码模型

摘自man gcc:

-mcmodel=large

为大模型生成代码:这个模型不对地址及段的大小进行假设。

下面是以非PIC大代码模型编译的main的汇编代码:

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 0000    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 0000    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 0000    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 0000    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 0000    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 5entries:

  Offset          Info           Type           Sym. Value    Sym. Name + Addend

000000000030  001500000001R_X86_64_64       0000000000000000global_func + 0

00000000003f  001100000001R_X86_64_64       0000000000000000global_arr + 0

00000000004f  000300000001R_X86_64_64       0000000000000000 .data+ 1a0

00000000005f  001200000001R_X86_64_64       0000000000000340global_arr_big + 0

00000000006f  000300000001R_X86_64_64       0000000000000000 .data+ 31080

大代码模型也是相当统一——没有代码段及数据段的大小的假定,因此所有的数据都以类似的方式访问。让我们再次选global_arr:

  t += global_arr[7];

3d:       48 b8 00 00 0000 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的地址。第二条将 (rax) + 0x1c处的字载入eax。

这样,让我们关注0x3d处的指令。它是movabs——x64上绝对64位版本的mov。它可以将一个完整的64位立即数转入一个寄存器。汇编代码中这个立即数的值是0,因此我们必须求助于重定位表。0x3f处的操作数有一个R_X86_64_64重定位。这是一个绝对重定位,它只是表示——将符号值+addend放进偏移。换而言之,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 0000          mov    $0x0,%eax

2e:       48 ba 00 00 0000 00    movabs $0x0,%rdx

35:       00 00 00

38:       ff d2                   callq  *%rdx

3a:       89 45 fc                mov    %eax,-0x4(%rbp)

在一个熟悉的movabs之后,我们有一条调用一个地址在rdx的函数的call指令。瞄一眼相关的重定位,显然这非常类似于数据访问。

很明显,大代码模型完全没有对数据与代码段大小,或者符号在哪里做出任何假设。它只是到处都采取“安全道路”,使用绝对64位move来引用符号。当然,这是有代价的。注意到与小代码模型相比,现在访问任何符号都需要一条额外的指令。

因此,我们注意到了两个极端。小代码模型很高兴地假定任何东西都放进低2GB内存,而大模型假定任何东西及符号都可能出现在整个64位地址空间的任何地方。中间代码模型则是一个折衷。

中间代码模型

像前面那样,让我们以查询man gcc开始:

-mcmodel=medium

为中间模型生成代码:程序被链接进地址空间的低2GB。小符号也放在那里。大小超过-mlarge-data-threshold的符号放在大数据段或bss段,并且可以位于2GB以上。程序可以被静态或动态链接。

类似于小代码模型,中间代码模型假定所有的代码被链接进低2GB。另一方面,数据被分为“大数据”及“小数据”。小数据被假定链接进低2GB。相反,不限制大数据在内存的位置。在大于给定的门限选项时,数据被认为是大的,这个值缺省是64KB。

有趣的是注意到在中间代码模型里,为大数据构建了特殊的段——.ldata与.lbss(对应.data与.bss)。不过对本文而言它并不重要,因此我准备绕过这个议题。更多细节参考ABI。

现在应该清楚为什么例子C代码有那些_big数组。这旨在让中间代码模式视它们为“大数据”(每个200KB,它们当然是)。下面是汇编:

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 0000       mov    0x0(%rip),%eax

  3c: 01 45 fc                add    %eax,-0x4(%rbp)

    t += static_arr[7];

  3f: 8b 05 00 00 0000       mov    0x0(%rip),%eax

  45: 01 45 fc                add    %eax,-0x4(%rbp)

    t += global_arr_big[7];

  48: 48 b8 00 00 00 0000    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 0000    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],因此让它超过2GB门限的是静态链接的大块的数据(可能是某些大的查找表。)。中间代码模型将这些大块数据与其他部分分开,特别地处理它们。所有调用函数及访问其他较小符号的代码将像小代码模型那么高效。仅实际访问大符号的代码将使用类似于大代码模型的完整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 5entries:

  Offset          Info           Type           Sym. Value    Sym. Name + Addend

00000000002f  001600000004R_X86_64_PLT32    0000000000000000global_func - 4

000000000039  001100000009R_X86_64_GOTPCREL 0000000000000000 global_arr - 4

000000000045  000300000002R_X86_64_PC32     0000000000000000 .data+ 1b8

00000000004f  001200000009R_X86_64_GOTPCREL 0000000000000340 global_arr_big - 4

00000000005b  000300000002R_X86_64_PC32     0000000000000000 .data+ 31098

因为数据大小的区别在小模型里不起作用,我们将关注局部(静态)与全局符号间的差别,在生成PIC时,这个差别发挥了作用。

正如你能看到的,为静态数组生成的代码与非PIC情形里生成的代码完全等价。这是x64架构的恩赐之一——除非从外部访问符号,归因于数据的RIP相对取址,无需等价你就能得到PIC。使用的指令与重定位也是一样的,因此我们不再过一遍了。

这里有趣的地方是全局数组。回忆在PIC里,全局数据必须通过GOT,因为它可能最终在其他共享库中找到或被使用[6]。下面是访问global_arr的生成代码:

  t += global_arr[7];

36:   48 8b 05 00 00 0000    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+ addend,减去所采用重定位的Offset。换句话说,RIP(下一条指令)与GOT中global_arr的项之间的相对偏移被填充到指令。因此在0x36处指令里放入rax的是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)

在0x2e处callq的操作数有一个R_X86_64_PLT32重定位。这个重定位表示:该符号的PLT项地址 + addend,减去所采用重定位的Offset 。换句话说,该callq将正确调用global_func的PLTtrampoline。

注意编译器所做的隐含假设——可以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 ffff    lea    -0x7(%rip),%rbx

  25: 49 bb 00 00 00 0000    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 0000    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 0000    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 0000    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 0000    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 0000    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 6entries:

  Offset          Info           Type           Sym. Value    Sym. Name + Addend

000000000027  00150000001dR_X86_64_GOTPC64  0000000000000000_GLOBAL_OFFSET_TABLE_ + 9

000000000045  00160000001fR_X86_64_PLTOFF64 0000000000000000 global_func + 0

000000000057  00110000001bR_X86_64_GOT64    0000000000000000global_arr + 0

00000000006b  000800000019R_X86_64_GOTOFF64 00000000000001a0 static_arr + 0

00000000007c  00120000001bR_X86_64_GOT64    0000000000000340global_arr_big + 0

000000000090  000900000019R_X86_64_GOTOFF64 0000000000031080 static_arr_big + 0

又一次,数据大小的区别不重要,因此我们关注在static_arr及global_arr。但首先,在这个代码里有一个我们之前没遇到过的新的prologue:

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指针,进而不需要设置它的函数prologue。在中间及大代码模型里,必须分配一个寄存器来保存位置无关对象里GOT的地址,因为AMD64 ISA不支持大于32位的立即数。

让我们看一下上面显示的prologue如何计算GOT的地址。首先,0x1e处的指令将自己的地址载入rbx。然后,一个绝对64位move执行到r11,伴随一个R_X86_64GOTPC64重定位。这个重定位表示:获取GOT地址,减去被重定位的偏移,加上addend。最后,0x2f处的指令将两者相加。结果在rbx中是GOT的绝对地址[7]。

为什么要这么麻烦去计算GOT的地址呢?嗯,一方面,正如文摘中说的,在大模型里我们不能假定32位RIP相对偏移足以访问GOT,因此我们需要一个完整64位地址。另一方面,我们仍然希望PIC,因此我们不能只是将绝对地址放进寄存器。相反,这个地址必须相对于RIP计算。这正是这里prologue做的事。它只是一个64位RIP相关计算。

无论如何,现在在rbx里我们牢牢地掌握了GOT的地址,让我们看一下如何访问static_arr:

  t += static_arr[7];

69:       48 b8 00 00 0000 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,它表示:symbol+ addend – 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 0000 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更为传统的用法。movabs的重定位R_X86_64_GOT64表示它将偏移保存到GOT,那样global_arr的地址将位于rax。0x5f处的指令从GOT获取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,它表示:global_func的PLT项地址,减去GOT地址。这被放入rdx,随后加上rbx(GOT的绝对地址)。rdx中的结果就是global_func的PLT项地址。

同样,注意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 6entries:

  Offset          Info           Type           Sym. Value    Sym. Name + Addend

000000000021  00160000001aR_X86_64_GOTPC32  0000000000000000_GLOBAL_OFFSET_TABLE_ - 4

000000000037  001700000004R_X86_64_PLT32    0000000000000000global_func - 4

000000000041  001200000009R_X86_64_GOTPCREL 0000000000000000 global_arr - 4

00000000004d  000300000002R_X86_64_PC32     0000000000000000 .data+ 1b8

000000000057  001300000009R_X86_64_GOTPCREL 0000000000000000 global_arr_big - 4

000000000063  000a00000019R_X86_64_GOTOFF64 0000000000030d40 static_arr_big + 0

首先,让我们清除不相关的函数调用。类似于小模型,在中间模型里我们假设代码的访问在RIP的32位偏移范围内。因此,调用global_func的代码完全类似于小PIC模型。小数据数组arraysstatic_arr与global_arr也一样。因此我们将关注在大数据数组上,但首先让我们讨论prologue,它与大模型是不同的:

1e:   48 8d 1d 00 00 0000    lea    0x0(%rip),%rbx

就是说,单条指令(而不是大模型里的3条)将GOT的地址获取到rbx(在重定位R_X86_64_GOTPC32的辅助下)。为什么不一样?因为在中间代码模型里,我们假设GOT本身可以32位偏移访问,因为它不是“大数据段”的部分。在大代码模型里,我们不能做这个假设,必须使用完整的64位偏移来访问GOT。

有趣的是,我们注意到访问global_arr_big的代码也类似于小PIC模型。为什么?出于相同的原因,prologue比大模型中的要短。在中间模型里,我们假设GOT本身可以32位RIP相对取址访问。global_arr_big本身却不是,但这可以被GOT覆盖,因为global_arr_big的地址实际位于GOT里,在那里它是一个完整64位地址。

至于static_arr_big,情形是不同的:

  t += static_arr_big[7];

61:   48 b8 00 00 00 0000    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里。因为这是一个大符号,不能假设它位于低2GB,这里我们需要64位PIC偏移,类似于大模型。



[1] 不要把代码模型与64位数据模型及Inte内存模型混淆,后来者是不同的议题。

[2]要记住一个重要的事情:实际指令由编译器产生,取址模式在那个阶段“成型”。编译器没有办法知道它正在编译的对象将进入哪些程序或共享库。有些可能是小的,但有些可能是大的。链接器知道结果程序的大小,但已太迟,因为链接器不能更改指令,只能根据重定位在指令中填补偏移。因此,代码模型“合同”只能由程序员在编译阶段“签署”。

[3] 如果这不够清楚,参考这篇文章.

 

[4] 尽管这成立。上次我检查,Clang的Debug+Asserts build几乎有半GB大(归咎于相当大的自动生成代码)。

[5] 除非你已经知道PIC如何工作(总体上的与x64特定的),这将是复习我更早的关于这个主题的文章的好机会—— #1与#2

[6] 因此链接器不能单凭自己完全解析这些引用,将GOT的处理留给动态载入器。

[7] 0x25 - 0x7 + GOT - 0x27 + 0x9 = GOT

0 0