【问题标题】:Jump table implementation in MASM x64?MASM x64 中的跳转表实现?
【发布时间】:2019-08-20 09:52:35
【问题描述】:

我正在尝试使用跳转表在程序集(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

现在它编译得很好,但它没有与我的主 C++ 插件(一个 DLL)链接,给我以下链接器错误:

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

我怎样才能正确地实现这样的事情?也许更好的措辞:如何在 MASM64 中正确实现跳转表并从这些表中跳转/调用地址?

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

【问题讨论】:

  • Mach-O 64-bit format does not support 32-bit absolute addresses. NASM Accessing Array基本一样的问题——[table + reg*scale]不能使用RIP-relative寻址。
  • 我不明白你为什么想要一连串的间接跳转。在这个例子中,除了第一个 jmp [jumpTable1 + rcx*8] 之外的所有跳转都只有一个可能的目标,所以应该是 jmp rel32 直接跳转(因为只有一个已知值 rcx 才能到达这个块),或者更好地只是下降 -通过。您可能需要一些宏的东西来跟踪应该跳转到哪里。或者将代码块放入宏中,以便您可以内联它们。
  • @PeterCordes 感谢您的 cmets。我没有看到与您链接的线程相同的问题。除了 (N)asm 与 (M)asm 之外,还有完全不同的事情发生。我刚刚从 scatch 编写的示例,真正的问题要复杂得多,但是这个示例应该足以(如果填充了数据操作)在将代码包含在 C++ dll 项目中时生成链接器错误。
  • 这是完全相同的问题:[table + rcx*8] 只能在 x86 机器代码中编码为 [disp32 + rcx*8] ,因此仅适用于适合 32 位带符号绝对地址的非大地址。 MacOS(总是)和 Windows(使用 LARGEADDRESSAWARE:YES)都不允许这样做,所以这是同一个问题,解决方案是相同的。这是机器代码编码限制,与您使用的汇编程序无关。也是和32-bit absolute addresses no longer allowed in x86-64 Linux?一样的问题,其中-no-pie=没有大地址
  • 是的,你仍然可以索引静态数组!我链接的第一个问答向您展示了如何;这就是我链接它的原因。

标签: assembly x86-64 masm


【解决方案1】:

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 对静态数组进行索引,以将表地址放入寄存器,同时避免使用 32 位绝对地址。

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


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

如果后面的块很小,内联会更好,因为任何给定 RCX 值可访问的块无法以任何其他方式访问。因此,最好将所有func_21func_31 内联到func_11,以此类推func_12。您可以使用汇编器宏来简化此操作。

实际上重要的是func_11 末尾的跳转总是 转到func_21。可以通过其他方式到达该区块,例如来自跳过表 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

或者如果你只想为一个表绑定一个寄存器,也许使用:

    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

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


跳转目标的缓存位置

在您的跳跃目标矩阵中,任何一次使用都会沿着“列”向下移动以跟随一些跳跃链。转置布局显然会更好,这样一连串的跳转会沿着“行”移动,这样所有的目标都来自同一个缓存行。

即安排你的桌子,使func_1121可以以jmp [r10+8]结尾,然后jmp [r10+16],而不是+表格之间的一些偏移,以改善空间局部性。 L1d 加载延迟只有几个周期,因此 CPU 在检查分支预测的正确性时没有太多额外的延迟,而不是在第一个间接分支之前加载到寄存器中。 (我正在考虑第一个分支预测错误的情况,因此 OoO exec 在正确的路径开始发出之前无法“看到”内存间接 jmp。)


避免使用 64 位绝对地址:

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

例如,看看 GCC 在以位置无关代码编译 switch 跳转表时做了什么,即使对于允许运行时修复绝对地址的目标也是如此。

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=84011 包含一个测试用例;在Godbolt with GCC's MASM-style .intel_syntax 上查看。它使用表中的movsxd 加载,然后使用add rax, rdx / jmp rax。表格条目是 dd L27 - L4dd L25 - L4 之类的东西(其中这些是标签名称,给出了从跳转目标到“锚点”L4 的距离)。

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

【讨论】:

  • 现在我开始明白了。非常感谢你。 P.S.:正如我所说,表格并没有像示例中那样均匀填充,所以虽然只有一个索引告诉应该从表格中取出哪些地址,但并不是没有无法访问的代码。
  • @St0fF:嗯?我没有说你有任何无法访问的代码。但是,如果到达func_21唯一 方法是从func_11 的末尾,您应该直接进入它而不是通过间接分支到达它。即使这不是真的,并且有其他方法可以到达 func_21(例如,如果有其他东西跳过表 1),您仍然可以将它放在那里,以便 func_11 可以通过它。如果它像一个 vtable,那么这就像将 foo::f1() 内联到 foo::f2() 因为(使用静态 vtables)“类型”是静态已知的,所以我们知道没有覆盖。
  • 抱歉,我说得不够清楚(非母语人士的问题)。事实是:真实的表看起来有很大的不同,因此 func_21 后面可以跟多个其他函数,这就是我想这样做的原因。构建几个表比编写大量代码来覆盖所有可能的组合要容易得多。 P.S.:它链接,所以现在我可以开始调试并发现更多错误;)
  • @St0fF:您实际上不必编写大量代码;您可以将每个块定义为 macro 并通过编写 func_1: BLOCK11 将它们粘合在一起; BLOCK21 ; BLOCK31 什么的。您也可以在其他任何需要的地方使用 BLOCK21 宏。如果块很小,这种内联可能是值得的。或者,如果块之间存在冗余,只需在 C++ 中编写内联函数并让编译器内联 + 优化函数之间!如果你的块足够大,分支开销可能不会受到太大影响(特别是如果分支在实际使用中预测良好)。
  • @St0fF:在您的实际用例中,我猜您可能在 table1 或其他东西中有一些重复的条目,并且您的跳转矩阵的每个“列”都是一些独特的块链?这仍然不排除使用宏进行内联。 (顺便说一句,沿着“行”走显然会更好,这样所有目标都来自同一个缓存行。所以你只需要jmp [r10+8]而不是+表之间的一些偏移量)。
猜你喜欢
  • 2013-10-17
  • 1970-01-01
  • 2015-11-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-07
  • 1970-01-01
相关资源
最近更新 更多