Go语言函数的底层实现

  • 内容
  • 评论
  • 相关

基于堆栈式的程序执行模型决定了函数是语言的一个核心元素,分析Go语言函数的底层实现,对理解整个程序的执行过程有很大的帮助,研究底层实现有两种办法,一种是看语言编译器源码,分析其对函数的各个特性的处理逻辑,另一种是反汇编,将可执行程序反汇编出来。

本节使用反汇编这种短、平、快的方法,首先介绍Go语言的函数调用规约,接着介绍Go语言使用汇编语言的基本概念,然后通过反汇编技术来剖析Go语言函数某些特性的底层实现。

提示:阅读本节需要有一定的汇编基础,想学习汇编的同学,我们这里准备了一套《汇编语言入门教程》供大家学习。

函数调用规约

Go语言函数使用的是 caller-save 的模式,即由调用者负责保存寄存器,所以在函数的头尾不会出现push ebp; mov esp ebp这样的代码,相反其是在主调函数调用被调函数的前后有一个保存现场和恢复现场的动作。

主调函数保存和恢复现场的通用逻辑如下:

//开辟栈空间,压栈 BP 保存现场
    SUBQ $x, SP    //为函数开辟裁空间
    MOVQ BP, y(SP) //保存当前函数 BP 到 y(SP)位直, y 为相对 SP 的偏移量
    LEAQ y(SP), BP //重直 BP,使其指向刚刚保存 BP 旧值的位置,这里主要
                   //是方便后续 BP 的恢复

//弹出栈,恢复 BP
    MOVQ y(SP), BP //恢复 BP 的值为调用前的值
    ADDQ $x, SP    //恢复 SP 的值为函数开始时的位

汇编基础

Go 编译器产生的汇编代码是一种中间抽象态,它不是对机器码的映射,而是和平台无关的一个中间态汇编描述,所以汇编代码中有些寄存器是真实的,有些是抽象的,几个抽象的寄存器如下:

  • SB (Static base pointer):静态基址寄存器,它和全局符号一起表示全局变量的地址。
  • FP (Frame pointer):栈帧寄存器,该寄存器指向当前函数调用栈帧的栈底位置。
  • PC (Program counter):程序计数器,存放下一条指令的执行地址,很少直接操作该寄存器,一般是 CALL、RET 等指令隐式的操作。
  • SP (Stack pointer):栈顶寄存器,一般在函数调用前由主调函数设置 SP 的值对栈空间进行分配或回收。

Go 汇编简介

1) Go 汇编器采用 AT&T 风格的汇编,早期的实现来自 plan9 汇编器,源操作数在前,目的操作数在后。

2) Go 内嵌汇编和反汇编产生的代码并不是一一对应的,汇编编译器对内嵌汇编程序自动做了调整,主要差别就是增加了保护现场,以及函数调用前的保持 PC 、SP 偏移地址重定位等逻辑,反汇编代码更能反映程序的真实执行逻辑。

3) Go 的汇编代码并不是和具体硬件体系结构的机器码一一对应的,而是一种半抽象的描述,寄存器可能是抽象的,也可能是具体的。

下面代码的分析基于 AMD64 位架构下的 Linux 环境。

多值返回分析

多值返回函数 swap 的源码如下:

package main

func swap (a, b int) (x int, y int) {
    x = b
    y = a
    return
}

func main() {
    swap(10, 20)
}

编译生成汇编如下

//- S 产生汇编的代码
//- N 禁用优化
//- 1 禁用内联

GOOS=linux GOARCH=amd64 go tool compile -1 -N -S swap.go >swap.s 2>&1

汇编代码分析

1) swap 函数和 main 函数汇编代码分析。例如:

"".swap STEXT nosplit size=39 args=0x20 locals=0x0
    0x0000 00000 (swap.go:4) TEXT  "".swap(SB), NOSPLIT, $0 - 32
    0x0000 00000 (swap.go:4) FUNCDATA  $0, gclocals.ff19ed39bdde8a01a800918ac3ef0ec7(SB)
    0x0000 00000 (swap.go:4) FUNCDATA  $1, gclocals.33cdeccccebe80329flfdbee7f5874cb(SB)
    0x0000 00000 (swap.go:4)  MOVQ  $0, "".x+24(SP)
    0x0009 00009 (swap.go:4)  MOVQ  $0, "".y+32(SP)
    0x0012 00018 (swap.go:5)  MOVQ  "".b+16(SP), AX
    0x0017 00023 (swap.go:5)  MOVQ  AX, "".x+24(SP)
    0xOO1c 00028 (swap.go:6)  MOVQ  "".a+8(SP), AX
    0x0021 00033 (swap.go:6)  MOVQ  AX, "".y+32(SP)
    0x0026 00038 (swap.go:7)  RET



"".main STEXT size=68 args=0x0 locals=0x28
    0x0000 00000 (swap.go:10) TEXT "".main(SB), $40 - 0
    0x0000 00000 (swap.go:10) MOVQ (TLS), CX
    0x0009 00009 (swap.go:10) CMPQ SP, 16(CX)
    0x000d 00013 (swap.go:10) JLS 61
    0x000f 00015 (swap.go:10) SUBQ $40, SP
    0x0013 00019 (swap.go:10) MOVQ BP, 32 (SP)
    0x0018 00024 (swap.go:10) LEAQ 32(SP), BP
    0x001d 00029 (swap.go:10) FUNCDATA $0, gclocals ·33cdeccccebe80329flfdbee7f5874cb(SB)
    0x001d 00029 (swap.go:10) FUNCDATA $1, gclocals ·33cdeccccebe80329flfdbee7f5874cb(SB)
    0x001d 00029 (swap.go:11) MOVQ $10, (SP)
    0x0025 00037 (swap.go:11) MOVQ $20 , 8 (SP)
    0x002e 00046 (swap.go:11) PCDATA $0 , $0
    0x002e 00046 (swap.go:11) CALL "". swap(SB)
    0x0033 00051 (swap.go:12) MOVQ 32(SP), BP
    0x0038 00056 (swap.go:12) ADDQ $40, SP
    0x003c 00060 (swap.go:12) RET
    0x003d 00061 (swap.go:12) NOP
    0x003d 00061 (swap.go:10) PCDATA $0, $ - 1
  • 第 5 行初始化返回值 x 为 0。
  • 第 6 行初始化返回值 y 为 0。
  • 第 7~8 行取第 2 个参数赋值给返回值 x。
  • 第 9~10 行取第 1 个参数赋值给返回值 y。
  • 第 11 行函数返回,同时进行栈回收,FUNCDATA 和垃圾收集可以忽略。
  • 第 15~24 行 main 函数堆栈初始化:开辟栈空间,保存 BP 寄存器。
  • 第 25 行初始化 add 函数的调用参数 1 的值为 10。
  • 第 26 行初始化 add 函数的调用参数 2 的值为 20。
  • 第 28 行调用 swap 函数,注意 call 隐含一个将 swap 下一条指令地址压栈的动作,即 sp=sp+8。
  • 所以可以看到在 swap 里面的所有变量的相对位置都发生了变化,都在原来的地址上 +8。
  • 第 29~30 行恢复措空间。

从汇编的代码得知:

  • 函数的调用者负责环境准备,包括为参数和返回值开辟栈空间。
  • 寄存器的保存和恢复也由调用方负责。
  • 函数调用后回收栈空间,恢复 BP 也由主调函数负责。

函数的多值返回实质上是在栈上开辟多个地址分别存放返回值,这个并没有什么特别的地方,如果返回值是存放到堆上的,则多了一个复制的动作。

main 调用 swap 函数栈的结构如下图所示。

Go函数栈
图:Go函数栈

本文标题:Go语言函数的底层实现

本文地址:http://www.hosteonscn.com/6175.html

评论

0条评论

发表评论

邮箱地址不会被公开。 必填项已用*标注