网站地图    收藏    合作   
<

快捷菜单 返回顶部

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

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

函数调用规约

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

主调函数保存和恢复现场的通用逻辑如下:
//开辟栈空间,压找 PB 保存现场
    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 的值为函数开始时的位

汇编基础

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

Go 汇编简介

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

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

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

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

多值返回分析

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

//go :noinline
func swap (a, b int) (x int, y int) {
    x = b
    y = a
    return
}

func main() {
    swap(lO, 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)
    0xOOlc 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:l2) MOVQ 32(SP), BP
    0x0038 00056 (swap.go:l2) ADDQ $40, SP
    0x003c 00060 (swap.go:l2) RET
    0x003d 00061 (swap.go:l2) NOP
    0x003d 00061 (swap.go:10) PCDATA $0, $ - 1

从汇编的代码得知:

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

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


图:Go函数栈

函数调用前己经为返回值和参数分配了技空间,分配顺序是从右向左的,先是返回值,然后是参数,通用的技模型如下:

+----------+
 | 返回值 y    |
 |------------|
 | 返回值 x    |
 |------------|
 |  参数 b      |
 |------------|
 |  参数 a      |
+----------+

函数的多值返回是主调函数预先分配好空间来存放返回值,被调函数执行时将返回值复制到该返回位置来实现的。

闭包底层实现

下面通过汇编和源码对照的方式看一下 Go 闭包的内部实现。

程序源码如下:
package main
//函数返回引用了外部交量i 的闭包
func a(i int) func () {
    return func() {
        print(i)
    }
}
func main() {
    f := a (1)
    f ()
}
编译汇编如下:

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

关键汇编代码及分析如下:

//函数 a 对应的汇编代码和 main 函数对应的汇编代码
"".a STEXT size=91 args=0x10 locals=0x18
    0x0000 00000 (c2_7_4a.go:3) TEXT "".a(SB), $24-16
    0x0000 00000 (c2_7_4a.go:3) MOVQ (TLS), CX
    0x0009 00009 (c2_7_4a.go:3) CMPQ SP, 16(CX)
    0x000d 00013 (c2_7_4a.go:3) JLS 84
    0x000f 00015 (c2_7_4a.go:3) SUBQ $24, SP
    0x0013 00019 (c2_7_4a.go:3) MOVQ BP , 16(SP)
    0x0018 00024 (c2_7_4a.go:3) LEAQ 16(SP), BP
    0x001d 00029 (c2_7_4a.go:3) FUNCDATA $0, gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB)
    0x001d 00029 (c2_7_4a.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329flfdbee7f5874cb (SB)
    0x001d 00029 (c2_7_4a.go:4) LEAQ type.noalg.struct{ F uintptr; "".i int}(SB), AX
    0x0024 00036 (c2_7_4a.go:4) MOVQ AX, (SP)
    0x0028 00040 (c2_7_4a.go:4) PCDATA $0, $0
    0x0028 00040 (c2_7_4a.go:4) CALL runtime.newobject(SB)
    0x002d 00045 (c2_7_4a.go:4) MOVQ 8(SP), AX
    0x0032 00050 (c2_7_4a.go:4) LEAQ "".a.funcl(SB), CX
    0x0039 00057 (c2_7_4a.go:4) MOVQ CX, (AX)
    0x003c 00060 (c2_7_4a.go:3) MOVQ "".i+32(SP), CX
    0x0041 00065 (c2_7_4a.go:4) MOVQ CX, 8(AX)
    0x0045 00069 (c2_7_4a.go:4) MOVQ AX, "".~r1+40(SP)
    0x004a 00074 (c2_7_4a.go:4) MOVQ 16(SP), BP
    0x004f 00079 (c2_7_4a.go:4) ADDQ $24, SP
"".main STEXT size=69 args=0x0 locals=0x18
    0x0000 00000 (c2_7_4a.go:9) TEXT "".main(SB), $24-0
    0x0000 00000 (c2_7_4a.go:9) MOVQ (TLS), CX
    0x0009 00009 (c2_7_4a.go:9) CMPQ SP, 16(CX)
    0x000d 00013 (c2_7_4a.go:9) JLS 62
    0x000f 00015 (c2_7_4a.go:9) SUBQ $24, SP
    0x0013 00019 (c2_7_4a.go:9) MOVQ BP, 16(SP)
    0x0018 00024 (c2_7_4a.go:9) LEAQ 16(SP), BP
    0x00ld 00029 (c2_7_4a.go:9) FUNCDATA $0, gclocals·33cdeccccebe80329flfdbee7f5874cb(SB)
    0x00ld 00029 (c2_7_4a.go:9) FUNCDATA $1, gclocals·33cdeccccebe80329flfdbee7f5874cb(SB)
    0x00ld 00029 (c2_7_4a.go:10) MOVQ $1, (SP)
    0x0025 00037 (c2_7_4a.go:10) PCDATA $0, $0
    0x0025 00037 (c2_7_4a.go:10) CALL "".a(SB)
    0x002a 00042 (c2_7_4a.go:10) MOVQ 8(SP), DX
    0x002f 00047 (c2_7_4a.go:11) MOVQ (DX), AX
    0x0032 00050 (c2_7_4a.go:11) PCDATA $0, $0
    0x0032 00050 (c2_7_4a.go:11) CALL AX
    0x0034 00052 (c2_7_4a.go:15) MOVQ 16(SP), BP
    0x0039 00057 (c2_7_4a.go:15) ADDQ $24, SP
    0x003d 00061 (c2_7_4a.go:15) RET

func a() 函数分析

第 1~10 行技环境准备。
第 11 行这里我们看到type.noalg.struct { F uintptr; "".i int }(SB)这个符号是一个闭包类型的数据,闭包类型的数据结构如下:

type Closure struct {
    F uintptr
    i int
}

闭包的结构很简单:一个是函数指针,另一个是对外部环境的引用。注意,这里仅仅是打印 i,并没有修改 i,Go 编译器并没有传递地址而是传递值。

第 11 行将闭包类型元信息放到 (SP) 位置,(SP) 地址存放的是 CALL 函数调用的第一个参数。

第 14 行创建闭包对象,我们来看一下 runtime.newobject 的函数原型,该函数的输入参数是一个类型信息,返回值是根据该类型信息构造出来的对象地址。

// src/runtime/malloc.go
func newobject(typ *_type) unsafe.Pointer

第 15 行将 newobject 返回的对象地址复制给 AX 寄存器。
第 16 行将 a 函数里面的匿名函数 a.funcl 指针复制到 CX 寄存器。
第 17 行将 CX 寄存器中存放的 a.funcl 函数指针复制到闭包对象的函数指针位置。
第 18、19 行将外部闭包变量 i 的值复制到闭包对象的 i 处。
第 20 行复制闭包对象指针值到函数返回值位置 "".~rl+40(SP)。

main ()函数分析

第 23~32 行准备环境。
第 33 行将立即数 1 复制到 (SP) 位置,为后续的 CALL 指令准备参数。
第 35 行调用函数 a()。
第 36 行复制函数返回值到 DX 寄存器。
第 37 行间接寻址,复制闭包对象中的函数指针到 AX 寄存器。
第 39 行调用 AX 寄存器指向的函数。
第 40~42 行恢复环境,并返回。

通过汇编代码的分析,我们清楚地看到 Go 实现闭包是通过返回一个如下的结构来实现的。

type Closure struct {
    F uintptr
    env *Type
}

F 是返回的匿名函数指针,env 是对外部环境变量的引用集合。如果闭包内没有修改外部变量,则 Go 编译器直接优化为值传递,如上面的例子中的代码所示;反之则是通过指针传递的。

自学PHP网专注网站建设学习,PHP程序学习,平面设计学习,以及操作系统学习

京ICP备14009008号@版权所有www.zixuephp.com

网站声明:本站所有视频,教程都由网友上传,站长收集和分享给大家学习使用,如由牵扯版权问题请联系站长邮箱904561283@qq.com