基于堆栈式的程序执行模型决定了函数是语言的一个核心元素。分析Go函数的内部实现,对理解整个程序的执行模型有很大的好处。研究底层实现有两种办法,一种是看语言编译器源码,分析其对函数的各个特性的处理逻辑,另一种是反汇编,将可执行程序反汇编出来。
本节使用反汇编这种短、平、快的方法。首先介绍Go语言的函数调用规约,接着介绍Go语言使用的汇编语言的基本概念,然后通过反汇编技术来剖析Go的函数某些特性的底层实现。
函数调用规约
Go函数使用的是caller-save的模式,即由调用者负责保存寄存器,所以在函数的头尾不会出现pushebp;movespebp这样的代码,相反其是在主调函数调用被调函数的前后有一个保存现场和恢复现场的动作。
主调函数保存和恢复现场的通用逻辑如下:
//开辟栈空间,压找PB保存现场SUBQ$x,SP//为函数开辟裁空间MOVQBP,y(SP)//保存当前函数BP到y(SP)位直,y为相对SP的偏移量LEAQy(SP),BP//重直BP,使其指向刚刚保存BP旧值的位直,这里主要//是方便后续BP的恢复//弹出栈,恢复BPMOVQy(SP),BP//恢复BP的值为调用前的值ADDQ$x,SP//恢复SP的值为函数开始时的位
汇编基础
基于AT&T风格的汇编格式,Go编译器产生的汇编代码是一种中间抽象态,它不是对机器码的映射,而是和平台无关的一个中间态汇编描述。所以汇编代码中有些寄存器是真实的,有些是抽象的。几个抽象的寄存器如下:
SB(Staticbasepointer):静态基址寄存器,它和全局符号一起表示全局变量的地址。
FP(Framepointer):栈帧寄存器,该寄存器指向当前函数调用技帧的技底位置。
PC(Programcounter):程序计数器,存放下一条指令的执行地址,很少直接操作该寄存器,一般是CALL、RET等指令隐式的操作。
SP(Stackpointer):栈顶寄存器,一般在函数调用前由主调函数设置SP的值对技空间进行分配或回收。
Go汇编简介
1)Go汇编器采用AT&T风格的汇编,早期的实现来自plan9汇编器:源操作数在前,目的操作数在后。
2)Go内嵌汇编和反汇编产生的代码并不是一一对应的,汇编编译器对内嵌汇编程序自动做了调整,主要差别就是增加了保护现场,以及函数调用前的保持PC、SP偏移地址重定位等逻辑。反汇编代码更能反映程序的真实执行逻辑。
3)Go的汇编代码并不是和具体硬件体系结构的机器码一一对应的,而是一种半抽象的描述,寄存器可能是抽象的,也可能是具体的。
下面代码的分析基于AMD64位架构下的Linux环境。
多值返回分析
多值返回函数swap的源码如下:
packagemain//go:noinlinefuncswap(a,bint)(xint,yint){x=by=areturn}funcmain(){swap(lO,20)}
编译生成汇编如下
//-S产生汇编的代码
//-N禁用优化
//-1禁用内联
GOOS=linuxGOARCH=amd64gotoolcompile-1-N-Sswap.go>swap.s2>&1
汇编代码分析
1)swap函数和main函数汇编代码分析。例如:
"".swapSTEXTnosplitsize=39args=0x20locals=0x00x000000000(swap.go:4)TEXT"".swap(SB),NOSPLIT,$0-320x000000000(swap.go:4)FUNCDATA$0,gclocals.ff19ed39bdde8a01a800918ac3ef0ec7(SB)0x000000000(swap.go:4)FUNCDATA$1,gclocals.33cdeccccebe80329flfdbee7f5874cb(SB)0x000000000(swap.go:4)MOVQ$0,"".x+24(SP)0x000900009(swap.go:4)MOVQ$0,"".y+32(SP)0x001200018(swap.go:5)MOVQ"".b+16(SP),AX0x001700023(swap.go:5)MOVQAX,"".x+24(SP)0xOOlc00028(swap.go:6)MOVQ"".a+8(SP),AX0x002100033(swap.go:6)MOVQAX,"".y+32(SP)0x002600038(swap.go:7)RET"".mainSTEXTsize=68args=0x0locals=0x280x000000000(swap.go:10)TEXT"".main(SB),$40-00x000000000(swap.go:10)MOVQ(TLS),CX0x000900009(swap.go:10)CMPQSP,16(CX)0x000d00013(swap.go:10)JLS610x000f00015(swap.go:10)SUBQ$40,SP0x001300019(swap.go:10)MOVQBP,32(SP)0x001800024(swap.go:10)LEAQ32(SP),BP0x001d00029(swap.go:10)FUNCDATA$0,gclocals·33cdeccccebe80329flfdbee7f5874cb(SB)0x001d00029(swap.go:10)FUNCDATA$1,gclocals·33cdeccccebe80329flfdbee7f5874cb(SB)0x001d00029(swap.go:11)MOVQ$10,(SP)0x002500037(swap.go:11)MOVQ$20,8(SP)0x002e00046(swap.go:11)PCDATA$0,$00x002e00046(swap.go:11)CALL"".swap(SB)0x003300051(swap.go:l2)MOVQ32(SP),BP0x003800056(swap.go:l2)ADDQ$40,SP0x003c00060(swap.go:l2)RET0x003d00061(swap.go:l2)NOP0x003d00061(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函数栈
函数调用前己经为返回值和参数分配了技空间,分配顺序是从右向左的,先是返回值,然后是参数,通用的技模型如下:
+———-+
|返回值y |
|————|
|返回值x|
|————|
| 参数b |
|————|
| 参数a |
+———-+
函数的多值返回是主调函数预先分配好空间来存放返回值,被调函数执行时将返回值复制到该返回位置来实现的。
闭包底层实现
下面通过汇编和源码对照的方式看一下Go闭包的内部实现。
程序源码如下:
packagemain//函数返回引用了外部交量i的闭包funca(iint)func(){returnfunc(){print(i)}}funcmain(){f:=a(1)f()}
编译汇编如下:
GOOS=linuxGOARCH=amd64gotoolcompile-Sc2_7_4a.go>c2_7_4a.s2&1
关键汇编代码及分析如下:
//函数a对应的汇编代码和main函数对应的汇编代码
"".aSTEXTsize=91args=0x10locals=0x180x000000000(c2_7_4a.go:3)TEXT"".a(SB),$24-160x000000000(c2_7_4a.go:3)MOVQ(TLS),CX0x000900009(c2_7_4a.go:3)CMPQSP,16(CX)0x000d00013(c2_7_4a.go:3)JLS840x000f00015(c2_7_4a.go:3)SUBQ$24,SP0x001300019(c2_7_4a.go:3)MOVQBP,16(SP)0x001800024(c2_7_4a.go:3)LEAQ16(SP),BP0x001d00029(c2_7_4a.go:3)FUNCDATA$0,gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB)0x001d00029(c2_7_4a.go:3)FUNCDATA$1,gclocals·33cdeccccebe80329flfdbee7f5874cb(SB)0x001d00029(c2_7_4a.go:4)LEAQtype.noalg.struct{Fuintptr;"".iint}(SB),AX0x002400036(c2_7_4a.go:4)MOVQAX,(SP)0x002800040(c2_7_4a.go:4)PCDATA$0,$00x002800040(c2_7_4a.go:4)CALLruntime.newobject(SB)0x002d00045(c2_7_4a.go:4)MOVQ8(SP),AX0x003200050(c2_7_4a.go:4)LEAQ"".a.funcl(SB),CX0x003900057(c2_7_4a.go:4)MOVQCX,(AX)0x003c00060(c2_7_4a.go:3)MOVQ"".i+32(SP),CX0x004100065(c2_7_4a.go:4)MOVQCX,8(AX)0x004500069(c2_7_4a.go:4)MOVQAX,"".~r1+40(SP)0x004a00074(c2_7_4a.go:4)MOVQ16(SP),BP0x004f00079(c2_7_4a.go:4)ADDQ$24,SP"".mainSTEXTsize=69args=0x0locals=0x180x000000000(c2_7_4a.go:9)TEXT"".main(SB),$24-00x000000000(c2_7_4a.go:9)MOVQ(TLS),CX0x000900009(c2_7_4a.go:9)CMPQSP,16(CX)0x000d00013(c2_7_4a.go:9)JLS620x000f00015(c2_7_4a.go:9)SUBQ$24,SP0x001300019(c2_7_4a.go:9)MOVQBP,16(SP)0x001800024(c2_7_4a.go:9)LEAQ16(SP),BP0x00ld00029(c2_7_4a.go:9)FUNCDATA$0,gclocals·33cdeccccebe80329flfdbee7f5874cb(SB)0x00ld00029(c2_7_4a.go:9)FUNCDATA$1,gclocals·33cdeccccebe80329flfdbee7f5874cb(SB)0x00ld00029(c2_7_4a.go:10)MOVQ$1,(SP)0x002500037(c2_7_4a.go:10)PCDATA$0,$00x002500037(c2_7_4a.go:10)CALL"".a(SB)0x002a00042(c2_7_4a.go:10)MOVQ8(SP),DX0x002f00047(c2_7_4a.go:11)MOVQ(DX),AX0x003200050(c2_7_4a.go:11)PCDATA$0,$00x003200050(c2_7_4a.go:11)CALLAX0x003400052(c2_7_4a.go:15)MOVQ16(SP),BP0x003900057(c2_7_4a.go:15)ADDQ$24,SP0x003d00061(c2_7_4a.go:15)RET
funca()函数分析
第1~10行技环境准备。
第11行这里我们看到type.noalg.struct{Fuintptr;"".iint}(SB)这个符号是一个闭包类型的数据,闭包类型的数据结构如下:
typeClosurestruct{
Fuintptr
iint
}
闭包的结构很简单:一个是函数指针,另一个是对外部环境的引用。注意,这里仅仅是打印i,并没有修改i,Go编译器并没有传递地址而是传递值。
第11行将闭包类型元信息放到(SP)位置,(SP)地址存放的是CALL函数调用的第一个参数。
第14行创建闭包对象,我们来看一下runtime.newobject的函数原型,该函数的输入参数是一个类型信息,返回值是根据该类型信息构造出来的对象地址。
//src/runtime/malloc.go
funcnewobject(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实现闭包是通过返回一个如下的结构来实现的。
typeClosurestruct{
Fuintptr
env*Type
}
F是返回的匿名函数指针,env是对外部环境变量的引用集合。如果闭包内没有修改外部变量,则Go编译器直接优化为值传递,如上面的例子中的代码所示;反之则是通过指针传递的。