Go语言函数的底层实现
来源:云海天教程
时间:2022-12-30 11:03:33 212浏览 收藏
IT行业相对于一般传统行业,发展更新速度更快,一旦停止了学习,很快就会被行业所淘汰。所以我们需要踏踏实实的不断学习,精进自己的技术,尤其是初学者。今天golang学习网给大家整理了《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 mainfunc 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函数栈
函数调用前己经为返回值和参数分配了栈空间,分配顺序是从右向左的,先是返回值,然后是参数,通用的栈模型如下:
+----------+
| 返回值 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
}
// src/runtime/malloc.go
func newobject(typ *_type) unsafe.Pointer
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
}
文中关于golang的知识介绍,希望对你的学习有所帮助!若是受益匪浅,那就动动鼠标收藏这篇《Go语言函数的底层实现》文章吧,也可关注golang学习网公众号了解相关技术文章。
-
366 收藏
-
272 收藏
-
312 收藏
-
297 收藏
-
451 收藏
-
444 收藏
-
311 收藏
-
266 收藏
-
188 收藏
-
317 收藏
-
430 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习
-
- 苗条的金针菇
- 太全面了,已加入收藏夹了,感谢作者大大的这篇技术文章,我会继续支持!
- 2023-06-06 12:11:59
-
- 细心的棒球
- 写的不错,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,看完之后很有帮助,总算是懂了,感谢作者大大分享博文!
- 2023-05-10 11:32:37
-
- 寒冷的大碗
- 这篇文章内容出现的刚刚好,很详细,赞 👍👍,已加入收藏夹了,关注博主了!希望博主能多写Golang相关的文章。
- 2023-03-28 17:27:02
-
- 着急的冰棍
- 这篇文章内容太及时了,很详细,受益颇多,码起来,关注作者大大了!希望作者大大能多写Golang相关的文章。
- 2023-02-25 14:13:26
-
- 忧郁的灯泡
- 好细啊,已加入收藏夹了,感谢博主的这篇技术文章,我会继续支持!
- 2023-01-06 16:15:41
-
- 哭泣的书包
- 感谢大佬分享,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,看完之后很有帮助,总算是懂了,感谢作者大大分享博文!
- 2023-01-05 00:40:35
-
- 大气的纸鹤
- 这篇技术文章太及时了,很详细,赞 👍👍,码住,关注作者了!希望作者能多写Golang相关的文章。
- 2023-01-04 13:52:03
-
- 乐观的篮球
- 很棒,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,帮助很大,总算是懂了,感谢作者分享文章!
- 2023-01-02 16:25:47
-
- 背后的芹菜
- 这篇技术文章太及时了,好细啊,太给力了,码起来,关注作者了!希望作者能多写Golang相关的文章。
- 2023-01-02 05:45:43