1、背景介绍
很多开发者面对C++都很犯怵,其中主要的一块就是内存操作。不合理的内存操作,比如数组越界、内存泄露、释放已释放的地址,可能会引起程序性能问题:内存消耗大,卡顿,更严重的会导致程序出现崩溃。当应用运行发生错误使应用进程终止时,应用将会抛出错误日志以通知应用崩溃的原因,开发者可通过查看错误日志分析应用崩溃的原因及引起崩溃的代码位置。FaultLog由系统自动从设备进行收集,包括如下几类故障信息:
- App Freeze
- CPP Crash
- JS Crash
- System Freeze
- ASan
- TSan
其他工具我们日常使用较多,不做过多介绍。由于ArkTS是单线程,提供的多线程机制也是基于非共享内存,很多多线程场景可能会放到C++层实现,所以本文主要介绍DevEco Studio提供的内存调试和线程调试工具:AScan检测与TSan检测。
2、 ASan检测
C++内存问题主要体现在访问越界、内存未释放、内存释放后再次释放,出于性能考虑,编译器和运行框架不会对内存操作进行安全检查,DevEco Studio提供了ASan帮助开发者检测地址越界问题。
ASan全称Address-Sanitizer,首先我们先了解工程中配置ASan的方式。
2.1 ASan配置
ASan主要由ASAN_OPTIONS 参数控制,主要设置检测级别、输出格式、内存错误报告的详细程度等。
参数配置主要有两个地方:
- 工程的app.json5文件中(优先级较高)
- Run/Debug Configurations中
2.1.1 在app.json5中配置
AppScope > app.json5文件中:
{"app": {"appEnvironments": [{"name": "ASAN_OPTIONS","value": "log_exe_name=true abort_on_error=0 print_cmdline=true" // 示例仅供参考,具体以实际为准},],...}
}
2.1.2 在Run/Debug Configurations中配置
点击加号按钮新增名称为ASAN_OPTIONS的配置,value值可以设置为:log_exe_name=true abort_on_error=0 print_cmdline=true
2.1.3 可配置参数说明
参数 | 默认值 | 是否必填 | 含义 |
---|---|---|---|
log_exe_name | true | 是 | 不可修改。指定内存错误日志中是否包含执行文件的名称。 |
log_path | /dev/asanlog/asan.log | 否 | ROM版本小于NEXT.0.0.68时必填,值不可修改;NEXT.0.0.68及以上版本不再需要该参数。 |
abort_on_error | false | 是 | 指定在打印错误报告后调用abort()或_exit()。 - false:打印错误报后使用_exit()结束进程 - true:打印错误报后使用abort()结束进程 |
strip_path_prefix | - | 否 | 内存错误日志的文件路径中去除所配置的前缀。 如:/data/storage/el1 |
detect_stack_use_after_return | false | 否 | 指定是否检查访问指向已被释放的栈空间。 - true:检查。 - false:不检查。 |
halt_on_error | 0 | 否 | 检测内存错误后是否继续运行。 - 0表示继续运行。 - 1表示结束运行。 |
malloc_context_size | - | 否 | 内存错误发生时,显示的调用栈层数。 |
suppressions | “” | 否 | 屏蔽文件名。 |
handle_segv | - | 否 | 检查段错误。 |
handle_sigill | - | 否 | 检查SIGILL信号。 |
quarantine_size_mb | 256 | 否 | 指定检测访问指向已被释放的栈空间错误的隔离区大小。 |
2.2 启用和启动ASan
有两种方式使用ASan:
-
运行调试窗口,点击Diagnostics,勾选Address Sanitizer。
-
AppScope/app.json5,添加ASan配置开关。
建议可以在IDE配置中修改的就尽量不要在代码中改动。
如果有依赖的本地library,需在library模块的build-profile.json5文件中,配置arguments字段值为“-DOHOS_ENABLE_ASAN=ON”,表示以ASan模式编译so文件。
接下来我们运行或调试当前应用时,当程序出现内存错误时,弹出ASan log信息,点击信息中的链接即可跳转至引起内存错误的代码处。
下面是模拟数组越界异常崩溃后的asan日志,点击错误直接跳转到引起崩溃位置,而且会提示UNKNOWN memory access,这种日志对我们定位日志帮助很大。
2.3 ASan检测异常码
ASan会提示具体错误码,下面列出插件错误码和对应原因。
2.3.1 heap-buffer-overflow
原因和影响:访问越界,导致程序存在安全漏洞,并有崩溃风险。
优化建议:已知大小的集合注意访问不要越界,位置大小的集合访问前先判断大小。
错误代码示例:
int heapBufferOverflow() {char *buffer;buffer = (char *)malloc(10);*(buffer + 11) = 'n';*(buffer + 12) = 'n';free(buffer);return buffer[1];
}
2.3.2 stack-buffer-underflow
原因和影响:访问越下界,导致程序存在安全漏洞,并有崩溃风险。
优化建议:访问索引不应小于下界。
错误代码示例:
int stackBufferUnderflow() {int subscript = -1;char buffer[42];buffer[subscript] = 42;return 0;
}
2.3.3 stack-use-after-scope
原因和影响:栈变量在作用域之外被使用。导致程序存在安全漏洞,并有崩溃风险。
优化建议:注意变量的作用域。
错误代码示例:
int *gp;
bool b = true;
int stackUseAfterScope() {if (b) {int x[5];gp = x + 1;}return *gp;
}
2.3.4 attempt-free-nonallocated-memory
原因和影响:尝试释放了非堆对象(non-heap object)或未分配内存。导致程序存在安全漏洞,并有崩溃风险。
优化建议:不要对非堆对象或未分配的内存使用free函数。
错误代码示例:
int main() {int value = 42;free(&value);return 0;
}
2.3.5 double-free
原因和影响:重复释放内存。导致程序存在安全漏洞,并有崩溃风险。
优化建议:变量定义声明时初始化为NULL,释放内存后也应立即将变量重置为NULL,这样每次释放之前都可以通过判断变量是否为NULL来判断是否可以释放。
错误代码示例:
int main() {int *x = new int[42];delete [] x;delete [] x;return 0;
}
2.3.6 heap-use-after-free
原因和影响:当指针指向的内存被释放后,仍然通过该指针访问已经被释放的内存,就会触发heap-use-after-free。
优化建议:实现一个free()的替代版本或者 delete析构器来保证指针的重置。
错误代码示例:
#include <stdlib.h>
int main() {int *array = new int[5];delete[] array;return array[5];
}
2.4 其他注意事项
- 如果应用内的任一模块使能ASan,那么entry模块需同时使能ASan。如果entry模块未使能ASan,该应用在启动时将闪退,出现CPP Crash报错。
3、 TSan检测
C++开发中的另一个问题是线程问题,引入多线程会导致代码的执行顺利不可预测,导致调试和定位问题困难。幸好DevEco Studio提供了TSan工具,TSan 全称ThreadSanitizer,是一个检测数据竞争的工具,它包含一个编译器插桩模块和一个运行时库。
3.1 主要应用场景
多线程开发中常见的问题包括线程安全、死锁、资源竞争和线程池管理问题,这些问题都可以通过TSan帮助我们检查,TSan能够检测出如下问题:
- 数据竞争检测:数据竞争(Data Race)是指两个或多个线程在没有适当的同步机制情况下同时访问相同的内存位置,其中至少有一个线程在写入。数据竞争是导致多线程程序行为不可预测的主要原因之一。
- 锁错误检测:TSan 不仅能检测数据竞争,还能检测与锁相关的错误:
- 死锁(Deadlock):死锁是指两个或多个线程互相等待对方释放锁,导致程序无法继续执行。
- 双重解锁(Double Unlock):同一线程尝试解锁已经解锁的锁。
- 未持有锁解锁:一个线程尝试解锁一个它未持有的锁。
- 条件变量错误检测,条件变量用于线程之间的通信和同步,常见错误包括:
- 未持有锁等待:一个线程在未持有相关锁的情况下调用 wait。
- 未持有锁唤醒:一个线程在未持有相关锁的情况下调用 signal 或 broadcast。
3.2 TSan配置
有两种方式配置开启TSan:
-
在运行调试窗口,点击Diagnostics,勾选Thread Sanitizer。
-
修改工程目录下AppScope/app.json5,添加TSan配置开关。
注意: 如果有引用本地library,需在library模块的build-profile.json5文件中,配置arguments字段值为“-DOHOS_ENABLE_TSAN=ON”,表示以TSan模式编译so文件。
3.3 开启TSan
运行或调试当前应用,当程序出现线程错误时,弹出TSan log信息,点击信息中的链接即可跳转至引起线程错误的代码处。
![[HarmonyOS Next 工具介绍:ASan与TSan检测根治你的C++恐惧症-10.png]]
当 TSan 检测到错误时,它会生成详细的报告,包括:
- 错误类型:例如数据竞争、死锁等。
- 内存地址:涉及的内存地址。
- 线程信息:涉及的线程ID和线程创建的堆栈跟踪。
- 源代码位置:每一个内存访问的源代码位置和堆栈跟踪。
- 上下文信息:访问类型(读/写)、访问大小等。
注意:
当前使用call_once接口会存在TSan误报的现象,开发者可以在调用该接口的函数前添加__attribute__((no_sanitize(“thread”)))来屏蔽该问题。
3.4 其他注意事项
- TSan开启后,会使性能降低5到15倍,同时使内存占用率提高5到10倍,其他申请大虚拟内存的功能(如gpu图形渲染)可能会受影响。
- ASan与TSan不可同时开启。
- TSan仅支持API 12及以上版本。
4、总结
本文主要介绍了 DevEco Studio 提供了两种内存调试和线程调试工具:ASan(Address Sanitizer)和TSan(Thread Sanitizer)。这两个工具可以帮助开发者检测并解决C++程序中的内存错误和线程问题。ASan用于检测地址越界、未释放的内存和释放后再次释放的问题,而TSan则用于检测数据竞争、锁错误以及条件变量错误。
DevEco Studio相当给力,对开发者很友好,把C++开发中遇到的问题都通过工具化来协助解决和定位,通过内存和线程检测工具,开发者在面对C++开发任务时可以得心应手,手到擒来。推荐大家可以熟悉下这两个工具,在面对复杂的C++开发场景,可以做到胸有成竹,借助工具消除隐患。