golang-汇编


golang汇编使用的是plan9汇编,这相当于是一个帮助文档,帮助理解golang底层汇编代码的实现。
由于汇编不具备跨平台,所以这里使用的是linux amd64平台。

寄存器

通用寄存器

下面是通用通用寄存器的名字在 IA64 和 plan9 中的对应关系:

IA64 RAX RBX RCX RDX RDI RSI RBP RSP R8 R9 R10 R11 R12 R13 R14 RIP
Plan9 AX BX CX DX DI SI BP SP R8 R9 R10 R11 R12 R13 R14 PC

伪寄存器

汇编中还引入了4个伪寄存器:

  • FP(Frame pointer):指向第一个参数(即参数列表最下面),通过其加上偏移量可以获取函数入参和返回值。
    FP使用形式是symbol+offset(FP),例如 arg0+0(FP)arg1+8(FP),使用 FP 不加 symbol 时,无法通过编译,在汇编层面来讲,symbol 并没有什么用,加 symbol 主要是为了提升代码可读性。
  • PC(Program counter):指向当前指令,用于跳转指令或分支。
  • SB(Static base pointer):指向全局基底,可通过其引用全局变量或声明函数。
  • SP(Stack pointer):指向第一个局部变量(即本地变量最上面),通过其减偏移量可以获取局部变量。SP使用形如 symbol+offset(SP) 的方式,引用函数的局部变量。offset的合法取值是[-framesize, 0),注意是个左闭右开的区间。

FP指向内容是有低地址往高地址增长,SP指向内容是由高地址往低地址增长,所以对于offsetFP是正数,SP是负数。
如果SP前面的偏移量是正数,表示这里SP是硬件SP寄存器,且硬件SP寄存器没有symbol。当使用反汇编工具后显示的汇编代码中的SP都是硬件SP寄存器。

数据

变量声明

常量

常量以$为前缀。常量的类型有整数常量、浮点数常量、字符常量和字符串常量等几种类型。以下是几种类型常量的例子:

$1           // 十进制
$0xf4f8fcff  // 十六进制
$1.5         // 浮点数
$'a'         // 字符
$"abcd"      // 字符串
$·myvar(SB)  // 取地址

Go汇编语言中的常量其实不仅仅只有编译时常量,还包含运行时常量。比如包中全局的变量和全局函数在运行时地址也是固定不变的,这里地址不会改变的包变量和函数的地址也是一种汇编常量。

下面是本章第一节用汇编定义的字符串代码:

GLOBL ·NameData(SB),$8
DATA  ·NameData(SB)/8,$"gopher"

GLOBL ·Name(SB),$16
DATA  ·Name+0(SB)/8,$·NameData(SB)
DATA  ·Name+8(SB)/8,$6

其中$·NameData(SB)也是以$美元符号为前缀,因此也可以将它看作是一个常量,它对应的是NameData包变量的地址。在汇编指令中,我们也可以通过LEA指令来获取NameData变量的地址。

全局变量

要定义全局变量,首先要声明一个变量对应的符号,以及变量对应的内存大小。导出变量符号的语法如下:

GLOBL symbol(SB), width

GLOBL汇编指令用于定义名为symbol的变量,变量对应的内存宽度为width,内存宽度部分必须用常量初始化。下面的代码通过汇编定义一个int32类型的count变量:

GLOBL ·count(SB),$4

其中符号·count以中点开头表示是当前包的变量,最终符号名为被展开为path/to/pkg.count。count变量的大小是4个字节,常量必须以$美元符号开头。内存的宽度必须是2的指数倍,编译器最终会保证变量的真实地址对齐到机器字倍数。需要注意的是,在Go汇编中我们无法为count变量指定具体的类型。在汇编中定义全局变量时,我们只关心变量的名字和内存大小,变量最终的类型只能在Go语言中声明。

变量定义之后,我们可以通过DATA汇编指令指定对应内存中的数据,语法如下:

DATA symbol+offset(SB)/width, value

具体的含义是从symbol+offset偏移量开始,width宽度的内存,用value常量对应的值初始化。DATA初始化内存时,width必须是1、2、4、8几个宽度之一,因为再大的内存无法一次性用一个uint64大小的值表示。

变量寻址

寻址的方式有很多很迷惑的行为。
利用MOV来说明问题

MOVQ $10, AX           // Ax = 10
MOVQ $0x10, AX         // Ax = 0x10
MOVQ BX, AX            // AX = BX
MOVQ (BX), AX          // AX = *BX
MOVQ 8(BX), AX         // AX = *(8+BX)
MOVQ 16(CX)(BX*1), AX  // AX = *(16 + CX + BX*1)
MOVQ ·myvar(SB), AX    // AX = *myvar
MOVQ $8(CX)(BX*1), AX  // AX = 8 + CX + BX
MOVQ $·myvar(SB), AX   // AX = &myvar

函数声明

 静态基地址(static-base) 指针
                        |    标签
                        |    |      add函数入参+返回值总大小
                        |    |      |
TEXT pkgname·funcname<>(SB),TAG,$16-24
     |       |       |           |
函数所属包名  函数名    |           add函数栈帧大小
                     表示堆栈是否符合GOABI
  • TEXT,定义函数标识
  • pkgname,函数包名,可以不写,也可以""替代
  • ·,在程序链接后会转换为.
  • <>,表示堆栈结构是否符合GOABI,也可以写作<ABIInternal>
  • (SB),让SB认识这个函数,即生成全局符号,有用绝对地址
  • TAG,标签,表示函数某些特殊功能,多个标签可以通过|连接,常用标签
    • NOSPLIT,向编译器表明,不应该插入stack-split的用来检查栈需要扩张的前导指令,减少开销,一般用于叶子节点函数(函数内部不调用其他函数)
    • NOFRAME,不分配函数堆栈,函数必须是叶子节点函数,且以0标记堆栈函数,没有保存帧指针(或link寄存器架构上的返回地址)
  • $16-2416表示函数栈帧大小,24表示入参和返回大小

预处理

  • #include,引入头文件
  • #define,宏定义
  • #undef,取消宏定义
  • #ifdef,判断是否有宏定义
  • #ifndef,判断是否没有宏定义
  • #elseif的另一个情况
  • #endif,结束if

例如在启动函数runtime·rt0_go中,会根据不同平台执行响应指令:

#ifndef GOOS_windows
    JMP ok
#endif
needtls:
#ifdef GOOS_plan9
    // skip TLS setup on Plan 9
    JMP ok
#endif
#ifdef GOOS_solaris
    // skip TLS setup on Solaris
    JMP ok
#endif
#ifdef GOOS_illumos
    // skip TLS setup on illumos
    JMP ok
#endif
#ifdef GOOS_darwin
    // skip TLS setup on Darwin
    JMP ok
#endif

标签声明

标签用于JMP跳转,用symbol:表示。

函数栈帧

调用者caller,被调用者callee,以被调用callee的角度理解函数栈帧,并显示伪寄存器指向的位置

noframe函数栈帧

frame函数栈帧

常见指令

与数据相关的指令结尾会跟上BWDQ,分别表示操作字节数1248,常见的MOVQADDQ等都是对8字节数进行操作。下面指令皆以Q结尾即8字节操作数。

MOVB $1, DI      // 1 byte
MOVW $0x10, BX   // 2 bytes
MOVD $1, DX      // 4 bytes
MOVQ $-10, AX    // 8 bytes

栈操作

由于golang栈帧是固定大小,没有提供压栈弹栈操作,但这些操作可以通过伪指针加偏移量来模拟。

移动操作

MOV允许赋值,移动数据,解引用,地址偏移等操作

MOVQ $10, AX           // Ax = 10
MOVQ $0x10, AX         // Ax = 0x10
MOVQ BX, AX            // AX = BX
MOVQ (BX), AX          // AX = *BX
MOVQ 8(BX), AX         // AX = *(8+BX)
MOVQ 16(CX)(BX*1), AX  // AX = *(16 + CX + BX*1)
MOVQ ·myvar(SB), AX    // AX = *myvar
MOVQ $8(CX)(BX*1), AX  // AX = 8 + CX + BX
MOVQ $·myvar(SB), AX   // AX = &myvar

运算操作

数值运算

  • ADDQ,加法运算
  • SUBQ,减法运算
  • IMULQ,乘法运算

示例:

ADDQ  AX, BX   // BX += AX
SUBQ  AX, BX   // BX -= AX
IMULQ AX, BX   // BX *= AX

逻辑运算

  • ANDQ,
  • ORQ,

移位运算

  • SALQ,有符号左移
  • SARQ,有符号右移
  • SHLQ,无符号左移
  • SHRQ,无符号右移

条件操作

条件操作即为处理当前操作并设置标志寄存器相关标志位的值,常见的标志位有:

  • CF (Carry Flag),进位标志位,运算过程是否产生进位或借位
  • PF (Parity Flag),奇偶标志位,运算结果中1的个数是奇数还是偶数
  • ZF (Zero Flag),零标志位,运算结果是否为0
  • SF (Sign Flag),符号标志位,运算结果的最高位
  • OF (Overflow Flag),溢出标志位,运算结果超过运算数表示范围

条件指令:

  • TESTQ,源操作数和目标操作数按位逻辑与,结果不置目标操作数,根据响应的结果设置SFZF、和PF标志位,并将CFOF标志位清零。常见TESTQ AX AX配合ZF即可得出AX是否为0。
  • CMP,前操作数减去后操作数,结果不置目标操作数,根据结果设置标志寄存器标志位。
    • 等于,ZF为1
    • 不等于,ZF为0
    • 小于,CF为1
    • 小于等于,CF为1或ZF为1
    • 大于等于,CF为0
    • 大于,CF为0并且ZF为0

跳转操作

跳转操作即为检测标志寄存器相关的标志位,进而处理逻辑。

  • JMP (JuMP),无条件跳转,可以跳转标签,跳转函数,跳过指定个数的指令。
    例如,如果是2(PC)则是跳过两条指令,也可以为负数-2(PC)向上跳2个指令
    JMP 2(PC)  // ---
    NOP        //   |
    NOP        // <--
    

NOP // <–
NOP // |
JMP -2(PC) // —


* `JA (Jump if Above)`,无符号大于就跳转
* `JAE (Jump if Above or Equal)`,无符号大于等于就跳转
* `JB (Jump if Below)`,无符号小于就跳转
* `JBE (Jump if Below or Equal)`,无符号小于等于就跳转
* `JC (Jump if Carry)`,如果有进位就跳转
* `JNC (Jump if No Carry)`,没有进位就跳转
* `JE (Jump if Equal)`,相等就跳转
* `JNE (Jump if Not Equal)`,不等就跳转
* `JL (Jump if Less)`,有符号小于就跳转
* `JLE (Jump if Less or Equal)`,有符号小于等于就跳转
* `JG (Jump if Greater)`,有符号大于就跳转
* `JGE (Jump if Greater or Equal)`,有符号大于等于就跳转
* `JHI (Jump if HIgher)`,无符号大于就跳转
* `JHS (Jump if Higher or Same)`,无符号大于就跳转或等于就跳转,同`JC`
* `JLO (Jump if LOwer)`,无符号小于就跳转,同`JNC`
* `JLS (Jump if Lower or Same)`,无符号小于或等于就跳转
* `JN (Jump if Negative)`,为负就跳转
* `JZ (Jump if equal to Zero)`,等于零值就跳转
* `JNZ (Jump if Not equal to Zero)`,不等于零值就跳转


## 其他操作
* `LEAQ`,解引用
* `CALL`,调用函数,也可以理解为跳转到函数
* `RET`,退出函数
* `NOP`,空操作
* `INT`,中断

# 反汇编

对于写好的 go 源码,生成对应的 Go 汇编,大概有下面几种

*   方法 1 先使用 go build -gcflags "-N -l" main.go 生成对应的可执行二进制文件 再使用 go tool objdump -s "main\." main 反编译获取对应的汇编
反编译时"main\." 表示只输出 main 包中相关的汇编"main\.main" 则表示只输出 main 包中 main 方法相关的汇编
*   方法 2 使用 go tool compile -S -N -l main.go 这种方式直接输出汇编
*   方法 3 使用go build -gcflags="-N -l -S" main.go 直接输出汇编

注意:在使用这些命令时,加上对应的 flag,否则某些逻辑会被编译器优化掉,而看不到对应完整的汇编代码

> -l 禁止内联 -N 编译时,禁止优化 -S 输出汇编代码

# 参考文献
[Go 系列文章3 :plan9 汇编入门](https://xargin.com/plan9-assembly/)
[状态标志寄存器](https://baike.baidu.com/item/FLAG/6050220)
[Go functions in assembly language](https://github.com/golang/go/files/447163/GoFunctionsInAssembly.pdf)

文章作者: djaigo
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 djaigo !
评论
  目录