Go 汇编详解

探讨Go语言受Plan9影响的汇编风格,解析AMD64寄存器与伪寄存器的使用,特别是TLS与goroutine的关系,以及如何利用RCU算法优化行号获取的性能。文章还涉及内存管理操作如mprotect的汇编实现,和对栈帧、函数调用机制的深入分析。

动手点关注

dd372ddede2283d3df0e761a7b918bd1.gif

干货不迷路

前言

我们知道 Go 语言的三位领导者中有两位来自 Plan 9 项目,这直接导致了 Go 语言的汇编采用了比较有个性的 Plan 9 风格。不过,我们不能因咽废食而放弃无所不能的汇编。

1、 Go 汇编基础知识

1.1、通用寄存器

不同体系结构的 CPU,其内部寄存器的数量、种类以及名称可能大不相同,这里我们只介绍 AMD64 的寄存器。AMD64 有 20 多个可以直接在汇编代码中使用的寄存器,其中有几个寄存器在操作系统代码中才会见到,而应用层代码一般只会用到如下三类寄存器。

304f35c2e8d622d2c781447bbdbdd827.png

上述这些寄存器除了段寄存器是 16 位的,其它都是 64 位的,也就是 8 个字节,其中的 16 个通用寄存器还可以作为 32/16/8 位寄存器使用,只是使用时需要换一个名字,比如可以用 EAX 这个名字来表示一个 32 位的寄存器,它使用的是 RAX 寄存器的低 32 位。

AMD64 的通用通用寄存器的名字在 plan9 中的对应关系:

AMD64 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

Go 语言中寄存器一般用途:

a8856bee50d3b8e4c657edfaf52eb11c.png

1.2、伪寄存器

伪寄存器是 plan9 伪汇编中的一个助记符, 也是 Plan9 比较有个性的语法之一。常见伪寄存器如下表所示:

b557ec9193adfe42e5016c4bfa2d1962.png

SB:指向全局符号表。相对于寄存器,SB 更像是一个声明标识,用于标识全局变量、函数等。通过 symbol(SB) 方式使用,symbol<>(SB)表示 symbol 只在当前文件可见,跟 C 中的 static 效果类似。此外可以在引用上加偏移量,如 symbol+4(SB) 表示 symbol+4bytes 的地址。

PC:程序计数器(Program Counter),指向下一条要执行的指令的地址,在 AMD64 对应 rip 寄存器。个人觉得,把他归为伪寄存器有点令人费解,可能是因为每个平台对应的物理寄存器名字不一样。

SP:SP 寄存器比较特殊,既可以当做物理寄存器也可以当做伪寄存器使用,不过这两种用法的使用语法不同。其中,伪寄存器使用语法是  symbol+offset(SP),此场景下 SP 指向局部变量的起始位置(高地址处);x-8(SP) 表示函数的第一个本地变量;物理 SP(硬件SP) 的使用语法则是 +offset(SP),此场景下 SP 指向真实栈顶地址(栈帧最低地址处)。

FP:用于标识函数参数、返回值。被调用者(callee)的 FP 实际上是调用者(caller)的栈顶,即 callee.SP(物理SP) == caller.FP;x+0(FP) 表示第一个请求参数(参数返回值从右到左入栈)。

实际上,生成真正可执行代码时,伪 SP、FP 会由物理 SP 寄存器加上偏移量替换。所以执行过程中修改物理 SP,会引起伪 SP、FP 同步变化,比如执行 SUBQ $16, SP 指令后,伪 SP 和伪 FP 都会 -16。而且,反汇编二进制而生成的汇编代码中,只有物理 SP 寄存器。即 go tool objdump/go tool compile -S 输出的汇编代码中,没有伪 SP 和 伪 FP 寄存器,只有物理 SP 寄存器。

另外还有 1 个比较特殊的伪寄存器:TLS:存储当前 goroutine 的 g 结构体的指针。实际上,X86 和 AMD64 下的 TLS 是通过段寄存器 FS 或 GS 实现的线程本地存储基地址,而当前 g 的指针是线程本地存储的第一个变量。

比如 github.com/petermattis/goid.Get 函数的汇编实现如下:

// func Get() int64
TEXT ·Get(SB),NOSPLIT,$0-8
    MOVQ (TLS), R14
    MOVQ g_goid(R14), R13
    MOVQ R13, ret+0(FP)
    RET

编译成二进制之后,再通过 go tool objdump 反编译成汇编(Go 1.18),得到如下代码:

TEXT github.com/petermattis/goid.Get.abi0(SB) /Users/bytedance/go/pkg/mod/github.com/petermattis/goid@v0.0.0-20221215004737-a150e88a970d/goid_go1.5_amd64.s
  goid_go1.5_amd64.s:28 0x108adc0   654c8b342530000000  MOVQ GS:0x30, R14 
  goid_go1.5_amd64.s:29 0x108adc9   4d8bae98000000    MOVQ 0x98(R14), R13 
  goid_go1.5_amd64.s:30 0x108add0   4c896c2408    MOVQ R13, 0x8(SP) 
  goid_go1.5_amd64.s:31 0x108add5   c3      RET

可以知道 MOVQ (TLS), R14 指令最终编译成了 MOVQ GS:0x30, R14 ,使用了 GS 段寄存器实现相关功能。

操作系统对内存的一般划分如下图所示:

高地址 +------------------+
        |                  |
        |     内核空间      |
        |                  |
        --------------------
        |                  |
        |       栈         |
        |                  |
        --------------------
        |                  |
        |     .......      |
        |                  |
        --------------------
        |                  |
        |       堆         |
        |                  |
        --------------------
        |     全局数据      |
        |------------------|
        |                  |
        |     静态代码      |
        |                  |
        |------------------|
        |     系统保留      |
  低地址 |------------------|

这里提个疑问,我们知道协程分为有栈协程和无栈协程,go 语言是有栈协程。那你知道普通 gorutine 的调用栈是在哪个内存区吗?

1.3、函数调用栈帧

我们先熟悉几个名词。

caller:函数调用者。callee:函数被调用者。比如函数 main 中调用 sum 函数,那么 main 就是 caller,而 sum 函数就是 callee。栈帧:stack frame,即执行中的函数所持有的、独立连续的栈区段。一般用来保存函数参数、返回值、局部变量、返回 PC 值等信息。golang 的 ABI 规定,由 caller 管理函数参数和返回值。

下图是 golang 的调用栈,源于曹春晖老师的 github 文章《汇编 is so easy》 ,做了简单修改:

caller                                                                                 
                                 +------------------+                                                                         
                                 |                  |                                                                         
       +---------------------->  +------------------+                                                                        
       |                         |                  |                                                                         
       |                         | caller parent BP |                                                                         
       |           BP(pseudo SP) +------------------+                                                                         
       |                         |                  |                                                                         
       |                         |   Local Var0     |                                                                         
       |                         +------------------+                                                                         
       |                         |                  |                                                                         
       |                         |   .......        |                                                                         
       |                         +------------------+                                                                         
       |                         |                  |                                                                         
       |                         |   Local VarN     |                                                                         
                                 +------------------+                                                                         
 caller stack frame              |                  |                                                                         
                                 |   callee arg2    |                                                                         
       |                         +------------------+                                                                         
       |                         |                  |                                                                         
       |                         |   callee arg1    |                                                                         
       |                         +------------------+                                                                         
       |                         |                  |                                                                         
       |                         |   callee arg0    |                                                                         
       |   SP(Real Register) ->  +------------------+--------------------------+   FP(virtual register)                       
       |                         |                  |                          |                                              
       |                         |   return addr    |  parent return address   |                                              
       +---------------------->  +------------------+--------------------------+    <-----------------------+         
                                                    |  caller BP               |                            |         
                                         
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值