最近更新于 2022-04-05 12:37

发布于 blog.iyatt.com

简介

sanitize 本身是来自于 Google 的开源项目 sanitizers,而 sanitizer 本身又属于 llvm 的一部分。sanitizer 对于 C/C++ 代码调试而言十分方便,当程序出现内存泄漏、可寻址性问题、未初始化内存使用等情况,程序自己就会终止并会提示出现问题的地方。GNU(gcc/g++)编译器从 4.8 开始就已经支持 sanitizer 的功能了,编译时添加选项参数就能加入 sanitizer 检测。

测试环境

(1) 树莓派官方系统 64 位(Debian 11 arm64)

GNU gcc 10.2.1

(2)Windows 子系统 Ubuntu 20.04

GNU gcc 9.4.0

(3)Ubuntu 20.04 x86_64

GNU gcc 9.4.0

sanitize 参数选项使用

gcc 选项参数官方说明文档:https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html

这里主要是介绍几个常用的 sanitizer 参数,更多可以查阅上面的文档。

(1) -fsanitize=address

AddressSanitizer 主要是检查内存相关的错误,不能和 -fsanitize=thread 参数共用。

(2)-fsanitize=leak

LeakSanitizer 主要是检查内存泄露,不能和 -fsanitize=thread 参数共用。

(3)-fsanitize=undefined

UndefinedBehaviorSanitizer 用于检查 C/C++ 标准未定义的行为(简称 ub),下面我举一个例子。

#include <stdio.h>


int main()
{
    int i = 3;
    printf("%d\n", i++ * i++);
}

在 C 语言的标准中,并没有定义 i++ * i++ 如何计算,不同编译器执行的结果可能会不同。

MSVC 2022 编译运行结果为 9,即先计算的 i * i

GNU gcc 10.2.1 编译运行结果为 12,即前一个 i 先自增再计算的 i * i

对于开发程序而言,这就可能是一个 bug 了,标准没有规定会发生什么,则什么都可能发生。

对于上面的示例代码用参数 -Werror=sequence-point 在编译时也能检查出来,但是 ub 并不是只有这一种,其它的 ub 可能不在这个参数检查范围内。

写到这里突然又觉得尴尬了,本来我以为 -fsanitize=undefined 能够检查所有的 ub,结果发现我上面举例的情况就检查不出来,还能正常运行。

然后我去看了下文档,似乎这个不在检查范围内,不过问题也不大,实际调试项目的时候也不只是用 sanitize,还有其它参数配合使用(见文末)

(4)-fsanitize=thread

ThreadSanitizer 用于检查数据竞争,用在多线程程序中。多个线程在未能成功加锁的情况下访问同一数据,并且至少有一个线程是写操作,而产生读和写的竞争。不能和 -fsanitize=address 及 -fsanitize=leak共用。

测试案例

int main()
{
    int nums[2];
    nums[0] = 0;
    nums[1] = 1;
    nums[2] = 2;
}

上面代码定义了一个容量为 2 的数组,但是却试图向不存在的第 3 个位置写数据,就引起了栈缓存访问溢出的错误,如果不使用上面的参数,可能程序会正常运行,但是又将是一个潜在的 bug,说不定什么时候就导致程序崩溃了。通过 sanitize 编译,执行程序时就会指出出错的地方和原因。

当然上面是属于 sanitize 的参数,gcc 还有其它很多参数可以用于检查代码问题,下面是我调试用的参数,可以检查各种常见的问题。

-no-pie -std=c17 -Wall -Werror=return-type -Werror=address -Werror=sequence-point -Werror=format-security -Wextra -pedantic -Wimplicit-fallthrough -Wsequence-point -Wswitch-unreachable -Wswitch-enum -Wstringop-truncation -Wbool-compare -Wtautological-compare -Wfloat-equal -Wshadow=global -Wpointer-arith -Wpointer-compare -Wcast-align -Wcast-qual -Wwrite-strings -Wdangling-else -Wlogical-op -Wconversion -g -O0

① 其中 -no-pie 会让编译出的程序性质为可执行程序,不加这个参数时,gcc 编译出的二进制文件就是动态库类型,通过终端命令可以运行,但是在文件管理器双击就不能运行了,会被当做是普通文件。

pie

no-pie

PIE全称是position-independent executable,中文解释为地址无关可执行文件,该技术是一个针对代码段(.text)、数据段(.data)、未初始化全局变量段(.bss)等固定地址的一个防护技术,如果程序开启了PIE保护的话,在每次加载程序时都变换加载地址,gcc 默认就是开启 pie,通过指定 -no-pie 可以关闭,那么程序就会被分配静态的地址,每次运行都一样。

因为 pie 程序无法直接双击运行,一般编写程序我会指定 -no-pie,如果是写库就没必要加 -no-pie 了。当然对于一般的程序能不能双击运行也没有关系,都是在终端中运行的,但是对于图形用户界面程序还是要能够直接双击运行最好。

② -std=c17 是指定编译标准,目前的 C语言标准有:C89, C95, C99, C11, C17 以及正在制定中的 c23,对于标准支持的情况也要看编译器版本,标准代号大致就对应了发布年份,一个 2010 发布的编译器肯定也无法支持 c17,那时候都还没有 c17。

可以查看编译版本判断年份,这里就能看到 Copyright 2020,晚于 17,实际也是支持 c17 的

也可以执行命令尝试,不支持会提示

目前我测试和构建都是基于 C17

ps: C++ 标准代号:C++98, C++03, C++11, C++14, C++17, C++20, C++23

③ -g 就是编译时向向二进制文件中添加调试信息,可以使用 gdb 调试运行编译的程序。