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

原创 Linux二进制 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 
#include

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编程、内核模块、网络原创文章分享,欢迎关注"Linux二进制"微信公众号
评论
  • 在智能家居领域中,Wi-Fi、蓝牙、Zigbee、Thread与Z-Wave等无线通信协议是构建短距物联局域网的关键手段,它们常在实际应用中交叉运用,以满足智能家居生态系统多样化的功能需求。然而,这些协议之间并未遵循统一的互通标准,缺乏直接的互操作性,在进行组网时需要引入额外的网关作为“翻译桥梁”,极大地增加了系统的复杂性。 同时,Apple HomeKit、SamSung SmartThings、Amazon Alexa、Google Home等主流智能家居平台为了提升市占率与消费者
    华普微HOPERF 2025-01-06 17:23 141浏览
  • 大模型的赋能是指利用大型机器学习模型(如深度学习模型)来增强或改进各种应用和服务。这种技术在许多领域都显示出了巨大的潜力,包括但不限于以下几个方面: 1. 企业服务:大模型可以用于构建智能客服系统、知识库问答系统等,提升企业的服务质量和运营效率。 2. 教育服务:在教育领域,大模型被应用于个性化学习、智能辅导、作业批改等,帮助教师减轻工作负担,提高教学质量。 3. 工业智能化:大模型有助于解决工业领域的复杂性和不确定性问题,尽管在认知能力方面尚未完全具备专家级的复杂决策能力。 4. 消费
    丙丁先生 2025-01-07 09:25 80浏览
  • 根据Global Info Research项目团队最新调研,预计2030年全球封闭式电机产值达到1425百万美元,2024-2030年期间年复合增长率CAGR为3.4%。 封闭式电机是一种电动机,其外壳设计为密闭结构,通常用于要求较高的防护等级的应用场合。封闭式电机可以有效防止外部灰尘、水分和其他污染物进入内部,从而保护电机的内部组件,延长其使用寿命。 环洋市场咨询机构出版的调研分析报告【全球封闭式电机行业总体规模、主要厂商及IPO上市调研报告,2025-2031】研究全球封闭式电机总体规
    GIRtina 2025-01-06 11:10 104浏览
  • By Toradex 秦海1). 简介嵌入式平台设备基于Yocto Linux 在开发后期量产前期,为了安全以及提高启动速度等考虑,希望将 ARM 处理器平台的 Debug Console 输出关闭,本文就基于 NXP i.MX8MP ARM 处理器平台来演示相关流程。 本文所示例的平台来自于 Toradex Verdin i.MX8MP 嵌入式平台。  2. 准备a). Verdin i.MX8MP ARM核心版配合Dahlia载板并
    hai.qin_651820742 2025-01-07 14:52 44浏览
  • PLC组态方式主要有三种,每种都有其独特的特点和适用场景。下面来简单说说: 1. 硬件组态   定义:硬件组态指的是选择适合的PLC型号、I/O模块、通信模块等硬件组件,并按照实际需求进行连接和配置。    灵活性:这种方式允许用户根据项目需求自由搭配硬件组件,具有较高的灵活性。    成本:可能需要额外的硬件购买成本,适用于对系统性能和扩展性有较高要求的场合。 2. 软件组态   定义:软件组态主要是通过PLC
    丙丁先生 2025-01-06 09:23 85浏览
  • 根据环洋市场咨询(Global Info Research)项目团队最新调研,预计2030年全球无人机锂电池产值达到2457百万美元,2024-2030年期间年复合增长率CAGR为9.6%。 无人机锂电池是无人机动力系统中存储并释放能量的部分。无人机使用的动力电池,大多数是锂聚合物电池,相较其他电池,锂聚合物电池具有较高的能量密度,较长寿命,同时也具有良好的放电特性和安全性。 全球无人机锂电池核心厂商有宁德新能源科技、欣旺达、鹏辉能源、深圳格瑞普和EaglePicher等,前五大厂商占有全球
    GIRtina 2025-01-07 11:02 68浏览
  • 这篇内容主要讨论三个基本问题,硅电容是什么,为什么要使用硅电容,如何正确使用硅电容?1.  硅电容是什么首先我们需要了解电容是什么?物理学上电容的概念指的是给定电位差下自由电荷的储藏量,记为C,单位是F,指的是容纳电荷的能力,C=εS/d=ε0εrS/4πkd(真空)=Q/U。百度百科上电容器的概念指的是两个相互靠近的导体,中间夹一层不导电的绝缘介质。通过观察电容本身的定义公式中可以看到,在各个变量中比较能够改变的就是εr,S和d,也就是介质的介电常数,金属板有效相对面积以及距离。当前
    知白 2025-01-06 12:04 170浏览
  • 村田是目前全球量产硅电容的领先企业,其在2016年收购了法国IPDiA头部硅电容器公司,并于2023年6月宣布投资约100亿日元将硅电容产能提升两倍。以下内容主要来自村田官网信息整理,村田高密度硅电容器采用半导体MOS工艺开发,并使用3D结构来大幅增加电极表面,因此在给定的占位面积内增加了静电容量。村田的硅技术以嵌入非结晶基板的单片结构为基础(单层MIM和多层MIM—MIM是指金属 / 绝缘体/ 金属) 村田硅电容采用先进3D拓扑结构在100um内,使开发的有效静电容量面积相当于80个
    知白 2025-01-07 15:02 75浏览
  •     为控制片内设备并且查询其工作状态,MCU内部总是有一组特殊功能寄存器(SFR,Special Function Register)。    使用Eclipse环境调试MCU程序时,可以利用 Peripheral Registers Viewer来查看SFR。这个小工具是怎样知道某个型号的MCU有怎样的寄存器定义呢?它使用一种描述性的文本文件——SVD文件。这个文件存储在下面红色字体的路径下。    例:南京沁恒  &n
    电子知识打边炉 2025-01-04 20:04 100浏览
  • 每日可见的315MHz和433MHz遥控模块,你能分清楚吗?众所周知,一套遥控设备主要由发射部分和接收部分组成,发射器可以将控制者的控制按键经过编码,调制到射频信号上面,然后经天线发射出无线信号。而接收器是将天线接收到的无线信号进行解码,从而得到与控制按键相对应的信号,然后再去控制相应的设备工作。当前,常见的遥控设备主要分为红外遥控与无线电遥控两大类,其主要区别为所采用的载波频率及其应用场景不一致。红外遥控设备所采用的射频信号频率一般为38kHz,通常应用在电视、投影仪等设备中;而无线电遥控设备
    华普微HOPERF 2025-01-06 15:29 125浏览
  • 彼得·德鲁克被誉为“现代管理学之父”,他的管理思想影响了无数企业和管理者。然而,关于他的书籍分类,一种流行的说法令人感到困惑:德鲁克一生写了39本书,其中15本是关于管理的,而其中“专门写工商企业或为企业管理者写的”只有两本——《为成果而管理》和《创新与企业家精神》。这样的表述广为流传,但深入探讨后却发现并不完全准确。让我们一起重新审视这一说法,解析其中的矛盾与根源,进而重新认识德鲁克的管理思想及其著作的真正价值。从《创新与企业家精神》看德鲁克的视角《创新与企业家精神》通常被认为是一本专为企业管
    优思学院 2025-01-06 12:03 116浏览
  • 本文介绍Linux系统更换开机logo方法教程,通用RK3566、RK3568、RK3588、RK3576等开发板,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。制作图片开机logo图片制作注意事项(1)图片必须为bmp格式;(2)图片大小不能大于4MB;(3)BMP位深最大是32,建议设置为8;(4)图片名称为logo.bmp和logo_kernel.bmp;开机
    Industio_触觉智能 2025-01-06 10:43 87浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦