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),%rdx41e1c4: 48 8d 42 08 lea 0x8(%rdx),%rax41e1c8: 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-0MOVQ DX, BXxMOVQ g_racectx(R14), RARG0 // RSI存放thrMOVQ R11, RARG1 // RDI存放pcMOVQ $__tsan_func_enter(SB), AX // AX存放__tsan_func_enter函数指针CALL racecall<>(SB)MOVQ BX, DXRETTEXT 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() #1systemstack(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协程上运行目标函数fnMOVQ DI, DXMOVQ 0(DI), DICALL 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.govar pkgSpecialsOnce = sync.OnceValue(func() map[string]PkgSpecial {...for _, pkg := range runtimePkgs {set(pkg, func(ps *PkgSpecial) {ps.Runtime = trueps.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 框架,广泛应用于企业级应用…...
Vim 调用外部命令学习笔记
Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...
label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...
Mybatis逆向工程,动态创建实体类、条件扩展类、Mapper接口、Mapper.xml映射文件
今天呢,博主的学习进度也是步入了Java Mybatis 框架,目前正在逐步杨帆旗航。 那么接下来就给大家出一期有关 Mybatis 逆向工程的教学,希望能对大家有所帮助,也特别欢迎大家指点不足之处,小生很乐意接受正确的建议&…...
条件运算符
C中的三目运算符(也称条件运算符,英文:ternary operator)是一种简洁的条件选择语句,语法如下: 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true,则整个表达式的结果为“表达式1”…...
在四层代理中还原真实客户端ngx_stream_realip_module
一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡(如 HAProxy、AWS NLB、阿里 SLB)发起上游连接时,将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后,ngx_stream_realip_module 从中提取原始信息…...
接口自动化测试:HttpRunner基础
相关文档 HttpRunner V3.x中文文档 HttpRunner 用户指南 使用HttpRunner 3.x实现接口自动化测试 HttpRunner介绍 HttpRunner 是一个开源的 API 测试工具,支持 HTTP(S)/HTTP2/WebSocket/RPC 等网络协议,涵盖接口测试、性能测试、数字体验监测等测试类型…...
解析奥地利 XARION激光超声检测系统:无膜光学麦克风 + 无耦合剂的技术协同优势及多元应用
在工业制造领域,无损检测(NDT)的精度与效率直接影响产品质量与生产安全。奥地利 XARION开发的激光超声精密检测系统,以非接触式光学麦克风技术为核心,打破传统检测瓶颈,为半导体、航空航天、汽车制造等行业提供了高灵敏…...
MyBatis中关于缓存的理解
MyBatis缓存 MyBatis系统当中默认定义两级缓存:一级缓存、二级缓存 默认情况下,只有一级缓存开启(sqlSession级别的缓存)二级缓存需要手动开启配置,需要局域namespace级别的缓存 一级缓存(本地缓存&#…...
鸿蒙(HarmonyOS5)实现跳一跳小游戏
下面我将介绍如何使用鸿蒙的ArkUI框架,实现一个简单的跳一跳小游戏。 1. 项目结构 src/main/ets/ ├── MainAbility │ ├── pages │ │ ├── Index.ets // 主页面 │ │ └── GamePage.ets // 游戏页面 │ └── model │ …...
五子棋测试用例
一.项目背景 1.1 项目简介 传统棋类文化的推广 五子棋是一种古老的棋类游戏,有着深厚的文化底蕴。通过将五子棋制作成网页游戏,可以让更多的人了解和接触到这一传统棋类文化。无论是国内还是国外的玩家,都可以通过网页五子棋感受到东方棋类…...
