IL2CPP堆栈C#行号恢复记录
自从Unity6开始官方就支持IL2CPP堆栈打印出具体行号 : https://docs.unity3d.com/6000.1/Documentation/Manual/il2cpp-managed-stack-traces.html
但是Unity6(Unity2023)之前直接查无此人,默认情况只能继续忍受 00000000000000000 的文件和行号,查起问题来可以说痛苦不堪
通过Claude Code + IDA Pro MCP + IDA Pro的组合,反编译Unity6 il2cpp.exe之后,发现事情并没有我想象的那么复杂,很多事情Unity都已经做好了,只差临门一脚而已
符号表
在开始之前我们先了解一下符号表是个什么东西
一、符号表(Symbol Table)
符号表是编译器/链接器生成的数据结构,存储标识符(变量、函数、类等)的元信息,用于编译、链接和调试。
-
核心作用:
- 编译阶段:语义检查(类型匹配、作用域验证)。
- 链接阶段:解析跨模块的符号引用(地址重定位)。
- 调试阶段:映射机器码到源代码(变量名、函数名、行号)。
-
符号类型:
类型 示例 存储位置 全局符号 int global_var;
.data
/.bss
函数符号 void func() {}
.text
局部静态符号 static int local_static;
.data
/.bss
类型信息 class MyClass;
调试段(如 .debug_info
) -
工具与操作:
- 查看符号表:
1
2
3nm -C a.out # 查看符号(C++ 名称修饰)
readelf -s a.out # ELF 文件符号详情
objdump -t a.out # 显示符号表条目 - 名称修饰(Name Mangling):
C++ 为支持重载,将void Foo(int)
编译为类似_Z3Fooi
的符号,通过c++filt
解析:1
c++filt _Z3Fooi # 输出: Foo(int)
- 剥离符号表(发布优化):
1
strip --strip-all a.out # 移除所有符号
- 查看符号表:
-
调试符号格式:
- DWARF(Linux/Unix):
.debug_info
、.debug_line
等段存储源码映射。 - PDB(Windows):独立文件(
.pdb
)存储调试信息。
- DWARF(Linux/Unix):
二、内存转储(Memory Dump)
内存转储是程序运行时内存状态的快照,用于分析崩溃或异常。
-
核心转储(Core Dump):
- 触发场景:段错误(SIGSEGV)、手动触发(
gcore
)。 - 生成配置:
1
2ulimit -c unlimited # 启用 core dump
echo "/tmp/core.%t" > /proc/sys/kernel/core_pattern # 设置保存路径
- 触发场景:段错误(SIGSEGV)、手动触发(
-
分析工具:
1
2
3
4gdb ./executable core_file # 加载转储文件
(gdb) bt # 查看崩溃堆栈
(gdb) print variable # 检查变量值
(gdb) x/8xw address # 检查内存内容(8字,16进制) -
手动转储:
- 代码中生成:
1
2
3
4
void GenerateDump() {
system("gcore <pid>"); // 调用 gcore
} - GDB 命令:
1
(gdb) generate-core-file # 保存当前状态为 core 文件
- 代码中生成:
三、行号(Line Numbers)
行号信息关联机器指令与源代码位置,是调试的核心。
-
生成与存储:
- 编译时添加
-g
选项(GCC/Clang):1
g++ -g -O0 main.cpp -o prog # -O0 关闭优化避免行号错乱
- DWARF 段:
.debug_line
:指令地址 ↔ 源文件行号的映射。.debug_info
:类型、变量结构信息。
- 编译时添加
-
工具应用:
- 反汇编与行号:
1
objdump -d -S ./prog # 混合显示汇编与源码
- 地址转行号:
1
addr2line -e ./prog 0x401000 # 将地址转换为文件:行号
- GDB 行号操作:
1
2(gdb) list main.c:15 # 显示第15行附近代码
(gdb) break main.c:20 # 在第20行设断点
- 反汇编与行号:
四、调试(Debug)
综合符号表、行号与内存分析进行问题定位。
-
调试器工作流(GDB):
1
2
3
4
5
6
7
8gdb ./prog # 启动调试
(gdb) break main # 在 main 函数设断点
(gdb) run # 运行程序
(gdb) next # 单步跳过(不进入函数)
(gdb) step # 单步进入(进入函数)
(gdb) watch variable # 监视变量变化
(gdb) info locals # 显示局部变量
(gdb) frame 2 # 切换到堆栈帧2 -
多线程调试:
1
2
3(gdb) info threads # 列出所有线程
(gdb) thread 3 # 切换到线程3
(gdb) break foo thread 2 # 在线程2的 foo 函数设断点 -
内存泄漏检测工具:
- Valgrind:
1
valgrind --leak-check=full ./prog
- 报告未释放内存(
definitely lost
)。 - 检测越界访问(
Invalid read/write
)。
- 报告未释放内存(
- Valgrind:
-
高级调试技术:
- 条件断点:
1
(gdb) break foo if x==5 # 当 x=5 时触发断点
- 反向调试(GDB 7.0+):
1
2(gdb) record # 开始记录执行轨迹
(gdb) reverse-step # 反向单步执行
- 条件断点:
总结:
符号表提供程序结构映射,内存转储捕获运行时状态,行号关联机器码与源码,调试器整合三者实现问题定位。掌握这些工具链(gcc
/gdb
/valgrind
/objdump
)是C++开发者诊断复杂问题的核心能力。
(有了AI这些知识总结就是好写)
总的来说,通过崩溃堆栈提供的地址,有符号表的话就可以还原对应的文件和行号
Unity符号对应关系分析
本来想写的,发现网上已经有文章了,而且还很详细: https://zhuanlan.zhihu.com/p/132717069
Unity IL2CPP构建流程分析
大体构建过程可以参考: https://www.lfzxb.top/il2cpp-code-gen/
那么问题来了,先不说符号表的问题,Unity就没给我们输出地址,C++地址都没,只能说硬着头皮去分析Unity6了
好巧不巧的是,原本基于C#编写的IL2CPP流程,全变成exe了,正常反汇编手段根本不知道里面在干啥,凑巧前阵子学了一手AI反汇编,直接用上: https://www.52pojie.cn/forum.php?mod=viewthread&tid=2032638&highlight=mcp
具体流程和细节此处不再概述,搭好环境之后其实就非常简单了,你问什么,AI就能帮你还原逻辑,得到你想要的答案,那么答案其实就在一个名为 usymtool 这个工具上
Unity6封装了整个流程,是不可见的,通过分析汇编得知Unity最后会通过usymtool分析两个东西:
- C++符号表,Windows为GameAssembly.pdb,Android为libil2cpp.dbg.so,IOS为UnityFramework.framework.dSYM
- 通过IL2CPP转义的C++源码以及一个记录了C++到C#行号映射的LineNumberMappings.json文件
实现基于Unity2022及其之前版本的C#行号恢复功能
首先,如果图方便,完全可以将我们这部分构建出的il2cpp.usym随包发布,Unity会自动读取这个符号表,并实现行号转换
1 | Exception: !!!!!!!!! |
但个人并不推荐这个做法,原因有二
- 性能问题,一旦有了符号表,在生成堆栈的时候Unity会多一些查找和构造对象操作,会造成一些性能和内存问题,虽然不多
- 安全问题,毕竟这个符号表相当于包含了所有反汇编所需要的信息,有心者完全可以在il2cpp dump之后,写一个自动化工具,结合反编译的C++,C#和GlobalMetaData最大限度还原整个游戏的所有逻辑。不过这个倒是其次,毕竟大家平时在写什么代码自己心里有数
所以本文的此部分会提供离线的il2cpp.dsym行号恢复功能,最大限度保留原有性能和安全性
il2cpp源码更改
il2cpp本身也并没有输出函数地址的逻辑,所以我们需要做更改 Unity安装目录/Data/il2cpp/libil2cpp/icalls/mscorlib/System.Diagnostics/StackTrace.cpp ,使得异常堆栈会输出一个地址:
1 |
|
然后我们就可以发现异常堆栈会从这样:
1 | Exception: !!!!!!!!! |
变成这样:
1 | Exception: !!!!!!!!! |
获取Unity6的usymtool
经过测试,只有Unity6版本的usymtool可以正常输出可用的il2cpp.usym文件,所以建议从Unity6复制一份usymtool到项目中
Windows和Android都可以直接使用:UnityHub\6000.1.11f1\Editor\Data\Tools\usymtool.exe (如果你Android和Windows都在PC打包的话)
IOS其实反而更简单,用Unity6导出一个空的xcode工程,在根目录就能看到一个 usymtool,直接复制即可
C++符号表位置
Windows
Library/Bee/artifacts/WinPlayerBuildProgram,由于机器环境不同,最终会生成在一个带有随机编码的文件夹中,问题不大,直接通过C#脚本搜索即可
Android
Library/Bee/artifacts/Android,同Windows
IOS
IOS就比较特殊一些,因为Unity并不会直接构建最终的IOS应用,而是通过xcode构建最终产物,经过测试,IOS的符号表会位于:/Users/username/Library/Developer/Xcode/DerivedData/Unity-iPhone-btwqofoxzceueqbjidljtygpwlpq/Build/Products/ReleaseForRunning-iphoneos/UnityFramework.framework.dSYM 下,这是一个中间产物目录,DerivedData中
构建il2cpp.usym
上面提到符号表的构建依赖于行号信息,需要在构建前开启:
1 | using UnityEditor; |
Windows && Android
直接将下面这个脚本复制到项目即可,打包结束后会自动构建il2cpp.usym文件,并复制到 Logs/USYMTemp 下
1 | using UnityEngine; |
IOS
两种方案:
- 更改xcode工程脚本,实现自动化
- 通过后处理的方式,修改CI流水线,不修改已有xcode工程,保证稳定
个人推荐第一种
更改xcode工程脚本,实现自动化
在xcode构建完毕后执行此脚本,注意传递相应的环境变量
1 | !/bin/sh |
追求自动化的话,可以更改xcode工程的配置(通过C#),在Build Phases栏目有个Run Scripts选项,将脚本复制进去即可
修改CI流水线
或者也可以可以参考Unity6导出的xcode工程,在构建完毕后,我们在Build Log里可以找到类似Log:
1 | Showing Recent Messages |
参考第二行的命令行参数自己在打包CI流水线上执行即可
解析地址得到文件名和行号
注意,工具目前会接受十进制的地址作为输入,可根据需求自行更改
此时万事俱备,再编写一个C# dll来解析即可,工具已开源: https://github.com/wqaetly/il2cpp_usym_resolver
使用示例: