【问题标题】:GCC Jump Table initialization code generating movsxd and add?GCC跳转表初始化代码生成movsxd并添加?
【发布时间】:2019-02-10 21:43:24
【问题描述】:

当我在 GCC 中编译一个带有优化的 switch 语句时,它会设置一个这样的跳转表,

(fcn) sym.foo 148
  sym.foo (unsigned int arg1);
; arg unsigned int arg1 @ rdi
0x000006e0      83ff06         cmp edi, 6                              ; arg1
0x000006e3      0f87a7000000   ja case.default.0x790
0x000006e9      488d156c0100.  lea rdx, [0x0000085c]
0x000006f0      89ff           mov edi, edi
0x000006f2      4883ec08       sub rsp, 8
0x000006f6      486304ba       movsxd rax, dword [rdx + rdi*4]
0x000006fa      4801d0         add rax, rdx                            ; '('
;-- switch.0x000006fd:
0x000006fd      ffe0           jmp rax                                 ; switch table (7 cases) at 0x85c

MOVSXDADD 是最好的方法吗,

movsxd rax, dword [rdx + rdi*4]
add rax, rdx

这与使用LEAdisplacement 不一样

lea rax, [rdx + rdi*4 + rdx]

我突然想到我可能不明白这里发生了什么。 RDX 似乎是跳表的开始。 RDI 是 switch 语句的传入参数。为什么我们要添加两次RDX

这是我用-O3编译的switch语句,

int foo (int x) {
  switch(x) {
    //case 0: puts("\nzero"); break;
    case 1: puts("\none"); break;
    case 2: puts("\ntwo"); break;
    case 3: puts("\nthree"); break;
    case 4: puts("\nfour"); break;
    case 5: puts("\nfive"); break;
    case 6: puts("\nsix"); break;
  }
  return 0;
}

【问题讨论】:

    标签: gcc x86 switch-statement x86-64 jump-table


    【解决方案1】:

    GCC 在其跳转表中使用相对 位移(相对于表的基数),而不是绝对地址。 因此,跳转表本身与位置无关,并且在重新定位时不需要修复, 例如作为加载 PIE 可执行文件或 PIC 共享库的一部分。

    如果您使用-fno-pie -no-pie 编译,gcc 可能会选择使用带有jmp [table + rdi*8] 的跳转目标表

    像 x86-64 Linux 这样的目标确实支持运行时数据修复,所以一个简单的跳转表是可能的。但是有些目标根本不支持修复,这就是 gcc -fPIC / -fpie 完全避免它的原因。这种潜在的优化是gcc bug 84011。有关更多信息,请参见那里的讨论。


    不幸的是 gcc 使用的是跳转表,而不是意识到每种情况之间的唯一区别是数据,而不是代码。所以实际上它只需要一个字符串指针的表查找。 (如果愿意,可以使用相对位移来完成。)

    这是一个单独的错过优化,我报告为bug 85585。 (这提醒了我,我有一个半写的后续,我应该完成并发布。)

    【讨论】:

    • 啊,这就是相对位移的样子,现在完全有道理了!你有什么建议我读的关于相对位移的吗?
    • @EvanCarroll:我认为这个词可以自我解释。 disp = target - base,它适合 32 位,因为静态代码 + 数据始终在 32 位位移范围内。要解码,只需使用 MOVSXD 和 ADD 执行 target = base + sign_extend(disp)。在调试器中单步执行并查看值(如果这还不明显)。和/或查看编译器输出的表数据。
    【解决方案2】:

    MOVSXD 和 ADD 是最好的方法吗?

    这可以通过 addqword 内存操作数来完成。当然,缺点是它会使桌子变大一倍。

    这和使用带位移的 LEA 不一样

    不,lea 不访问内存。

    为什么我们要添加两次 RDX?

    第一次使用它作为表的基础来索引它。该表包含相对于自身的地址,因此将 RDX 添加到表中的值会创建一个绝对地址。

    顺便说一句,这很容易改进:

    mov edi, edi     ; truncate rdi to 32bit
    

    在当前架构上,self-mov 不能被 mov 消除,因此最好移动到其他寄存器。

    【讨论】:

      猜你喜欢
      • 2011-07-14
      • 2016-10-26
      • 2021-04-05
      • 2012-05-31
      • 1970-01-01
      • 2011-06-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多