@@ -58,7 +58,7 @@ jump_stack是bthread挂起或者运行的必经之路,也是STB的拦截点。
5858
5959# 使用方法
6060
61- 1 . 下载安装libunwind和abseil-cpp。
61+ 1 . 下载安装libunwind和abseil-cpp。** 注意:libunwind 必须从源码编译,不要使用系统包管理器安装的 ` libunwind-dev ` / ` libunwind-devel ` ** ,否则会触发本文末尾「 [ 已知问题:libunwind 与 libgcc_s 的 ` _Unwind_* ` 符号冲突 ] ( #已知问题libunwind-与-libgcc_s-的-_unwind_-符号冲突 ) 」中描述的崩溃。bazel 构建可以跳过此步,直接使用 brpc 仓库自维护的 libunwind 版本。
62622 . 给config_brpc.sh增加` --with-bthread-tracer ` 选项或者给cmake增加` -DWITH_BTHREAD_TRACER=ON ` 选项或者给bazel(Bzlmod模式)增加` --define with_bthread_tracer=true ` 选项。
63633 . 访问服务的内置服务:` http://ip:port/bthreads/<bthread_id>?st=1 ` 或者代码里调用` bthread::stack_trace() ` 函数。
64644 . 如果希望追踪pthread的调用栈,在对应pthread上调用` bthread::init_for_pthread_stack_trace() ` 函数获取一个伪bthread_t,然后使用步骤3即可获取pthread调用栈。
@@ -73,6 +73,148 @@ jump_stack是bthread挂起或者运行的必经之路,也是STB的拦截点。
7373# 5 0x00007fdbbfa58dc0 bthread::TaskGroup::task_runner()
7474```
7575
76+ # 已知问题
77+
78+ ## libunwind 与 libgcc_s 的 ` _Unwind_* ` 符号冲突
79+
80+ ### 现象
81+
82+ 启用 bthread tracer 后,可能在 ` bthread_exit ` / ` pthread_exit ` 或者 C++ 异常处理路径上偶发段错误,类似如下调用栈:
83+
84+ ``` text
85+ #0 0x0000000000000000 in ?? ()
86+ #1 _ULx86_64_dwarf_find_proc_info () in libunwind.so
87+ #2 fetch_proc_info () in libunwind.so
88+ #3 _ULx86_64_dwarf_make_proc_info () in libunwind.so
89+ #4 _ULx86_64_get_proc_info () in libunwind.so
90+ #5 __libunwind_Unwind_GetLanguageSpecificData () in libunwind.so
91+ #6 __gxx_personality_v0 () in libstdc++.so.6
92+ #7 ?? () in libgcc_s.so.1
93+ #8 _Unwind_ForcedUnwind () in libgcc_s.so.1
94+ #9 __GI___pthread_unwind () in libc / libpthread
95+ #10 __do_cancel () in libc / libpthread
96+ #11 __GI___pthread_exit () in libc / libpthread
97+ ```
98+
99+ ### 根因
100+
101+ libunwind 的 ` src/unwind/*.c ` 实现了 GCC 的 ` _Unwind_* ` ABI 兼容层(` _Unwind_GetLanguageSpecificData ` 、` _Unwind_ForcedUnwind ` 、` _Unwind_Resume ` 等),与 ` libgcc_s.so.1 ` 提供同名的全局符号。当 libunwind 以** 共享库** 形式被链接,并且在最终二进制的 ` DT_NEEDED ` 列表中位置比 ` libgcc_s.so.1 ` 靠前时,运行时动态链接器 ` ld.so ` 会把 ` pthread_exit ` / 异常处理触发的 ` _Unwind_* ` 调用解析到 libunwind 中的 DWARF 实现。该实现需要的内部上下文在 ` pthread_exit ` 路径上未被 brpc 初始化好,从而触发空指针访问。
102+
103+ 这是一个 ELF ** 运行时符号解析顺序** 问题,与编译器(GCC / Clang)无关 —— Clang 默认运行时同样使用 ` libstdc++ + libgcc_s ` ,会复现完全一致的崩溃。
104+
105+ ### 解决方案
106+
107+ > ** 重要:不要使用系统包管理器安装的 libunwind** (例如 ` apt install libunwind-dev ` 、` yum install libunwind-devel ` )。多数发行版打包的 ` libunwind.so ` 仍把 ` _Unwind_* ` 暴露在动态符号表中,会触发本节描述的崩溃。
108+ >
109+ > 必须使用** 从源码编译的 libunwind** 。上游 ` ./configure ` + ` make ` 默认会通过 ` -Wl,--version-script ` 把 ` _Unwind_* ` 标为 local,不导出到动态符号表,从而避免冲突。
110+
111+ 下表汇总了三种构建方式的推荐方案:
112+
113+ | 构建方式 | 推荐方案 |
114+ | ---| ---|
115+ | ` config_brpc.sh ` + ` make ` | 从源码编译并安装 libunwind,把头文件与库目录显式传给 ` config_brpc.sh ` |
116+ | ` cmake ` | 从源码编译并安装 libunwind,把头文件与库目录显式传给 ` cmake ` |
117+ | ` bazel ` (Bzlmod) | 直接使用 brpc 仓库自维护的 libunwind 版本(已默认配置好,零额外操作) |
118+
119+ ### ` make ` (` config_brpc.sh ` )
120+
121+ 源码编译并安装 libunwind 到一个独立目录(避免污染系统目录),然后让 ` config_brpc.sh ` 显式从该目录查找 libunwind。下面的步骤与 brpc CI [ ` .github/actions/init-ut-make-config/action.yml ` ] ( ../../.github/actions/init-ut-make-config/action.yml:16-19 ) 完全一致:
122+
123+ ``` bash
124+ # 1) 源码编译 libunwind(推荐 v1.8.1 或以上版本)
125+ git clone https://github.com/libunwind/libunwind.git
126+ cd libunwind && git checkout tags/v1.8.1
127+ mkdir -p /libunwind
128+ autoreconf -i
129+ ./configure --prefix=/libunwind
130+ make -j$( nproc) && make install
131+ cd ..
132+
133+ # 2) 让 config_brpc.sh 使用 /libunwind 下的头文件与库(不要让它自动找到系统的 libunwind-dev)
134+ cd brpc
135+ sh config_brpc.sh \
136+ --with-bthread-tracer \
137+ --headers=" /libunwind/include /usr/include" \
138+ --libs=" /libunwind/lib /usr/lib /usr/lib64"
139+ make -j$( nproc)
140+ ```
141+
142+ 构建完成后可用以下命令确认 libunwind.so 没有导出 ` _Unwind_* ` :
143+
144+ ``` bash
145+ nm -D /libunwind/lib/libunwind.so | grep ' T _Unwind_' \
146+ && echo " WARN: _Unwind_* exported" \
147+ || echo " OK: _Unwind_* hidden"
148+ ```
149+
150+ ### ` cmake `
151+
152+ [ ` CMakeLists.txt ` ] ( ../../CMakeLists.txt:90-100 ) 通过 ` find_library(... NAMES unwind unwind-x86_64) ` 查找 libunwind。同样需要先源码编译 libunwind 到独立前缀,再用 ` CMAKE_PREFIX_PATH ` 让 cmake 优先在该前缀下查找:
153+
154+ ``` bash
155+ # 1) 源码编译 libunwind(同 make 章节)
156+ git clone https://github.com/libunwind/libunwind.git
157+ cd libunwind && git checkout tags/v1.8.1
158+ mkdir -p /libunwind
159+ autoreconf -i
160+ ./configure --prefix=/libunwind
161+ make -j$( nproc) && make install
162+ cd ..
163+
164+ # 2) 让 cmake 在 /libunwind 下优先查找头文件和库
165+ cd brpc
166+ mkdir build && cd build
167+ cmake -DWITH_BTHREAD_TRACER=ON \
168+ -DCMAKE_PREFIX_PATH=/libunwind \
169+ ..
170+ make -j$( nproc)
171+ ```
172+
173+ > 提示:如果系统已经装了 ` libunwind-dev ` ,` find_library ` 仍可能优先匹配到 ` /usr/lib ` 。可在 cmake 命令上额外指定
174+ > ` -DLIBUNWIND_LIB=/libunwind/lib/libunwind.so -DLIBUNWIND_X86_64_LIB=/libunwind/lib/libunwind-x86_64.so -DLIBUNWIND_INCLUDE_PATH=/libunwind/include `
175+ > 强制走自编译版本,避免系统包混入。
176+
177+ ### ` bazel ` (Bzlmod)
178+
179+ Bazel 默认 fastbuild 模式会为每个 ` cc_library ` 生成中间 ` .so ` (例如 ` libexternal_Slibunwind~_Slibunwind.so ` ),把 libunwind 强制变成一个独立共享库,其 ` _Unwind_* ` 符号会被导出抢占 ` libgcc_s.so.1 ` —— ** 问题极易复现** 。
180+
181+ brpc 仓库已经在 [ ` registry/modules/libunwind/ ` ] ( ../../registry/modules/libunwind/ ) 自维护了一份 libunwind 的 Bzlmod overlay,并通过 [ ` .bazelrc ` ] ( ../../.bazelrc ) 中的 ` --registry=file://%workspace%/registry ` 让它覆盖默认 BCR 中的 libunwind。版本号采用 ` <base>.brpc-no-unwind ` 后缀(例如 ` 1.8.3.brpc-no-unwind ` ),用以区别于 BCR 上的同基版本,并直观体现"由 brpc 维护,不导出 ` _Unwind_* ` 符号"。该 overlay 增加了一个开关:
182+
183+ ```
184+ --define=libunwind_hide_unwind_symbols=true
185+ ```
186+
187+ 开启后,libunwind 的 ` src/unwind/*.c ` (即 GCC ` _Unwind_* ` 兼容层)整体不参与编译,等效于上游 autoconf ` --version-script ` 的隐藏效果。brpc 只使用 libunwind 的 ` unw_* ` 原生 API(` unw_getcontext ` 、` unw_init_local ` 、` unw_step ` 等),不依赖 ` _Unwind_* ` 兼容层,因此该开关安全无副作用。
188+
189+ ` .bazelrc ` 已默认在 ` build:test ` / ` test ` 配置下打开此开关:
190+
191+ ```
192+ build:test --define libunwind_hide_unwind_symbols=true
193+ test --define libunwind_hide_unwind_symbols=true
194+ ```
195+
196+ ** 因此 bazel 用户无需任何额外操作** ,按文档使用方法第 2 步加上 ` --define=with_bthread_tracer=true ` 即可:
197+
198+ ``` bash
199+ # 测试场景,.bazelrc 中 test 配置已自动带上 hide 开关
200+ bazel test //test:bthread_unittest
201+
202+ # 非测试构建(生产部署),需要显式同时带上两个 define
203+ bazel build --define=with_bthread_tracer=true \
204+ --define=libunwind_hide_unwind_symbols=true \
205+ //...
206+ ```
207+
208+ > ** 特别注意** :如果在生产构建中只启用 ` --define=with_bthread_tracer=true ` 而漏掉 ` --define=libunwind_hide_unwind_symbols=true ` ,binary 在 ` pthread_exit ` / 异常路径上会概率性崩溃。
209+
210+ 构建后可用以下命令验证 libunwind 共享库没有导出 ` _Unwind_* ` :
211+
212+ ``` bash
213+ nm -D bazel-bin/external/_solib_* /libexternal* libunwind* .so 2> /dev/null \
214+ | grep ' T _Unwind_' || echo " OK: no _Unwind_* exported by libunwind.so"
215+ ```
216+
217+
76218# 相关flag
77219
78220- ` signal_trace_timeout_ms ` :信号追踪模式的超时时间,默认为50ms。
0 commit comments