1 行命令引发的 Go 应用崩溃
一、前言
不久前,阿里云 ARMS 团队、编译器团队、MSE 团队携手合作,共同发布并开源了 Go 语言的编译时自动插桩技术。该技术以其零侵入的特性,为 Go 应用提供了与 Java 监控能力相媲美的解决方案。开发者只需将 go build 替换为新编译命令 otel go build,就能实现对 Go 应用的全面监控和治理。
二、问题描述
近期,我们收到用户反馈,使用 otel go build -race 替代正常的 go build -race 命令后,编译生成的程序会导致崩溃。-race[3]是 Go 编译器的一个参数,用于检测数据竞争(data race)问题。通过为每个变量的访问添加额外检查,确保多个 goroutine 不会以不安全方式同时访问这些变量。
理论上,我们的工具不应影响-race 竞态检查的代码,因此出现崩溃的现象是非预期的,所以我们花了一些时间排查这个崩溃问题,崩溃的堆栈信息如下:
(gdb) bt
#0 0x000000000041e1c0 in __tsan_func_enter ()
#1 0x00000000004ad05a in racecall ()
#2 0x0000000000000001 in ?? ()
#3 0x00000000004acf99 in racefuncenter ()
#4 0x00000000004ae7f1 in runtime.racefuncenter (callpc=4317632)
#5 0x0000000000a247d8 in ../sdk/trace.(*traceContext).TakeSnapShot (tc=<optimized out>, ~r0=...)
#6 0x00000000004a2c25 in runtime.contextPropagate
#7 0x0000000000480185 in runtime.newproc1.func1 ()
#8 0x00000000004800e2 in runtime.newproc1 (fn=0xc00030a1f0, callergp=0xc0000061e0, callerpc=12379404, retVal0=0xc0002c8f00)
#9 0x000000000047fc3f in runtime.newproc.func1 ()
#10 0x00000000004a992a in runtime.systemstack ()
....
可以看到崩溃源于 __tsan_func_enter,而引发该问题的关键点是 runtime.contextPropagate。我们的工具在 runtime.newproc1 函数的开头插入了以下代码:
func newproc1(fn *funcval, callergp *g, callerpc uintptr) (retVal0 *g) {
// 我们插入的代码
retVal0.otel_trace_context = contextPropagate(callergp.otel_trace_context)
...
}
// 我们插入的代码
func contextPropagate(tls interface{}) interface{} {
if tls == nil {
return nil
}
if taker, ok := tls.(ContextSnapshoter); ok {
return taker.TakeSnapShot()
}
return tls
}
// 我们插入的代码
func (tc *traceContext) TakeSnapShot() interface{} {
...
}
TakeSnapShot 被 Go 编译器在函数入口和出口分别注入了 racefuncenter() 和 racefuncexit(),最终调用 __tsan_func_enter 导致崩溃。由此确定崩溃问题确实是我们的注入代码导致的,继续深入排查。
三、排查过程
3.1 崩溃根源
使用 objdump 查看 __tsan_func_enter 的源码,看到它接收两个函数参数,出错的地方是第一行 mov 0x10(%rdi),%rdx,它约等于 rdx = *(rdi + 0x10)。打印寄存器后发现 rdi = 0,根据调用约定,rdi 存放的是第一个函数参数,因此这里的问题就是函数第一个参数 thr 为 0。
// void __tsan_func_enter(ThreadState *thr, void *pc);
000000000041e1c0 <__tsan_func_enter>:
41e1c0: 48 8b 57 10 mov 0x10(%rdi),%rdx
41e1c4: 48 8d 42 08 lea 0x8(%rdx),%rax
41e1c8: a9 f0 0f 00 00 test $0xff0,%eax
...
那么第一个参数 thr 是谁传进来的呢?接着往上分析调用链。
3.2 调用链分析
出错的整个调用链是 racefuncenter(Go) -> racecall(Go) -> __tsan_func_enter(C)。需要注意的是,前两个函数都是 Go 代码,Go 函数调用 Go 函数遵循 Go 的调用约定。在 amd64 平台,前九个函数参数使用以下寄存器:

另外以下寄存器用于特殊用途:

后两个函数一个 Go 代码一个 C 代码,Go 调用 C 的情况下,遵循 System V AMD64 调用约定,在 Linux 平台上使用以下寄存器作为前六个参数:

理解了 Go 和 C 的调用约定之后,再来看整个调用链的代码:
TEXT racefuncenter<>(SB), NOSPLIT|NOFRAME, $0-0
MOVQ DX, BXx
MOVQ g_racectx(R14), RARG0 // RSI存放thr
MOVQ R11, RARG1 // RDI存放pc
MOVQ $__tsan_func_enter(SB), AX // AX存放__tsan_func_enter函数指针
CALL racecall<>(SB)
MOVQ BX, DX
RET
TEXT racecall<>(SB), NOSPLIT|NOFRAME, $0-0
...
CALL AX // 调用__tsan_func_enter函数指针
...
racefuncenter 将 g_racectx(R14) 和 R11 分别放入 C 调用约定的参数寄存器 RSI(RARG0) 和 RDI(RARG1),并将 __tsan_func_enter 放入 Go 调用约定的参数寄存器 RAX,然后调用 racecall,它进一步调用 __tsan_func_enter(RAX),这一系列操作大致相当于 __tsan_func_enter(g_racectx(R14), R11)。
不难看出,问题的根源在于 g_racectx(R14) 为 0。根据 Go 的调用约定 R14 存放当前 goroutine ,它不可能为 0 ,因此出问题的必然是 R14.racectx 字段为 0。为了避免无效努力,通过调试器 dlv 二次确认:
(dlv) p *(*runtime.g)(R14)
runtime.g {
racectx: 0,
...
}
那么为什么当前 R14.racectx 为 0?下一步看看 R14 具体的状态。
3.3 协调程度
func newproc(fn *funcval) {
gp := getg()
pc := sys.GetCallerPC() #1
systemstack(func() {
newg := newproc1(fn, gp, pc, false, waitReasonZero) #2
...
})
}
经过排查,在代码 #1 处,R14.racectx 是正常的,但到了代码 #2 处,R14.racectx 就为空了,原因是 systemstack 被调用,它有一个切换协程的动作,具体如下:
// func systemstack(fn func())
TEXT runtime·systemstack(SB), NOSPLIT, $0-8
...
// 切换到g0协程
MOVQ DX, g(CX)
MOVQ DX, R14 // 设置 R14 寄存器
MOVQ (g_sched+gobuf_sp)(DX), SP
// 在g0协程上运行目标函数fn
MOVQ DI, DX
MOVQ 0(DI), DI
CALL DI
// 切换回原始协程
...
原来 systemstack 有一个切换协程的动作,会先把当前协程切换成 g0,然后执行 fn,最后恢复原始协程执行。
在 Go 语言的 GMP(Goroutine-Machine-Processor)调度模型中,每个系统级线程 M 都拥有一个特殊的 g0 协程,以及若干用于执行用户任务的普通协程 g。g0 协程主要负责当前 M 上用户 g 的调度工作。由于协程调度是不可抢占的,调度过程中会临时切换到系统栈(system stack)上执行代码。在系统栈上运行的代码是隐式不可抢占的,并且垃圾回收器不会扫描系统栈。
到这里我们已经知道执行 newproc1 时的协程总是 g0,而 g0.racectx 是在 main 执行开始时被主动设置为 0,最终导致程序崩溃:
// src/runtime/proc.go#main
// The main goroutine.
func main() {
mp := getg().m
// g0 的 racectx 仅用于作为主 goroutine 的父级。
// 不应将其用作其他目的。
mp.g0.racectx = 0
...
四、解决方案
到这里基本上可以做一个总结了,程序崩溃的原因如下:
-
newproc1 中插入的 contextPropagate 调用 TakeSnapshot,而 TakeSnapshot 被 go build -race 强行在函数开始插入了 racefuncenter() 函数调用,该函数将使用 racectx。
-
newproc1 是在 g0 协程执行下运行,该协程的 racectx 字段是 0,最终导致崩溃。
一个解决办法是给 TakeSnapshot 加上 Go 编译器的特殊指令 //go:norace,该指令需紧跟在函数声明后面,用于指定该函数的内存访问将被竞态检测器忽略,Go 编译器将不会强行插入 racefuncenter()调用。
五、疑惑一
runtime.newproc1 中不只调用了我们注入的 contextPropagate,还有其他函数调用,为什么这些函数没有被编译器插入 race 检查的代码(如 racefuncenter)?
经过排查后发现,Go 编译器会特殊处理 runtime 包,针对 runtime 包中的代码设置 NoInstrument 标志,从而跳过生成 race 检查的代码:
// /src/cmd/internal/objabi/pkgspecial.go
var pkgSpecialsOnce = sync.OnceValue(func() map[string]PkgSpecial {
...
for _, pkg := range runtimePkgs {
set(pkg, func(ps *PkgSpecial) {
ps.Runtime = true
ps.NoInstrument = true
})
}
...
})
六、疑惑二
理论上插入 //go:norace 之后问题应该得到解决,但实际上程序还是发生了崩溃。经过排查发现,TakeSnapShot 中有 map 初始化和 map 循环操作,这些操作会被编译器展开成 mapinititer() 等函数调用。这些函数直接手动启用了竞态检测器,而且无法加上 //go:norace:
func mapiterinit(t *abi.SwissMapType, m *maps.Map, it *maps.Iter) {
if raceenabled && m != nil {
// 主动的race检查
callerpc := sys.GetCallerPC()
racereadpc(unsafe.Pointer(m), callerpc, abi.FuncPCABIInternal(mapiterinit))
}
...
}
对此问题的解决办法是在 newproc1 注入的代码里面,避免使用 map 数据结构。
七、总结
以上就是 Go 自动插桩工具在使用 go build -race 时出现崩溃的分析全过程。通过对崩溃内容和调用链的排查,我们找到了产生问题的根本原因以及相应的解决方案。这将有助于我们在理解运行时机制的基础上,更加谨慎地编写注入到运行时的代码。
参考链接
[01] Go 自动插桩开源项目
https://github.com/alibaba/opentelemetry-go-auto-instrumentation
[02] 阿里云 ARMS Go Agent 商业版
https://help.aliyun.com/zh/arms/tracing-analysis/monitor-go-applications/
[03] Go 竞态检查
https://go.dev/doc/articles/race_detector
相关文章:

1 行命令引发的 Go 应用崩溃
一、前言 不久前,阿里云 ARMS 团队、编译器团队、MSE 团队携手合作,共同发布并开源了 Go 语言的编译时自动插桩技术。该技术以其零侵入的特性,为 Go 应用提供了与 Java 监控能力相媲美的解决方案。开发者只需将 go build 替换为新编译命令 o…...

ScratchLLMStepByStep:训练自己的Tokenizer
1. 引言 分词器是每个大语言模型必不可少的组件,但每个大语言模型的分词器几乎都不相同。如果要训练自己的分词器,可以使用huggingface的tokenizers框架,tokenizers包含以下主要组件: Tokenizer: 分词器的核心组件,定…...

G1原理—10.如何优化G1中的FGC
大纲 1.G1的FGC可以优化的点 2.一个bug导致的FGC(Kafka发送重试 subList导致List越来越大) 3.为什么G1的FGC比ParNew CMS要更严重 4.FGC的一些参数及优化思路 1.G1的FGC可以优化的点 (1)FGC的基本原理 (2)遇到FGC应该怎么处理 (3)应该如何操作来规避FGC (4)应该如何操…...

Java基础——概念和常识(语言特点、JVM、JDK、JRE、AOT/JIT等介绍)
我是一个计算机专业研0的学生卡蒙Camel🐫🐫🐫(刚保研) 记录每天学习过程(主要学习Java、python、人工智能),总结知识点(内容来自:自我总结网上借鉴࿰…...

2025.1.16——三、supersqli 绕过|堆叠注入|handler查询法|预编译绕过法|修改原查询法
题目来源:攻防世界supersqli 目录 一、打开靶机,整理已知信息 二、sqlmap解题 step 1:爆数据库 step 2:爆表 二、手工注入解题 step 1:判断注入类型 step 2:判断字段数 step 3:查询数据…...

浅谈计算机网络03 | 现代网络组成
现代网络组成 一 、网络生态体系1.1网络生态系统的多元主体1.2 网络接入设施的多样类型 二、现代网络的典型体系结构解析三、高速网络技术3.1 以太网技术3.2 Wi-Fi技术的深度剖析3.2.1 应用场景的多元覆盖3.2.2 标准升级与性能提升 3.3 4G/5G蜂窝网的技术演进3.3.1 蜂窝技术的代…...

Red Hat8:搭建FTP服务器
目录 一、匿名FTP访问 1、新建挂载文件 2、挂载 3、关闭防火墙 4、搭建yum源 5、安装VSFTPD 6、 打开配置文件 7、设置配置文件如下几个参数 8、重启vsftpd服务 9、进入图形化界面配置网络 10、查看IP地址 11、安装ftp服务 12、遇到拒绝连接 13、测试 二、本地…...

EWM 批次管理 / Batch Management
目录 1 简介 2 业务数据 2.1 基于 PO,创建 ERP LE - Delivery 内向交货单,同时同步到 EWM 内向交货单 2.2 在 EWM 内向交货单,创建批次。EWM 批次创建的前提条件来自于物料主数据批次分类(023)。SAP 提供的标准条件…...
Java 面试题 - ArrayList 和 LinkedList 的区别,哪个集合是线程安全的?
Java 面试题 - ArrayList 和 LinkedList 的区别,哪个集合是线程安全的? 在 Java 开发中,ArrayList和LinkedList是两个常用的集合类,它们在数据结构和性能上有诸多不同,同时线程安全性也各有特点。深入理解这些差异&am…...

初学SpringBoot
目录 什么是SpringBoot 使用 Spring Boot有什么好处 Spring Boot 特点 在线构建 IntelliJ IDEA在线模板构建 IntelliJ IDEA 通maven项目构建 SpringBoot的常用配置 入口类和相关注解 定制Banner 修改banner图标 关闭banner 常规属性修改 tomcat端口号修改 常规属性…...
【网络云SRE运维开发】2025第3周-每日【2025/01/15】小测-【第14章ospf高级配置】理论和实操解析
文章目录 14.1 选择题解题思路和参考答案14.2 理论题解题思路和参考答案14.3 实操题解题思路和参考答案思科(Cisco)设备华为(Huawei)设备小米/锐捷(或其他支持标准CLI命令的设备)通过网络管理工具注意事项 …...
AWS S3 跨账户访问 Cross Account Access
进入S3对应的存储桶,上面选项选权限,存储桶策略 -- 编辑,输入对应的policy。 完全控制,包含上传删除权限,policy如下: {"Version": "2012-10-17","Statement": [{"Si…...
Ubuntu20.4和docker终端指令、安装Go环境、安装搜狗输入法、安装WPS2019:保姆级图文详解
目录 前言1、docker、node、curl版本查看终端命令1.1、查看docker版本1.2、查看node.js版本1.3、查看curl版本1.4、Ubuntu安装curl1.5、Ubuntu终端保存命令 2、安装docker-compose、Go语言2.1、安装docker-compose2.2、go语言安装步骤2.3、git版本查看 3、Ubuntu20.4安装搜狗输…...
Kotlin语言的正则表达式
Kotlin语言中的正则表达式 引言 正则表达式(Regular Expression,简称Regex)是一种用于匹配字符串中字符组合的工具。在数据处理、文本解析等领域,正则表达式以其强大的字符串处理能力得到了广泛的应用。而Kotlin作为一种现代的编…...

npm的包管理
从哪里下载包 国外有一家 IT 公司,叫做 npm,Inc.这家公司旗下有一个非常著名的网站: https://www.npmjs.com/,它是全球最大的包共享平台,你可以从这个网站上搜索到任何你需要的包,只要你有足够的耐心!到目前位置,全球约…...
深度学习在文本情感分析中的应用
引言 情感分析是自然语言处理(NLP)中的一个重要任务,旨在识别和提取文本中的主观信息。随着深度学习技术的发展,我们可以使用深度学习模型来提高情感分析的准确性和效率。本文将介绍如何使用深度学习进行文本情感分析,…...

【大模型系列篇】数字人音唇同步模型——腾讯开源MuseTalk
之前有一期我们体验了阿里开源的半身数字人项目EchoMimicV2,感兴趣的小伙伴可跳转至《AI半身数字人开箱体验——开源项目EchoMimicV2》,今天带大家来体验腾讯开源的数字人音唇同步模型MuseTalk。 MuseTalk 是一个实时高品质音频驱动的唇形同步模型&#…...

Formality:参考设计/实现设计以及顶层设计
相关阅读 Formalityhttps://blog.csdn.net/weixin_45791458/category_12841971.html?spm1001.2014.3001.5482 Formality存在两个重要的概念:参考设计/实现设计和顶层设计,本文就将对此进行详细阐述。参考设计/实现设计是中两个重要的全局概念&am…...

RPA赋能内容创作:打造小红书入门词语图片的全自动化流程
🌟 嗨,我是LucianaiB! 🌍 总有人间一两风,填我十万八千梦。 🚀 路漫漫其修远兮,吾将上下而求索。 用RPA全自动化批量生产【入门词语】图片做小红书商单,保姆级工具开发教程 最近由…...
RPC 源码解析~Apache Dubbo
解析 RPC(远程过程调用)的源码可以帮助你深入理解其工作原理和实现细节。为了更好地进行源码解析,我们选择一个流行的 RPC 框架——Apache Dubbo 作为示例。Dubbo 是一个高性能、轻量级的开源 Java RPC 框架,广泛应用于企业级应用…...

突破不可导策略的训练难题:零阶优化与强化学习的深度嵌合
强化学习(Reinforcement Learning, RL)是工业领域智能控制的重要方法。它的基本原理是将最优控制问题建模为马尔可夫决策过程,然后使用强化学习的Actor-Critic机制(中文译作“知行互动”机制),逐步迭代求解…...
FFmpeg 低延迟同屏方案
引言 在实时互动需求激增的当下,无论是在线教育中的师生同屏演示、远程办公的屏幕共享协作,还是游戏直播的画面实时传输,低延迟同屏已成为保障用户体验的核心指标。FFmpeg 作为一款功能强大的多媒体框架,凭借其灵活的编解码、数据…...

selenium学习实战【Python爬虫】
selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...
Element Plus 表单(el-form)中关于正整数输入的校验规则
目录 1 单个正整数输入1.1 模板1.2 校验规则 2 两个正整数输入(联动)2.1 模板2.2 校验规则2.3 CSS 1 单个正整数输入 1.1 模板 <el-formref"formRef":model"formData":rules"formRules"label-width"150px"…...

html-<abbr> 缩写或首字母缩略词
定义与作用 <abbr> 标签用于表示缩写或首字母缩略词,它可以帮助用户更好地理解缩写的含义,尤其是对于那些不熟悉该缩写的用户。 title 属性的内容提供了缩写的详细说明。当用户将鼠标悬停在缩写上时,会显示一个提示框。 示例&#x…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

macOS 终端智能代理检测
🧠 终端智能代理检测:自动判断是否需要设置代理访问 GitHub 在开发中,使用 GitHub 是非常常见的需求。但有时候我们会发现某些命令失败、插件无法更新,例如: fatal: unable to access https://github.com/ohmyzsh/oh…...
DAY 26 函数专题1
函数定义与参数知识点回顾:1. 函数的定义2. 变量作用域:局部变量和全局变量3. 函数的参数类型:位置参数、默认参数、不定参数4. 传递参数的手段:关键词参数5 题目1:计算圆的面积 任务: 编写一…...

java高级——高阶函数、如何定义一个函数式接口类似stream流的filter
java高级——高阶函数、stream流 前情提要文章介绍一、函数伊始1.1 合格的函数1.2 有形的函数2. 函数对象2.1 函数对象——行为参数化2.2 函数对象——延迟执行 二、 函数编程语法1. 函数对象表现形式1.1 Lambda表达式1.2 方法引用(Math::max) 2 函数接口…...
node.js的初步学习
那什么是node.js呢? 和JavaScript又是什么关系呢? node.js 提供了 JavaScript的运行环境。当JavaScript作为后端开发语言来说, 需要在node.js的环境上进行当JavaScript作为前端开发语言来说,需要在浏览器的环境上进行 Node.js 可…...