鸿蒙开发全栈指南:TSan数据竞争检测全解析
鸿蒙开发技术专栏精选 本文介绍了鸿蒙开发中的ThreadSanitizer(TSan)工具,主要包含以下内容: TSan原理与功能 检测数据竞争、锁错误和条件变量错误 包含编译器插桩模块和运行时库 性能影响:5-15倍速度降低,5-10倍内存增加 使用方式 两种使能方法(DevEco Studio和流水线) 需配置-DOHOS_ENABLE_TSAN=ON参数 不支持与其他内存检测工具同时使用 常
📑往期推文全新看点(文中附带最新·鸿蒙全栈学习笔记)
✒️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?
✒️ 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~
✒️ 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?
✒️ 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?
✒️ 记录一场鸿蒙开发岗位面试经历~
✒️ 持续更新中……
原理概述
TSan(ThreadSanitizer)是一个检测数据竞争的工具。它包含一个编译器插桩模块和一个运行时库。TSan开启后,会使性能降低5到15倍,同时使内存占用率提高5到10倍。
Tsan使能分为两个阶段,Tsan Instrumentation阶段完成对用户代码的插装,Tsan Runtime阶段负责对竞争情况做判断,然后输出对应的报告。
功能介绍
应用场景
TSan能够检测出如下问题:
- 数据竞争检测数据竞争(Data Race)是指两个或多个线程在没有适当的同步机制情况下同时访问相同的内存位置,其中至少有一个线程在写入。数据竞争是导致多线程程序行为不可预测的主要原因之一。
- 锁错误检测TSan 不仅能检测数据竞争,还能检测与锁相关的错误:
- 死锁(Deadlock):死锁是指两个或多个线程互相等待对方释放锁,导致程序无法继续执行。
- 双重解锁(Double Unlock):同一线程尝试解锁已经解锁的锁。
- 未持有锁解锁:一个线程尝试解锁一个它未持有的锁。
- 条件变量错误检测条件变量用于线程之间的通信和同步,常见错误包括:
- 未持有锁等待:一个线程在未持有相关锁的情况下调用 wait。
- 未持有锁唤醒:一个线程在未持有相关锁的情况下调用 signal 或 broadcast。
常见TSan异常检测类型有data race,heap-use-after-free,signal handler spoils errno等
错误报告
当 TSan 检测到错误时,它会生成详细的报告,包括:
- 错误类型:例如数据竞争、死锁等。
- 内存地址:涉及的内存地址。
- 线程信息:涉及的线程ID和线程创建的堆栈跟踪。
- 源代码位置:每一个内存访问的源代码位置和堆栈跟踪。
- 上下文信息:访问类型(读/写)、访问大小等。
使用约束
- TSan仅支持API 12及以上版本。
- ASan、TSan、UBSan、HWASan、GWP-Asan不能同时开启,五个只能开启其中一个。
- TSan开启后会申请大量虚拟内存,其他申请大虚拟内存的功能(如gpu图形渲染)可能会受影响。
- TSan不支持静态链接libc或libc++库。
使能Tsan
可通过以下两种方式使能TSan。每种方式分为DevEco Studio场景和流水线场景。
方式一
DevEco Studio场景
1. 点击Run > Edit Configurations > Diagnostics,勾选Thread Sanitizer。
2. 如果有引用本地library,需在library模块的build-profile.json5文件中,配置arguments字段值为“-DOHOS_ENABLE_TSAN=ON”,表示以TSan模式编译so文件。
流水线场景
在hvigorw命令后加上ohos-debug-tsan=true的选项,执行hvigorw命令,
hvigorw [taskNames...] ohos-debug-tsan=true <options>
同上,如果有引用本地library,需在library模块的build-profile.json5文件中,配置arguments字段值为“-DOHOS_ENABLE_TSAN=ON”,表示以TSAN模式编译so文件。
方式二
DevEco Studio场景
1. 修改工程目录下AppScope/app.json5,添加TSan配置开关。
"tsanEnabled": true
2. 设置模块级构建TSan插桩。
在需要使能TSan的模块中,通过添加构建参数开启TSan检测插桩,在对应模块的模块级build-profile.json5中添加命令参数:
"arguments": "-DOHOS_ENABLE_TSAN=ON"
流水线场景
在hvigorw命令后加上ohos-debug-tsan=true的选项,执行hvigorw命令
hvigorw [taskNames...] ohos-debug-tsan=true <options>
同上,如果有引用本地library,需在library模块的build-profile.json5文件中,配置arguments字段值为“-DOHOS_ENABLE_TSAN=ON”,表示以TSAN模式编译so文件。
Tsan异常检测类型
Data race
背景
多个线程在没有正确加锁的情况下,同时访问同一块数据,并且至少有一个线程是写操作,对数据的读取和修改产生了竞争,从而导致各种不可预计的问题
错误代码实例
int Global = 12;
void Set1() {
*(char *)&Global = 4;
}
void Set2() {
Global=43;
}
void *Thread1(void *x){
Set1();
return x;
}
static napi_value Add(napi_env env, napi_callback_info info){
...
pthread_t t;
pthread_create(&t, NULL, Thread1, NULL);
Set2();
pthread_join(t, NULL);
...
}
影响
对数据的读取和修改产生了竞争,从而导致各种不可预计的问题
开启Tsan检测后,触发demo中的函数,应用闪退报Tsan,包含字段:ThreadSanitizer: data race
定位思路
如果有工程代码,直接开启Tsan检测,debug模式运行后复现该错误,可以触发Tsan,直接点击堆栈中的超链接定位到代码行,能看到错误代码的位置。
Reason:TSAN
==appspawn==54331==ThreadSanitizer: WARNING: unexpected format specifier in printf interceptor: %{ (reported once per process)
==================
WARNING: ThreadSanitizer: data race (pid=54331)
Write of size 1 at 0x007f1b9c4f84 by thread T32:
#0 0x7f1b9c22cc (/data/storage/el1/bundle/libs/arm64/libentry.so+0x22c8) (BuildId: fe67e101795e12687f395b21194511c9d69851c2)
#1 0x7f1b9c2354 (/data/storage/el1/bundle/libs/arm64/libentry.so+0x2350) (BuildId: fe67e101795e12687f395b21194511c9d69851c2)
#2 0x7e012b84f8 (/system/lib64/libclang_rt.tsan.so+0x784f4) (BuildId: ae7f86a9a081faf0a0a327001ad1c8b20bf33783)
Previous write of size 4 at 0x007f1b9c4f84 by main thread:
#0 0x7f1b9c2310 (/data/storage/el1/bundle/libs/arm64/libentry.so+0x230c) (BuildId: fe67e101795e12687f395b21194511c9d69851c2)
#1 0x7f1b9c25ac (/data/storage/el1/bundle/libs/arm64/libentry.so+0x25a8) (BuildId: fe67e101795e12687f395b21194511c9d69851c2)
#2 0x7e8df3cb8c (/system/lib64/platformsdk/libace_napi.z.so+0x3cb88) (BuildId: 57a073cc8fa34c10cb354df9ed7e2e4b)
修改方法
加锁或者其它线程同步的方法
推荐建议
多线程访问同一内存时,需要注意线程同步机制,必要时加锁
data race on vptr
背景
一个线程在删除某个对象(obj)、一个线程在调用虚函数(obj->vcall)
错误代码实例
#include <semaphore.h>
#include <pthread.h>
struct A {
A() {
sem_init(&sem_, 0, 0);
}
virtual void F() {
}
void Done() {
sem_post(&sem_);
}
virtual ~A() {
sem_wait(&sem_);
sem_destroy(&sem_);
}
sem_t sem_;
};
struct B : A {
virtual void F() {
}
virtual ~B() { }
};
static A *obj = new B;
void *Thread1(void *x) {
obj->F();
obj->Done();
return NULL;
}
void *Thread2(void *x) {
delete obj;
return NULL;
}
static napi_value Add(napi_env env, napi_callback_info info){
...
pthread_t t[2];
pthread_create(&t[0], NULL, Thread1, NULL);
pthread_create(&t[1], NULL, Thread2, NULL);
pthread_join(t[0], NULL);
pthread_join(t[1], NULL);
...
}
影响
线程行为发生冲突,程序崩溃
开启Tsan检测后,触发demo中的函数,应用闪退报Tsan,包含字段:ThreadSanitizer: data race on vptr (ctor/dtor vs virtual call)
定位思路
如果有工程代码,直接开启Tsan检测,debug模式运行后复现该错误,可以触发Tsan,直接点击堆栈中的超链接定位到代码行,能看到错误代码的位置。
Reason:TSAN
==appspawn==26789==ThreadSanitizer: WARNING: unexpected format specifier in printf interceptor: %{ (reported once per process)
==================
WARNING: ThreadSanitizer: data race on vptr (ctor/dtor vs virtual call) (pid=26789)
Write of size 8 at 0x007f078bb140 by thread T31:
#0 0x7f1b943304 (/data/storage/el1/bundle/libs/arm64/libentry.so+0x3300) (BuildId: 313582257cb30f740bec5d3616a5588b41b7de89)
#1 0x7f1b943258 (/data/storage/el1/bundle/libs/arm64/libentry.so+0x3254) (BuildId: 313582257cb30f740bec5d3616a5588b41b7de89)
#2 0x7f1b943298 (/data/storage/el1/bundle/libs/arm64/libentry.so+0x3294) (BuildId: 313582257cb30f740bec5d3616a5588b41b7de89)
#3 0x7f1b943138 (/data/storage/el1/bundle/libs/arm64/libentry.so+0x3134) (BuildId: 313582257cb30f740bec5d3616a5588b41b7de89)
#4 0x7e012b84f8 (/system/lib64/libclang_rt.tsan.so+0x784f4) (BuildId: ae7f86a9a081faf0a0a327001ad1c8b20bf33783)
Previous read of size 8 at 0x007f078bb140 by thread T30:
#0 0x7f1b94301c (/data/storage/el1/bundle/libs/arm64/libentry.so+0x3018) (BuildId: 313582257cb30f740bec5d3616a5588b41b7de89)
#1 0x7e012b84f8 (/system/lib64/libclang_rt.tsan.so+0x784f4) (BuildId: ae7f86a9a081faf0a0a327001ad1c8b20bf33783)
Location is heap block of size 40 at 0x007f078bb140 allocated by main thread:
#0 0x7e012b68d4 (/system/lib64/libclang_rt.tsan.so+0x768d0) (BuildId: ae7f86a9a081faf0a0a327001ad1c8b20bf33783)
#1 0x7f1b8b2438 (/data/storage/el1/bundle/libs/arm64/libc++_shared.so+0xb2434) (BuildId: cdf97be9396a35e8f4806f252f90a11320d26ec6)
#2 0x7f1b942f34 (/data/storage/el1/bundle/libs/arm64/libentry.so+0x2f30) (BuildId: 313582257cb30f740bec5d3616a5588b41b7de89)
#3 0x7e0009125c (/lib/ld-musl-aarch64.so.1+0x91258) (BuildId: ec44c498ff1525a6f5d1a1a23c105a8b)
#4 0x7e8bf786ac (/system/lib64/platformsdk/libace_napi.z.so+0x386a8) (BuildId: 57a073cc8fa34c10cb354df9ed7e2e4b)
修改方法
设置合适的线程同步机制,如锁
推荐建议
确保设置合适的线程同步机制,来保证线程执行逻辑先后的准确性
Use After Free
heap-use-after-free
背景
使用了释放的内存(多线程层面)
错误代码实例
#include <pthread.h>
int *mem;
pthread_mutex_t mtx;
void *Thread1(void *x) {
pthread_mutex_lock(&mtx);
free(mem);
pthread_mutex_unlock(&mtx);
return NULL;
}
__attribute__((noinline)) void *Thread2(void *x) {
pthread_mutex_lock(&mtx);
mem[0] = 42;
pthread_mutex_unlock(&mtx);
return NULL;
}
static napi_value Add(napi_env env, napi_callback_info info){
...
mem = (int*)malloc(100);
pthread_mutex_init(&mtx, 0);
pthread_t t;
pthread_create(&t, NULL, Thread1, NULL);
Thread2(0);
pthread_join(t, NULL);
pthread_mutex_destroy(&mtx);
...
}
影响
导致程序存在安全漏洞,并有崩溃风险。
开启Tsan检测后,触发demo中的函数,应用闪退报Tsan,包含字段:ThreadSanitizer: heap-use-after-free
定位思路
如果有工程代码,直接开启Tsan检测,debug模式运行后复现该错误,可以触发Tsan,直接点击堆栈中的超链接定位到代码行,能看到错误代码的位置。
Reason:TSAN
==appspawn==4830==ThreadSanitizer: WARNING: unexpected format specifier in printf interceptor: %{ (reported once per process)
==================
WARNING: ThreadSanitizer: heap-use-after-free (pid=4830)
Write of size 4 at 0x007f0764e420 by main thread (mutexes: write M0):
#0 0x7f1b8026bc (/data/storage/el1/bundle/libs/arm64/libentry.so+0x26b8) (BuildId: cf997f64a72b3a470193dd0b38a782fc3aef5075)
#1 0x7f1b80297c (/data/storage/el1/bundle/libs/arm64/libentry.so+0x2978) (BuildId: cf997f64a72b3a470193dd0b38a782fc3aef5075)
#2 0x7e8f77cb8c (/system/lib64/platformsdk/libace_napi.z.so+0x3cb88) (BuildId: 57a073cc8fa34c10cb354df9ed7e2e4b)
Previous write of size 8 at 0x007f0764e420 by thread T31 (mutexes: write M0):
#0 0x7e012b6f94 (/system/lib64/libclang_rt.tsan.so+0x76f90) (BuildId: ae7f86a9a081faf0a0a327001ad1c8b20bf33783)
#1 0x7f1b802630 (/data/storage/el1/bundle/libs/arm64/libentry.so+0x262c) (BuildId: cf997f64a72b3a470193dd0b38a782fc3aef5075)
#2 0x7e012b84f8 (/system/lib64/libclang_rt.tsan.so+0x784f4) (BuildId: ae7f86a9a081faf0a0a327001ad1c8b20bf33783)
修改方法
已释放的内存不要使用,释放的内存需要标记,方便其它线程判断
推荐建议
使用合理的线程同步机制
Signal Check
signal handler spoils errno
背景
信号处理函数中修改了errno变量
错误代码实例
#include "napi/native_api.h"
#include <signal.h>
#include <sys/types.h>
#include <errno.h>
#include <malloc.h>
#include <pthread.h>
static void MyHandler(int, siginfo_t *s, void *c) {
errno = 1;
done = 1;
}
static void* sendsignal(void *p) {
pthread_kill(mainth, SIGPROF);
return 0;
}
static __attribute__((noinline)) void loop() {
while (done == 0) {
volatile char *p = (char*)malloc(1);
p[0] = 0;
free((void*)p);
}
}
static napi_value Add(napi_env env, napi_callback_info info){
...
mainth = pthread_self();
struct sigaction act = {};
act.sa_sigaction = &MyHandler;
sigaction(SIGPROF, &act, 0);
pthread_t th;
pthread_create(&th, 0, sendsignal, 0);
loop();
pthread_join(th, 0);
...
}
影响
导致程序存在安全漏洞,并有崩溃风险。
开启Tsan检测后,触发demo中的函数,应用闪退报Tsan,包含字段:ThreadSanitizer: signal handler spoils errno
定位思路
如果有工程代码,直接开启Tsan检测,debug模式运行后复现该错误,可以触发Tsan,直接点击堆栈中的超链接定位到代码行,能看到错误代码的位置。
Reason:TSAN
==appspawn==42144==ThreadSanitizer: WARNING: unexpected format specifier in printf interceptor: %{ (reported once per process)
==================
WARNING: ThreadSanitizer: signal handler spoils errno (pid=42144)
Signal 27 handler invoked at:
#0 0x7f1b7c2730 (/data/storage/el1/bundle/libs/arm64/libentry.so+0x272c) (BuildId: 6c5d69edd55e55ece087ccc4dadb8ffd2712681d)
#1 0x7f1b7c2868 (/data/storage/el1/bundle/libs/arm64/libentry.so+0x2864) (BuildId: 6c5d69edd55e55ece087ccc4dadb8ffd2712681d)
#2 0x7f1b7c26b4 (/data/storage/el1/bundle/libs/arm64/libentry.so+0x26b0) (BuildId: 6c5d69edd55e55ece087ccc4dadb8ffd2712681d)
#3 0x7e8f03cdcc (/system/lib64/platformsdk/libace_napi.z.so+0x3cdc8) (BuildId: dc293a22b36a4db17dc689ff9413b64e)
修改方法
不要在信号处理函数中修改error变量
推荐建议
将MyHandler中的error赋值语句去掉
signal-unsafe call inside of a signal
背景
信号处理函数中调用了非信号安全的函数(比如malloc)
错误代码实例
#include "napi/native_api.h"
#include <signal.h>
#include <sys/types.h>
#include <malloc.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
pthread_t mainth;
volatile int done;
static void handler(int, siginfo_t*, void*) {
volatile char *p = (char*)malloc(1);
p[0] = 0;
free((void*)p);
}
static napi_value Add(napi_env env, napi_callback_info info)
{
...
struct sigaction act = {};
act.sa_sigaction = &handler;
sigaction(SIGPROF, &act, 0);
kill(getpid(), SIGPROF);
sleep(1);
fprintf(stderr, "DONE\n");
...
}
影响
导致程序存在安全漏洞,并有崩溃风险。
开启Tsan检测后,触发demo中的函数,应用闪退报Tsan,包含字段:ThreadSanitizer: signal-unsafe call inside of a signal
定位思路
如果有工程代码,直接开启Tsan检测,debug模式运行后复现该错误,可以触发Tsan,直接点击堆栈中的超链接定位到代码行,能看到错误代码的位置**。**
Reason:TSAN
==appspawn==54255==ThreadSanitizer: WARNING: unexpected format specifier in printf interceptor: %{ (reported once per process)
==================
WARNING: ThreadSanitizer: signal-unsafe call inside of a signal (pid=54255)
#0 0x7e012b68d4 (/system/lib64/libclang_rt.tsan.so+0x768d0) (BuildId: ae7f86a9a081faf0a0a327001ad1c8b20bf33783)
#1 0x7f1b9025fc (/data/storage/el1/bundle/libs/arm64/libentry.so+0x25f8) (BuildId: 872d8f3822a374492bda4a455431bcc95f290ad1)
#2 0x7e012bfb34 (/system/lib64/libclang_rt.tsan.so+0x7fb30) (BuildId: ae7f86a9a081faf0a0a327001ad1c8b20bf33783)
#3 0x7f1b902530 (/data/storage/el1/bundle/libs/arm64/libentry.so+0x252c) (BuildId: 872d8f3822a374492bda4a455431bcc95f290ad1)
#4 0x7e8d47cdcc (/system/lib64/platformsdk/libace_napi.z.so+0x3cdc8) (BuildId: dc293a22b36a4db17dc689ff9413b64e)
修改方法
将信号处理函数中的malloc去掉,在其外部预先分配内存。
推荐建议
建议信号处理程序之外预先分配内存,或者尽可能避免在信号处理程序中进行内存分配和复杂的操作。如果需要在程序中替换malloc,可以考虑使用__malloc_hook或者宏定义等方法
Mutex Check
unlock of an unlocked mutex (or by a wrong thread)
背景
解锁一个已经解锁/自己不拥有的锁
错误代码实例
#include "napi/native_api.h"
#include <pthread.h>
#include <iostream>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* unlocker(void* arg) {
pthread_mutex_unlock(&mutex);
return nullptr;
}
static napi_value Add(napi_env env, napi_callback_info info){
...
pthread_t tid;
pthread_create(&tid, nullptr, unlocker, nullptr);
pthread_join(tid, nullptr);
...
}
影响
导致程序存在安全漏洞,并有崩溃风险。
开启Tsan检测后,触发demo中的函数,应用闪退报Tsan,包含字段:ThreadSanitizer: unlock of an unlocked mutex (or by a wrong thread)
Reason:TSAN
==appspawn==30577==ThreadSanitizer: WARNING: unexpected format specifier in printf interceptor: %{ (reported once per process)
==================
WARNING: ThreadSanitizer: unlock of an unlocked mutex (or by a wrong thread) (pid=30577)
#0 0x7e012d4cd8 (/system/lib64/libclang_rt.tsan.so+0x94cd4) (BuildId: ae7f86a9a081faf0a0a327001ad1c8b20bf33783)
#1 0x7f1b8421bc (/data/storage/el1/bundle/libs/arm64/libentry.so+0x21b8) (BuildId: 4221fdd9cad8fe19e56e4a38ccf49ffc5c637855)
#2 0x7e012b84f8 (/system/lib64/libclang_rt.tsan.so+0x784f4) (BuildId: ae7f86a9a081faf0a0a327001ad1c8b20bf33783)
Location is global '<null>' at 0x000000000000 (libentry.so+0x4db0)
Mutex M0 (0x007f1b844db0) created at:
#0 0x7e012d4cd8 (/system/lib64/libclang_rt.tsan.so+0x94cd4) (BuildId: ae7f86a9a081faf0a0a327001ad1c8b20bf33783)
#1 0x7f1b8421bc (/data/storage/el1/bundle/libs/arm64/libentry.so+0x21b8) (BuildId: 4221fdd9cad8fe19e56e4a38ccf49ffc5c637855)
#2 0x7e012b84f8 (/system/lib64/libclang_rt.tsan.so+0x784f4) (BuildId: ae7f86a9a081faf0a0a327001ad1c8b20bf33783)
定位思路
如果有工程代码,直接开启Tsan检测,debug模式运行后复现该错误,可以触发Tsan,直接点击堆栈中的超链接定位到代码行,能看到错误代码的位置。
修改方法
先使用try_lock()接口获取锁,再使用unlock()接口解锁
更多推荐
所有评论(0)