我遇到了以下Go代码:
type Element [12]uint64
//go:noescape
func CSwap(x, y *Element, choice uint8)
//go:noescape
func Add(z, x, y *Element)
其中CSwap
和Add
函数基本上来自一个程序集,如下所示:
TEXT ·CSwap(SB), NOSPLIT, $0-17
MOVQ x+0(FP), REG_P1
MOVQ y+8(FP), REG_P2
MOVB choice+16(FP), AL // AL = 0 or 1
MOVBLZX AL, AX // AX = 0 or 1
NEGQ AX // RAX = 0x00..00 or 0xff..ff
MOVQ (0*8)(REG_P1), BX
MOVQ (0*8)(REG_P2), CX
// Rest removed for brevity
TEXT ·Add(SB), NOSPLIT, $0-24
MOVQ z+0(FP), REG_P3
MOVQ x+8(FP), REG_P1
MOVQ y+16(FP), REG_P2
MOVQ (REG_P1), R8
MOVQ (8)(REG_P1), R9
MOVQ (16)(REG_P1), R10
MOVQ (24)(REG_P1), R11
// Rest removed for brevity
我尝试做的是将程序集转换为我更熟悉的语法(我认为我的更像是NASM),而上面的语法是Go汇编程序。关于Add
方法,我没有太多问题,并正确翻译(根据测试结果)。在我的情况下看起来像这样:
.text
.global add_asm
add_asm:
push r12
push r13
push r14
push r15
mov r8, [reg_p1]
mov r9, [reg_p1+8]
mov r10, [reg_p1+16]
mov r11, [reg_p1+24]
// Rest removed for brevity
但是,我在翻译CSwap
函数时遇到问题,我有这样的事情:
.text
.global cswap_asm
cswap_asm:
push r12
push r13
push r14
mov al, 16
mov rax, al
neg rax
mov rbx, [reg_p1+(0*8)]
mov rcx, [reg_p2+(0*8)]
但这似乎并不完全正确,因为我在编译时遇到错误。任何想法如何将上述CSwap
组装部件翻译成像NASM这样的东西?
编辑(解决方案):
好的,在下面的两个答案,以及一些测试和挖掘之后,我发现代码使用以下三个寄存器进行参数传递:
#define reg_p1 rdi
#define reg_p2 rsi
#define reg_p3 rdx
因此,rdx
具有choice
参数的值。所以,我所要做的就是用这个:
movzx rax, dl // Get the lower 8 bits of rdx (reg_p3)
neg rax
使用byte [rdx]
或byte [reg_3]
给出了错误,但使用dl
似乎对我来说很好。
关于Go's asm的基本文档:https://golang.org/doc/asm。它并不完全等同于NASM或AT&T语法:FP
是一个伪寄存器名称,用于决定用作帧指针的寄存器。 (通常是RSP或RBP)。 go asm似乎也省略了函数序言(可能是结语)指令。 As @RossRidge comments,它有点像LLVM IR这样的内部表示而不是真正的asm。
Go也有自己的对象文件格式,所以我不确定你是否可以使用NASM制作Go兼容的目标文件。
如果你想从Go以外的东西调用这个函数,你还需要将代码移植到不同的调用约定。与普通的x86-64 System V ABI或x86-64 Windows调用约定不同,Go似乎使用了一个stack-args调用约定,即使对于x86-64也是如此。 (或者也许那些mov
函数args进入REG_P1
等等,当Go为register-arg调用约定构建这个源时,指令会消失?)
(这就是为什么你必须使用movzx eax, dl
而不是从堆栈加载。)
顺便说一句,用C代替NASM重写这段代码可能会更有意义,如果你想在C中使用它。小函数最好由编译器内联和优化。
通过与Go汇编程序组装并使用反汇编程序来检查您的翻译或获得起点是一个好主意。
objdump -drwC -Mintel
或Agner Fog's objconv
disassembler会很好,但是他们不了解Go的对象文件格式。如果Go有一个工具来提取实际的机器代码或将其放入ELF目标文件中,那就这样做吧。
如果没有,你可以使用ndisasm -b 64
(它将输入文件视为平面二进制文件,将所有字节分解为它们是指令)。如果可以找到函数的起始位置,则可以指定偏移量/长度。 x86指令是可变长度的,并且在函数开始时反汇编可能会“不同步”。你可能想为反汇编程序添加一堆单字节NOP指令(一种NOP sled),所以如果它将一些0x90字节解码为立即的一部分,或者为一条长指令解析,而该指令实际上不属于该函数的一部分,它会同步。 (但功能序言仍然会搞砸)。
您可以在Go asm函数中添加一些“路标”指令,以便通过反汇编元数据作为指令轻松找到疯狂的asm中的正确位置。例如把pmuludq xmm0, xmm0
放在某个地方,或其他一些带有独特助记符的指令,你可以搜索Go代码不包含的内容。或者像addq $0x1234567, SP
那样突出的指令会脱颖而出。 (一个会崩溃的指令,所以你不要忘记再把它取出来这里很好。)
或者你可以使用gdb
的内置反汇编程序:添加一个段错误的指令(如来自伪绝对地址的加载(movl 0, AX
null-pointer deref),或者一个包含非指针值的寄存器,例如movl (AX), AX
)。然后你将有一个指令在内存中的指令值,并可以从后面的某些点进行反汇编。 (函数start可能是16字节对齐的。)
MOVBLZX AL, AX
读取AL,因此它绝对是一个8位操作数。 AX的大小由助记符的L
部分给出,意思是32位的long
,就像GAS AT&T语法一样。 (那种形式的movzx
的气体助记符是movzbl %al, %eax
)。请参阅What does cltq do in assembly?以获取cdq / cdqe和AT&T等效表以及等效MOVSX指令的AT&T / Intel助记符。
你想要的NASM指令是movzx eax, al
。使用rax
作为目的地将浪费REX前缀。使用ax
作为目的地将是一个错误:它不会零扩展到完整的寄存器,并将留下任何高垃圾。当你不习惯时,使用x86的asm语法非常混乱,因为AX可能意味着AX,EAX或RAX,具体取决于操作数大小。
显然mov rax, al
不可能:像大多数指令一样,mov
要求它的操作数都是相同的大小。 movzx
是罕见的例外之一。
MOVB choice+16(FP), AL
是加载到AL
的字节,而不是立即移动。 choice+16
是与FP
的抵消。该语法与AT&T寻址模式基本相同,FP为寄存器,choice
为汇编时常数。
FP
是一个伪注册名称。很明显它应该只是加载第3个arg传递槽的低字节,因为choice
是函数arg的名称。 (在Go asm中,choice
只是语法糖,或者定义为零的常量。)
在call
指令之前,rsp
指向第一个堆栈arg,因此+ 16是第3个arg。似乎FP
是那个基地址(实际上可能是rsp+8
或其他东西)。在call
(推送一个8字节的返回地址)之后,第三个堆栈arg位于rsp + 24
。在更多推动之后,偏移将更大,因此必要时进行调整以到达正确的位置。
如果要移植此函数以使用标准调用约定进行调用,则3个整数args将在寄存器中传递,没有堆栈参数。哪3个寄存器取决于您是在构建Windows还是非Windows。 (参见Agner Fog的召唤惯例doc:http://agner.org/optimize/)
BTW,一个字节加载到AL然后movzx eax, al
只是愚蠢。所有现代CPU都可以更有效地一步到位地完成它
movzx eax, byte [rsp + 24] ; or rbp+32 if you made a stack frame.
我希望问题中的来源是来自未优化的Go编译器输出?或者汇编程序本身进行了这样的优化?
我认为你可以将这些翻译成公正的
mov rbx, [reg_p1]
mov rcx, [reg_p2]
除非我遗漏了一些微妙之处,否则零点的偏移量可以忽略不计。 *8
不是一个大小提示,因为它已经在指令中。
其余的代码看起来很错误。原始中的MOVB choice+16(FP), AL
应该将choice
参数提取到AL中,但是您将AL设置为常量16,并且加载其他参数的代码似乎完全丢失,所有参数的代码也是如此在另一个功能。