将 Go 汇编器翻译成 NASM

Translating Go assembler to NASM

我遇到了以下 Go 代码:

type Element [12]uint64

//go:noescape
func CSwap(x, y *Element, choice uint8)

//go:noescape
func Add(z, x, y *Element)

其中 CSwapAdd 函数基本上来自程序集,如下所示:

TEXT ·CSwap(SB), NOSPLIT, [=12=]-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, [=12=]-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 似乎对我来说没问题。

我想你可以把这些翻译成

mov rbx, [reg_p1]
mov rcx, [reg_p2]

除非我遗漏了一些细微之处,否则可以忽略为零的偏移量。 *8 不是尺寸提示,因为它已经在指令中。

但您的其余代码看起来不对。原文中的MOVB choice+16(FP), AL应该是将choice参数取入AL,但是你将AL设置为常量16,加载其他参数的代码似乎完全没有了,因为是另一个函数中所有参数的代码。

关于 Go 的 asm 的基本文档:https://golang.org/doc/asm. It's not totally equivalent to NASM or AT&T syntax: FP is a pseudo-register name for whichever register it decides to use as the frame pointer. (Typically RSP or RBP). Go asm also seems to omit function prologue (and probably epilogue) instructions. ,它更像是像 LLVM IR 这样的内部表示,而不是真正的 asm。

Go 也有自己的对象文件格式,所以我不确定您是否可以使用 NASM.

制作与 Go 兼容的对象文件

如果你想从 Go 以外的东西调用这个函数,你还需要将代码移植到不同的调用约定。与正常的 x86-64 System V ABI 或 x86-64 Windows 调用约定不同,Go 似乎甚至对 x86-64 使用堆栈参数调用约定。 (或者当 Go 为 register-arg 调用约定构建此源时,那些 mov 函数参数到 REG_P1 等指令消失了吗?)

(这就是为什么你必须使用 movzx eax, dl 而不是从堆栈加载的原因。)

顺便说一句,如果您想将它与 C 一起使用,用 C 而不是 NASM 重写此代码可能更有意义。小函数最好由编译器内联和优化。


通过使用 Go assembler 和 disassembler 进行汇编来检查您的翻译或获得起点是个好主意。

objdump -drwC -MintelAgner Fog's objconv disassembler 会很好,但他们不理解 Go 的目标文件格式。如果 Go 有一个工具可以提取实际的机器代码或将其放入 ELF 目标文件中,那就去做吧。

如果不是,您可以使用 ndisasm -b 64(它将输入文件视为平面二进制文件,反汇编 所有 字节,就好像它们是指令一样)。如果可以找到函数的起始位置,则可以指定 offset/length。 x86 指令是可变长度的,反汇编很可能是 "out of sync" 在函数的开头。您可能想为 disassembler 添加一堆单字节 NOP 指令(一种 NOP sled),因此如果它解码一些 0x90 字节作为立即数的一部分或 disp32 用于长指令真的不是功能的一部分,它会同步。 (但是函数prologue还是会乱的)

您可以向您的 Go asm 函数添加一些 "signpost" 指令,以便通过反汇编元数据作为指令,在疯狂的 asm 混乱中轻松找到正确的位置。例如在某个地方放一个 pmuludq xmm0, xmm0 ,或者一些其他带有唯一助记符的指令,你可以搜索 Go 代码不包含的助记符。或者带有立即数的指令会脱颖而出,例如 addq [=19=]x1234567, SP。 (一个会崩溃的指令,所以你不会忘记再次取出它在这里很好。)

或者您可以使用 gdb 的内置 disassembler:添加一条会出现段错误的指令(例如从伪造的绝对地址 (movl 0, AX null- pointer deref), 或保存非指针值的寄存器,例如 movl (AX), AX)。然后您将获得内存中指令的指令指针值,并且可以从它后面的某个点 disassemble 。 (可能函数开始将是 16 字节对齐的。)


具体说明。

MOVBLZX AL, AX 读取 AL,所以这绝对是一个 8 位操作数。 AX 的大小由助记符的 L 部分给出,意思是 long 表示 32 位,就像在 GAS AT&T 语法中一样。 (movzx 形式的气体助记符是 movzbl %al, %eax)。请参阅 What does cltq do in assembly? 了解 cdq / cdqe 的 table 和 AT&T 等效指令,以及等效 MOVSX 指令的 AT&T / Intel 助记符。

你要的NASM指令是movzx eax, al。使用 rax 作为目的地会浪费 REX 前缀。使用 ax 作为目标将是一个错误:它不会零扩展到完整寄存器,并且会留下任何高垃圾。如果您不习惯 x86 的 Go asm 语法,您会非常困惑,因为根据操作数的大小,AX 可以表示 AX、EAX 或 RAX。

显然 mov rax, al 是不可能的:与大多数指令一样,mov 要求其两个操作数的大小相同。 movzx 是罕见的例外之一。


MOVB choice+16(FP), AL 是字节加载到 AL,而不是立即移动。 choice+16FP 的偏移量。此语法与 AT&T 寻址模式基本相同,FP 为寄存器,choice 为 assemble-时间常数。

FP 是伪寄存器名。很明显,它应该只是加载第三个参数传递槽的低字节,因为 choice 是函数参数的名称。 (在 Go asm 中,choice 只是语法糖,或者定义为零的常量。)

call 指令之前,rsp 指向第一个堆栈参数,因此 +16 是第三个参数。看起来 FP 是那个基地址(实际上可能是 rsp+8 之类的)。在 call(压入 8 字节 return 地址)之后,第 3 个堆栈参数位于 rsp + 24。推多了,偏移量会更大,所以要根据需要调整到正确的位置。

如果您移植此函数以使用标准调用约定调用,则 3 个整数参数将在寄存器中传递,没有堆栈参数。哪 3 个寄存器取决于您是为 Windows 还是非 Windows 构建。 (参见 Agner Fog 的调用约定文档:http://agner.org/optimize/


顺便说一句,一个字节加载到 AL 然后 movzx eax, al 只是愚蠢的 。使用

一步完成所有现代 CPU 的效率更高
movzx  eax, byte [rsp + 24]      ; or rbp+32 if you made a stack frame.

我希望问题中的源代码来自未优化的 Go 编译器输出?还是assembler自己做了这样的优化?