St0*_*0fF 1 assembly x86-64 masm
我正在尝试使用跳转表在程序集(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
Run Code Online (Sandbox Code Playgroud)
现在,它可以很好地编译,但是它不与我的主要C ++插件(DLL)链接,给了我以下链接器错误:
LINK : warning LNK4075: ignoring '/LARGEADDRESSAWARE:NO' due to '/DLL' specification
error LNK2017: 'ADDR32' relocation to 'jumpTable1' invalid without /LARGEADDRESSAWARE:NO
Run Code Online (Sandbox Code Playgroud)
我怎样才能正确地实现这样的东西?也许更好的表述:如何在MASM64中正确实现跳转表以及从这些表中正确地跳转/调用地址?
PS:我可以在C ++中设置一个函数表,并通过参数将其告知superFunc。如果找不到更好的解决方案,那将是我要做的。
仅当在寻址模式下没有其他寄存器时,RIP相对寻址才有效。
[table + rcx*8]只能在x86-64机器代码中以编码[disp32 + rcx*8],因此只能与适合32位带符号绝对地址的非大地址一起使用。Windows显然可以使用来支持此功能LARGEADDRESSAWARE:NO,例如在Linux上使用-no-pie来解决相同的问题。
MacOS尚无解决方法,您根本无法使用64位绝对寻址。 Mach-O 64位格式不支持32位绝对地址。NASM访问数组显示了如何使用相对RIP相对索引静态数组lea以将表地址保存到寄存器中,同时避免使用32位绝对地址。
跳转表本身很好:它们使用64位绝对地址,可以在虚拟地址空间中的任何位置重定位。(在ASLR之后使用加载时间修正。)
我认为您的间接寻址级别过多。由于您已经将函数指针加载到寄存器中,因此应该使用jmp r10not jmp [r10]。在所有可能的分支错误预测之前,将所有负载预先存储到寄存器中会更快地将它们放入管道中,因此,如果您有许多空闲的寄存器,这也许是个好主意。
如果它们较小,则最好内联一些后面的块,因为任何给定的RCX值可到达的块都无法以其他任何方式到达。因此,这将是更好的内联所有的func_21和func_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
Run Code Online (Sandbox Code Playgroud)
或者,如果您只想为一个表绑定一个寄存器,则可以使用:
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
Run Code Online (Sandbox Code Playgroud)
这充分利用了表之间的已知静态偏移量。
跳转目标的缓存位置
在跳转目标矩阵中,任何单个用法都会沿“列”向下移动以遵循某些跳转链。显然,最好对布局进行转置,以使跳转链沿着“行”行,因此所有目标都来自同一缓存行。
例如,将您的桌子排列成func_11,21可以以jmp [r10+8],然后jmp [r10+16]以结束,而不是在桌子之间加上一些偏移量,以改善空间局部性。L1d加载延迟只有几个周期,因此与检查是否在第一个间接分支之前加载到寄存器相比,CPU在检查分支预测的正确性方面没有太多额外的延迟。(我正在考虑第一个分支预测错误的情况,因此OoO exec直到发出正确的路径后才能“看到”间接内存的jmp。)
您还可以存储相对于跳转目标附近某个参考地址或表本身的32位(或16或8位)偏移量。
例如,查看一下GCC switch在用位置无关的代码编译跳转表时所做的事情,即使对于确实允许运行时修复绝对地址的目标也是如此。
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=84011包含一个测试用例;在GCC的MASM风格的Godbolt上.intel_syntax看到它。它使用movsxd表中的负载,然后是add rax, rdx/ jmp rax。表条目是dd L27 - L4和的dd L25 - L4 (其中的是标签名称,给出从跳转目标到“锚点” L4的距离)。
(与此情况也相关https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85585)。