ARM 代码中 Switch case 的跳转 table 位置
Switch case's jump table position within code on ARM
在 C/C++ 中,编译器可以将 switch 语句降低为 a jump table。我注意到 ARM 和 x86 之间跳转 table 的位置不同。
x86
对于 x86(和 x86_64),跳转 table 通常位于函数 外部 (例如 .rodata)
4005e0: 48 8b 45 d8 mov -0x28(%rbp),%rax
4005e4: 48 8b 0c c5 b0 0c 40 mov 0x400cb0(,%rax,8),%rcx
4005eb: 00
4005ec: ff e1 jmpq *%rcx
4005ee: 8b 45 e8 mov -0x18(%rbp),%eax
4005f1: 83 e8 66 sub [=11=]x66,%eax
手臂
对于 ARM,跳转 table 与函数代码 交错 。
15c: e28f2004 add r2, pc, #4
160: e7911002 ldr r1, [r1, r2]
164: e1a0f001 mov pc, r1
168: 000001a4 .word 0x000001a4
16c: 000001b4 .word 0x000001b4
170: 000001e4 .word 0x000001e4
174: 00000214 .word 0x00000214
178: 00000214 .word 0x00000214
17c: 00000214 .word 0x00000214
180: 00000214 .word 0x00000214
184: 00000214 .word 0x00000214
188: 000001c4 .word 0x000001c4
18c: 000001f4 .word 0x000001f4
以上代码是用 clang 3.5 -target arm-none-eabi -march=armv7
生成的,但类似的代码是用 gcc
生成的。
MIPS
为了完整起见,这里是 MIPS 上 switch 语句的代码。跳转 table 放在 .rodata
部分。
4002b8: 2c85000b sltiu a1,a0,11
4002bc: afc40018 sw a0,24(s8) //local var that we switch on
4002c0: 10a00021 beqz a1,400348 <main0+0xb4> // default case
4002c4: 00000000 nop
4002c8: 8fc10018 lw at,24(s8) //the var that we switch on is in at
4002cc: 00011080 sll v0,at,0x2 // v0 = at<<2
4002d0: 3c030040 lui v1,0x40 // v1 = 0x40<<16
4002d4: 00431021 addu v0,v0,v1 // v0 = (at<<2) + v1
4002d8: 8c421848 lw v0,6216(v0) // v0 = *((at<<2)+0x401848)
4002dc: 00400008 jr v0 // jump
4002e0: 00000000 nop
跳转地址table(0x00401848
)在.rodata
.
$ readelf -e /tmp/muti-sw.mips.o | grep .rodata
[ 7] .rodata PROGBITS 00401848 001848 00069a 00 A 0 0 4
以上代码是用 clang 3.9 生成的。
问题
为什么在 ARM 架构上,跳转 table 经常与函数代码交错,而在 x86 上却没有?
缓存在 ARM 上的工作方式与此有关。还有其他原因吗?
这主要与 RISC 与 CISC 哲学有关。在 ARM 上,PC 几乎是一个通用寄存器。你可以看到 add r2, pc, #4
;这会将 table 的地址放入 r2
。由于 table 是通过 PC 加载的,因此需要与代码一起使用。一个更简单的开关是可能的,
ldr r1, [r1, pc] ; get table data via 'pc'
add pc, r1 ; do switch
table:
.word offset_first_case ; ... etc.
以上完全是PC
相对的。看起来您的代码可能需要重定位。如果案例代码是高度对称的,那么 table 甚至可能不需要 pc += case * case_code_size
.
一些 ARM CPUs 支持像 xlat
这样的指令和 switch/case 实现可能取决于编译器、目标 ARM/x86 CPU、数据类型和案件的密度。例如,table 可能包含 'case,case_offset' 并进行排序,因此在 'sparse case' 情况下执行二进制搜索。
注意:由于原始 ARM 流水线大小,ARM pc
提前了两条指令(八个字节)。 ARM 在使用 PC
时保持此偏移量以保持兼容性。
在 C/C++ 中,编译器可以将 switch 语句降低为 a jump table。我注意到 ARM 和 x86 之间跳转 table 的位置不同。
x86
对于 x86(和 x86_64),跳转 table 通常位于函数 外部 (例如 .rodata)
4005e0: 48 8b 45 d8 mov -0x28(%rbp),%rax
4005e4: 48 8b 0c c5 b0 0c 40 mov 0x400cb0(,%rax,8),%rcx
4005eb: 00
4005ec: ff e1 jmpq *%rcx
4005ee: 8b 45 e8 mov -0x18(%rbp),%eax
4005f1: 83 e8 66 sub [=11=]x66,%eax
手臂
对于 ARM,跳转 table 与函数代码 交错 。
15c: e28f2004 add r2, pc, #4
160: e7911002 ldr r1, [r1, r2]
164: e1a0f001 mov pc, r1
168: 000001a4 .word 0x000001a4
16c: 000001b4 .word 0x000001b4
170: 000001e4 .word 0x000001e4
174: 00000214 .word 0x00000214
178: 00000214 .word 0x00000214
17c: 00000214 .word 0x00000214
180: 00000214 .word 0x00000214
184: 00000214 .word 0x00000214
188: 000001c4 .word 0x000001c4
18c: 000001f4 .word 0x000001f4
以上代码是用 clang 3.5 -target arm-none-eabi -march=armv7
生成的,但类似的代码是用 gcc
生成的。
MIPS
为了完整起见,这里是 MIPS 上 switch 语句的代码。跳转 table 放在 .rodata
部分。
4002b8: 2c85000b sltiu a1,a0,11
4002bc: afc40018 sw a0,24(s8) //local var that we switch on
4002c0: 10a00021 beqz a1,400348 <main0+0xb4> // default case
4002c4: 00000000 nop
4002c8: 8fc10018 lw at,24(s8) //the var that we switch on is in at
4002cc: 00011080 sll v0,at,0x2 // v0 = at<<2
4002d0: 3c030040 lui v1,0x40 // v1 = 0x40<<16
4002d4: 00431021 addu v0,v0,v1 // v0 = (at<<2) + v1
4002d8: 8c421848 lw v0,6216(v0) // v0 = *((at<<2)+0x401848)
4002dc: 00400008 jr v0 // jump
4002e0: 00000000 nop
跳转地址table(0x00401848
)在.rodata
.
$ readelf -e /tmp/muti-sw.mips.o | grep .rodata
[ 7] .rodata PROGBITS 00401848 001848 00069a 00 A 0 0 4
以上代码是用 clang 3.9 生成的。
问题
为什么在 ARM 架构上,跳转 table 经常与函数代码交错,而在 x86 上却没有?
这主要与 RISC 与 CISC 哲学有关。在 ARM 上,PC 几乎是一个通用寄存器。你可以看到 add r2, pc, #4
;这会将 table 的地址放入 r2
。由于 table 是通过 PC 加载的,因此需要与代码一起使用。一个更简单的开关是可能的,
ldr r1, [r1, pc] ; get table data via 'pc'
add pc, r1 ; do switch
table:
.word offset_first_case ; ... etc.
以上完全是PC
相对的。看起来您的代码可能需要重定位。如果案例代码是高度对称的,那么 table 甚至可能不需要 pc += case * case_code_size
.
一些 ARM CPUs 支持像 xlat
这样的指令和 switch/case 实现可能取决于编译器、目标 ARM/x86 CPU、数据类型和案件的密度。例如,table 可能包含 'case,case_offset' 并进行排序,因此在 'sparse case' 情况下执行二进制搜索。
注意:由于原始 ARM 流水线大小,ARM pc
提前了两条指令(八个字节)。 ARM 在使用 PC
时保持此偏移量以保持兼容性。