Go语言进阶:一丢丢的汇编知识

汇编语言

我们都知道,Go语言是一门编译型语言,我们编写的xxx.go文件,最终都会被编译成可执行文件来运行(在Windows系统上为后缀名为exe的文件,在Linux系统上为文件头信息里带可执行信息的文件)。

其实在go文件到最后的可执行文件之间,还有一个目标文件

go文件是我们按照Go语言语法编写的代码文件。

可执行文件里面存放的是操作系统认识的01二进制文件。

而这个目标文件里面放的就是汇编语言的代码。

举个例子

本文不会深耕汇编语言的相关知识,而是想通过一个小例子,来跟大家分享一下学习Go语言的其中一种思考方法。

我们都知道,Go语言可以对变量进行多重赋值,也就是在一行代码里对多个变量同时赋值:

a, b := 1, 2
复制代码

可以看到,我们在一行代码里同时对ab两个变量都进行了赋值。

由于多重赋值是先计算等号右边的结果,再将其整体赋值等号左边的变量,因此,我们可以一行里完成两个变量值的互换过程:

a, b = b, a
复制代码

那么,Go语言在底层是如何实现这个操作的呢?

我们编写一个完整的main.go文件:

package main

func main() {
        a := 888
        b := 999
        a, b = b, a
}
复制代码

然后,我们去网上随便搜一下,生成汇编代码的命令如下:

$ go tool compile -S main.go
复制代码

得到了如下输出:

"".main STEXT nosplit size=1 args=0x0 locals=0x0 funcid=0x0
            0x0000 00000 (main.go:3) TEXT "".main(SB), NOSPLIT|ABIInternal, $0-0
            0x0000 00000 (main.go:3)        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
            0x0000 00000 (main.go:3)        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
            0x0000 00000 (main.go:7)        RET
            0x0000 c3                                               .
go.cuinfo.packagename. SDWARFCUINFO dupok size=0
            0x0000 6d 61 69 6e                                      main
""..inittask SNOPTRDATA size=24
            0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
            0x0010 00 00 00 00 00 00 00 00                          ........
gclocals·33cdeccccebe80329f1fdbee7f5874cb SRODATA dupok size=8
            0x0000 01 00 00 00 00 00 00 00                          ........

复制代码

我们发现,汇编代码里直接从main.go:3跳到了main.go:7,将我们中间4、5、6三行代码给省略了。

猜测一下:由于ab变量只进行了声明和赋值,并没有使用,所以在生成汇编代码的时候,这三行被优化掉了。

我们查找一下官方文档,看一下关于go tool compile的各个参数是怎么使用的。

官方文档地址:golang.google.cn/cmd/compile…

从前往后看,发现似乎第四个参数就跟编译时的优化有关:

-N
        Disable optimizations.
复制代码

翻译过来就是:禁用优化

咦?这不正好是我们要找的参数吗?

我们将其加入到命令里,完整的命令如下:

$ go tool compile -N -S main.go
复制代码

这次可以得到完整的汇编代码内容,但我们仅关注重要的几行:

0x000e 00014 (main.go:4)        MOVQ    $888, "".a+8(SP)
0x0017 00023 (main.go:5)        MOVQ    $999, "".b(SP)
0x001f 00031 (main.go:6)         MOVQ    "".a+8(SP), AX
0x0024 00036 (main.go:6)        MOVQ    AX, ""..autotmp_2+16(SP)
0x0029 00041 (main.go:6)        MOVQ    "".b(SP), AX
0x002d 00045 (main.go:6)        MOVQ    AX, "".a+8(SP)
0x0032 00050 (main.go:6)        MOVQ    ""..autotmp_2+16(SP), AX
0x0037 00055 (main.go:6)        MOVQ    AX, "".b(SP)
复制代码

由于Go语言使用的汇编语言为Plan9汇编,因此所有的汇编指令跟我们之前学过的x86的不太一样,但还是有类似的地方。

首先,介绍一下MOVQ这个汇编代码的意思,MOVQ后面跟两个参数,将第一个参数中的8个字节的数据写入到第二个参数的地址里面。

然后,介绍一下几个用到的寄存器:

  • AX:临时寄存器,用于存在临时数据,对应rax
  • BX:临时寄存器,用于存在临时数据,对应rbx
  • SP:Stack Pointer栈顶指针,此处为伪寄存器

我们这个地方不用关心SP是伪寄存器还是硬件寄存器,只需要知道它是一个指定栈顶的指针即可,使用的方法如下:

  1. 在使用的时候需要用小括号包围起来;
  2. 前面加减的数字,代表以当前栈顶指针地址进行的位移
  3. 数字前面的ab只是一个符号,没用具体意义,我们可以简单的理解为了增加可读性而增加的。事实上是根据这个符号来区分伪寄存器与硬件寄存器。

下面来逐条讲解这几条汇编代码:

MOVQ $888, "".a+8(SP):将十进制数888写入到地址为SP+8的内存当中。也就是a := 888

MOVQ $999, "".b(SP):将十进制数999写入到地址为SP的内存当中。也就是b := 999

MOVQ "".a+8(SP), AX:将地址为SP+8的内存中的数据写入到AX寄存器当中。也就是将变量a的值放到AX寄存器当中。

MOVQ AX, ""..autotmp_2+16(SP):将AX寄存器当中的值放到一个临时变量当中,该临时变量的地址为SP+16

MOVQ "".b(SP), AX:同理,将变量b的值放到AX寄存器当中。

MOVQ AX, "".a+8(SP):将AX寄存器中的数据写入地址为SP+8的内存当中,也就是将AX寄存器中的数据赋值给变量a

MOVQ ""..autotmp_2+16(SP), AX:将临时变量中的值放到AX寄存器中。

MOVQ AX, "".b(SP):将AX寄存器中的数据赋值给变量b

至此,完成了变量ab的互换。

可以看到,在汇编代码里,变量a对应的就是地址为SP+8的这块内存,变量b对应的就是地址为SP的这块内存。

而地址为SP+16的这块内存,是用来辅助两个变量交换的临时变量的内存。

总结

虽然我们在Go语言中不用像C语言那样引入临时变量来交换变量,但通过观察其对应的汇编代码,我们发现,Go语言依然使用了临时变量来辅助变量的交换。

扩展

给大家留一个小作业,身边有电脑的话,可以尝试看一下三个变量交换的汇编代码是如何实现的:

a := 777
b := 888
c := 999
a, b, c  = b, c, a
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享