MASM x64中的跳转表实现?

问题描述 投票:1回答:1

我正在尝试使用跳转表在程序集(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。如果找不到更好的解决方案,那将是我要做的。

assembly x86-64 masm
1个回答
5
投票

仅当在寻址模式下没有其他寄存器时,RIP相对寻址才起作用。

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

MacOS尚无解决方法,您不能在那里使用64位绝对寻址。 -no-pie显示如何使用相对RIP的Mach-O 64-bit format does not support 32-bit absolute addresses. NASM Accessing Array索引静态数组以将表地址存入寄存器,同时避免使用32位绝对地址。

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


我认为您有太多的间接级别。由于您已经将函数指针加载到寄存器中,因此应该使用lea而不是jmp r10。在所有可能的分支错误预测之前,将所有负载预先存储在寄存器中会使它们更快地进入流水线,因此,如果您有许多备用寄存器,也许是个好主意。

最好是内联一些后面的块,如果它们很小,因为任何给定的RCX值可到达的块都无法通过其他方式到达。因此,最好将所有jmp [r10]func_21内联到func_31,以此类推,对于func_11。您可以使用汇编程序宏来简化此操作。

实际上重要的是func_12末尾的跳变[[总是转到func_11。可以采用其他方法达到目标,例如来自跳过表1的其他间接分支。这没有理由不让func_21进入表1。如果func_11仍然是不是从func_21落入的执行路径的有效入口点,则它仅限制了可以在这2个块之间进行的优化。


但是无论如何,您可以像这样实现代码。如果您对其进行了优化,则可以删除以后的调度步骤和相应的负载。

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

func_11

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

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_11可以以

21,然后是jmp [r10+8]

结尾,而不是在表格之间加上+偏移量,以提高空间局部性。 L1d加载延迟只有几个周期,因此与检查是否在第一个间接分支之前加载到寄存器相比,CPU在检查分支预测的正确性方面没有太多额外的延迟。 (我正在考虑第一个分支的预测错误的情况,因此OoO exec在发出正确的路径之前,无法“看到”内存间接的jmp。)

避免使用64位绝对地址:

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

例如,即使对于确实允许绝对地址运行时修复的目标,也要看一下GCC在与位置无关的代码中编译jmp [r10+16]跳转表时的作用。

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

(与此情况下也相关dd L27 - L4

© www.soinside.com 2019 - 2024. All rights reserved.