栈的调整是通过对硬件 SP 寄存器进行运算来实现的,例如:
SUBQ $24, SP // 对 sp 做减法,为函数分配函数栈帧 ... ADDQ $24, SP // 对 sp 做加法 ,清除函数栈帧由于栈是往下增长的,所以 SUBQ 对 SP 做减法的时候实际上是为函数分配栈帧,ADDQ 则是清除栈帧。
常见指令加减法操作:
ADDQ AX, BX // BX += AX SUBQ AX, BX // BX -= AX数据搬运:
常数在 plan9 汇编用 $num 表示,可以为负数,默认情况下为十进制。搬运的长度是由 MOV 的后缀决定。
MOVB $1, DI // 1 byte MOVW $0x10, BX // 2 bytes MOVD $1, DX // 4 bytes MOVQ $-10, AX // 8 bytes还有一点区别是在使用 MOVQ 的时候会有看到带括号和不带括号的区别。
// 加括号代表是指针的引用 MOVQ (AX), BX // => BX = *AX 将AX指向的内存区域8byte赋值给BX MOVQ 16(AX), BX // => BX = *(AX + 16) //不加括号是值的引用 MOVQ AX, BX // => BX = AX 将AX中存储的内容赋值给BX,注意区别跳转:
// 无条件跳转 JMP addr // 跳转到地址,地址可为代码中的地址 JMP label // 跳转到标签,可以跳转到同一函数内的标签位置 JMP 2(PC) // 以当前指令为基础,向前/后跳转 x 行 // 有条件跳转 JLS addr地址运算:
LEAQ (AX)(AX*2), CX // => CX = AX + (AX * 2) = AX * 3上面代码中的 2 代表 scale,scale 只能是 0、2、4、8。
解析 G 的创建因为栈都是在 Goroutine 上的,所以先从 G 的创建开始看如何创建以及初始化栈空间的。由于我在《详解Go语言调度循环源码实现 https://www.luozhiyun.com/archives/448 》中已经讲过 G 的创建,所以这里只对栈的初始化部分的代码进行讲解。
G 的创建会调用 runtime·newproc进行创建:
runtime.newproc
func newproc(siz int32, fn *funcval) { argp := add(unsafe.Pointer(&fn), sys.PtrSize) gp := getg() // 获取 caller 的 PC 寄存器 pc := getcallerpc() // 切换到 G0 进行创建 systemstack(func() { newg := newproc1(fn, argp, siz, gp, pc) ... }) }newproc 方法会切换到 G0 上调用 newproc1 函数进行 G 的创建。
runtime.newproc1
const _StackMin = 2048 func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) *g { _g_ := getg() ... _p_ := _g_.m.p.ptr() // 从 P 的空闲链表中获取一个新的 G newg := gfget(_p_) // 获取不到则调用 malg 进行创建 if newg == nil { newg = malg(_StackMin) casgstatus(newg, _Gidle, _Gdead) allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack. } ... return newg }newproc1 方法很长,里面主要是获取 G ,然后对获取到的 G 做一些初始化的工作。我们这里只看 malg 函数的调用。
在调用 malg 函数的时候会传入一个最小栈大小的值:_StackMin(2048)。
runtime.malg
func malg(stacksize int32) *g { // 创建 G 结构体 newg := new(g) if stacksize >= 0 { // 这里会在 stacksize 的基础上为每个栈预留系统调用所需的内存大小 _StackSystem // 在 Linux/Darwin 上( _StackSystem == 0 )本行不改变 stacksize 的大小 stacksize = round2(_StackSystem + stacksize) // 切换到 G0 为 newg 初始化栈内存 systemstack(func() { newg.stack = stackalloc(uint32(stacksize)) }) // 设置 stackguard0 ,用来判断是否要进行栈扩容 newg.stackguard0 = newg.stack.lo + _StackGuard newg.stackguard1 = ^uintptr(0) *(*uintptr)(unsafe.Pointer(newg.stack.lo)) = 0 } return newg }在调用 malg 的时候会将传入的内存大小加上一个 _StackSystem 值预留给系统调用使用,round2 函数会将传入的值舍入为 2 的指数。然后会切换到 G0 执行 stackalloc 函数进行栈内存分配。
分配完毕之后会设置 stackguard0 为 stack.lo + _StackGuard,作为判断是否需要进行栈扩容使用,下面会谈到。
栈的初始化文件位置:src/runtime/stack.go
// 全局的栈缓存,分配 32KB以下内存 var stackpool [_NumStackOrders]struct { item stackpoolItem _ [cpu.CacheLinePadSize - unsafe.Sizeof(stackpoolItem{})%cpu.CacheLinePadSize]byte } //go:notinheap type stackpoolItem struct { mu mutex span mSpanList } // 全局的栈缓存,分配 32KB 以上内存 var stackLarge struct { lock mutex free [heapAddrBits - pageShift]mSpanList // free lists by log_2(s.npages) } // 初始化stackpool/stackLarge全局变量 func stackinit() { if _StackCacheSize&_PageMask != 0 { throw("cache size must be a multiple of page size") } for i := range stackpool { stackpool[i].item.span.init() lockInit(&stackpool[i].item.mu, lockRankStackpool) } for i := range stackLarge.free { stackLarge.free[i].init() lockInit(&stackLarge.lock, lockRankStackLarge) } }