目录
我的golang学习之路
本文持续更新记录我的golang学习历程。
本人2018年接触golang,主要做业务开发,对golang的使用较为熟练,但缺乏系统性的学习。除去从业务开发过程中掌握golang知识外,其他获取知识的途径有限,借由此文促使我学习,并将一些总结分享给各位读者。
本文所有代码均是go 1.14.2版本
一些资料收集
感谢这篇教程,使我受益匪浅,文中也有引用这篇教程的地方。
golang知识地图
channel
go的多线程实现采用CSP模型。其中线程的实体是goroutine,goroutine间通过channel来交换数据。每个线程独立顺序执行,两个goroutine间表面上没有耦合,而是采用channel作为其通信的媒介,达到线程间同步的目的。channel顺序遵循先入先出(FIFO)规则。
由于channel避免不了两个goroutine同写同读的并发数据竞争,channel维护的资源(channel数据结构里的数据)还是需要用锁来保护。一句话概括就是,channel 是一个用于同步和通信的有锁FIFO队列。
channel行为分析与数据结构
根据go的设计理念和chan使用中的各种现象,我们可以推演下go channel的实现方式,以加深对channel的理解。
写channel现象:
- 向nil channel写,会导致阻塞。
- 向关闭的channel写,会导致panic。
- 如果另一个goroutine在等待读,则通信内容直接发送给另一个goroutine,自己不阻塞。
- 上一种现象中如果有多个goroutine都在等待读,则发给第一个等待的,FIFO顺序。
- 如果没有另一个goroutine在等待读,如果缓存队列没满,那么将通信内容放入队列,自己不阻塞。
- 如果没有另一个goroutine在等待读,如果缓存队列满了,那么自己将阻塞,直到被其他go读取。
读channel现象:
- 从nil channel读,会导致阻塞。
- 从关闭的channel读,如果缓冲区有,则取出;没有则会读出0值,不阻塞。
- 如果存在缓冲区,则优先向缓冲区写,其次阻塞写的goroutine。因此,读channel的优先级是先从缓存队列读,再从被阻塞的写channel的goroutine读;
- 上面现象可以细分几种情况。如下表
读chan场景 | 无缓冲队列 | 有缓冲队列但是为空 | 有缓冲队列且有消息 |
---|---|---|---|
有被阻塞的写g | 从阻塞的第一个里取,并释放阻塞的写g | 不可能 | 从缓冲队列取消息;并释放一个阻塞的写g,将其消息放入队列。 |
无被阻塞的写g | 阻塞读g | 阻塞读g | 从缓冲队列取消息 |
channel的数据结构
根据以上现象,大致可以猜到,channel里有个FIFO的队列,有保存阻塞的读\写的goroutine的队列,还有一个锁。
runtime.hchan
里的定义如下。
type hchan struct {
qcount uint // 运行时缓冲区消息个数
dataqsiz uint // 缓冲区大小,即make的第二个参数
buf unsafe.Pointer // 缓冲区的指针
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
lock mutex
}
FIFO队列的状态由 qcount, datasiz, sendx, recvx
(x表示index)来维护,存储消息的类型大小信息由buf, elemsize, elemtype, *_type
维护。阻塞的读写goroutine由recvq, sendq
(q表示queue,内部是一个链表)维护。
缓冲区巧妙的使用了一个循环队列来实现,sendx
先跑,recvx
追sendx
,发送方每往队列里塞一个数据,sendx
就+1,读取方每读一个,recvx
就+1。qcount
维护消息的数量。
结合上面描述的现象、数据结构和代码实现,我们可以简单描述下发送和接收的过程。
向channel发送数据
实现在runtime.chansend()
里。
先读下代码中几个和goroutine调度相关的函数\数据,大致理解下调度的API:
runtime.gopark()
调用点会挂起当前goroutine,形成阻塞runtime.g
及相关函数getg()
,获取当前的goroutine,使用汇编指令实现。runtime.sudog
及相关函数acquireSudog()
,releaseSudog()
,sudog代表一个等待队列中的g,代码注释中已经解释的比较清楚了。我的理解它表达的是一个goroutine和一个等待队列的关系。
再刨去非核心的逻辑:
debugChan
常量,字面意思,调试打印用throw()
函数,造成panic
用- 注释中的 Fast path: 加锁是一个有消耗的行为,如果加锁前就可以对channel行为有定论,则不加锁,走快速判定。
atomic.Load()
,atomic.Loaduint()
在加锁之前使用的原子操作raceenabled
常量,默认关,字面意思是“允许数据竞争”,即不考虑数据冲突情况的channel行为控制。个人猜测可能是调试时、或者单process
时可以加速channel相关行为时使用
剩下的核心调度逻辑起始挺好理解,参考上面一节描述的发送行为,总的来说就是维护着消息队列和阻塞队列,根据不同情况将自己的数据送走,继续执行后续代码,或者阻塞在调用 c<-xxx
处。
下表:场景<—>代码行为。
场景 | 数据状态、行为 |
---|---|
chan为空 | c == nil |
chan关闭 | c.closed == true |
另一个g在等待读 | sg := c.recvq.dequeue(); sg != nil |
发送给等待读的chan | send(c, sg, ...) |
缓存区存在剩余空间 | c.qcount < c.dataqsiz |
将通信内容放入队列 | typedmemmove(c.elemtype, qp, ep) 同时维护c.sendx |
阻塞自己 | acquireSudog() 设置sudog ,调c.sendq.enqueue(sudog) 将本goroutine(对应的sudog )放入等待对列,并调gopark(..) |
直到被其他go读取 | gopark() 返回并释放sudog ,继续执行 c <- xxx 后续语句 |
写channel现象:
- 向nil channel写①,会导致阻塞。
- 向关闭的channel写②,会导致panic。
- 如果另一个goroutine在等待读,则通信内容直接发送给另一个goroutine,自己不阻塞③。
- 上一种现象中如果有多个goroutine都在等待读,则发给第一个等待的,FIFO顺序。
- 如果没有另一个goroutine在等待读,如果缓存队列没满,那么将通信内容放入队列④,自己不阻塞。
- 如果没有另一个goroutine在等待读,如果缓存队列满了,那么自己将阻塞⑤,直到被其他go读取⑥。
执行判断的顺序如上面标注。
从channel接收数据
实现在runtime.chanrecv()
里。
和发送函数比较像。直接分析场景对应代码行为。
下表:场景<—>代码行为。
场景 | 数据状态、行为 |
---|---|
chan为空 | c == nil |
chan关闭 | c.closed == true |
有被阻塞的写g | sg := c.sendq.dequeue(); sg != nil |
直接读取并释放一个g | recv(c, sg, ...) 其中c.dataqsiz == 0 直接从sendq 里取,否则从队列recvx 处取 |
无缓冲队列 | c.dataqsiz == 0 |
将通信内容放入队列 | typedmemmove(c.elemtype, qp, ep) 同时维护c.sendx |
阻塞自己 | acquireSudog() 设置sudog ,调c.recvq.enqueue(sudog) 将本goroutine(对应的sudog )放入等待对列,并调gopark(..) |
直到其他go向这个chan 发送数据 | gopark() 返回并释放sudog ,继续执行后续语句 |
读channel现象:【重新列举如下】
- 从nil channel读,会导致阻塞。①
- 从关闭的channel读,如果缓冲区有,则取出;没有则会读出0值,不阻塞②。(注:关闭一个channel时,会将sendq和recvq中的goroutine取出来进行调度)
- 如果存在缓冲区,则优先向缓冲区写,其次阻塞写的goroutine。因此,读channel的优先级是先从缓存队列读,再从被阻塞的写channel的goroutine读;
- 上面现象可以细分几种情况。如下表
读chan场景 | 无缓冲队列 | 有缓冲队列但是为空 | 有缓冲队列且有消息 |
---|---|---|---|
有被阻塞的写g ③ | 从阻塞的第一个里取,并释放阻塞的写g ④ | 不可能 | 从缓冲队列取消息;并释放一个阻塞的写g,将其消息放入队列。⑤ |
无被阻塞的写g | 阻塞读g ⑦ | 阻塞读g | 从缓冲队列取消息 ⑥ |
阻塞解除后继续执行后续语句 ⑧。
执行判断的顺序如上面标注。
select专题
select和channel关系密切,理解channel之后,select就好理解了。
参考 [draveness.me][1]
参考https://draveness.me/golang/docs,待自己理解和总结。
我们简单总结一下 select 结构的执行过程与实现原理,首先在编译期间,Go 语言会对 select 语句进行优化,它会根据 select 中 case 的不同选择不同的优化路径:
空的 select 语句会被转换成 runtime.block 函数的调用,直接挂起当前 Goroutine;
如果 select 语句中只包含一个 case,就会被转换成 if ch == nil { block }; n; 表达式;
首先判断操作的 Channel 是不是空的;
然后执行 case 结构中的内容;
如果 select 语句中只包含两个 case 并且其中一个是 default,那么会使用 runtime.selectnbrecv 和 runtime.selectnbsend 非阻塞地执行收发操作;
在默认情况下会通过 runtime.selectgo 函数获取执行 case 的索引,并通过多个 if 语句执行对应 case 中的代码;
在编译器已经对 select 语句进行优化之后,Go 语言会在运行时执行编译期间展开的 runtime.selectgo 函数,该函数会按照以下的流程执行:
随机生成一个遍历的轮询顺序 pollOrder 并根据 Channel 地址生成锁定顺序 lockOrder;
根据 pollOrder 遍历所有的 case 查看是否有可以立刻处理的 Channel;
如果存在就直接获取 case 对应的索引并返回;
如果不存在就会创建 runtime.sudog 结构体,将当前 Goroutine 加入到所有相关 Channel 的收发队列,并调用 runtime.gopark 挂起当前 Goroutine 等待调度器的唤醒;
当调度器唤醒当前 Goroutine 时就会再次按照 lockOrder 遍历所有的 case,从中查找需要被处理的 runtime.sudog 结构对应的索引;
select 关键字是 Go 语言特有的控制结构,它的实现原理比较复杂,需要编译器和运行时函数的通力合作。
gopark 做了什么
https://blog.csdn.net/u010853261/article/details/85887948
context.Context专题
WithContext
的用法
// NewContext returns a new Context carrying userIP.
func NewContext(ctx context.Context, userIP net.IP) context.Context {
return context.WithValue(ctx, userIPKey, userIP)
}
WithTimeout
和WithCancel
的用法
timeout, err := time.ParseDuration(req.FormValue("timeout"))
if err == nil {
// The request has a timeout, so create a context that is
// canceled automatically when the timeout expires.
ctx, cancel = context.WithTimeout(context.Background(), timeout)
} else {
ctx, cancel = context.WithCancel(context.Background())
}
defer cancel() // Cancel ctx as soon as handleSearch returns.
Done()
的使用
// httpDo issues the HTTP request and calls f with the response. If ctx.Done is
// closed while the request or f is running, httpDo cancels the request, waits
// for f to exit, and returns ctx.Err. Otherwise, httpDo returns f's error.
func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
// Run the HTTP request in a goroutine and pass the response to f.
c := make(chan error, 1)
req = req.WithContext(ctx)
go func() { c <- f(http.DefaultClient.Do(req)) }()
select {
case <-ctx.Done():
<-c // Wait for f to return.
return ctx.Err()
case err := <-c:
return err
}
}
Value(key interface{}) interface{}
的使用
// FromContext extracts the user IP address from ctx, if present.
func FromContext(ctx context.Context) (net.IP, bool) {
// ctx.Value returns nil if ctx has no value for the key;
// the net.IP type assertion returns ok=false for nil.
userIP, ok := ctx.Value(userIPKey).(net.IP)
return userIP, ok
}
defer,panic,recovery专题
数据:
type _defer struct {
siz int32
started bool
sp uintptr
pc uintptr
fn *funcval
_panic *_panic
link *_defer
}
type _panic struct {
argp unsafe.Pointer // pointer to arguments of deferred call run during panic
arg interface{} // argument to panic
link *_panic // link to earlier panic
recovered bool // whether this panic is over
aborted bool // the panic was aborted
}
行为
runtime.deferproc
函数负责创建新的延迟调用;
runtime.deferreturn
函数负责在函数调用结束时执行所有的延迟调用;
runtime.gopanic
对应程序中 panic
runtime.gorecover
对应程序中recover
参考 [draveness.me][1]
参考https://draveness.me/golang/docs,待自己理解和总结。 编译期:
将 defer 关键字被转换 runtime.deferproc;
在调用 defer 关键字的函数返回之前插入 runtime.deferreturn;
运行时:
runtime.deferproc 会将一个新的 runtime._defer 结构体追加到当前 Goroutine 的链表头;
runtime.deferreturn 会从 Goroutine 的链表中取出 runtime._defer 结构并依次执行;
我们在本节前面提到的两个现象在这里也可以解释清楚了:
后调用的 defer 函数会先执行:
后调用的 defer 函数会被追加到 Goroutine _defer 链表的最前面;
运行 runtime._defer 时是从前到后依次执行;
函数的参数会被预先计算;
调用 runtime.deferproc 函数创建新的延迟调用时就会立刻拷贝函数的参数,函数的参数不会等到真正执行时计算;
分析程序的崩溃和恢复过程比较棘手,代码不是特别容易理解。我们在本节的最后还是简单总结一下程序崩溃和恢复的过程:
编译器会负责做转换关键字的工作;
将 panic 和 recover 分别转换成 runtime.gopanic 和 runtime.gorecover;
将 defer 转换成 deferproc 函数;
在调用 defer 的函数末尾调用 deferreturn 函数;
在运行过程中遇到 gopanic 方法时,会从 Goroutine 的链表依次取出 _defer 结构体并执行;
如果调用延迟执行函数时遇到了 gorecover 就会将 _panic.recovered 标记成 true 并返回 panic 的参数;
在这次调用结束之后,gopanic 会从 _defer 结构体中取出程序计数器 pc 和栈指针 sp 并调用 recovery 函数进行恢复程序;
recovery 会根据传入的 pc 和 sp 跳转回 deferproc;
编译器自动生成的代码会发现 deferproc 的返回值不为 0,这时会跳回 deferreturn 并恢复到正常的执行流程;
如果没有遇到 gorecover 就会依次遍历所有的 _defer 结构,并在最后调用 fatalpanic 中止程序、打印 panic 的参数并返回错误码 2;
分析的过程涉及了很多语言底层的知识,源代码阅读起来也比较晦涩,其中充斥着反常规的控制流程,通过程序计数器来回跳转,不过对于我们理解程序的执行流程还是很有帮助。
map专题
runtime.hmap
为map定义
runtime.bmap
为桶定义
for range遍历的顺序:
随机一个桶 --> 溢出区 --> 其他桶
同步,锁专题
to be done
GMP调度器
go routine的创建过程
fn
为函数入口指针,用于得到要执行的代码的位置。
siz
为参数的以byte
记的长度
Create a new g running fn with siz bytes of arguments.
func newproc(siz int32, fn *funcval) {
argp := add(unsafe.Pointer(&fn), sys.PtrSize) // 参数栈的start地址
gp := getg() // 获取创造这个goroutine的goroutine
pc := getcallerpc() // 程序计数器,返回时的执行代码位置
systemstack(func() { // 切到g0执行
newproc1(fn, (*uint8)(argp), siz, gp, pc)
})
}
注:g0栈又名per-OS-thread
栈。可以看出g0是thread粒度的。
g0是每个线程上的goroutine调度器执行者,每次切换goroutine都少不了g0的动作。
g0职责。
- 创建新的goroutine
- 调度goroutine
- defer函数分配者
- GC执行者,STW,扫描标记等
- 栈的动态分配管理,
prolog
函数
Go通过GOMAXPROCS同时变量来限制运行的OS线程数。这意味着Go必须在每个正在运行的线程上调度和管理goroutine。该角色被委派给一个特殊的goroutine,称为g0,这是为每个OS线程创建的第一个goroutine:
g0的解释,Go: g0, Special Goroutine
在g0中调用newproc1
真正创建一个goroutine
Go 实现了所谓的 M:N 模型,执行用户代码的 goroutine 可以认为都是对等的 goroutine。不考虑 g0 和 gsignal 的话,我们可以简单地认为调度就是将 m 绑定到 p,然后在 m 中不断循环执行调度函数(runtime.schedule),寻找可用的 g 来执行:
调度器的启动
G数据机构
goroutine粒度
数据:
routine运行的栈
panic链表
defer链表
当前m
sched gobuf: 保存了sp,pc,g的指针等调度信息
atomicStatus: goroutine的信息,有idle, runnable, running, syscall, waiting, dead, stackcopy, preempted, scan。
preempt, preemptStop, preemptShrink 抢占相关
P数据结构
调度goroutine。令每个M能够在不切换线程的情况下串行执行多个P。当G进行系统调用时(IO操作)及时把P与M脱钩,提高线程的利用率。
处理器的运行时表示,职能很多。与调度相关的如下:
m 反连至m的指针。
runq, runqhead, runqtail, runnext g的runq队列,以及下一个将被执行的g。
status是 idle, running, syscall, gcstop, dead
M数据结构
线程粒度:
最多10000个,最多有GOMAXPROC个活跃的,其他的可能陷入系统调用。
g0 调度栈所在的goroutine,深度参与调度过程
curg 当前运行的goroutine
p 正在运行的p
nextp 暂存的处理器的p
oldp 执行系统调用之前的使用线程的处理器p
线程状态、锁、调度、系统调用…
热补丁
go可以使用 plugin加载.so,实现热补丁。
go编译原理
走进Golang之编译器原理
Golang之运行与Plan9汇编
go plan9汇编入门
go编译工具的使用之汇编
内存区域 | 解释 |
---|---|
代码区 | 存放的就是我们编译后的机器码,一般来说这个区域只能是只读。 |
静态数据区 | 存放的是全局变量与常量。这些变量的地址编译的时候就确定了(这也是使用虚拟地址的好处,如果是物理地址,这些地址编译的时候是不可能确定的)。Data与BSS都属于这一部分。这部分只有程序中止(kill掉、crasg掉等)才会被销毁。 |
栈区 | 主要是 Golang 里边的函数、方法以及其本地变量存储的地方。这部分伴随函数、方法开始执行而分配,运行完后就被释放,特别注意这里的释放并不会清空内存。后面文章讲内存分配的时候再详细说;还有一个点需要记住栈一般是从高地址向低地址方向分配,换句话说:高地址属于栈低,低地址属于栈底,它分配方向与堆是相反的。 |
堆区 | 像 C/C++ 语言,堆完全是程序员自己控制的。但是 Golang 里边由于有GC机制,我们写代码的时候并不需要关心内存是在栈还是堆上分配。Golang 会自己判断如果变量的生命周期在函数退出后还不能销毁或者栈上资源不够分配等等情况,就会被放到堆上。堆的性能会比栈要差一些。原因也留到内存分配相关的文章再给大家介绍。 |
栈帧,一个go函数所用到的栈空间。。
具体例子学习go汇编阅读。
package main
func main() {
a := 3
b := 2
returnTwo(a, b)
}
func returnTwo(a, b int) (c, d int) {
tmp := 1 // 这一行的主要目的是保证栈桢不为0,方便分析
c = a + b
d = b - tmp
return
}
go tool compile -S -N -l
后如下
-S : 显示汇编代码 Print assembly listing to standard output (code only).
-N : 无优化. Disable optimizations.
-l: 无内联优化 Disable inlining.
"".main STEXT size=94 args=0x0 locals=0x38 ;# 第一行是go汇编的固定开头,指定过程名字为"".main,args=0x0 locals=0x38则对应第二行的$56-0是十六进制和十进制的转化。
0x0000 00000 (ocr.go:3) TEXT "".main(SB), ABIInternal, $56-0 ;# 56 表示的该函数栈桢大小(两个本地变量,两个参数是int类型,两个返回值是int类型,1个保存base pointer,合计7 * 8 = 56);0 表示 main 函数的参数与返回值大小(调用方传入的参数大小)。ABIInternal:程序二进制接口内部(Application Binary Interface Internal)
0x0000 00000 (ocr.go:3) MOVQ TLS, CX ;# 数据传送 把一个(TLS)赋值给CX(计数寄存器)。 TLS它实际上也是一个伪寄存器,保存了指向当前G(保存goroutine的一种数据结构)的指针
0x0009 00009 (ocr.go:3) MOVQ (CX)(TLS*2), CX
0x0010 00016 (ocr.go:3) CMPQ SP, 16(CX) ;# 比较当前 栈顶指针(SP) 和G指针正偏移16字节的地址大小。 数字表示偏移!
0x0014 00020 (ocr.go:3) JLS 87 ;# jump if less, 左小于右,则到87指令的地址(24行)
0x0016 00022 (ocr.go:3) SUBQ $56, SP ;# 减法。SP = SP - 常量56。 分配56单位的堆栈。
0x001a 00026 (ocr.go:3) MOVQ BP, 48(SP) ;# main的调用者BP存的值放入SP+offset(48),保存在进入函数前的栈顶基址
0x001f 00031 (ocr.go:3) LEAQ 48(SP), BP ;# 48(SP)的地址放入BP,BP就是进入main函数后的栈顶地址。
0x0024 00036 (ocr.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) ;# Gc用
0x0024 00036 (ocr.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) ;# Gc用
0x0024 00036 (ocr.go:3) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) ;# Gc用
0x0024 00036 (ocr.go:4) PCDATA $0, $0 ;# Gc用
0x0024 00036 (ocr.go:4) PCDATA $1, $0 ;# Gc用
0x0024 00036 (ocr.go:4) MOVQ $3, "".a+40(SP) ;# 把常量3放入a SP+40, symbol+offset(SP)的形式【本例子中是"".a+40(SP)】表示go汇编的伪寄存器
0x002d 00045 (ocr.go:5) MOVQ $2, "".b+32(SP) ;# 把常量2放入b SP+32, symbol+offset(SP)的形式【本例子中是"".a+40(SP)】表示go汇编的伪寄存器
0x0036 00054 (ocr.go:6) MOVQ "".a+40(SP), AX ;# 把a的值放入AX,AX:累加寄存器。 AX = 3
0x003b 00059 (ocr.go:6) MOVQ AX, (SP) ;# 把AX的值放入SP+0
0x003f 00063 (ocr.go:6) MOVQ $2, 8(SP) ;# 把2放入SP+8
0x0048 00072 (ocr.go:6) CALL "".returnTwo(SB) ;# foo(SB)用于表示变量在内存中的地址,foo+4(SB)表示foo起始地址往后偏移四字节。一般用来声明函数或全局变量
0x004d 00077 (ocr.go:7) MOVQ 48(SP), BP ;# 调用返回,把调用者的BP(存在SPoffset48处)赋值给BP
0x0052 00082 (ocr.go:7) ADDQ $56, SP ;# 回收栈, SP += 56
0x0056 00086 (ocr.go:7) RET
0x0057 00087 (ocr.go:7) NOP
0x0057 00087 (ocr.go:3) PCDATA $1, $-1
0x0057 00087 (ocr.go:3) PCDATA $0, $-1
0x0057 00087 (ocr.go:3) CALL runtime.morestack_noctxt(SB)
0x005c 00092 (ocr.go:3) JMP 0
0x0000 65 48 8b 0c 25 28 00 00 00 48 8b 89 00 00 00 00 eH..%(...H......
0x0010 48 3b 61 10 76 41 48 83 ec 38 48 89 6c 24 30 48 H;a.vAH..8H.l$0H
0x0020 8d 6c 24 30 48 c7 44 24 28 03 00 00 00 48 c7 44 .l$0H.D$(....H.D
0x0030 24 20 02 00 00 00 48 8b 44 24 28 48 89 04 24 48 $ ....H.D$(H..$H
0x0040 c7 44 24 08 02 00 00 00 e8 00 00 00 00 48 8b 6c .D$..........H.l
0x0050 24 30 48 83 c4 38 c3 e8 00 00 00 00 eb a2 $0H..8........
rel 12+4 t=16 TLS+0
rel 73+4 t=8 "".returnTwo+0
rel 88+4 t=8 runtime.morestack_noctxt+0
"".returnTwo STEXT nosplit size=79 args=0x20 locals=0x10 ;# go汇编的固定开头,指定过程名字为"".returnTwo,args=0x20 (外部参数,包括入参和返回值)locals=0x10(函数本地的栈)则对应第二行的$16-32是十六进制和十进制的转化。
0x0000 00000 (ocr.go:9) TEXT "".returnTwo(SB), NOSPLIT|ABIInternal, $16-32
0x0000 00000 (ocr.go:9) SUBQ $16, SP ;# SP = SP - 16; SP扩展16长度 (分配栈内存) locals=0x10指定可以用8个字节。
0x0004 00004 (ocr.go:9) MOVQ BP, 8(SP) ;# 把BP的值(调用者的BP)放入8(SP)
0x0009 00009 (ocr.go:9) LEAQ 8(SP), BP ;# 把8(SP)的地址放入BP,作为returnTwo函数的栈顶基址
0x000e 00014 (ocr.go:9) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) ;# Gc用
0x000e 00014 (ocr.go:9) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) ;# Gc用
0x000e 00014 (ocr.go:9) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) ;# Gc用
0x000e 00014 (ocr.go:9) PCDATA $0, $0 ;# Gc用
0x000e 00014 (ocr.go:9) PCDATA $1, $0 ;# Gc用
0x000e 00014 (ocr.go:9) MOVQ $0, "".c+40(SP) ;# 常量0放入c返回值(40)offset SP 。 args = 0x20可以用16个字节,4个int,对应两个入参,两个出参
0x0017 00023 (ocr.go:9) MOVQ $0, "".d+48(SP) ;# 常量0放入d返回值(48)offset SP 。 args = 0x20可以用16个字节,4个int,对应两个入参,两个出参
0x0020 00032 (ocr.go:10) MOVQ $1, "".tmp(SP) ;# tmp:=1; 1放入tmp SP+0
0x0028 00040 (ocr.go:11) MOVQ "".a+24(SP), AX ;# a(SP+24)放入AX AX = a
0x002d 00045 (ocr.go:11) ADDQ "".b+32(SP), AX ;# AX += b(SP+32) AX = a+b AX = AX + b
0x0032 00050 (ocr.go:11) MOVQ AX, "".c+40(SP) ;# c = AX c = a+b c = AX
0x0037 00055 (ocr.go:12) MOVQ "".b+32(SP), AX ;# b(SP+32)放入AX AX = b
0x003c 00060 (ocr.go:12) SUBQ "".tmp(SP), AX ;# AX -= tmp AX = AX - tmp
0x0040 00064 (ocr.go:12) MOVQ AX, "".d+48(SP) ;# d = AX d = AX
0x0045 00069 (ocr.go:13) MOVQ 8(SP), BP ;# 调用返回,把调用者的BP(存在SPoffset8处)赋值给BP
0x004a 00074 (ocr.go:13) ADDQ $16, SP ;# 回收栈, SP += 16
0x004e 00078 (ocr.go:13) RET
寄存器 | 说明 |
---|---|
SP | 堆栈顶指针(StackPointer) 如果是symbol+offset(SP)的形式表示go汇编的伪寄存器;如果是offset(SP)的形式表示硬件寄存器 |
BP | 堆栈基指针(BasePointer) 保存在进入函数前的栈顶基址 |
几个细节:
- BP 与 SP 是寄存器,它保存的是栈上的地址,所以执行中可以对 SP 做运算找到下一个指令的位置;
- 栈被回收
ADDQ $56, SP
,只是改变了 SP 指向的位置,内存中的数据并不会清空,只有下次被分配使用的时候才会清空; - callee的参数、返回值内存都是caller分配的;
- returnTwo ret的时候,call returnTwo的next指令 所在栈位置会被弹出,也就是图中 0x0d00 地址所保存的指令,所以 returnTwo 函数返回后,SP 又指向了 0x0d08 地址。
Gc
为什么要有写屏障?
可以看出,有两个问题, 在三色标记法中,是不希望被发生的
条件1: 一个白色对象被黑色对象引用(白色被挂在黑色下)
条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏(灰色同时丢了该白色)