当我在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 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 bug 84011。请参阅那里的讨论。
不幸的是,gcc使用跳转表而不是意识到每种情况之间的唯一区别是数据,而不是代码。所以它真的只需要查找字符串指针。 (如果想要,可以通过相对位移来完成。)
这是一个单独的错过优化,我报告为bug 85585。 (这提醒我,我有一个半成品的后续跟进,我应该完成并发布。)
MOVSXD和ADD是最好的方法,
它可以通过add
和qword
内存操作数来完成。当然,缺点是它使得桌子的重量增加了一倍。
与使用位移的LEA不同
不,lea
无法访问内存。
为什么我们两次添加RDX?
它第一次被用作表的基础来索引它。该表保存相对于自身的地址,因此将RDX添加到表中的值会创建一个绝对地址。
顺便说一下,这很容易改进:
mov edi, edi ; truncate rdi to 32bit
在当前架构上不能移动自动移动,因此移动到其他寄存器会更好。