大家好,我是杂烩君。本次我们来分享一个开发调试利器——Sanitizer。
Sanitizer简介
Sanitizer是由Google发起的开源工具集,用于检测内存泄露等问题。
它包括了AddressSanitizer、MemorySanitizer、ThreadSanitizer、LeakSanitizer等多种工具。这些工具最初是LLVM项目的一部分,后来也被GNU的GCC编译器支持。从GCC的4.8版本开始,就已经支持AddressSanitizer和ThreadSanitizer,而4.9版本则开始支持LeakSanitizer。
Sanitizer使用
1、AddressSanitizer的使用例子
AddressSanitizer(ASan) 是一个快速内存检测器,可以检测出缓冲区溢出、使用已释放内存等问题。编译时带上参数 -fsanitize=address及-g。
(1)捕捉栈缓冲区溢出问题:
AddressSanitizer.c:
// 微信公众号:嵌入式大杂烩
#include <stdlib.h>
void test_func(void)
{
int a[6] = {0};
int b = a[6]; // 栈缓冲区溢出
}
int main(int argc, char **argv)
{
test_func();
return 0;
}
编译、运行:
gcc AddressSanitizer.c -fsanitize=address -g -o AddressSanitizer
执行结果分析:
触发了检测错误级别,终止程序并给出了程序运行异常的原因及异常的代码位置。
(2)捕捉使用已释放内存问题:
ThreadSanitizer.c:
// 微信公众号:嵌入式大杂烩
#include <stdlib.h>
void test_func(void)
{
char *p = malloc(10);
p[0] = 1;
free(p);
p[0] = 1; // 使用已释放内存
}
int main(int argc, char **argv)
{
test_func();
return 0;
}
2、ThreadSanitizer的使用例子
ThreadSanitizer(TSan) 是一个数据竞争检测器,可以用来分析线程竞态、死锁等线程相关问题。编译时带上参数 -fsanitize=thread及-g。
捕捉 线程间数据竞争 问题:
// 微信公众号:嵌入式大杂烩
#include <stdio.h>
#include <pthread.h>
int g_counter = 0; // thread1、thread2竞争的数据
void *increment(void *arg)
{
g_counter++;
}
void *decrement(void *arg)
{
g_counter--;
}
void test_func(void)
{
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, increment, NULL);
pthread_create(&thread2, NULL, decrement, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("Counter value: %d\n", g_counter);
}
int main(int argc, char **argv)
{
test_func();
return 0;
}
编译、运行:
gcc ThreadSanitizer.c -fsanitize=thread -g -pthread -o ThreadSanitizer
执行结果分析:
触发了检测警告级别,程序仍能运行,并给出了程序运行有风险的原因及有风险的代码位置。
3、程序中同时存在多处风险?
上面的例子分别使用AddressSanitizer检测器与ThreadSanitizer检测器来检测对应的异常,可以较为精准地检测到对应的异常。
如果程序中同时存在多处风险呢?
这也是比较贴近我们的实际应用的,毕竟我们并不知道我们的代码里有哪些可能存在的风险。这种情况我们要怎么检测?
编译时能同时带上多个-fsanitize参数调用多个检测器吗?
可以同时带,但有些检测器不能同时使用。
AddressSanitizer与ThreadSanitizer检测器不能同时使用。
但是,假如我们的程序中恰好存在address异常与thread异常呢,单独使用AddressSanitizer检测器、ThreadSanitizer检测器的表现是怎样的?
比如,我们把上面3个例子的代码放在一起:
test.c:
// 微信公众号:嵌入式大杂烩
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
int g_counter = 0; // thread1、thread2竞争的数据
void *increment(void *arg)
{
g_counter++;
}
void *decrement(void *arg)
{
g_counter--;
}
// 测试:资源竞争
void test_func(void)
{
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, increment, NULL);
pthread_create(&thread2, NULL, decrement, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("Counter value: %d\n", g_counter);
}
// 测试:使用已释放内存
void test_func1(void)
{
char *p = malloc(10);
printf("This is test_func1\n");
p[0] = 1;
free(p);
p[0] = 1; // 使用已释放内存
}
// 测试:栈缓冲区溢出
void test_func2(void)
{
int a[6] = {0};
int b = a[6]; // 栈缓冲区溢出
}
int main(int argc, char **argv)
{
test_func();
test_func1();
test_func2();
return 0;
}
带-fsanitize=thread参数编译、运行:
执行结果分析:
ThreadSanitizer检测器能正常检测出资源竞争的问题,也检测出了test_func1中的使用已释放的堆内存的问题并以警告级别报告,但没有检测出test_func2的栈缓冲区溢出问题。
是不是因为test_func2运行在test_func1后面了,所以test_func2的异常没有被ThreadSanitizer检测器检测出来?
我们调换个位置看看:
int main(int argc, char **argv)
{
test_func();
test_func2();
test_func1();
return 0;
}
显然,执行结果还是一样的,test_func2的栈缓冲区溢出问题还是没有被ThreadSanitizer检测器检测出来。
所以,大致得出结论:当程序里存在thread异常与address异常时,使用ThreadSanitizer检测器能准确检测到thread异常,能检测到部分address异常。
带-fsanitize=address参数编译、运行:
执行顺序:
int main(int argc, char **argv)
{
test_func();
test_func1();
test_func2();
return 0;
}
执行结果分析:
AddressSanitizer检测器检测到了test_func1中的已使用释放的堆内存的异常并以错误级别报告,并终止了程序;没有检测到test_func的资源竞争的风险;也没有检测到test_func2的栈缓冲区溢出的问题,因为执行到test_func1的时候程序已经被终止了,如果把test_func2放在test_func1之前运行,就能检测到test_func2的异常。
结论:当程序里存在thread异常与address异常时,使用AddressSanitizer检测器能准确检测到第一个触发的address异常,不能检测到thread异常。
如果程序中存在多种可能存在的风险时,需要使用多个检测器单独挨个检测。每个检测器都有其擅长检测的方面,可以经过初步分析之后确定大致地方向,选择适合地检测器来做检测。
以上就是关于Sanitizer的一些简单介绍及使用的分享,更多的关于Sanitizer的资料可查阅:https://github.com/google/sanitizers/wiki/
码字不易,如果文章对你有帮助,麻烦帮忙点赞、关注,谢谢大家!