我正在尝试使用跳转表在程序集(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。如果找不到更好的解决方案,那将是我要做的。
仅当在寻址模式下没有其他寄存器时,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]
例如,即使对于确实允许绝对地址运行时修复的目标,也要看一下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, rdx
和jmp rax
之类的东西(其中的标签名称,给出了从跳转目标到“锚点” L4的距离)。
(与此情况下也相关dd L27 - L4
。