Sanitizer 的应用笔记
Sanitizer 的功能
Sanitizer 是 Google 研发的一个用于检查工具。有下面的4种类型:
类型 | 应用于检查以下问题 |
---|---|
Address Sanitizer | 内存泄漏,地址越界,野指针,悬停指针,等等 |
Leak Sanitizer | 只有内存泄漏 |
Thread Sanitizer | 数据竞争(Data Race) |
Unknown Behavior Sanitizer | 未定义行为(内存对齐,运算异常,等等) |
注意:
- 由于有的组合是不可以同时使用的。所以我们每次只用一种类型就好了。
Sanitizer 的安装
Sanitizer 最好使用 gcc 4.8 以上的版本。这样可以提供好的调试信息。
所以,这里以 gcc 7 为例子 进行安装。
# 安装 gcc 7
yum install -y devtoolset-7
# 安装 Address Sanitizer
yum install -y devtoolset-7-libasan-devel
# 安装 Leak Sanitizer
yum install -y devtoolset-7-liblsan-devel
# 安装 Thread Sanitizer
yum install -y devtoolset-7-libtsan-devel
# 安装 Unknown Behavior Sanitizer
yum install -y devtoolset-7-libubsan-devel
Sanitizer 的库
# 切换到 gcc 7
scl enable devtoolset-7 bash
# 检查一下 gcc 版本
gcc -v
> Using built-in specs.
> COLLECT_GCC=gcc
> COLLECT_LTO_WRAPPER=/opt/rh/devtoolset-7/root/usr/libexec/gcc/x86_64-redhat-linux/7/lto-wrapper
> Target: x86_64-redhat-linux
> Configured with: ...
> Thread model: posix
> gcc version 7.3.1 20180303 (Red Hat 7.3.1-5) (GCC) <-- Version of gcc
```bash
# 切换到 Sanitizer 库所在的目录
cd /opt/rh/devtoolset-7/root/usr/lib/gcc/x86_64-redhat-linux/7
# 看看 Sanitizer 的静态库,后续我们会使用静态库。
ll | grep san.a
> -rw-r--r-- 1 root root 2921486 Sep 13 2019 libasan.a
> -rw-r--r-- 1 root root 860830 Sep 13 2019 liblsan.a
> -rw-r--r-- 1 root root 2032876 Sep 13 2019 libtsan.a
> -rw-r--r-- 1 root root 805202 Sep 13 2019 libubsan.a
# 看看 Sanitizer 的动态库,会发现文件的大小实在太小了。
ll | grep san.so
> -rw-r--r-- 1 root root 82 Sep 13 2019 libasan.so
> -rw-r--r-- 1 root root 82 Sep 13 2019 liblsan.so
> -rw-r--r-- 1 root root 82 Sep 13 2019 libtsan.so
> -rw-r--r-- 1 root root 83 Sep 13 2019 libubsan.so
# 打开一个看看,发现Sanitizer 的动态库,只是一个链接。所以还是不要用了。
cat libasan.so
> /* GNU ld script */
> OUTPUT_FORMAT(elf64-x86-64)
> INPUT ( /usr/lib64/libasan.so.4 )
至此,我们已经安装好 Sanitizer 的静态库。
将它们列表如下:
类型 | 静态库 |
---|---|
Address Sanitizer | libasan.so |
Leak Sanitizer | liblsan.so |
Thread Sanitizer | libtsan.so |
Unknown Behavior Sanitizer | libubsan.so |
Sanitizer 的 flag
使用 man gcc
打开 帮助,可以看到 Sanitizer 的使用方法,是这样的。
-static-libasan
When the -fsanitize=address option is used to link a program,
the GCC driver automatically links against libasan.If libasan is available as a shared library, and the -static option is not used,
then this links against the shared version of libasan.The -static-libasan option directs the GCC driver to link libasan statically,
without necessarily linking other libraries statically.-static-libtsan
When the -fsanitize=thread option is used to link a program,
the GCC driver automatically links against libtsan.If libtsan is available as a shared library, and the -static option is not used,
then this links against the shared version of libtsan.The -static-libtsan option directs the GCC driver to link libtsan statically,
without necessarily linking other libraries statically.-static-liblsan
When the -fsanitize=leak option is used to link a program,
the GCC driver automatically links against liblsan.If liblsan is available as a shared library, and the -static option is not used,
then this links against the shared version of liblsan.The -static-liblsan option directs the GCC driver to link liblsan statically,
without necessarily linking other libraries statically.-static-libubsan
When the -fsanitize=undefined option is used to link a program,
the GCC driver automatically links against libubsan.If libubsan is available as a shared library, and the -static option is not used,
then this links against the shared version of libubsan.The -static-libubsan option directs the GCC driver to link libubsan statically,
without necessarily linking other libraries statically.
参透了帮助信息,我们就可以编写 makefile 了。
Sanitizer 的 CFLAGS
假设Sanitizer 的 CFLAGS 为 SAN_CFLAGS
,
那么为了使用静态库,我们需要再追加 -static-lib*san
。
另外,为了可以便于提供更多的调试信息。我们还需要追加 -fno-omit-frame-pointer
。
类型 | SAN_CFLAGS |
---|---|
Address Sanitizer | -fsanitize=address -static-libasan -fno-omit-frame-pointer |
Leak Sanitizer | -fsanitize=leak -static-liblsan -fno-omit-frame-pointer |
Thread Sanitizer | -fsanitize=thread -static-libtsan -fno-omit-frame-pointer |
Unknown Behavior Sanitizer | -fsanitize=undefined -static-libubsan -fno-omit-frame-pointer |
以下以 Address Sanitizer 为例,写出 CFLAGS 的设置。
#-------------------------------------------------------------------------------
# compiler flag
## base linker flag
CFLAGS :=
#...............................................................................
## version compiler flag
VERSION_CFLAGS = -O0 -g3
## sanitizer compiler flags
ASAN_CFLAGS = -fsanitize=address -static-libasan -fno-omit-frame-pointer
SAN_CFLAGS = $(ASAN_CFLAGS)
#...............................................................................
# assemble compiler flag
CFLAGS += $(VERSION_CFLAGS)
CFLAGS += $(SAN_CFLAGS)
Sanitizer 的 LDLAGS
由于我们使用的是 Address Sanitizer 的静态库,所以链接的时候,需要用 -l:libasan.a
来指定链接到静态库。
并且还需要使用 -Wl,--whole-archive
链接入一整个的静态库。
所以LDFLAGS需要设置如下:
## address sanitizer linker flags
LDFLAGS += -Wl,--whole-archive
LDFLAGS += -l:libasan.a
LDFLAGS += -Wl,--no-whole-archive
再者,Address Sanitizer 的静态库,是需要依赖于 librt.so, libdl.so, libpthread.so, libm.so 动态库的。
我们定义 SAN_LDFLAGS_DEPENDED_ON
表示 Sanitizer 所依赖的动态库。
SAN_LDFLAGS_DEPENDED_ON 的定义如下:
SAN_LDFLAGS_DEPENDED_ON = -lrt -ldl -lrt -lpthread -lm
注意:
-lrt -ldl -lrt
中-lrt
出现了两次,是为了避免链接时的次序问题。
LDFLAGS 在 -l:libasan.a
的前后,插入 SAN_LDFLAGS_DEPENDED_ON
。
#...............................................................................
## sanitizer linker flags depended on
SAN_LDFLAGS_DEPENDED_ON = -lrt -ldl -lrt -lpthread -lm
## address sanitizer linker flags
LDFLAGS += $(SAN_LDFLAGS_DEPENDED_ON)
LDFLAGS += -Wl,--whole-archive
LDFLAGS += -l:libasan.a
LDFLAGS += -Wl,--no-whole-archive
LDFLAGS += $(SAN_LDFLAGS_DEPENDED_ON)
endif
注意:
SAN_LDFLAGS_DEPENDED_ON
插入了两次,也是为了避免链接时的次序问题。
最后为了可以直接从 CFLAGS
就推断出 Sanitizer 的类型,从而设置好 LDFLAGS
。
我们使用 findstring 来对 CFLAGS
进行查找,来判断 Sanitizer 的类型。
#...............................................................................
## sanitizer linker flags depended on
SAN_LDFLAGS_DEPENDED_ON = -lrt -ldl -lrt -lpthread -lm
## address sanitizer linker flags
SAN_LDFLAGS=$(findstring sanitize=address, $(CFLAGS))
ifeq ($(SAN_LDFLAGS), sanitize=address)
LDFLAGS += $(SAN_LDFLAGS_DEPENDED_ON)
LDFLAGS += -Wl,--whole-archive
LDFLAGS += -l:libasan.a
LDFLAGS += -Wl,--no-whole-archive
LDFLAGS += $(SAN_LDFLAGS_DEPENDED_ON)
endif
完整的例子如下:
这是 makefile。
其中 定义了 4种类型的 Sanitizer CFLAGS。
在使用的时候,只需要设置 SAN_CFLAGS
,让其指定使用其中一种就可以了。
LDFLAGS
会在后面自动识别。
#-------------------------------------------------------------------------------
TARGET = a.out
OBJS = main.o
CC = gcc
#-------------------------------------------------------------------------------
# compiler flag
## base linker flag
CFLAGS :=
#...............................................................................
## version compiler flag
VERSION_CFLAGS = -O0 -g3
## sanitizer compiler flags
ASAN_CFLAGS = -fsanitize=address -static-libasan -fno-omit-frame-pointer
LSAN_CFLAGS = -fsanitize=leak -static-liblsan -fno-omit-frame-pointer
TSAN_CFLAGS = -fsanitize=thread -static-libtsan -fno-omit-frame-pointer
UBSAN_CFLAGS = -fsanitize=undefined -static-libubsan -fno-omit-frame-pointer
SAN_CFLAGS = $(ASAN_CFLAGS)
#...............................................................................
# assemble compiler flag
CFLAGS += $(VERSION_CFLAGS)
CFLAGS += $(SAN_CFLAGS)
#-------------------------------------------------------------------------------
# linker flag
## base linker flag
LDFLAGS :=
#...............................................................................
## sanitizer linker flags depended on
SAN_LDFLAGS_DEPENDED_ON = -lrt -ldl -lrt -lpthread -lm
## address sanitizer linker flags
SAN_LDFLAGS=$(findstring sanitize=address, $(CFLAGS))
ifeq ($(SAN_LDFLAGS), sanitize=address)
LDFLAGS += $(SAN_LDFLAGS_DEPENDED_ON)
LDFLAGS += -Wl,--whole-archive
LDFLAGS += -l:libasan.a
LDFLAGS += -Wl,--no-whole-archive
LDFLAGS += $(SAN_LDFLAGS_DEPENDED_ON)
endif
## link sanitizer linker flags
SAN_LDFLAGS=$(findstring sanitize=leak, $(CFLAGS))
ifeq ($(SAN_LDFLAGS), sanitize=leak)
LDFLAGS += $(SAN_LDFLAGS_DEPENDED_ON)
LDFLAGS += -Wl,--whole-archive
LDFLAGS += -l:liblsan.a
LDFLAGS += -Wl,--no-whole-archive
LDFLAGS += $(SAN_LDFLAGS_DEPENDED_ON)
endif
## thread sanitizer linker flags
SAN_LDFLAGS=$(findstring sanitize=thread, $(CFLAGS))
ifeq ($(SAN_LDFLAGS), sanitize=thread)
LDFLAGS += $(SAN_LDFLAGS_DEPENDED_ON)
LDFLAGS += -Wl,--whole-archive
LDFLAGS += -l:libtsan.a
LDFLAGS += -Wl,--no-whole-archive
LDFLAGS += $(SAN_LDFLAGS_DEPENDED_ON)
endif
## undefined sanitizer linker flags
SAN_LDFLAGS=$(findstring sanitize=undefined, $(CFLAGS))
ifeq ($(SAN_LDFLAGS), sanitize=undefined)
LDFLAGS += $(SAN_LDFLAGS_DEPENDED_ON)
LDFLAGS += -Wl,--whole-archive
LDFLAGS += -l:libubsan.a
LDFLAGS += -Wl,--no-whole-archive
LDFLAGS += $(SAN_LDFLAGS_DEPENDED_ON)
endif
#-------------------------------------------------------------------------------
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(LDFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $<
#-------------------------------------------------------------------------------
clean:
rm -f $(TARGET)
rm -f $(OBJS)
用于测试的C代码如下。
其中是带有内存泄漏的。用于触发 Address Sanitizer 报警。
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
int
main(int argc, char **argv)
{
int *a;
a = malloc(sizeof(int));
return 0;
}
例子的输出:
./a.out
> =================================================================
> ==1433==ERROR: LeakSanitizer: detected memory leaks
>
> Direct leak of 4 byte(s) in 1 object(s) allocated from:
> #0 0x4c35f0 in __interceptor_malloc (/home/andrew/Desktop/AddressSanitizer/a.out+0x4c35f0)
> #1 0x501b25 in main /home/andrew/Desktop/AddressSanitizer/main.c:11
> #2 0x7f5fabc22554 in __libc_start_main (/lib64/libc.so.6+0x22554)
>
> SUMMARY: AddressSanitizer: 4 byte(s) leaked in 1 allocation(s).
可见,Address Sanitizer 检查出了内存泄漏的问题。与我们设想的一致。