当我在 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
MOVSXD
和ADD
是最好的方法吗?
movsxd rax, dword [rdx + rdi*4]
add rax, rdx
这不和用LEA
displacement
一样
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 在其跳转表中使用相对位移(相对于表的底部(,而不是绝对地址。因此,跳转表本身与位置无关,并且在重新定位时不需要修正,例如作为加载 PIE 可执行文件或 PIC 共享库的一部分。
如果你使用-fno-pie -no-pie
编译,gcc 可能会选择使用带有jmp [table + rdi*8]
像x86-64 Linux这样的目标确实支持运行时数据修复,因此一个简单的跳转表是可能的。 但是有些目标根本不支持修复,这就是为什么 gcc-fPIC
/-fpie
完全避免它的原因。 这个潜在的优化是 gcc 错误 84011。 有关详细信息,请参阅那里的讨论。
不幸的是,gcc 使用的是跳转表,而不是意识到每种情况之间的唯一区别是数据,而不是代码。 所以实际上它只需要字符串指针的表查找。 (如果愿意,可以用相对位移来完成。
这是一个单独的错过优化,我将其报告为错误 85585。 (这提醒了我,我有一个后续的半写,我应该完成并发布。
MOVSXD 和 ADD 是最好的方法吗?
只需一个具有qword
内存操作数的add
即可完成此操作。当然,缺点是它使桌子大了两倍。
这和用 LEA 位移不一样
吗
否,lea
不访问内存。
为什么我们要添加两次RDX?
第一次将其用作表的基础以索引到表中。该表保存相对于自身的地址,因此将 RDX 添加到表中的值会创建一个绝对地址。
顺便说一下,这可以很容易地改进:
mov edi, edi ; truncate rdi to 32bit
在当前架构上无法消除自移动,因此最好将自移动移动到其他寄存器。