在深入学习 Golang 的 runtime 和标准库实现的时候创造,如果对 Golang 汇编没有一定理解的话,很难深入理解其底层实现机制。在这里整理总结了一份根本的 Golang 汇编入门知识,通过学习之后能够对其底层实现有一定的认识。
0. 为什么写本文平时业务中一贯利用 PHP 编写代码,但是一贯对 Golang 比较感兴趣,空隙、周末之余会看一些 Go 底层源码。
近日在剖析 go 的某些特性底层功能实现时创造:有些又跟 runtime 运行时有关,而要节制这一部分的话,有一道坎是绕不过去的,那便是 Go 汇编。索性就查阅了很多大佬们写的资料,在阅读之余整理总结了一下,并在这里分享给大家。

本文利用 Go 版本为 go1.14.1
1. 为什么须要汇编众所周知,在打算机的天下里,只有 2 种类型。那便是:0 和 1。
打算机事情是由一系列的机器指令进行驱动的,这些指令又是一组二进制数字,其对应打算机的高低电平。而这些机器指令的凑集便是机器措辞,这些机器措辞在最底层是与硬件逐一对应的。
显而易见,这样的机器指令有一个致命的缺陷:可阅读性太差(恐怕也只有天才和疯子才有能力把控得了)。
为理解决可读性的问题以及代码编辑的需求,于是就出身了最靠近机器的措辞:汇编措辞(在我看来,汇编措辞更像一种助记符,这些人们随意马虎记住的每一条助记符都映射着一条不随意马虎记住的由 0、1 组成的机器指令。你以为像不像域名与 IP 地址的关系呢?)。
1.1 程序的编译过程以 C 措辞为例来说,从 hello.c 的源码文件到 hello 可实行文件,经由编译器处理,大致分为几个阶段:
编译器在不同的阶段会做不同的事情,但是有一步是可以确定的,那便是:源码会被编译成汇编,末了才是二进制。
2. 程序与进程源码经由编译之后,得到一个二进制的可实行文件。文件这两个字也就表明,目前得到的这个文件跟其他文件比拟,除了是具有一定的格式(Linux 中是 ELF 格式,即:可运行可链接。executable linkable formate)的二进制组成,并没什么差异。
在 Linux 中文件类型大致分为 7 种:
b:块设备文件c:字符设备文件d:目录-:普通文件l:链接s:socketp:管道
通过上面可以看到,可实行文件 main 与源码文件 main.go,都是同一种类型,属于普通文件。(当然了,在 Unix 中有一句很经典的话:统统皆文件)。
那么,问题来了:
什么是程序?什么是进程?2.1 程序维基百科见告我们:程序是指一组指示打算机或其他具有处理能力设备每一步动作的指令,常日用某种程序设计措辞编写,运行于某种目标体系构造上。
从某个层面来看,可以把程序分为静态程序、动态程序:静态程序:纯挚的指具有一定格式的可实行二进制文件。动态程序:则是静态可实行程序文件被加载到内存之后的一种运行时模型(又称为进程)。
2.2 进程首先,要知道的是,进程是分配系统资源的最小单位,线程(带有韶光片的函数)是系统调度的最小单位。进程包含线程,线程所属于进程。
创建进程一样平常利用 fork 方法(常日会有个拉动身序,先 fork 自身天生一个子进程。然后,在该子进程中通过 exec 函数把对应程序加载进来,进而启动目标进程。当然,实际上要繁芜得多),而创建线程则是利用 pthread 线程库。
以 32 位 Linux 操作系统为例,进程经典的虚拟内存构造模型如下图所示:
个中,有两处构造是静态程序所不具有的,那便是运行时堆(heap)与运行时栈(stack)。
运行时堆从低地址向高地址增长,申请的内存空间须要程序员自己或者由 GC 开释。运行时栈从高地址向低地址增长,内存空间在当前栈桢调用结束之后自动开释(并不是打消其所占用内存中数据,而是通过栈顶指针 SP 的移动,来标识哪些内存是正在利用的)。
3. Go 汇编对付 Go 编译器而言,其输出的结果是一种抽象可移植的汇编代码,这种汇编(Go 的汇编是基于 Plan9 的汇编)并不对应某种真实的硬件架构。Go 的汇编器会利用这种伪汇编,再为目标硬件天生详细的机器指令。
伪汇编这一个额外层可以带来很多好处,最紧张的一点是方便将 Go 移植到新的架构上。
干系的信息可以参考 Rob Pike 的 The Design of the Go Assembler。
要理解 Go 的汇编器最主要的是要知道 Go 的汇编器不是对底层机器的直接表示,即 Go 的汇编器没有直策应用目标机器的汇编指令。Go 汇编器所用的指令,一部分与目标机器的指令逐一对应,而其余一部分则不是。这是由于编译器套件不须要汇编器直接参与常规的编译过程。
相反,编译器利用了一种半抽象的指令集,并且部分指令是在代码天生后才当选择的。汇编器基于这种半抽象的形式事情,以是虽然你看到的是一条 MOV 指令,但是工具链针对对这条指令实际天生可能完备不是一个移动指令,大概会是打消或者加载。也有可能精确的对应目标平台上同名的指令。概括来说,特定于机器的指令会以他们的本尊涌现, 然而对付一些通用的操作,如内存的移动以及子程序的调用以及返回常日都做了抽象。细节因架构不同而不一样,我们对这样的禁绝确性表示歉意,情形并不明确。
汇编器程序的事情是对这样半抽象指令集进行解析并将其转变为可以输入到链接器的指令。
The most important thing to know about Go’s assembler is that it is not a direct representation of the underlying machine. Some of the details map precisely to the machine, but some do not. This is because the compiler suite needs no assembler pass in the usual pipeline. Instead, the compiler operates on a kind of semi-abstract instruction set, and instruction selection occurs partly after code generation. The assembler works on the semi-abstract form, so when you see an instruction like MOV what the toolchain actually generates for that operation might not be a move instruction at all, perhaps a clear or load.
Or it might correspond exactly to the machine instruction with that name. In general, machine-specific operations tend to appear as themselves, while more general concepts like memory move and subroutine call and return are more abstract. The details vary with architecture, and we apologize for the imprecision; the situation is not well-defined.
The assembler program is a way to parse a description of that semi-abstract instruction set and turn it into instructions to be input to the linker.
Go 汇编利用的是caller-save模式,被调用函数的入参参数、返回值都由调用者掩护、准备。因此,当须要调用一个函数时,须要先将这些事情准备好,才调用下一个函数,其余这些都须要进行内存对齐,对齐的大小是 sizeof(uintptr)。
3.1 几个观点在深入理解 Go 汇编之前,须要知道的几个观点:
栈:进程、线程、goroutine 都有自己的调用栈,前辈后出(FILO)栈帧:可以理解是函数调用时,在栈上为函数所分配的内存区域调用者:caller,比如:A 函数调用了 B 函数,那么 A 便是调用者被调者:callee,比如:A 函数调用了 B 函数,那么 B 便是被调者3.2 Go 的核心寄存器go 汇编中有 4 个核心的伪寄存器,这 4 个寄存器是编译器用来掩护高下文、分外标识等浸染的:
寄存器解释SB(Static base pointer)global symbolsFP(Frame pointer)arguments and localsPC(Program counter)jumps and branchesSP(Stack pointer)top of stack
FP: 利用如 symbol+offset(FP)的办法,引用 callee 函数的入参参数。例如 arg0+0(FP),arg1+8(FP),利用 FP 必须加 symbol ,否则无法通过编译(从汇编层面来看,symbol 没有什么用,加 symbol 紧张是为了提升代码可读性)。其余,须要把稳的是:每每在编写 go 汇编代码时,要站在 callee 的角度来看(FP),在 callee 看来,(FP)指向的是 caller 调用 callee 时通报的第一个参数的位置。如果当前的 callee 函数是 add,在 add 的代码中引用 FP,该 FP 指向的位置不在 callee 的 stack frame 之内。而是在 caller 的 stack frame 上,指向调用 add 函数时通报的第一个参数的位置,常常在 callee 中用symbol+offset(FP)来获取入参的参数值。SB: 全局静态基指针,一样平常用在声明函数、全局变量中。SP: 该寄存器也是最具有迷惑性的寄存器,由于会有伪 SP 寄存器和硬件 SP 寄存器之分。plan9 的这个伪 SP 寄存器指向当前栈帧第一个局部变量的结束位置(为什么说是结束位置,可以看下面寄存器内存布局图),利用形如 symbol+offset(SP) 的办法,引用函数的局部变量。offset 的合法取值是 [-framesize, 0),把稳是个左闭右开的区间。如果局部变量都是 8 字节,那么第一个局部变量就可以用 localvar0-8(SP) 来表示。与硬件寄存器 SP 是两个不同的东西,在栈帧 size 为 0 的情形下,伪寄存器 SP 和硬件寄存器 SP 指向同一位置。手写汇编代码时,如果是 symbol+offset(SP)形式,则表示伪寄存器 SP。如果是 offset(SP)则表示硬件寄存器 SP。务必把稳:对付编译输出(go tool compile -S / go tool objdump)的代码来讲,所有的 SP 都是硬件 SP 寄存器,无论是否带 symbol(这一点非常具有迷惑性,须要逐步理解。每每在剖析编译输出的汇编时,看到的便是硬件 SP 寄存器)。PC: 实际上便是在体系构造的知识中常见的 pc 寄存器,在 x86 平台下对应 ip 寄存器,amd64 上则是 rip。除了个别跳转之外,手写 plan9 汇编代码时,很少用到 PC 寄存器。通过上面的讲解,想必已经对 4 个核心寄存器的差异有了一定的认识(或者是更加的迷惑、一头雾水)。那么,须要留神的是:如果是在剖析编译输出的汇编代码时,要重点看 SP、SB 寄存器(FP 寄存器在这里是看不到的)。如果是,在手写汇编代码,那么要重点看 FP、SP 寄存器。
3.2.1 伪寄存器的内存模型下图描述了栈桢与各个寄存器的内存关系模型,值得把稳的是要站在 callee 的角度来看。
有一点须要把稳的是,return addr 也是在 caller 的栈上的,不过往栈上插 return addr 的过程是由 CALL 指令完成的(在剖析汇编时,是看不到关于 addr 干系空间信息的。在分配栈空间时,addr 所占用空间大小不包含在栈帧大小内)。
在 AMD64 环境,伪 PC 寄存器实在是 IP 指令计数器寄存器的别名。伪 FP 寄存器对应的是 caller 函数的帧指针,一样平常用来访问 callee 函数的入参参数和返回值。伪 SP 栈指针对应的是当前 callee 函数栈帧的底部(不包括参数和返回值部分),一样平常用于定位局部变量。伪 SP 是一个比较分外的寄存器,由于还存在一个同名的 SP 真寄存器,真 SP 寄存器对应的是栈的顶部。
在编写 Go 汇编时,当须要区分伪寄存器和真寄存器的时候只须要记住一点:伪寄存器一样平常须要一个标识符和偏移量为前缀,如果没有标识符前缀则是真寄存器。比如(SP)、+8(SP)没有标识符前缀为真 SP 寄存器,而 a(SP)、b+8(SP)有标识符为前缀表示伪寄存器。
3.2.2 几点解释我们这里对随意马虎稠浊的几点大略进行解释:
伪 SP 和硬件 SP 不是一回事,在手写汇编代码时,伪 SP 和硬件 SP 的区分方法是看该 SP 前是否有 symbol。如果有 symbol,那么即为伪寄存器,如果没有,那么解释是硬件 SP 寄存器。伪 SP 和 FP 的相对位置是会变的,以是不应该考试测验用伪 SP 寄存器去找那些用 FP+offset 来引用的值,例如函数的入参和返回值。官方文档中说的伪 SP 指向 stack 的 top,可能是有问题的。其指向的局部变量位置实际上是全体栈的栈底(除 caller BP 之外),以是说 bottom 更得当一些。在 go tool objdump/go tool compile -S 输出的代码中,是没有伪 SP 和 FP 寄存器的,我们上面说的区分伪 SP 和硬件 SP 寄存器的方法,对付上述两个命令的输出结果是没法利用的。在编译和反汇编的结果中,只有真实的 SP 寄存器。3.2.3 IA64 和 plan9 的对应关系在 plan9 汇编里还可以直策应用的 amd64 的通用寄存器,运用代码层面会用到的通用寄存器紧张是: rax, rbx, rcx, rdx, rdi, rsi, r8~r15 这些寄存器,虽然 rbp 和 rsp 也可以用,不过 bp 和 sp 会被用来管理栈顶和栈底,最好不要拿来进走运算。
plan9 中利用寄存器不须要带 r 或 e 的前缀,例如 rax,只要写 AX 即可: MOVQ $101, AX = mov rax, 101
下面是通用通用寄存器的名字在 IA64 和 plan9 中的对应关系:
3.3 常用操作指令
下面列出了常用的几个汇编指令(指令后缀Q 解释是 64 位上的汇编指令)
助记符指令种类用场示例MOVQ传送数据传送MOVQ 48, AX // 把 48 传送到 AXLEAQ传送地址传送LEAQ AX, BX // 把 AX 有效地址传送到 BXPUSHQ传送栈压入PUSHQ AX // 将 AX 内容送入栈顶位置POPQ传送栈弹出POPQ AX // 弹出栈顶数据后修正栈顶指针ADDQ运算相加并赋值ADDQ BX, AX // 等价于 AX+=BXSUBQ运算相减并赋值SUBQ BX, AX // 等价于 AX-=BXCMPQ运算比较大小CMPQ SI CX // 比较 SI 和 CX 的大小CALL转移调用函数CALL runtime.printnl(SB) // 发起调用JMP转移无条件转移指令JMP 0x0185 //无条件转至 0x0185 地址处JLS转移条件转移指令JLS 0x0185 //左边小于右边,则跳到 0x0185
4. 汇编剖析说了那么多,it is code show time。
4.1 如何输出 Go 汇编对付写好的 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 输出汇编代码
4.2 Go 汇编示例go 示例代码
packagemainfuncadd(a,bint)int{sum:=0//不设置该局部变量sum,add栈空间大小会是0sum=a+breturnsum}funcmain(){println(add(1,2))}
编译 go 源代码,输出汇编
gotoolcompile-N-l-Smain.go
截取紧张汇编如下:
"".add STEXT nosplit size=60 args=0x18 locals=0x10 0x0000 00000 (main.go:3) TEXT "".add(SB), NOSPLIT, $16-24 0x0000 00000 (main.go:3) SUBQ $16, SP ;;天生add栈空间 0x0004 00004 (main.go:3) MOVQ BP, 8(SP) 0x0009 00009 (main.go:3) LEAQ 8(SP), BP ;; ...omitted FUNCDATA stuff... 0x000e 00014 (main.go:3) MOVQ $0, "".~r2+40(SP) ;;初始化返回值 0x0017 00023 (main.go:4) MOVQ $0, "".sum(SP) ;;局部变量sum赋为0 0x001f 00031 (main.go:5) MOVQ "".a+24(SP), AX ;;取参数a 0x0024 00036 (main.go:5) ADDQ "".b+32(SP), AX ;;等价于AX=a+b 0x0029 00041 (main.go:5) MOVQ AX, "".sum(SP) ;;赋值局部变量sum 0x002d 00045 (main.go:6) MOVQ AX, "".~r2+40(SP) ;;设置返回值 0x0032 00050 (main.go:6) MOVQ 8(SP), BP 0x0037 00055 (main.go:6) ADDQ $16, SP ;;打消add栈空间 0x003b 00059 (main.go:6) RET ......"".main STEXT size=107 args=0x0 locals=0x28 0x0000 00000 (main.go:9) TEXT "".main(SB), $40-0 ...... 0x000f 00015 (main.go:9) SUBQ $40, SP ;; 天生main栈空间 0x0013 00019 (main.go:9) MOVQ BP, 32(SP) 0x0018 00024 (main.go:9) LEAQ 32(SP), BP ;; ...omitted FUNCDATA stuff... 0x001d 00029 (main.go:10) MOVQ $1, (SP) ;;add入参:1 0x0025 00037 (main.go:10) MOVQ $2, 8(SP) ;;add入参:2 0x002e 00046 (main.go:10) CALL "".add(SB) ;;调用add函数 0x0033 00051 (main.go:10) MOVQ 16(SP), AX 0x0038 00056 (main.go:10) MOVQ AX, ""..autotmp_0+24(SP) 0x003d 00061 (main.go:10) CALL runtime.printlock(SB) 0x0042 00066 (main.go:10) MOVQ ""..autotmp_0+24(SP), AX 0x0047 00071 (main.go:10) MOVQ AX, (SP) 0x004b 00075 (main.go:10) CALL runtime.printint(SB) 0x0050 00080 (main.go:10) CALL runtime.printnl(SB) 0x0055 00085 (main.go:10) CALL runtime.printunlock(SB) 0x005a 00090 (main.go:11) MOVQ 32(SP), BP 0x005f 00095 (main.go:11) ADDQ $40, SP ;;打消main栈空间 0x0063 00099 (main.go:11) RET ......
这里列举了一个大略的 int 类型加法示例,实际开拓中会碰着各种参数类型,要繁芜的多,这里只是抛砖引玉 :)
4.3 Go 汇编解析针对 4.2 输出汇编,对主要核心代码进行剖析。
4.3.1 add 函数汇编解析TEXT "".add(SB), NOSPLIT|ABIInternal, $16-24TEXT "".add TEXT 指令声明了 "".add 是 .text 代码段的一部分,并表明跟在这个声明后的是函数的函数体。在链接期,""这个空字符会被更换为当前的包名: 也便是说,"".add在链接到二进制文件后会变成 main.add
(SB) SB 是一个虚拟的伪寄存器,保存静态基地址(static-base) 指针,即我们程序地址空间的开始地址。"".add(SB) 表明我们的符号位于某个固定的相对地址空间起始处的偏移位置 (终极是由连接器打算得到的)。换句话来讲,它有一个直接的绝对地址: 是一个全局的函数符号。
NOSPLIT: 向编译器表明不应该插入 stack-split 的用来检讨栈须要扩展的前导指令。在我们 add 函数的这种情形下,编译器自己帮我们插入了这个标记: 它足够聪明地意识到,由于 add 没有任何局部变量且没有它自己的栈帧,以是一定不会超出当前的栈。不然,每次调用函数时,在这里实行栈检讨便是完备摧残浪费蹂躏 CPU 韶光了。
$0-16
24 指定了调用方传入的参数+返回值大小(24 字节=入参 a、b 大小8字节2+返回值8字节)
常日来讲,帧大小后一样平常都跟随着一个参数大小,用减号分隔。(这不是一个减法操作,只是一种分外的语法) 帧大小 $24-8 意味着这个函数有 24 个字节的帧以及 8 个字节的参数,位于调用者的帧上。如果 NOSPLIT 没有在 TEXT 中指定,则必须供应参数大小。对付 Go 原型的汇编函数,go vet 会检讨参数大小是否精确。
In the general case, the frame size is followed by an argument size, separated by a minus sign. (It’s not a subtraction, just idiosyncratic syntax.) The frame size $24-8 states that the function has a 24-byte frame and is called with 8 bytes of argument, which live on the caller’s frame. If NOSPLIT is not specified for the TEXT, the argument size must be provided. For assembly functions with Go prototypes, go vet will check that the argument size is correct.
SUBQ $16, SPSP 为栈顶指针,该语句等价于 SP-=16(由于栈空间是向下增长的,以是开辟栈空间时为减操作),表示天生 16 字节大小的栈空间。MOVQ $0, "".~r2+40(SP)此时的 SP 为 add 函数栈的栈顶指针,40(SP)的位置则是 add 返回值的位置,该位置位于 main 函数栈空间内。该语句设置返回值类型的 0 值,即初始化返回值,防止得到脏数据(返回值类型为 int,int 的 0 值为 0)。MOVQ "".a+24(SP), AX从 main 函数栈空间获取入参 a 的值,存到寄存器 AXADDQ "".b+32(SP), AX从 main 函数栈空间获取入参 b 的值,与寄存器 AX 中存储的 a 值相加,结果存到 AX。相称于 AX=a+bMOVQ AX, "".~r2+40(SP)把 a+b 的结果放到 main 函数栈中, add(a+b)返回值所在的位置ADDQ $16, SP归还 add 函数占用的栈空间4.3.2 函数栈桢构造模型根据 4.2 对应汇编绘制的函数栈桢构造模型
还记得前面提到的,Go 汇编利用的是caller-save模式,被调用函数的参数、返回值、栈位置都须要由调用者掩护、准备吗?
在函数栈桢构造中可以看到,add()函数的入参以及返回值都由调用者 main()函数掩护。也正是由于如此,GO 有了其他措辞不具有的,支持多个返回值的特性。
4.4 Go 汇编语法这里重点讲一下函数声明、变量声明。
4.4.1 函数声明来看一个范例的 Go 汇编函数定义
//funcadd(a,bint)int//该add函数声明定义在同一个packagename下的任意.go文件中//只有函数头,没有实现//add函数的Go汇编实现//pkgname默认是""TEXTpkgname·add(SB),NOSPLIT,$16-24MOVQa+0(FP),AXADDQb+8(FP),AXMOVQAX,ret+16(FP)RET
Go 汇编实现为什么是 TEXT 开头?仔细不雅观察上面的进程内存布局图就会创造,我们的代码在是存储在.text 段中的,这里也便是一种约定俗成的起名办法。实际上在 plan9 中 TEXT 是一个指令,用来定义一个函数。
定义中的 pkgname 是可以省略的,(非想写也可以写上,不过写上 pkgname 的话,在重命名 package 之后还须要改代码,默认为"") 编译器会在链接期自动加上所属的包名称。
中点 · 比较分外,是一个 unicode 的中点,该点在 mac 下的输入方法是 option+shift+9。在程序被链接之后,所有的中点·都会被更换为句号.,比如你的方法是runtime·main,在编译之后的程序里的符号则是runtime.main。
大略总结一下, Go 汇编实现函数声明,格式为:
静态基地址(static-base)指针||add函数入参+返回值总大小||TEXTpkgname·add(SB),NOSPLIT,$16-24|||函数所属包名函数名add函数栈帧大小
函数栈帧大小:局部变量+可能须要的额外调用函数的参数空间的总大小,不包括调用其它函数时的 ret address 的大小。(SB): SB 是一个虚拟寄存器,保存了静态基地址(static-base) 指针,即我们程序地址空间的开始地址。"".add(SB) 表明我们的符号位于某个固定的相对地址空间起始处的偏移位置 (终极是由链接器打算得到的)。换句话来讲,它有一个直接的绝对地址: 是一个全局的函数符号。NOSPLIT: 向编译器表明,不应该插入 stack-split 的用来检讨栈须要扩展的前导指令。在我们 add 函数的这种情形下,编译器自己帮我们插入了这个标记: 它足够聪明地意识到,add 不会超出当前的栈,因此没必要调用函数时在这里实行栈检讨。4.4.2 变量声明
汇编里的全局变量,一样平常是存储在.rodata或者.data段中。对应到 Go 代码,便是已初始化过的全局的 const、var 变量/常量。
利用 DATA 结合 GLOBL 来定义一个变量。
DATA 的用法为:
DATAsymbol+offset(SB)/width,value
大多数参数都是字面意思,不过这个 offset 须要把稳:其含义是该值相对付符号 symbol 的偏移,而不是相对付全局某个地址的偏移。
GLOBL 汇编指令用于定义名为 symbol 的全局变量,变量对应的内存宽度为 width,内存宽度部分必须用常量初始化。
GLOBL·symbol(SB),width
下面是定义了多个变量的例子:
DATA·age+0(SB)/4,$8;;数值8为4字节GLOBL·age(SB),RODATA,$4DATA·pi+0(SB)/8,$3.1415926;;数值3.1415926为float64,8字节GLOBL·pi(SB),RODATA,$8DATA·year+0(SB)/4,$2020;;数值2020为4字节GLOBL·year(SB),RODATA,$4;;变量hello利用2个DATA来定义DATA·hello+0(SB)/8,$"hellomy";;`hellomy`共8个字节DATA·hello+8(SB)/8,$"world";;`world`共8个字节(3个空格)GLOBL·hello(SB),RODATA,$16;;`hellomyworld`共16个字节DATA·hello<>+0(SB)/8,$"hellomy";;`hellomy`共8个字节DATA·hello<>+8(SB)/8,$"world";;`world`共8个字节(3个空格)GLOBL·hello<>(SB),RODATA,$16;;`hellomyworld`共16个字节
大部分都比较好理解,不过这里引入了新的标记<>,这个跟在符号名之后,表示该全局变量只在当前文件中生效,类似于 C 措辞中的 static。如果在其余文件中引用该变量的话,会报 relocation target not found 的缺点。
5. 手写汇编实现功能在 Go 源码中会看到一些汇编写的代码,这些代码跟其他 go 代码一起组成了全体 go 的底层功能实现。下面,我们通过一个大略的 Go 汇编代码示例来实现两数相加功能。
5.1 利用 Go 汇编实现 add 函数Go 代码
packagemainfuncadd(a,bint64)int64funcmain(){println(add(2,3))}
Go 源码中 add()函数只有函数署名,没有详细的实现(利用 GO 汇编实现)
利用 Go 汇编实现的 add()函数
TEXT·add(SB),$0-24;;add栈空间为0,入参+返回值大小=24字节MOVQx+0(FP), AX ;;从main中取参数:2ADDQy+8(FP), AX ;;从main中取参数:3MOVQAX,ret+16(FP);;保存结果到返回值RET
把 Go 源码与 Go 汇编编译到一起(我这里,这两个文件在同一个目录)
gobuild-gcflags"-N-l".
我这里目录为 demo1,以是得到可实行程序 demo1,运行得到结果:5
5.2 反编译可实行程序对 5.1 中得到的可实行程序 demo1 利用 objdump 进行反编译,获取汇编代码
gotoolobjdump-s"main\."demo1
得到汇编
......TEXTmain.main(SB)/root/go/src/demo1/main.gomain.go:50x4581d064488b0c25f8ffffffMOVQFS:0xfffffff8,CXmain.go:50x4581d9483b6110CMPQ0x10(CX),SPmain.go:50x4581dd7655JBE0x458234main.go:50x4581df4883ec28SUBQ$0x28,SP;;天生main栈桢main.go:50x4581e348896c2420MOVQBP,0x20(SP)main.go:50x4581e8488d6c2420LEAQ0x20(SP),BPmain.go:60x4581ed48c7042402000000MOVQ$0x2,0(SP);;参数值2main.go:60x4581f548c744240803000000MOVQ$0x3,0x8(SP);;参数值3main.go:60x4581fee83d000000CALLmain.add(SB);;calladdmain.go:60x458203488b442410MOVQ0x10(SP),AXmain.go:60x4582084889442418MOVQAX,0x18(SP)main.go:60x45820de8fe2dfdffCALLruntime.printlock(SB)main.go:60x458212488b442418MOVQ0x18(SP),AXmain.go:60x45821748890424MOVQAX,0(SP)main.go:60x45821be87035fdffCALLruntime.printint(SB)main.go:60x458220e87b30fdffCALLruntime.printnl(SB)main.go:60x458225e8662efdffCALLruntime.printunlock(SB)main.go:70x45822a488b6c2420MOVQ0x20(SP),BPmain.go:70x45822f4883c428ADDQ$0x28,SPmain.go:70x458233c3RETmain.go:50x458234e89797ffffCALLruntime.morestack_noctxt(SB)main.go:50x458239eb95JMPmain.main(SB);;反编译得到的汇编与add_amd64.s文件中的汇纪年夜致操作同等TEXTmain.add(SB)/root/go/src/demo1/add_amd64.sadd_amd64.s:20x458240488b442408MOVQ0x8(SP),AX;;获取第一个参数add_amd64.s:30x4582454803442410ADDQ0x10(SP),AX;;参数a+参数badd_amd64.s:50x45824a4889442418MOVQAX,0x18(SP);;保存打算结果add_amd64.s:70x45824fc3RET
通过上面操作,可知:
(FP)伪寄存器,只有在编写 Go 汇编代码时利用。FP 伪寄存器指向 caller 通报给 callee 的第一个参数利用 go tool compile / go tool objdump 得到的汇编中看不到(FP)寄存器的踪影6. Go 调试工具这里推举 2 个 Go 代码调试工具。
6.1 gdb 调试 Go 代码测试代码
packagemaintypeIerinterface{add(a,bint)intsub(a,bint)int}typedatastruct{a,bint}func(data)add(a,bint)int{returna+b}func(data)sub(a,bint)int{returna-b}funcmain(){vartIer=&data{3,4}println(t.add(1,2))println(t.sub(3,2))}
编译 go build -gcflags "-N -l" -o main
利用 GDB 调试
> gdb mainGNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-80.el7Copyright (C) 2013 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later http://gnu.org/licenses/gpl.htmlThis is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law. Type "show copying"and "show warranty" for details.This GDB was configured as "x86_64-redhat-linux-gnu".For bug reporting instructions, please see:<http://www.gnu.org/software/gdb/bugs/>...Reading symbols from /root/go/src/interface/main...done.Loading Go Runtime support.(gdb) list // 显示源码14 func (data) add(a, b int) int{15 return a+b16 }1718 func (data) sub(a, b int) int{19 return a-b20 }212223 func main(){(gdb) list24 var t Ier = &data{3,4}2526 println(t.add(1,2))27 println(t.sub(3,2))28 }29(gdb) b 26 // 在源码26行处设置断点Breakpoint 1 at 0x45827c: file /root/go/src/interface/main.go, line 26.(gdb) rStarting program: /root/go/src/interface/mainBreakpoint 1, main.main () at /root/go/src/interface/main.go:2626 println(t.add(1,2))(gdb) info locals // 显示变量t = {tab = 0x487020 <data,main.Ier>, data = 0xc000096000}(gdb) ptype t // 打印t的构造type = struct runtime.iface { runtime.itab tab; void data;}(gdb) p t.tab.inter // 打印t.tab.inter指针指向的数据$2 = {typ = {size = 16, ptrdata = 16, hash = 2491815843, tflag = 7 '\a', align = 8 '\b', fieldAlign = 8 '\b', kind = 20 '\024', equal = {void (void , void , bool )} 0x466ec0, gcdata = 0x484351 "\002\003\004\005\006\a\b\t\n\f\r\016\017\020\022\025\026\030\033\034\036\037\"&(,-5<BUXx\216\231\330\335\377", str = 6568, ptrToThis = 23808}, pkgpath = {bytes = 0x4592b4 ""}, mhdr = []runtime.imethod = {{name = 277, ityp = 48608}, {name = 649, ityp = 48608}}}(gdb) disass // 显示汇编Dump of assembler code for function main.main: 0x0000000000458210 <+0>: mov %fs:0xfffffffffffffff8,%rcx 0x0000000000458219 <+9>: cmp 0x10(%rcx),%rsp 0x000000000045821d <+13>: jbe 0x458324 <main.main+276> 0x0000000000458223 <+19>: sub $0x50,%rsp 0x0000000000458227 <+23>: mov %rbp,0x48(%rsp) 0x000000000045822c <+28>: lea 0x48(%rsp),%rbp 0x0000000000458231 <+33>: lea 0x10dc8(%rip),%rax # 0x469000 0x0000000000458238 <+40>: mov %rax,(%rsp) 0x000000000045823c <+44>: callq 0x40a5c0 <runtime.newobject>
常用的 gdb 调试命令
runcontinuebreakbacktrace 与 frameinfo break、localslist 命令print 和 ptype 命令disass除了 gdb,其余推举一款 gdb 的增强版调试工具 cgdb
https://cgdb.github.io/
效果如下图所示,分两个窗口:上面显示源代码,下面是详细的命令行调试界面(跟 gdb 一样):
6.2 delve 调试代码
delve 项目地址
https://github.com/go-delve/delve
带图形化界面的 dlv 项目地址
https://github.com/aarzilli/gdlv
dlv 的安装利用,这里不再做过多讲解,感兴趣的可以考试测验一下。
gdb 作为调试工具自是不用多说,比较老牌、强大,可以支持多种措辞。delve 则是利用 go 措辞开拓的,用来调试 go 的工具,功能也是十分强大,打印结果可以显示 gdb 支持不了的东西,这里不再做过多讲解,有兴趣的可以查阅干系资料。7. 总结对付 Go 汇编根本大致须要熟习下面几个方面:
通过上面的例子相信已经让你对 Go 的汇编有了一定的理解。当然,对付大部分业务开拓职员来说,只要看的懂即可。如果想进一步的理解,可以阅读干系的资料或者书本。
末了想说的是:鉴于个人能力有限,在阅读过程中你可能会创造存在的一些问题或者毛病,欢迎各位大佬示正。如果感兴趣的话,也可以一起私下互换。
8. 参考资料
在整理的过程中,部分参考、引用下面链接地址内容。有一些写的还是不错的,感兴趣的同学可以阅读。
[1] https://github.com/cch123/golang-notes/blob/master/assembly.md plan9 assembly
[2] https://segmentfault.com/a/1190000019753885 汇编入门
[3] https://www.davidwong.fr/goasm/ Go Assembly by Example
[4] https://juejin.im/post/6844904005630443533#heading-3
[5] https://github.com/go-internals-cn/go-internals/blob/master/chapter1_assembly_primer/README.md
[6] https://lrita.github.io/2017/12/12/golang-asm/
[7] https://chai2010.cn/advanced-go-programming-book/ch3-asm/ch3-01-basic.html