在 MASM x64 中跳转 table 实现?

Jump table implementation in MASM x64?

我正在尝试使用跳转 tables 在汇编(MASM64、Windows、x64)中实现一个算法。基本思想是:我需要对数据进行 3 种不同类型的操作。这些操作取决于一些变量,但我发现实现大量切换和许多长实现很乏味。

PUBLIC superFunc@@40 ;__vectorcall decoration
.DATA
ALIGN 16
jumpTable1 qword func_11, func_12, func_13, func_14
jumpTable2 qword func_21, func_22, func_23, func_24
jumpTable3 qword func_31, func_32, func_33, func_34

.CODE
superFunc@@40 PROC
        ;no stack actions, as we should do our stuff as a leaf function
        ;assume the first parameter (rcx) is our jumpTable index, and it's
        ;the same index for all functions
        mov     rax,    qword ptr [rcx*8 + offset jumpTable1]
        mov     r10,    qword ptr [rcx*8 + offset jumpTable2]
        mov     r11,    qword ptr [rcx*8 + offset jumpTable3]
        jmp     qword ptr [rax]
superFunc@@40 ENDP
func_11:
        [...] do something with data
        jmp     qword ptr [r10]
func_12: ; shorted, simply does something else to the data and jumps thru r10
[...]
func_21:
        [...] do something with data
        jmp     qword ptr [r11]
func_22: ; shorted, simply does something else to the data and jumps thru r11
[...]
func_31:
        [...] do something with data
        ret
func_32: ; shorted, simply does something else to the data and returns
END

现在可以很好地编译了,但是 link 我的主要 C++ 插件(一个 DLL)没有,给我以下 linker 错误:

LINK : warning LNK4075: ignoring '/LARGEADDRESSAWARE:NO' due to '/DLL' specification
error LNK2017: 'ADDR32' relocation to 'jumpTable1' invalid without /LARGEADDRESSAWARE:NO

我怎样才能正确地实现这样的东西?也许措辞更好:How do I implement jump tables and jumping/calling to addresses from those tables correctly in MASM64?

P.S.: 我可以在 C++ 中设置一个函数 table 并通过参数告诉 superFunc。如果找不到更好的解决方案,我将这样做。

RIP 相对寻址仅在寻址模式下没有其他寄存器时有效。

[table + rcx*8] 只能在 x86-64 机器代码中编码为 [disp32 + rcx*8],因此 仅适用于非大型适合 32 位带符号绝对地址的地址 。 Windows 显然可以用 LARGEADDRESSAWARE:NO 来支持这一点,就像 Linux compiling with -no-pie 一样可以解决同样的问题。

MacOS 没有解决方法,您根本不能在那里使用 64 位绝对寻址。 Mach-O 64-bit format does not support 32-bit absolute addresses. NASM Accessing Array 展示了 如何使用相对于 RIP lea 索引静态数组以将 table 地址放入寄存器 同时避免 32 位绝对地址。

你的跳转 tables 本身很好:它们使用 64 位 绝对地址,可以在虚拟地址 space 中的任何位置重定位。 (在 ASLR 之后使用加载时修正。)


我认为你的间接层级太多了。由于您已经将函数指针加载到寄存器中,因此您应该使用 jmp r10 而不是 jmp [r10]。在任何可能的分支预测错误之前,预先将所有负载加载到寄存器中可以使它们更快地进入管道,因此如果您有很多寄存器可以使用,可能 是个好主意。

更好的方法是内联一些后面的块,如果它们很小,因为任何给定 RCX 值可到达的块无法通过任何其他方式到达。因此,将所有 func_21func_31 内联到 func_11 中会好得多,对于 func_12 依此类推。您可以使用汇编程序宏来简化此过程。

实际上重要的是 func_11 总是 末尾的跳转到 func_21。有其他方法可以到达那个街区很好,例如来自其他跳过 table 的间接分支 1. func_11 没有理由不陷入其中;如果 func_21 仍然必须是未从 func_11.

落空的执行路径的有效入口点,它只会限制您可以在这两个块之间进行的优化

但无论如何,您可以像这样实现您的代码。如果做优化的话,可以去掉后面的调度步骤和对应的负载。

我认为这是有效的 MASM 语法。如果不是,应该仍然清楚所需的机器代码是什么。

    lea    rax,  [jumpTable1]          ; RIP-relative by default in MASM, like GAS [RIP + jumpTable1] or NASM [rel jumpTable1]

    ; The other tables are at assemble-time-constant small offsets from RAX
    mov    r10,  [rax + rcx*8 + jumpTable3 - jumpTable1]
    mov    r11,  [rax + rcx*8 + jumpTable2 - jumpTable1]
    jmp    [rax + rcx*8]


func_11:
    ...
    jmp  r10         ; TODO: inline func_21  or at least use  jmp func_21
                     ;  you can use macros to help with either of those

或者,如果您只想为一个 table 绑定一个寄存器,可以使用:

    lea    r10,  [jumpTable1]    ; RIP-relative LEA
    lea    r10,  [r10 + rcx*8]   ; address of the function pointer we want
    jmp    [r10]

align 8
func_11:
    ...
    jmp   [r10 + jumpTable2 - jumpTable1]    ; same index in another table


align 8
func_12:
    ...
    jmp   [r10 + jumpTable3 - jumpTable1]    ; same index in *another* table

这充分利用了 tables 之间的已知静态偏移。


跳转目标的缓存位置

在您的跳跃目标矩阵中,任何一次使用都会跨过 "column" 以跟随一些跳跃链。显然,转置您的布局会更好,这样一连串的跳转会沿着 "row" 进行,因此所有目标都来自同一缓存行。

即安排你的table,这样func_1121可以以jmp [r10+8]结尾,然后是jmp [r10+16],而不是+一些tables 之间的偏移量,以改善空间局部性。 L1d 加载延迟只有几个周期,因此 CPU 在检查分支预测的正确性时没有太多额外延迟,如果您在第一个间接分支之前加载到寄存器中。 (我正在考虑第一个分支预测错误的情况,所以 OoO exec 不能 "see" 内存间接跳转,直到它的正确路径开始发出。)


避免使用 64 位绝对地址:

您还可以存储 32 位(或 16 位或 8 位)相对于跳转目标附近的某个参考地址或相对于 table 本身的偏移量。

例如,看看 GCC 在编译 switch 跳转 table 位置无关代码时所做的事情,即使对于允许运行时绝对地址修正的目标也是如此。

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=84011 includes a testcase; see it on Godbolt with GCC's MASM-style .intel_syntax。它使用来自 table 的 movsxd 加载,然后是 add rax, rdx / jmp rax。 table 条目类似于 dd L27 - L4dd L25 - L4(其中这些是标签名称,给出了从跳跃目标到 "anchor" L4 的距离)。

(也与那个案例相关https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85585)。