让内存错误无所遁形 | GCC -fsanitize=address 实战解析

文摘   2024-09-22 08:20   江苏  

引言

在 C/C++ 编程中,内存错误如越界访问、内存泄漏、未初始化内存的读取等问题十分常见,这些错误通常难以排查和调试。为了帮助开发者检测这类内存问题,GCC 提供了 -fsanitize=address 选项,它通过集成 AddressSanitizer(地址消毒器)对内存问题进行动态检测。本文将介绍如何使用 GCC 的 -fsanitize=address 选项来定位内存问题,深入探讨其工作原理,并给出一个具体的代码演示。

工作原理

-fsanitize=address 是 AddressSanitizer(简称 ASan)的一部分,它通过编译时插入检测代码,在程序运行时进行内存访问监控。具体工作原理如下:

  • 内存区域标记ASan 会在程序启动时创建一个特殊的“影子内存”区域,该区域用来记录和标记程序的实际内存访问状态。每当程序分配或释放内存时,ASan 会更新影子内存中的状态。

  • 动态检测:程序运行时,ASan 会检测每次内存访问的合法性。比如,访问数组时会检测是否越界,访问已释放的内存时会检测是否为“悬空指针”。

  • 错误报告:一旦检测到内存访问违规,ASan 会立刻报告错误,并输出详细的调试信息,包括出错位置、违规的内存地址、堆栈信息等。这使得开发者可以快速定位并修复问题。

日常工作中, -fsanitize=address 主要用来监测以下几类问题:

  1. 堆缓冲区溢出:如访问数组超出范围。
  2. 栈缓冲区溢出:栈上的变量被非法访问。
  3. 全局缓冲区溢出:访问了超出全局变量定义的范围。
  4. 使用已释放的内存:使用了已经通过 free 释放的指针。
  5. 双重释放:对同一块内存重复调用 free
  6. 未初始化的堆内存使用

安装集成 libasan

在使用 -fsanitize=address 时,libasan 是关键组件。它是一个运行时库,负责在程序执行时动态检测内存错误并生成报告。安装和集成 libasan 是确保 AddressSanitizer 正常工作的必要步骤。

在大多数现代 Linux 发行版中,libasan 都已经预装在默认的 GCC 工具链中,因此开发者只需确保使用支持 AddressSanitizer 的 GCC 版本。通常来说,GCC 从 4.8 版本开始就支持 AddressSanitizer(对于 C/C++)。你可以通过运行 gcc --version 来查看你当前使用的 GCC 版本。若系统中未安装,您可以通过包管理器安装:

  • 对于 Ubuntu 或 Debian 系统,可以运行以下命令:
sudo apt-get install libasanX -y

其中,X 代表特定的 GCC 版本,例如 libasan6 适用于 GCC 10libasan5 适用于 GCC 9

  • 对于 Fedora 或 CentOS 系统,可以运行:
sudo dnf install libasan

可以通过以下命令确认安装的版本:

gcc --version

只要编译器支持 ASan 并且安装了相应的库,您就可以使用 -fsanitize=address 选项了。

代码演示

为了演示 -fsanitize=address 的实际效果,我们来看一个简单的示例代码,存在缓冲区溢出的问题。

源文件 overflow_example.c :

#include <stdio.h>
#include <stdlib.h>

int main() {
int *arr = (int*)malloc(5 * sizeof(int));

// 错误:数组越界访问
for (int i = 0; i <= 5; i++) {
arr[i] = i;
}

printf("Array write complete!\n");

free(arr);
return 0;
}

我们首先不使用 -fsanitize=address ,直接编译该程序:

[root@localhost gcc_sanitize]# gcc -o overflow_example overflow_example.c

接着运行该程序:

[root@localhost gcc_sanitize]# ./overflow_example
Array write complete!

根据上面运行结果可知,即使程序中发生了数组访问越界,但是程序依然正常运行,如果不借助相关工具进行检测,我们将无法发现这个问题。这在无形中给我们的程序带来了安全隐患,这种错误可能会引起未定义行为,包括程序崩溃、数据被意外修改或安全漏洞。在一些情况下,它可能不会立即导致问题,但在其他情况下,它可能会破坏程序状态或者让攻击者利用这个漏洞执行恶意代码。

我们再使用 -fsanitize=address 选项编译该程序:

[root@localhost gcc_sanitize]# gcc -fsanitize=address -g overflow_example.c -o overflow_example

在这里,-g 用于生成调试信息,以便 ASan 在输出错误信息时能够提供源代码的行号。

接着运行程序:

[root@localhost gcc_sanitize]# ./overflow_example

运行该程序后,ASan 会检测到数组越界的问题,并输出以下的错误信息:

=================================================================
==10251==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x603000000054 at pc 0x0000004008e1 bp 0x7ffc71bf1010 sp 0x7ffc71bf1000
WRITE of size 4 at 0x603000000054 thread T0
#0 0x4008e0 in main /home/example/gcc_sanitize/overflow_example.c:9
#1 0x7fec0c592d84 in __libc_start_main (/lib64/libc.so.6+0x3ad84)
#2 0x4007bd in _start (/home/example/gcc_sanitize/overflow_example+0x4007bd)

0x603000000054 is located 0 bytes to the right of 20-byte region [0x603000000040,0x603000000054)
allocated by thread T0 here:
#0 0x7fec0ca0cba8 in __interceptor_malloc (/lib64/libasan.so.5+0xefba8)
#1 0x400887 in main /home/example/gcc_sanitize/overflow_example.c:5
#2 0x7fec0c592d84 in __libc_start_main (/lib64/libc.so.6+0x3ad84)

SUMMARY: AddressSanitizer: heap-buffer-overflow /home/example/gcc_sanitize/overflow_example.c:9 in main
Shadow bytes around the buggy address:
0x0c067fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c067fff8000: fa fa 00 00 00 fa fa fa 00 00[04]fa fa fa fa fa
0x0c067fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==10251==ABORTING

在这段输出中,AddressSanitizer (ASan) 报告了 堆缓冲区溢出heap-buffer-overflow)的问题。下面,我将详细解析这个错误报告的每一部分。

  1. 错误摘要
=================================================================
==10251==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x603000000054 at pc 0x0000004008e1 bp 0x7ffc71bf1010 sp 0x7ffc71bf1000
WRITE of size 4 at 0x603000000054 thread T0
  • ERROR: AddressSanitizer: heap-buffer-overflow

    • heap-buffer-overflow 是一种错误,意味着程序试图访问堆上分配的内存之外的地址。这里具体发生的是缓冲区溢出,通常是由于对数组或缓冲区的索引越界访问。
  • address 0x603000000054

    • 发生溢出的地址是 0x603000000054,它是程序试图访问的内存地址。
  • at pc 0x0000004008e1

    • pc 表示程序计数器(Program Counter),即程序执行时的指令地址。地址 0x0000004008e1 是发生错误的程序指令的内存地址。
  • bp 0x7ffc71bf1010 sp 0x7ffc71bf1000

    • bp 是基指针(Base Pointer),sp 是栈指针(Stack Pointer),它们指向当前栈帧和栈顶,帮助调试器跟踪函数调用和局部变量。对栈帧分析有帮助,但对解决这个内存问题用处不大。
  • WRITE of size 4 at 0x603000000054

    • 程序试图向地址 0x603000000054 写入 4 字节 的数据。通常,写入 4 字节表示程序在写入一个 int 类型的数据(因为大多数系统中,int 是 4 字节)。
  • thread T0

    • T0 表示这是主线程(主程序)的错误,因为在这个简单的程序中只有一个线程。
  1. 调用栈(Call Stack)
#0 0x4008e0 in main /home/example/gcc_sanitize/overflow_example.c:9
#1 0x7fec0c592d84 in __libc_start_main (/lib64/libc.so.6+0x3ad84)
#2 0x4007bd in _start (/home/example/gcc_sanitize/overflow_example+0x4007bd)

这是程序执行时发生错误的调用栈信息。

  • #0 0x4008e0 in main /home/example/gcc_sanitize/overflow_example.c:9
    • 错误发生在 main 函数中, overflow_example.c 文件的第 9 行 (overflow_example.c:9)。这条栈帧指向程序的核心问题,即试图访问一个越界的数组元素或缓冲区。
  • #1 0x7fec0c592d84 in __libc_start_main (/lib64/libc.so.6+0x3ad84)
    • 这个栈帧属于标准 C 库的启动函数 __libc_start_main,它是用来启动 main 函数的。
  • #2 0x4007bd in _start (/home/example/gcc_sanitize/overflow_example+0x4007bd)
    • 这是程序启动时的底层汇编调用栈,位于 _start 函数中。通常不需要深入分析这个栈帧。
  1. 堆内存分配的细节
0x603000000054 is located 0 bytes to the right of 20-byte region [0x603000000040,0x603000000054)
allocated by thread T0 here:
#0 0x7fec0ca0cba8 in __interceptor_malloc (/lib64/libasan.so.5+0xefba8)
#1 0x400887 in main /home/example/gcc_sanitize/overflow_example.c:5
#2 0x7fec0c592d84 in __libc_start_main (/lib64/libc.so.6+0x3ad84)

这里显示了错误发生时相关内存的分配情况:

  • 0x603000000054 is located 0 bytes to the right of 20-byte region [0x603000000040,0x603000000054)

    • 地址 0x603000000054 刚好是一个 20 字节堆内存区域的边界([0x603000000040, 0x603000000054))。这意味着程序试图访问数组或缓冲区的下一个元素,而实际上它正位于分配内存的末尾。这个行为导致了 越界写入 错误。
  • allocated by thread T0 here

    • 该段内存是由 malloc 分配的。

    • #0 0x7fec0ca0cba8 in __interceptor_malloc (/lib64/libasan.so.5+0xefba8):堆内存是在 __interceptor_malloc 中分配的,这是 AddressSanitizer 用来拦截和跟踪内存分配的函数。

    • #1 0x400887 in main /home/example/gcc_sanitize/overflow_example.c:5:堆内存的分配发生在 main 函数中,overflow_example.c 文件的第 5 行 (overflow_example.c:5)。这通常是一个 malloc 函数调用,用于分配数组或缓冲区。

  1. 错误总结
SUMMARY: AddressSanitizer: heap-buffer-overflow /home/example/gcc_sanitize/overflow_example.c:9 in main

这一行总结了错误:

  • SUMMARY: AddressSanitizer: heap-buffer-overflow

    • ASan 检测到堆缓冲区溢出。
  • /home/example/gcc_sanitize/overflow_example.c:9 in main

    • 问题发生在 main 函数中,overflow_example.c 文件的第 9 行。
  1. Shadow Bytes(影子字节)
Shadow bytes around the buggy address:
0x0c067fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c067fff8000: fa fa 00 00 00 fa fa fa 00 00[04]fa fa fa fa fa
0x0c067fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa

影子字节 显示了程序访问的内存区域的状态。ASan 将内存分成影子字节(Shadow Bytes),用于标记内存的有效性和分配状态。

  • fa fa 表示这个区域是堆内存的 "左边界" 或 "红区"(redzone),即不可访问的内存区域。AddressSanitizer 在堆内存块周围设置 "红区",当程序试图访问红区时,它会检测到非法访问。

  • [04] 标记了导致错误的具体字节位置。这是程序试图访问的越界地址。

  1. Shadow Byte Legend(影子字节含义)
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb

这是 AddressSanitizer 提供的影子字节含义说明,用来帮助分析不同类型的内存错误。我们这里的 fa 表示 "堆左红区"(heap left redzone),这是由 ASan 添加的保护区域,用于检测越界访问。

  1. ==10251==ABORTING

这一句的含义是程序因为严重错误而 终止执行。这是 AddressSanitizer 检测到内存问题(在本例中是 heap-buffer-overflow,即堆缓冲区溢出)之后主动中止程序执行的标志。

详细解释如下:

  • ==10251==: 这是程序的进程 IDPID),在这次运行中,程序的 PID 是 10251。每个正在运行的程序都会被操作系统分配一个唯一的 PID,方便识别和管理。

  • ABORTING: 表示程序因为遇到了无法继续执行的错误而被中止。在这种情况下,AddressSanitizer 发现了严重的内存问题(如缓冲区溢出),如果不终止程序,继续运行可能会导致崩溃、数据损坏或其他不可预期的后果。为保证程序的安全性,AddressSanitizer 决定直接中止执行。

因此,==10251==ABORTING 表示 PID 为 10251 的程序因为 AddressSanitizer 检测到的错误而被迫终止,防止进一步执行造成更严重的问题。


从以上分析可以推测,代码试图在超出堆分配的缓冲区或数组边界时写入数据。这个问题通常是因为程序员试图访问超过数组或指针有效范围的元素。可能的修复方法如下:

  • 检查数组的大小和下标,确保访问不超过分配的范围。
  • 如果使用了 malloc 动态分配内存,请确保为数组或缓冲区分配足够的空间。
  • 使用像 calloc 这样的方法来分配并初始化内存,有助于避免一些潜在的未初始化问题。

我们可以很容易地修复这个问题,确保循环范围正确:

for (int i = 0; i < 5; i++) {
arr[i] = i;
}

修复后,使用 -fsanitize=address 再次编译并运行程序,不会再出现溢出错误。结果如下:

[root@localhost gcc_sanitize]# gcc -fsanitize=address -g overflow_example.c -o overflow_example
[root@localhost gcc_sanitize]# ./overflow_example
Array write complete!

总结

GCC 提供的 -fsanitize=address 是一个强大的工具,可以帮助开发者检测和定位各种内存相关的 Bug。通过实时监控程序的内存操作,ASan 能够快速发现问题并生成详细的错误报告,极大地提高了调试的效率。尤其是在处理复杂的内存操作时,ASan 是不可或缺的调试助手。

如果你在开发过程中遇到了难以复现或定位的内存 Bug,不妨试试 -fsanitize=address,相信它会为你节省大量的时间和精力。


Linux二进制
学习并分享Linux的相关技术,网络、内核、驱动、容器等。
 最新文章