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 时保持此偏移量以保持兼容性。