1. 前言
货拉拉移动端拥有一套完善的稳定性监控系统,该系统在帮助我们打造行业一流的产品、问题告警和问题归因等方面起到了非常好的作用。今天,我们将该系统中symbolic demange方向的实践分享出来,同时也将demangle工具直接开放到Github,希望能够帮助到同样有诉求的同学们。
2. 什么是symbolic demangle
在编程领域,demangle主要指的是将被编译器"mangle"的名称还原为原始的、可读性强的符号名称。需要理解demangle,首先需要知道什么是mangle。
什么是mangle?
在一些编程语言(特别是C++)中,编译器在生成目标代码时,会对函数、变量等符号的名称进行编码,这个过程称为"名称修饰"(name mangling)。这是因为C++支持函数重载和命名空间等高级语言特性,当这些特性引入到汇编级或机器级的目标代码时,需要一种机制来唯一地标识每一个符号。名称修饰会依照某种规则将原始名称转换为一个唯一的、包含类型信息的字符串。
例如,一个C++函数void foo(int)可能会被编译器修改成某种形式如 _Z3fooi(具体形式取决于编译器)。
那么,相反,demangle是将被名称修饰过的符号还原为原始符号的过程。这个通常在调试、堆栈追踪或诊断程序崩溃的时候非常有用,因为编译器生成的修饰名称往往难以理解。
在调试、堆栈追踪、诊断工具等场景下,demangle都显得尤为重要,能够帮助开发者更迅速地定位问题;
总结来说,demangle是一个在调试和诊断过程中非常重要的操作,它将编译器生成的复杂符号名称转换为更易懂的原始符号,帮助开发者更快地理解和定位问题。C++、Rust、Swift等语言和相应的工具链都提供了支持这个操作的工具。
下图是项目中未demangle的堆栈:
从图中能够看出,堆栈可读性较差,不利于快速锁定重要信息。
3. 为什么选择symbolic方案
在货拉拉移动端技术栈中,需要demangle的主要是C++和Swift语言。前者有c++filt,后者有Xcode工具链自带的swift-demangle,但这种独立的工具需要我们完成对语言的识别、环境的独立搭建以及效率的权衡等工作。字节的一篇文章《iOS符号解析重构之路》中曾提到:
为了解决部分C++与Rust符号demangle失效以及各种语言demangle工具不一致的问题。将原本llvm自带的demangle工具替换成了一个Rust实现,支持全语言的demangle工具symbolic-demangle(链接见参考资料),极大的降低了运维成本。
我们从这里得到了思路,在调研开源工具symbolic-demangle后,确认它是当下成本最低、效果最好的方案。很快,我们将它应用到了多个业务。
方案对比 | 支持多语言 | 支持linux | 维护成本 |
---|---|---|---|
swift-demangle | X | X | 高 |
c++ filt | X | ✓ | 高 |
llvm demangle | ✓(不支持swift) | ✓ | 高 |
symbolic-demangle | ✓ | ✓ | 一般 |
4. symbolic在货拉拉的实践落地
symbolic目前最高支持到Swift 5.5.1,更高的版本已经快3年没有迭代支持。然后Swift是创新业务和国际化业务的主流编程语言,于是支持更高版本的Swift demangle将是一件绕不开的事情。我们将重点阐述如何适配高版本Swift以及在Linux服务器上构建python库过程中遇到的问题。python库以开源的方式提供给大家,详见hll-symbolic-demangle
4.1 如何适配高版本swift
如何基于symbolic适配更高版本的Swift demangle呢?实践中主要分两大步:官方Swift demangle源码同步升级和symbolic项目编译;
首先,我们来介绍下官方Swift demangle源码同步升级.
4.1.1 Check out the latest release of Swift:
go
1. Create a directory that will house swift and its dependencies:
```
$ mkdir swift-source && cd swift-source
```
2. Clone the swift repository into a subdirectory:
```
$ git clone git@github.com:swiftlang/swift.git
```
3. Check out dependencies:
```
$ ./swift/utils/update-checkout --clone
```
如果执行过程中遇到部分项目clone失败的情况,比如icu 和llvm-project 总是clone失败,那么可以尝试下自行git clone到目标位置,而不是走上面的命令,完成后继续重试上面的命令。
4. Check out the release branch of the latest release:
```
$ cd swift
$ git checkout swift-5.10-RELEASE
```
5. Build the complete Swift project:
```
$ ./utils/build-script --skip-build
```
这一步执行一般需要一段时间,be patient~
4.1.2 Copy updated sources and headers from the checkout to this library:
go
直接运行当前目前下的update脚本:
```
$ ./update.py swift-source
```
经过上述流程后,就基本完成了源码更新的工作。
4.1.3 symbolic项目编译
完成源码更新的工作后,接下来,是symbolic项目编译的步骤。
symbolic是一个Rust项目,使用C++开发。因此我们可以执行cargo build来执行编译。编译过程中会遇到很多报错。以下是我们遇到的一些问题,希望能够帮助到大家。
【问题1】当出现llvm文件缺少时,去官网找到llvm-project中的相关文件拷贝进去。
目标路径:/Users/XXX/Workspace/learn/swift-source/llvm-project/llvm/include/llvm/ADT
【问题2】vendor/swift/include/llvm/ADT/Optional.h找不到template name
去官网swift/stdlib/include/llvm/Support/type_traits.h找到完整代码更新(依赖更新的脚本有点问题)
【问题3】诸如std之类的编译错误
使用C++17编译吧
【问题4】CPP源码修改后build仍然报错
最好删除项目根目录target中的构建文件,比如debug, release。
其他问题就不一一记录了。
4.1.4 测试
编译通过后,可通过本地验证的方式测试结果。
arduino
python3 setup.py install // 成功后pip list可见
安装前请使用pip list查看是否有其他版本的symbolic,如有,请先卸载
pip uninstall symbolic
install完成直接在/py/symbolic目录下执行如下:
java
python3
from symbolic.demangle import demangle_name
demangle_name('$s10Speediness17NetworkQualityCLIO3run10sequentialAC6ResultVSb_tYaKFZTf4nd_nTQ0_')
除了上面的测试方法,也可以在测试用例中追加一些高版本的用例,然后执行test-swift
进入到symbolic-demangle目录下执行脚本
bash
cargo test test_swift
4. 2 如何解决Linux上python库的构建
在Linux服务器上安装python分发包,理论上就能够通过Java跨进程调用Python的方式实现demangle。
Egg 和 Wheel 本质上都是一个 zip 格式包,Egg 文件使用 .egg 扩展名,Wheel 使用 .whl 扩展名。
Wheel 的出现是为了替代 Egg,其现在被认为是 Python 的二进制包的标准格式。
在终端执行以下命令,可生成 whl 分发包。
arduino
python3 setup.py bdist_wheel
命令执行执行完成后,在py/dist文件夹中,可以看到 symbolic-{版本号}*.whl的文件。
使用pip3 install 命令安装 wheel 文件
bash
pip3 install dist/symbolic-10.2.1-py2.py3-none-macosx_14_0_universal2.whl
安装成功,自此之后只需要将whl文件在服务器中进行安装即可。
但在部署服务器的过程中,遇到如下的报错。看来Mac上生成的whl分发包不适用于Linux,需要在Linux上打包whl分发包。
相比MacOS,Linux机器上,构建whl分发包变得困难起来。这也是为什么会单独抽出一节来说明如何解决Linux上构建python库的原因。
复杂之处主要体现在编译问题、库版本不匹配、环境搭建等环节。不同的环境遇到的问题或者错误也不尽相同,在此,我们仅记录下在实践过程中的一些问题;
4.2.1 环境搭建
Rust环境搭建
物理机操作系统:MacOS 14.5
虚拟机软件:VituralBox
虚拟机操作系统:Debian 12.6.0
虚拟机Python版本:3.9
4.2.2 编译失败
C++编译失败
为什么使用C++在Linux上编译时,会出现错误?
考虑到相同的代码,在不同的环境编译时出现的问题,首先考虑到应该是使用的编译器不同,但如何确认使用的是哪种编译器?
我们从执行打包的Python命令入手,找到并打开 py/setup.py
在setup()方法,出现了一个配置项:milksnake_tasks
Milksnake is an extension for setuptools that allows you to distribute dynamic linked libraries in Python wheels in the most portable way imaginable.
It gives you a hook to invoke your own build process and to then take the resulting dynamic linked library.
CFFI(C Foreign Function Interface)是 Python 中的一个库,它提供了一种在 Python 代码中调用 C 代码的方法。通过 cffi,你可以编写 Python 代码来直接使用 C 函数、调用共享库(.so 或 .dll 文件),或者甚至嵌入 C 代码。这使得在 Python 中执行低级系统调用或利用现有的 C 代码库变得更加容易和高效。
首先在MacOS中,执行打包命令。
由编译日志中,可以看来,这里使用的是GCC编译器。
再次,在Linux中,执行打包命令。
由编译日志中,可以看来,这里使用的是c++命令进行编译的,这个在之前声明的主流编译器中并没有出现。
那么gcc/g++/c++是什么关系呢?
版本号竟然一样,通过diff命令,确认两个命令完全一样。
通过名字猜想,GCC是编译c语言的,g++是编译C++语言的。
其实并不是,也可以通过gcc -xc++ -lstdc++ -shared-libgcc也编译c++源代码。
GCC和g++其实只是个外壳,在编译的时候,最终决定是调用后台的cc1还是cc1plus。
编译的语言是可以通过编译选项-x来指定。
那么gcc和g++主要的区别是:编译时自动连接的库的不同。
g++会自动连接std c++的库,而gcc不会。所以,使用gcc编译c++的代码,如果c++的代码中使用了std的类,例如:vector,会出现连接出错。
g++还有一个不同就是编译cpp文件时,会有一些预定义的宏:
arduino
#define GXX_WEAK 1
#define __cplusplus 1
#define __DEPRECATED 1
#define GNUG 4
#define __EXCEPTIONS 1
#define private_extern extern
这就定位到了,在MacOS与Linux中,分别使用了 GCC 和 C++(也就是G++)进行编译。
接着再看编译日志,发现一个标识:-fpermissive,初步判断是一个编译命令控制参数。
最终在其官网上,找到如下线索。
允许使用旧的语法进行编译,将错误降级为警告。因此,我们加入了新的编译参数如下:
css
fn main() {
#[cfg(feature = "swift")]
{
cc::Build::new()
.cpp(true)
.files(&[
"src/swiftdemangle.cpp",
"vendor/swift/lib/Demangling/Demangler.cpp",
"vendor/swift/lib/Demangling/Context.cpp",
"vendor/swift/lib/Demangling/ManglingUtils.cpp",
"vendor/swift/lib/Demangling/NodeDumper.cpp",
"vendor/swift/lib/Demangling/NodePrinter.cpp",
"vendor/swift/lib/Demangling/OldDemangler.cpp",
// "vendor/swift/lib/Demangling/OldRemangler.cpp",
"vendor/swift/lib/Demangling/Punycode.cpp",
"vendor/swift/lib/Demangling/Remangler.cpp",
])
.flag_if_supported("-std=c++17")
.flag("-DLLVM_DISABLE_ABI_BREAKING_CHECKS_ENFORCING=1")
.flag( "-fpermissive" )
.warnings(false)
.include("vendor/swift/include")
.compile("swiftdemangle");
}
}
重新执行构建打包命令,编译成功。
GLIBC版本未找到
虽然编译打包并部署成功后,再验证符号demangle的时候又出错了,提示GLIBC_2.29版本未找到。
GLIBC,全称GNU C Library,是Linux系统中非常重要的一个组成部分。它为开发者提供了丰富的API接口,使得开发者能够更方便地进行系统编程。随着Linux系统的不断发展,glibc也在不断升级,以满足新的需求和解决潜在的问题。
当前虚拟机操作系统是 Debian 12.6.0,GLIBC版本为 2.36。
方案一:是否可以指定使用低版本的GLIBC进行编译呢?
从搜索结果中,实现成本较复杂,不作展开;
方案二:选择一个低版本的Linux OS系统,从搜索结果中来看,CentOS 7中默认的GLIBC版本为 2.17,这正是所需要的最低版本。
于是安装新的虚拟机。
虚拟机软件:VituralBox
虚拟机操作系统:CentOS 7
配置网络,安装基础开发环境。
由于CentOS 7.0官方已不再维护,已不能从官方repo下载开发环境,又是一翻配置,替换为国内repo。
GCC版本不匹配
在新的虚拟机中,进行依赖安装和编译,出现了错误,提示GCC版本不匹配。
CentOS 7中,最高支持的GCC版本是4.8.5;不支持 C++17,因此需要升级GCC;
升级如下:
sql
yum -y install centos-release-scl
yum clean all
yum makecache
由于CentOS 7官方已不再维护,无法拉取到repo,此处需要手动替换下repo中的baseurl域名
4.2.4 安装库
在升级GCC后,编译再次出错,提示缺少libffi、python相关报错
什么是FFI?
FFI(Foreign Function Interface)允许以一种语言编写的代码调用另一种语言的代码,而libffi库提供了最底层的、与架构相关的、完整的FFI。libffi的作用就相当于编译器,它为多种调用规则提供了一系列高级语言编程接口,然后通过相应接口完成函数调用,底层会根据对应的规则,完成数据准备,生成相应的汇编指令代码。
安装libffi
yum install -y libffi-devel libffi
python-devel
yum install -y python3-devel
安装python
pip3 install wheel
在经历了环境搭建、解决编译失败、各种依赖版本冲突和依赖缺失后,终于在Linux中生成了whl分发包。
5. 最终效果
5.1 线上效果
5.2 线下对比Xcode工具
线下将升级后的symbolic和Xcode demangle工具做了对比,几乎一样;
升级后的symbolic demangle本地运行结果如下:
Xcode swift-demangle工具解析如下:
6. 货拉拉移动技术团队
货拉拉移动技术团队是一支由年轻人组成的有活力的团队,我们热爱技术,乐于分享和贡献。如果读者的你也有同样的热爱,欢迎加入我们~