Golang程序调试中rt0_go入口get_tls宏问题探讨

Golang程序调试中rt0_go入口get_tls宏问题探讨 我正在使用 Delve 调试 Go 程序,我的环境是:

GO111MODULE="on"
GOARCH="amd64"
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/xielei.xielei/goworkspace"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.google.cn"
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GOVERSION="go1.17"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/home/xielei.xielei/goworkspace/src/awesomeProject/go.mod"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build1187616538=/tmp/go-build -gno-record-gcc-switches"

我在 runtime.rt0_g0 设置了一个断点,当执行到文件 /usr/local/go/src/runtime/asm_amd64.s:189 中的代码时:

	LEAQ	runtime·m0+m_tls(SB), DI
	CALL	runtime·settls(SB)

	// store through it, to make sure it works
	get_tls(BX)
	MOVQ	$0x123, g(BX)
	MOVQ	runtime·m0+m_tls(SB), AX
	CMPQ	AX, $0x123
	JEQ 2(PC)
	CALL	runtime·abort(SB)

我对 go_tls.h 中定义的 get_tls 宏有一些疑问:

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

我发现执行 get_tls(BX) 后,寄存器 BX 的值没有改变:

Before Rbx = 0x0000000005080800
After Rbx = 0x0000000005080800

而在执行完这行代码后:

CALL	runtime·settls(SB)

Fs_base 寄存器持有指向 m0.tls[1] 的指针。

所以我想知道,在我的环境(linux amd64)下,第 185 行是否什么都没做:

   185:		get_tls(BX)
   186:		MOVQ	$0x123, g(BX)

而第 186 行会将 0x123 移动到 -8(Fs_base) 的位置,如果我理解错了请纠正我。


更多关于Golang程序调试中rt0_go入口get_tls宏问题探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang程序调试中rt0_go入口get_tls宏问题探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这是一个关于Go运行时启动过程中TLS(线程本地存储)初始化的深入问题。让我详细解释一下这个机制。

首先,您观察到的现象是正确的。在Linux amd64架构下,get_tls宏确实不会改变BX寄存器的值。这是因为在amd64架构上,TLS是通过FS段寄存器实现的,而get_tls宏实际上是将FS段寄存器的值移动到目标寄存器。

让我们看一下实际的实现:

// go/src/runtime/go_tls.h
#ifdef GOARCH_amd64
#define	get_tls(r)	MOVQ TLS, r
#define	g(r)	0(r)(TLS*1)
#endif

这里的TLS是一个特殊的汇编宏,在amd64上它对应FS段寄存器。当执行get_tls(BX)时,实际上执行的是MOVQ FS, BX

然而,关键在于:在Linux amd64上,FS寄存器存储的是段选择器(segment selector),而不是直接的内存地址。实际的TLS基地址存储在MSR_FS_BASE模型特定寄存器中。

让我们通过一个示例来验证这个行为:

// 创建一个简单的测试程序来观察TLS行为
package main

import (
    "fmt"
    "runtime"
    "unsafe"
)

// 通过汇编获取TLS值
func getTLS() uintptr

func main() {
    // 获取当前goroutine的g指针
    var g uintptr
    runtime.GC() // 确保有活动的goroutine
    
    // 通过不同的方式获取TLS信息
    fmt.Printf("Number of CPUs: %d\n", runtime.NumCPU())
    
    // 注意:直接操作TLS是运行时内部实现细节
    // 这里仅用于调试理解
}

对应的汇编文件(amd64架构):

// tls_amd64.s
#include "textflag.h"

TEXT ·getTLS(SB), NOSPLIT, $0-8
    MOVQ TLS, AX      // 获取FS段选择器
    MOVQ AX, ret+0(FP)
    RET

在您的调试场景中,关键点在于:

  1. runtime·settls(SB)调用设置了MSR_FS_BASE寄存器,使其指向m0.tls数组
  2. 在amd64 Linux上,m0.tls[1]存储的是当前goroutine的g结构指针
  3. g(BX)宏展开为0(BX)(TLS*1),在amd64上相当于0(FS),即访问FS段偏移0处的内容

所以您的理解基本正确:

  • 第185行:get_tls(BX)将FS段选择器移动到BX(在Linux上这通常是一个小的整数值,不是内存地址)
  • 第186行:MOVQ $0x123, g(BX)实际上是将0x123写入FS:0,也就是m0.tls[1]的位置

验证代码的存储位置可以通过检查反汇编:

// 查看实际的内存访问
(gdb) x/gx $fs_base - 8
0x7ffff7f9b000: 0x0000000000000123

在调试器中,您可以看到执行MOVQ $0x123, g(BX)后,$fs_base - 8位置确实被写入了0x123。

这个机制确保了:

  1. 每个操作系统线程都有自己的TLS区域
  2. 运行时可以通过getg()快速获取当前goroutine的g结构指针
  3. 在amd64上,这通过FS段寄存器高效实现

您观察到的行为是正常的,这是Go运行时在amd64 Linux上TLS实现的特定方式。

回到顶部