返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

4.6.5 扩容

发布于 2024-10-12 19:15:59 字数 5869 浏览 0 评论 0 收藏 0

编译器在函数头尾插入指令,用于栈内存检查和扩展。

// test/main.go

func main() {
	println("hello, world!")
}
# 查看 native code,而不是 compile -S 输出的中间汇编代码。
$ go build && go tool objdump -s "main\.main" ./test

TEXT main.main(SB) test/main.go

  ; R14 保存当前 G。
  ; 0x10(G) 指向 G.stackguard0。
  ; 与 SP 比较,以确定是否要跳转到 morestack 扩容。
  0x4551e0		CMPQ 0x10(R14), SP			
  0x4551e4		JBE  0x455219
  
  0x4551e6		SUBQ $0x18, SP				
  0x4551ea		MOVQ BP, 0x10(SP)			
  0x4551ef		LEAQ 0x10(SP), BP		
  
  0x4551f4		CALL runtime.printlock(SB)		
  0x4551f9		LEAQ 0xd7a6(IP), AX			
  0x455200		MOVL $0xe, BX				
  0x455205		CALL runtime.printstring(SB)		
  0x45520a		CALL runtime.printunlock(SB)	
  
  0x45520f		MOVQ 0x10(SP), BP			
  0x455214		ADDQ $0x18, SP				
  0x455218		RET					
  
  ; 扩容后跳回头部,重新执行。
  0x455219		CALL runtime.morestack_noctxt.abi0(SB)	
  0x45521e		NOPW					
  0x455220		JMP main.main(SB)
// runtime2.go

type g struct {
    stack       stack
    stackguard0 uintptr
    stackguard1 uintptr
}

type stack struct {
	lo uintptr
	hi uintptr
}

寄存器

不同平台使用不同寄存器存储 G 地址。

// src/cmd/internal/obj/x86/a.out.go

const REGG = REG_R14     // g register in ABIInternal
// src/cmd/asm/internal/arch/arch.go

func archX86(linkArch *obj.LinkArch) *Arch {
	// Pseudo-registers.
	register["SB"] = RSB
	register["FP"] = RFP
	register["PC"] = RPC
    
	if linkArch == &x86.Linkamd64 {
		register["g"] = x86.REGG  // Alias g to R14
	}
}

func archArm64() *Arch {
	// Pseudo-registers.
	register["SB"] = RSB
	register["FP"] = RFP
	register["PC"] = RPC
	register["SP"] = RSP
    
	register["g"] = arm64.REG_R28
}
$ GOARCH=arm64 go build && go tool objdump -s "main\.main" ./test

TEXT main.main(SB) test/main.go

  0x62c60			MOVD 16(R28), R16
  0x62c64			MOVD RSP, R17				
  0x62c68			CMP  R16, R17				
  0x62c6c			BLS  13(PC)

汇编代码里,用 get_tls 将 G 地址载入寄存器。

// doc/asm.html

The runtime pointer to the g structure is maintained through 
the value of an otherwise unused (as far as Go is concerned) 
register in the MMU. In the runtime package, assembly code can 
include go_tls.h, which defines an OS- and architecture-dependent 
macro get_tls for accessing this register. 

The get_tls macro takes one argument, which is the register to 
load the g pointer into.

get_tls(CX)
MOVL	g(CX), AX     // Move g into AX.
MOVL	g_m(AX), BX   // Move g.m into BX.    
// go_tls.h

#ifdef GOARCH_amd64

#define	get_tls(r)	MOVQ TLS, r
#define	g(r)	    0(r)(TLS*1)

#endif

扩容

扩容操作使用 g0 栈,注意保存 G 现场。

// asm_amd64.s

TEXT runtime·morestack_noctxt(SB),NOSPLIT,$0
    MOVL	$0, DX
    JMP		runtime·morestack(SB)

    
// Called during function prolog when more stack is needed.
TEXT runtime·morestack(SB),NOSPLIT,$0-0

    // Cannot grow scheduler stack (m->g0).
	// Cannot grow signal stack (m->gsignal).

	get_tls(CX)
	MOVQ	g(CX), BX       // g
	MOVQ	g_m(BX), BX     // m

	get_tls(CX)
	MOVQ	g(CX), SI
    
	// Set g->sched to context in f.
	MOVQ	0(SP), AX                    //  morestack 并未使用栈内存。
	MOVQ	AX, (g_sched+gobuf_pc)(SI)   //  仅 call morestack 指令将
	LEAQ	8(SP), AX                    //  f PC 入栈。
	MOVQ	AX, (g_sched+gobuf_sp)(SI)   //  
	MOVQ	BP, (g_sched+gobuf_bp)(SI)   //  因此,0(SP) -> f.PC
	MOVQ	DX, (g_sched+gobuf_ctxt)(SI) //       8(SP) -> f.SP (before call)
    
	// Call newstack on m->g0's stack.
	MOVQ	m_g0(BX), BX
	MOVQ	BX, g(CX)
	MOVQ	(g_sched+gobuf_sp)(BX), SP
	CALL	runtime·newstack(SB)
    
	CALL	runtime·abort(SB)	// crash if newstack returns
	RET

执行 newstack 分配更大的内存,并拷贝数据。

// stack.go

// Called from runtime·morestack when more stack is needed.
// Allocate larger stack and relocate to new stack.
// Stack growth is multiplicative, for constant amortized cost.

func newstack() {
    
    thisg := getg()     // g0
    gp := thisg.m.curg  // G
    
    // preempt ...
    
    // 2x
    oldsize := gp.stack.hi - gp.stack.lo
    newsize := oldsize * 2
    
    // 分配新栈内存,复制数据。
    casgstatus(gp, _Grunning, _Gcopystack)
    copystack(gp, newsize)
    
    // 使用 morestack 保存的现场恢复 G 执行。
    casgstatus(gp, _Gcopystack, _Grunning)
    gogo(&gp.sched)
}
// Copies gp's stack to a new stack of a different size.
// Caller must have changed gp status to Gcopystack.

func copystack(gp *g, newsize uintptr) {

    old := gp.stack
	used := old.hi - gp.sched.sp

    // 老栈释放需要垃圾清理。
	gcController.addScannableStack(getg().m.p.ptr(), int64(newsize)-int64(old.hi-old.lo))

	// 分配新栈。
	new := stackalloc(uint32(newsize))

	// 计算新旧地址偏移,以便调整指针。
	var adjinfo adjustinfo
	adjinfo.old = old
	adjinfo.delta = new.hi - old.hi

	// 复制栈数据。
	ncopy := used
	memmove(unsafe.Pointer(new.hi-ncopy), unsafe.Pointer(old.hi-ncopy), ncopy)

	// 调整指针。
	adjustctxt(gp, &adjinfo)
	adjustdefers(gp, &adjinfo)
	adjustpanics(gp, &adjinfo)
	if adjinfo.sghi != 0 {
		adjinfo.sghi += adjinfo.delta
	}

	// 切换到新栈。
	gp.stack = new
	gp.stackguard0 = new.lo + _StackGuard // NOTE: might clobber a preempt request
	gp.sched.sp = new.hi - used
	gp.stktopsp += adjinfo.delta

	// 释放旧栈。
	stackfree(old)
}

指针

指针调整只是简单的加上新旧地址偏移(+= delta)。

func adjustctxt(gp *g, adjinfo *adjustinfo) {
	adjustpointer(adjinfo, unsafe.Pointer(&gp.sched.ctxt))
	adjustpointer(adjinfo, unsafe.Pointer(&gp.sched.bp))
}
type adjustinfo struct {
	old   stack
	delta uintptr // ptr distance from old to new stack (newbase - oldbase)
}


// Adjustpointer checks whether *vpp is in the old stack described by adjinfo.
// If so, it rewrites *vpp to point into the new stack.

func adjustpointer(adjinfo *adjustinfo, vpp unsafe.Pointer) {
	pp := (*uintptr)(vpp)
	p := *pp
    
	if adjinfo.old.lo <= p && p < adjinfo.old.hi {
		*pp = p + adjinfo.delta
	}
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文