您的位置 首页 golang

大白话 golang 教程-29-反汇编和内存结构

使用 go tool compile -S main.go 可以显示出 go 汇编的代码(或 go build -gcflags -S main.go),go 汇编基于 plan 9,和系统底层的汇编代码有所区别, 但是做到了跨平台汇编与具体的系统架构无关。很多人都吐槽为什么是 plan 9? plan 9 是 1980 年代中期的一个分布式操作系统,被称为贝尔实验室 9 号项目,作为 UNIX 的后继,现在任然被爱好者研究和使用。

main.go 的源代码和对应的 go 汇编代码:

 package main

import (
  "runtime"
)

func main() {
  println(runtime.GOOS)
}  

先忽略掉 PCDATA 和 FUNCDATA,它们是给 GC 垃圾收集器提供附加信息的指令。执行 go build 把它编译成可执行文件,用 go tool objdump 就能看到生成的二进制文件的汇编代码,我目前的系统是 darwin intel i7:

 # go build -o main main.go 
# go tool objdump -s main.main main  

二进制汇编代码的结果:

官网有一个汇编入门的教程 , 注意 go 汇编是基于 package 的,从最简单的开始,首先看看如何定义包的导出变量:

 // id.go
package vardef

var ID= 9927

// str.go
package vardef

var Str = "hello"  

因为不涉及到任何函数调用,这两个文件产生的伪汇编代码比较简单:

 // id.go
vardef[master*]  go tool compile -S id.go     
go.cuinfo.packagename. SDWARFINFO dupok size=0
0x0000 76 61 72 64 65 66                                vardef
"".ID SNOPTRDATA size=8
0x0000 c7 26 00 00 00 00 00 00                          .&......

// str.go
vardef[master*]  go tool compile -S str.go
go.cuinfo.packagename. SDWARFINFO dupok size=0
0x0000 76 61 72 64 65 66                                vardef
go.string."hello" SRODATA dupok size=5
0x0000 68 65 6c 6c 6f                                   hello
"".Str SDATA size=16
0x0000 00 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00  ................
rel 0+8 t=1 go.string."hello"+0  

第一个程序 id.go 定义了一个整数,第二个 str.go 定义了一个字符串。在 go 的 asm 文档中找到数据的定义结构:

 DATAsymbol+offset(SB)/width, value
GLOBL   symbol(SB), width
GLOBL   symbol(SB), RODATA, width // 只读  

1. symbol 是标识符,.symbol 是当前包,比如 .ID、.Str

2. offset 是初始地址的偏移量

3. value 是值, width 是内存宽度,width 必须是 1、2、4、8、16 等

除了 DATA,还有 TEXT 定义函数、 GLOBL 导出全局变量 ,SRODATA 表示数据在内存只读,因为 go 字符串本质是一种只读的引用类型,dupok 表示只有一份,它完整的引用符号是 go.string.”hello”,如果再出现对 hello 的引用,就可以连接 go.string.”hello” 这个符号上,后面的 .Str 就引用了这个 hello 符号,而且 “”.Str 的 size=16,表示占用 16 个字节,为什么呢? 因为前面章节讲过 StringHeader 的定义是:

 type reflect.StringHeader struct {
  Data uintptr
  Len int 
}  

Data 字段是指向 go.string.”hello” 的指针,这个指针大小是 8 个字节,Len 表示有效数据的长度,也占 8 个字节,跳过引用地址的 0x0000 8 个字节后是 05,因为 hello 的长度是 5 个字节。相对于 SDATA,定义 “”.ID 的 SNOPTRDATA 表示不包含指针。

在继续函数之前,有必要先了解一下 plan 9 汇编指令和寄存器。plan 9 没有 intel 的 push、pop 指令,依靠 SP 的加减操作来操作函数栈帧,go 的伪寄存器、通用寄存器、指令和操作数基本的规则:

1. 伪寄存器 SP 局部变量的底部(栈方向高大到低,也是当前栈的 BP 基地址,往栈底方向是调用者 ret 返回地址),SP – 8 是倒数第一个 int 本地变量,SP -16 倒数第二个(注意栈的增长方向是高地址向低地址扩张),SP 增长方向就是返回地址和调用者参数了

2. 伪寄存器 SP 只有在手写会编是用到,它指向当前函数栈的栈顶位置,手写汇编使用伪寄存器前面要带 symbol,否则是调用硬件寄存器,反编译后的 SP 都是硬件 SP,无论前面带不带 symbol,要特别注意

3. 伪寄存器 FP 标识函数参数、返回值,直接 FP + 0 是第一个调用参数,FP +8 第二个调用参数

4. PC 就是 x86 的 IP 寄存器,EB 依然是基址寄存器,现代编译器中 EB 不是必须的但对调试有帮助

5. 伪寄存器 SB 全局静态基地址,用来申明函数和全局变量

6. 通用寄存器 rax、rbx、rcx 等,操作的时候 r 可以不写,和 Intel X86-64 不一样 (看第10条)

7. 常数使用 $num 表示,可以直接使用 $0x 表示16 进制数,可以是用-表示负数

8. 操作数的方向和 intel X86-64 是相反的,JMP 跳转同一函数内可以使用 label,会被转换成相对跳转

9. 和 Intel x86-64 一样,(Reg) 引用寄存器的值所对应内存中的数据,没有内存 to 内存的指令,必须通过寄存器中转

10. 指令决定操作数尺寸 XXXB = 1 XXXW = 2 XXXD = 4 XXXQ = 8,而 Intel 由操作的寄存器决定 AL/AH = 1、AX = 2、EAX = 4、RAX = 8

汇编操作指令的样例:

 SUBQ $0x18, SP   // 分配函数栈,操作数 8 个字节
ADDQ $0x18, SP   // 清除函数栈,操作数据 8 个字节
MOVB $1, DI      // 拷贝 1个字节
MOVW $0x10, BX   // 拷贝 2 个字节
MOVD $1, DX      // 拷贝 4 个字节
MOVQ $-10, AX    // 拷贝 8 个字节
ADDQ AX, BX      // BX = BX + AX 存 BX
SUBQ AX, BX      // BX = BX - AX 存 BX
IMULQ AX, BX     // BX = BX * AX 存 BX
MOVQ AX, BX      // BX = AX 将 AX 中的值赋给 BX
MOVQ (AX), BX    // BX = *AX 加载 AX 中指向内存地址的值给 BX
MOVQ 16(AX), BX  // BX = *(AX + 16) 偏移 16 个字节后地址中的值  

编写一个加法的函数,go tool compile -S 结果如下:

 // func.go
package funcasm

func add(a, b int) int {
  return a + b
}

"".add STEXT nosplit size=19 args=0x18 locals=0x0
  0x0000 00000 (func.go:3)TEXT"".add(SB), NOSPLIT|ABIInternal, $0-24
  ...
  0x0000 00000 (func.go:4)MOVQ"".b+16(SP), AX
  0x0005 00005 (func.go:4)MOVQ"".a+8(SP), CX
  0x000a 00010 (func.go:4)ADDQCX, AX
  0x000d 00013 (func.go:4)MOVQAX, "".~r2+24(SP)
  0x0012 00018 (func.go:4)RET  

SB 是程序地址空间开始的基地址,引用函数的输入参数表示形式 “”.a+8(SP),””.b+16(SP),因为硬件 SP 指向函数栈栈顶地址。

$0-24 表示函数栈帧大小是 0,参数和返回值一共需要 24 个字节(3 个 int),~r2+24(SP) 是返回值的地址, RET 清除函数栈,弹出栈顶的返回地址到 IP 寄存器,从调用 add 的下一行开始执行。另外 NOSPLIT 的意思不插入检查栈扩张的指令(因为栈帧大小是 0 没必要检查),它对应 go 的编译指示 //go:nosplit。栈调用的一般性结构如下:

修改一下 func.go,创建一个局部变量,观察栈帧的大小变化:

 // func2.go 
package main

func add2(a, b int) int {
  times := 2
  println(times)
  return (a - b) * times
}

// func2.o
"".add2 STEXT size=103 args=0x18 locals=0x10
  0x0000 00000 (func2.go:3)TEXT"".add2(SB), ABIInternal, $16-24
  0x0000 00000 (func2.go:3)MOVQ(TLS), CX   // * thread local storage 
  0x0009 00009 (func2.go:3)CMPQSP, 16(CX)  // *
  0x000d 00013 (func2.go:3)PCDATA$0, $-2     
  0x000d 00013 (func2.go:3)JLS96          // *
  0x000f 00015 (func2.go:3)PCDATA$0, $-1
  0x000f 00015 (func2.go:3)SUBQ$16, SP     // # 栈空间
  0x0013 00019 (func2.go:3)MOVQBP, 8(SP)   // # 老的基址(保存 BP 寄存器的值到内存地址 8(SP))
  0x0018 00024 (func2.go:3)LEAQ8(SP), BP   // # 新的基址(拷贝 8(SP) 的内存引用地址值到 BP)
  ...
  0x001d 00029 (func2.go:5)NOP
  0x0020 00032 (func2.go:5)CALLruntime.printlock(SB)
  0x0025 00037 (func2.go:5)MOVQ$2, (SP)
  0x002d 00045 (func2.go:5)CALLruntime.printint(SB)
  0x0032 00050 (func2.go:5)CALLruntime.printnl(SB)
  0x0037 00055 (func2.go:5)CALLruntime.printunlock(SB)
  0x003c 00060 (func2.go:6)MOVQ"".a+24(SP), AX
  0x0041 00065 (func2.go:6)MOVQ"".b+32(SP), CX
  0x0046 00070 (func2.go:6)SUBQCX, AX
  0x0049 00073 (func2.go:6)SHLQ$1, AX                      // * 左移
  0x004c 00076 (func2.go:6)MOVQAX, "".~r2+40(SP)
  0x0051 00081 (func2.go:6)MOVQ8(SP), BP
  0x0056 00086 (func2.go:6)ADDQ$16, SP
  0x005a 00090 (func2.go:6)RET
  0x005b 00091 (func2.go:6)NOP                                  
  0x005b 00091 (func2.go:3)PCDATA$1, $-1
  0x005b 00091 (func2.go:3)PCDATA$0, $-2
  0x005b 00091 (func2.go:3)NOP                                  
  0x0060 00096 (func2.go:3)CALLruntime.morestack_noctxt(SB) // * 扩容
  0x0065 00101 (func2.go:3)PCDATA$0, $-1                      
  0x0065 00101 (func2.go:3)JMP0                              

包含一个局部变量 times,add 函数的栈帧大小是 16 字节。注意 // * 标记的是编译器插入的用于检查栈空间的代码,add2 和 add 不同的还有 # 标记的代码,因为 add2 还要调用 println 函数,在 println 函数返回的时候还需要恢复调用 add 的时候的 BP 基地址,所以需要保存老的 BP 地址,参数和返回值依然 24 个字节,注意 LEAQ 指令是引用寄存器指向的内存地址,不是加载它指向的值(理解为 & 取地址符)。

手写汇编代码的时候,不需要考虑 CALL 和 RET 指令对 PC 寄存器的操作影响,也不需要考虑 PC 寄存器插入的 8 字节返回地址占用的栈空间,仿造汇编的代码,写一个 add.s 汇编模块:

 // add.s
#include "textflag.h"

TEXT ·add(SB), NOSPLIT, $0-24
  MOVQ b+16(SP), AX       // MOVQ b+8(FP), AX
  MOVQ a+8(SP), BX        // MOVQ a+0(FP), BX
  ADDQ BX, AX
  MOVQ AX, ret + 24(SP)   // MOVQ AX, ret + 16(FP)
  RET

// main.go 
package main

func add(a, b int) int

func main() {
  println(add(1, 2))
}  

在个例子的栈帧为 0 比较特殊,也无本地局部变量和调用其他函数,不需要保存 BP 基地址,只需要跨过返回地址的 8 字节就行了。

当然用 FP 显得更加直观一些,输入 go run . 运行显示:

 callasmfunc[master*]  go run .
3  

编写一个返回多个值的函数:

 package main

//go:noinline
func swap(a, b int) (int, int) {
  return b+1, a+1
}

func main() {
  _ = swap(0, 1)
}  

//go:noinline 表示不要内联函数,swap 函数的汇编代码:

 "".swap STEXT nosplit size=27 args=0x20 locals=0x0
  0x0000 00000 (main.go:4)TEXT"".swap(SB), NOSPLIT|ABIInternal, $0-32
  ...
  0x0000 00000 (main.go:5)MOVQ"".b+16(SP), AX
  0x0005 00005 (main.go:5)INCQAX
  0x0008 00008 (main.go:5)MOVQAX, "".~r2+24(SP) // *
  0x000d 00013 (main.go:5)MOVQ"".a+8(SP), AX
  0x0012 00018 (main.go:5)INCQAX
  0x0015 00021 (main.go:5)MOVQAX, "".~r3+32(SP) // *
  0x001a 00026 (main.go:5)RET  

参数 2 个和返回值 2 个,共 $0-32 栈帧大小为 0,返回值的方式直接操作 SP 偏移实现,main 调用 swap 函数:

 "".main STEXT size=71 args=0x0 locals=0x28
  0x0000 00000 (main.go:8)TEXT"".main(SB), ABIInternal, $40-0
  ...
  0x000f 00015 (main.go:8)SUBQ$40, SP
  0x0013 00019 (main.go:8)MOVQBP, 32(SP) // 基址
  0x0018 00024 (main.go:8)LEAQ32(SP), BP
  ...
  0x001d 00029 (main.go:9)MOVQ$0, (SP)   // *
  0x0025 00037 (main.go:9)MOVQ$1, 8(SP)  // *
  0x002e 00046 (main.go:9)PCDATA$1, $0
  0x002e 00046 (main.go:9)CALL"".swap(SB)
  0x0033 00051 (main.go:10)MOVQ32(SP), BP // *
  0x0038 00056 (main.go:10)ADDQ$40, SP    // *
  0x003c 00060 (main.go:10)RET
  ...  

编写使用结构体的代码,看看 new(Person) 和 &Person{} 有无区别:

 package main

type Person struct{}

func main() {
  // var p *Person = &Person{}
  p := &Person{}
  println(p)
}  

核心的汇编代码是:

 0x0020 00032 (main.go:8)CALLruntime.printlock(SB)
0x0025 00037 (main.go:8)LEAQ""..autotmp_2+8(SP), AX
0x002a 00042 (main.go:8)MOVQAX, (SP)
0x002e 00046 (main.go:8)CALLruntime.printpointer(SB)
0x0033 00051 (main.go:8)CALLruntime.printnl(SB)
0x0038 00056 (main.go:8)CALLruntime.printunlock(SB)
0x003d 00061 (main.go:9)MOVQ8(SP), BP
0x0042 00066 (main.go:9)ADDQ$16, SP
0x0046 00070 (main.go:9)RET  

如果使用 p := new(Person) 核心汇编代码是:

 0x0020 00032 (main.go:9)CALLruntime.printlock(SB)
0x0025 00037 (main.go:9)LEAQ""..autotmp_1+8(SP), AX
0x002a 00042 (main.go:9)MOVQAX, (SP)
0x002e 00046 (main.go:9)CALLruntime.printpointer(SB)
0x0033 00051 (main.go:9)CALLruntime.printnl(SB)
0x0038 00056 (main.go:9)CALLruntime.printunlock(SB)
0x003d 00061 (main.go:10)MOVQ8(SP), BP
0x0042 00066 (main.go:10)ADDQ$16, SP
0x0046 00070 (main.go:10)RET  

可见 new(Person) 和 &Person{} 没有性能上的区别,打印结构体地址的汇编:

 0x0020 00032 (main.go:5)CALLruntime.printlock(SB)
0x0025 00037 (main.go:5)LEAQ""..autotmp_2+8(SP), AX
0x002a 00042 (main.go:5)MOVQAX, (SP)
0x002e 00046 (main.go:5)CALLruntime.printpointer(SB)
0x0033 00051 (main.go:5)CALLruntime.printnl(SB)
0x0038 00056 (main.go:5)CALLruntime.printunlock(SB)
0x003d 00061 (main.go:6)MOVQ8(SP), BP
0x0042 00066 (main.go:6)ADDQ$16, SP
0x0046 00070 (main.go:6)RET  

前面章节测试过,空结构体不占用内存,编译后是一个固定的内存地址,在汇编级别看看:

 0x001d 00029 (main.go:7)XORPSX0, X0                    // 清空 X0 寄存器
0x0020 00032 (main.go:7)MOVUPSX0, ""..autotmp_15+64(SP) // 初始化栈上临时空间
0x0025 00037 (main.go:7)LEAQtype.struct {}(SB), AX
0x002c 00044 (main.go:7)MOVQAX, ""..autotmp_15+64(SP)
0x0031 00049 (main.go:7)LEAQruntime.zerobase(SB), AX
0x0038 00056 (main.go:7)MOVQAX, ""..autotmp_15+72(SP)  

runtime.zerobase(SB) 就是空结构体的固定内存地址。嵌套结构体参数如何传递呢? 测试代码:

 type Rank struct {
  Level int
}

type Address struct {
  Rank
  StreetNo int
}

//go:noinline
func printAddr(addr Address) {
  println(addr.Level)
  println(addr.StreetNo)
}

func main() {
  printAddr(Address{Rank: Rank{Level: 1}, StreetNo: 1})
}  

可见被嵌套的结构体也按照字段传递,和直接把字段写在一个结构体里没有区别:

 "".printAddr STEXT size=110 args=0x10 locals=0x10
  0x0000 00000 (main.go:12)TEXT"".printAddr(SB), ABIInternal, $16-16
  ...
  0x0020 00032 (main.go:13)CALLruntime.printlock(SB)
  0x0025 00037 (main.go:13)MOVQ"".addr+24(SP), AX
  0x002a 00042 (main.go:13)MOVQAX, (SP)
  0x002e 00046 (main.go:13)CALLruntime.printint(SB)
  0x0033 00051 (main.go:13)CALLruntime.printnl(SB)
  0x0038 00056 (main.go:13)CALLruntime.printunlock(SB)
  0x003d 00061 (main.go:13)NOP
  0x0040 00064 (main.go:14)CALLruntime.printlock(SB)
  0x0045 00069 (main.go:14)MOVQ"".addr+32(SP), AX
  0x004a 00074 (main.go:14)MOVQAX, (SP)
  0x004e 00078 (main.go:14)CALLruntime.printint(SB)
  0x0053 00083 (main.go:14)CALLruntime.printnl(SB)
  0x0058 00088 (main.go:14)CALLruntime.printunlock(SB)
  0x005d 00093 (main.go:15)MOVQ8(SP), BP
  0x0062 00098 (main.go:15)ADDQ$16, SP
  0x0066 00102 (main.go:15)RET

"".main STEXT size=71 args=0x0 locals=0x18
  0x0000 00000 (main.go:18)TEXT"".main(SB), ABIInternal, $24-0
  ...
  0x001d 00029 (main.go:19)MOVQ$1, (SP)
  0x0025 00037 (main.go:19)MOVQ$1, 8(SP)
  0x002e 00046 (main.go:19)PCDATA$1, $0
  0x002e 00046 (main.go:19)CALL"".printAddr(SB)
  0x0033 00051 (main.go:20)MOVQ16(SP), BP
  0x0038 00056 (main.go:20)ADDQ$24, SP
  0x003c 00060 (main.go:20)RET  

传递数组参数,为了函数体简单,只打印了第一个元素:

 func printArray(data [3]int) { 
  print(data[0])
}

"".printArray STEXT size=80 args=0x18 locals=0x18
  0x0000 00000 (array.go:3) TEXT  "".printArray(SB), ABIInternal, $24-24
  ...
  0x001d 00029 (array.go:4) MOVQ  "".data+32(SP), AX      // * 加上 ret 和 bp 的 16,定位第 0 个元素
  0x0022 00034 (array.go:4) MOVQ  AX, ""..autotmp_2+8(SP)
  0x0027 00039 (array.go:4) PCDATA  $1, $0
  0x0027 00039 (array.go:4) CALL  runtime.printlock(SB)
  0x002c 00044 (array.go:4) MOVQ  ""..autotmp_2+8(SP), AX
  0x0031 00049 (array.go:4) MOVQ  AX, (SP)
  0x0035 00053 (array.go:4) CALL  runtime.printint(SB)
  0x003a 00058 (array.go:4) CALL  runtime.printunlock(SB)
  0x003f 00063 (array.go:5) MOVQ  16(SP), BP
  0x0044 00068 (array.go:5) ADDQ  $24, SP
  0x0048 00072 (array.go:5) RET
  ...
  0x004e 00078 (array.go:3) JMP 0

"".main STEXT size=77 args=0x0 locals=0x20
  0x0000 00000 (array.go:8)TEXT"".main(SB), ABIInternal, $32-0
  ...
  0x001d 00029 (array.go:9)MOVQ$1, (SP)
  0x0025 00037 (array.go:9)MOVQ$2, 8(SP)
  0x002e 00046 (array.go:9)MOVQ$3, 16(SP)
  0x0037 00055 (array.go:9)PCDATA$1, $0
  0x0037 00055 (array.go:9)CALL"".printArray(SB)
  ...  

把数组的每个值从右到左都放到栈上(类似 N 个参数),切片和数组的区别是很大的,看看 slice 切片如何传递的:

 package main

import "fmt"

//go:noinline
func printSlice(slice []int) {
  fmt.Println(slice)
}

func main() {
  printSlice([]int{1, 2, 3})
}  

对应的 main 的汇编代码:

 "".main STEXT size=118 args=0x0 locals=0x20
  0x0000 00000 (main.go:10)TEXT"".main(SB), ABIInternal, $32-0
  ...
  0x000f 00015 (main.go:10)     SUBQ    $32, SP
  0x0013 00019 (main.go:10)     MOVQ    BP, 24(SP)
  0x0018 00024 (main.go:10)     LEAQ    24(SP), BP
  ...
  0x001d 00029 (main.go:11)LEAQtype.[3]int(SB), AX
  0x0024 00036 (main.go:11)MOVQAX, (SP)
  0x0028 00040 (main.go:11)PCDATA$1, $0
  0x0028 00040 (main.go:11)CALLruntime.newobject(SB)
  0x002d 00045 (main.go:11)MOVQ8(SP), AX
  0x0032 00050 (main.go:11)MOVQ$1, (AX)    // 元素1  8(SP)
  0x0039 00057 (main.go:11)MOVQ$2, 8(AX)   // 元素2 16(SP)
  0x0041 00065 (main.go:11)MOVQ$3, 16(AX)  // 元素3 24(SP)
  0x0049 00073 (main.go:11)MOVQAX, (SP)    // Data 地址
  0x004d 00077 (main.go:11)MOVQ$3, 8(SP)   // Len 大小
  0x0056 00086 (main.go:11)MOVQ$3, 16(SP)  // Cap 大小
  0x005f 00095 (main.go:11)NOP
  0x0060 00096 (main.go:11)CALL"".printSlice(SB)
  0x0065 00101 (main.go:12)MOVQ24(SP), BP
  0x006a 00106 (main.go:12)ADDQ$32, SP
  0x006e 00110 (main.go:12)RET  

对应 reflect.SliceHedaer 和 runtime.newobject(SB) 的定义:

 type SliceHeader struct {
  Data uintptr
  Len  int
  Cap  int
}

func newobject(typ *_type) unsafe.Pointer {
  return mallocgc(typ.size, typ, true)
}  

初始化 slice 有三个元素值指向堆中的地址,调用的时候也是传入三个值,实际的值通过 *AX 指向的内存区域 (AX) 来操作,$3 是因为切片的 len 和 cap 都是 3,切片的三个成员字段都被复制,这段代码不多,但是栈变化比较复杂:

被调用参数的处理:

 "".printSlice STEXT size=175 args=0x18 locals=0x58
  0x0000 00000 (main.go:6)TEXT"".printSlice(SB), ABIInternal, $88-24
  0x0000 00000 (main.go:6)MOVQ(TLS), CX
  0x0009 00009 (main.go:6)CMPQSP, 16(CX)
  0x000d 00013 (main.go:6)PCDATA$0, $-2
  0x000d 00013 (main.go:6)JLS165
  0x0013 00019 (main.go:6)PCDATA$0, $-1
  0x0013 00019 (main.go:6)SUBQ$88, SP
  0x0017 00023 (main.go:6)MOVQBP, 80(SP)             // 基址
  0x001c 00028 (main.go:6)LEAQ80(SP), BP
  ...
  0x0021 00033 (main.go:7)MOVQ"".slice+96(SP), AX    // *
  0x0026 00038 (main.go:7)MOVQAX, (SP)
  0x002a 00042 (main.go:7)MOVQ"".slice+104(SP), AX   // * 
  0x002f 00047 (main.go:7)MOVQAX, 8(SP)
  0x0034 00052 (main.go:7)MOVQ"".slice+112(SP), AX   // *
  0x0039 00057 (main.go:7)MOVQAX, 16(SP)
  0x003e 00062 (main.go:7)PCDATA$1, $1
  0x003e 00062 (main.go:7)NOP
  0x0040 00064 (main.go:7)CALLruntime.convTslice(SB) // *
  0x0045 00069 (main.go:7)MOVQ24(SP), AX             // *
  0x004a 00074 (main.go:7)XORPSX0, X0
  0x004d 00077 (main.go:7)MOVUPSX0, ""..autotmp_13+64(SP)
  0x0052 00082 (main.go:7)LEAQtype.[]int(SB), CX
  0x0059 00089 (main.go:7)MOVQCX, ""..autotmp_13+64(SP)
  0x005e 00094 (main.go:7)MOVQAX, ""..autotmp_13+72(SP)  

注意 runtime.convTslice 把普通类型转换成切片结构:

 // src/cmd/compile/internal/gc/builtin/runtime.go
func convTslice(val any) unsafe.Pointer

// src/runtime/iface.go
func convTslice(val []byte) (x unsafe.Pointer) {
  if (*slice)(unsafe.Pointer(&val)).array == nil {
    x = unsafe.Pointer(&zeroVal[0])
  } else {
    x = mallocgc(unsafe.Sizeof(val), sliceType, true)
    *(*[]byte)(x) = val
  }
   
  return
}

// sliceType 
sliceType  *_type = efaceOf(&sliceEface)._type

func efaceOf(ep *interface{}) *eface {
  return (*eface)(unsafe.Pointer(ep))
}

// eface
type eface struct {
  _type *_type
  data  unsafe.Pointer
}  

最后看看 defer 插入的代码:

 package main

func f() int {
  i := 0

  defer func(val int) {
    val++
    println(val)
  }(i)

  return i
}  

汇编后调用 runtime.deferreturn(SB):

 "".f STEXT size=144 args=0x8 locals=0x28
  0x0029 00041 (defer.go:3)   MOVB  $0, ""..autotmp_3+15(SP)
  0x002e 00046 (defer.go:3)   MOVQ  $0, "".~r0+48(SP)
  0x0037 00055 (defer.go:6)   LEAQ  "".f.func1·f(SB), AX          // *
  0x003e 00062 (defer.go:6)   MOVQ  AX, ""..autotmp_4+24(SP)
  0x0043 00067 (defer.go:6)   MOVQ  $0, ""..autotmp_5+16(SP)
  0x004c 00076 (defer.go:6)   MOVB  $1, ""..autotmp_3+15(SP)
  0x0051 00081 (defer.go:10)  MOVQ  $0, "".~r0+48(SP)
  0x005a 00090 (defer.go:10)  MOVB  $0, ""..autotmp_3+15(SP)
  0x005f 00095 (defer.go:10)  MOVQ  ""..autotmp_5+16(SP), AX
  0x0064 00100 (defer.go:10)  MOVQ  AX, (SP)
  0x0068 00104 (defer.go:10)  PCDATA  $1, $1
  0x0068 00104 (defer.go:10)  CALL  "".f.func1(SB)                // *
  0x006d 00109 (defer.go:10)  MOVQ  32(SP), BP
  0x0072 00114 (defer.go:10)  ADDQ  $40, SP
  0x0076 00118 (defer.go:10)  RET
  0x0077 00119 (defer.go:10)  CALL  runtime.deferreturn(SB)       // *
  0x007c 00124 (defer.go:10)  MOVQ  32(SP), BP
  0x0081 00129 (defer.go:10)  ADDQ  $40, SP
  0x0085 00133 (defer.go:10)  RET

"".f.func1 STEXT size=86 args=0x8 locals=0x10
  0x0020 00032 (right.go:9)   CALLruntime.printlock(SB)
  0x0025 00037 (right.go:8)   MOVQ"".val+24(SP), AX         //*
  0x002a 00042 (right.go:8)   INCQAX
  0x002d 00045 (right.go:9)   MOVQAX, (SP)
  0x0031 00049 (right.go:9)   CALLruntime.printint(SB)
  0x0036 00054 (right.go:9)   CALLruntime.printnl(SB)
  0x003b 00059 (right.go:9)   NOP
  0x0040 00064 (right.go:9)   CALLruntime.printunlock(SB)
  0x0045 00069 (right.go:10)  MOVQ8(SP), BP
  0x004a 00074 (right.go:10)  ADDQ$16, SP
  0x004e 00078 (right.go:10)  RET  

不希望的版本:

 package main 

func f() {
  i := 0

  defer func() {
    println(i) // 1
  }()

  i++
}  

汇编代码如下:

 "".f STEXT size=145 args=0x0 locals=0x30
  0x0025 00037 (wrong.go:3)   MOVB  $0, ""..autotmp_2+15(SP)
  0x002a 00042 (wrong.go:4)   MOVQ  $0, "".i+16(SP)
  0x0033 00051 (wrong.go:6)   LEAQ  "".f.func1·f(SB), AX
  0x003a 00058 (wrong.go:6)   MOVQ  AX, ""..autotmp_3+32(SP)
  0x003f 00063 (wrong.go:6)   LEAQ  "".i+16(SP), AX
  0x0044 00068 (wrong.go:6)   MOVQ  AX, ""..autotmp_4+24(SP)
  0x0049 00073 (wrong.go:6)   MOVB  $1, ""..autotmp_2+15(SP)
  0x004e 00078 (wrong.go:10)  MOVQ  "".i+16(SP), AX
  0x0053 00083 (wrong.go:10)  INCQ  AX
  0x0056 00086 (wrong.go:10)  MOVQ  AX, "".i+16(SP)
  0x005b 00091 (wrong.go:11)  MOVB  $0, ""..autotmp_2+15(SP)
  0x0060 00096 (wrong.go:11)  MOVQ  ""..autotmp_4+24(SP), AX
  0x0065 00101 (wrong.go:11)  MOVQ  AX, (SP)
  0x0069 00105 (wrong.go:11)  PCDATA  $1, $1
  0x0069 00105 (wrong.go:11)  CALL  "".f.func1(SB)
  0x006e 00110 (wrong.go:11)  MOVQ  40(SP), BP
  0x0073 00115 (wrong.go:11)  ADDQ  $48, SP
  0x0077 00119 (wrong.go:11)  RET
  0x0078 00120 (wrong.go:11)  CALL  runtime.deferreturn(SB)
  0x007d 00125 (wrong.go:11)  MOVQ  40(SP), BP
  0x0082 00130 (wrong.go:11)  ADDQ  $48, SP
  0x0086 00134 (wrong.go:11)  RET

"".f.func1 STEXT size=86 args=0x8 locals=0x10
  0x0020 00032 (wrong.go:7) CALL  runtime.printlock(SB)
  0x0025 00037 (wrong.go:7) MOVQ  "".&i+24(SP), AX      // *
  0x002a 00042 (wrong.go:7) MOVQ  (AX), AX
  0x002d 00045 (wrong.go:7) MOVQ  AX, (SP)
  0x0031 00049 (wrong.go:7) PCDATA  $1, $1
  0x0031 00049 (wrong.go:7) CALL  runtime.printint(SB)
  0x0036 00054 (wrong.go:7) CALL  runtime.printnl(SB)
  0x003b 00059 (wrong.go:7) NOP
  0x0040 00064 (wrong.go:7) CALL  runtime.printunlock(SB)
  0x0045 00069 (wrong.go:8) MOVQ  8(SP), BP
  0x004a 00074 (wrong.go:8) ADDQ  $16, SP
  0x004e 00078 (wrong.go:8) RET  

逃逸分析意思是编译器对代码进行分析,发现某些分配在堆上的对象没有必要转为分配在栈上,或者看起来是分配到栈上的对象需要更长的生命周期,转而分配到堆上。

 // go build -gcflags=-m main.go
func foo1() *person {
  // 分配到堆上
  return &person{Name: "zhangsan"}
}

/*
  0x001d 00029 (main.go:9)  LEAQ  type."".person(SB), AX
  0x0024 00036 (main.go:9)  MOVQ  AX, (SP)
  0x0028 00040 (main.go:9)  PCDATA  $1, $0
  0x0028 00040 (main.go:9)  CALL  runtime.newobject(SB)
  0x002d 00045 (main.go:9)  MOVQ  8(SP), AX
  0x0032 00050 (main.go:9)  MOVQ  $8, 8(AX)
  0x003a 00058 (main.go:9)  LEAQ  go.string."zhangsan"(SB), CX
  0x0041 00065 (main.go:9)  MOVQ  CX, (AX)
  0x0044 00068 (main.go:9)  MOVQ  AX, "".~r0+32(SP)
  0x0049 00073 (main.go:9)  MOVQ  16(SP), BP
  0x004e 00078 (main.go:9)  ADDQ  $24, SP
*/  

下面的对象是使用 new 函数分配的,但是编译器是在为期分配到栈上,也可使用 go build -gcflags=-m main.go 来查看编译器的分析。

 // go build -gcflags=-m main.go
// new(person) does not escape
func foo3() {
  // 分配到栈上
  p := new(person)
  p.Name = "zhangsan"
  println(p)
}

/*
  0x0020 00032 (main.go:21) CALL  runtime.printlock(SB)
  0x0025 00037 (main.go:21) LEAQ  go.string."zhangsan"(SB), AX
  0x002c 00044 (main.go:21) MOVQ  AX, (SP)
  0x0030 00048 (main.go:21) MOVQ  $8, 8(SP)
  0x0039 00057 (main.go:21) CALL  runtime.printstring(SB)
  0x003e 00062 (main.go:21) NOP
  0x0040 00064 (main.go:21) CALL  runtime.printnl(SB)
  0x0045 00069 (main.go:21) CALL  runtime.printunlock(SB)
  0x004a 00074 (main.go:22) MOVQ  16(SP), BP
  0x004f 00079 (main.go:22) ADDQ  $24, SP
  0x0053 00083 (main.go:22) RET
*/  

不过,改一下这个打印函数,new(person) 就会 escapses to heap,修改如下:

 // go build -gcflags=-m main.go
// new(person) escapes to heap
func foo3_1() {
  // 分配到堆上
  p := new(person)
  p.Name = "zhangsan"
  // 传递的是 []interface{}
  fmt.Printf("%v", p)
}  

原因是 fmt.Printf 函数传递的不定参数是 []interface,p 通过地址转换过去,不过事情并不总是像看到的这样,下面的函数按道理是分配到堆上:

 type point struct {
  x, y int
}

func move(p *point) {
  p.x = 10
  p.y = 10
}

func main() {
  // 分配到栈上,注意 move 函数被内敛了
  p := new(point)
  move(p)
  print(p)
}  

先使用 go build -gcflags=-m main.go 分析,结果显示:

 ./main.go:13:6: can inline move
./main.go:34:6: can inline main
./main.go:37:6: inlining call to move
./main.go:13:11: p does not escape
./main.go:36:10: new(point) does not escape  

注意 p 和 new(point) 都是 does not escape,为什么呢? 因为 move 函数被 inline 了(可以使用 //go:noinline 取消内敛),main 函数的汇编代码如下:

 /*
  0x001d 00029 (main.go:15) XORPS X0, X0
  0x0020 00032 (main.go:14) MOVUPS  X0, ""..autotmp_2+8(SP)
  0x0025 00037 (<unknown line number>)  NOP
  0x0025 00037 (main.go:8)  MOVQ  $10, ""..autotmp_2+8(SP)
  0x002e 00046 (main.go:9)  MOVQ  $10, ""..autotmp_2+16(SP)
  0x0037 00055 (main.go:16) PCDATA  $1, $0
  0x0037 00055 (main.go:16) CALL  runtime.printlock(SB)
  0x003c 00060 (main.go:16) LEAQ  ""..autotmp_2+8(SP), AX
  0x0041 00065 (main.go:16) MOVQ  AX, (SP)
  0x0045 00069 (main.go:16) CALL  runtime.printpointer(SB)
  0x004a 00074 (main.go:16) CALL  runtime.printunlock(SB)
  0x004f 00079 (main.go:17) MOVQ  24(SP), BP
  0x0054 00084 (main.go:17) ADDQ  $32, SP
  0x0058 00088 (main.go:17) RET
*/  

本章节的代码

文章来源:智云一二三科技

文章标题:大白话 golang 教程-29-反汇编和内存结构

文章地址:https://www.zhihuclub.com/97095.shtml

关于作者: 智云科技

热门文章

网站地图