基本概念
Jump Table即跳转表,它可以理解为一个数组,如下图所示,数组的每一项存储一个目标地址,外部代码通过不同的索引从跳转表获取对应表项,得到目标地址,实现一个间接跳转或者间接调用。跳表的应用非常广泛,比如在操作系统中用于存储系统调用和库函数,在一些计算机体系结构中也会采用跳表做中断处理。本文主要介绍跳转表在编译器中的应用。
简单案例
在C语言中,最直观的使用跳转表的例子就是访问函数指针数组并调用对应函数。函数原型如下:
typedef void (*func_ptr) (void);
void func1();
void func2();
void func3();
void func4();
void test(int argv) {
func_ptr jt[4] = {func1, func2, func3, func4};
jt[argv]();
}
采用LLVM编译器指定AArch64架构O2编译以上代码,会生成如下汇编,其中 .L__const.test(int).jt 代表一个跳转表,它包含4个表项,每个表项用于存储目标函数地址。程序通过 ldr 指令访问跳转表,获取目标函数地址,并通过 br 指令实现函数的间接调用。
test(int): // @test(int)
adrp x8, .L__const.test(int).jt
add x8, x8, :lo12:.L__const.test(int).jt
ldr x0, [x8, w0, sxtw #3]
br x0
.L__const.test(int).jt:
.xword _Z5func1v
.xword _Z5func2v
.xword _Z5func3v
.xword _Z5func4v
多路分支优化
1. 优化简介
在LLVM编译器中,存在一种将switch多路分支控制结构转化为跳转表的技术。合适的条件下将多个比较跳转转化为跳转表可以提高运行效率,该优化默认开启。
int func(int a, int b) {
int c;
switch (a) {
case 1:
c = b + 1;
break;
case 2:
c = b - 2;
break;
case 3:
c = b | 3;
break;
case 4:
c = b & 4;
break;
default:
break;
}
return c;
}
以上面的代码为例,这是一个典型的switch多路分支控制结构。如果不启用jump table优化,编译器会将其转化为一系列比较和跳转指令。如果启用jump table优化,编译器会在函数内构造一个跳转表,通过变量a索引到目标机器代码块,其生成的关键汇编如下:
...
// %bb.1: // %entry
adrp x9, .LJTI0_0
add x9, x9, :lo12:.L