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
更多关于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
在您的调试场景中,关键点在于:
runtime·settls(SB)调用设置了MSR_FS_BASE寄存器,使其指向m0.tls数组- 在amd64 Linux上,
m0.tls[1]存储的是当前goroutine的g结构指针 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。
这个机制确保了:
- 每个操作系统线程都有自己的TLS区域
- 运行时可以通过
getg()快速获取当前goroutine的g结构指针 - 在amd64上,这通过FS段寄存器高效实现
您观察到的行为是正常的,这是Go运行时在amd64 Linux上TLS实现的特定方式。

