Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >LLVM 工具系列 - Address Sanitizer 基本原理介绍及案例分析(1)

LLVM 工具系列 - Address Sanitizer 基本原理介绍及案例分析(1)

作者头像
JoeyBlue
发布于 2023-01-08 01:14:35
发布于 2023-01-08 01:14:35
2.7K00
代码可运行
举报
文章被收录于专栏:代码手工艺人代码手工艺人
运行总次数:0
代码可运行

Address Sanitizer 介绍

LLVM 提供了一系列的工具帮助 C/C++/Objc/Objc++ 开发者检查代码中可能的潜在问题,这些工具包括 Address Sanitizer,Memory Sanitizer,Thread Sanitizer,XRay 等等, 功能各异。

本篇主要介绍可能是最常用的一个工具 Address Sanitizer,它的主要作用是帮助开发者在运行时检测出内存地址访问的问题,比如访问了释放的内存,内存访问越界等。

全部种类如下,也都是非常常见的几类内存访问问题。

  1. Use after free
  2. Heap buffer overflow
  3. Stack buffer overflow
  4. Global buffer overflow
  5. Use after return,
  6. Use after scope
  7. Initialization order bugs
  8. Memory leaks

这里为了便于理解,先介绍一下大概的工作原理。然后从上面几种场景中挑出几个有代表性的介绍一下。

Address Sanitizer 的基本工作原理

我们对一个内存地址的 访问 无外乎两种操作:,也就是

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
*address = ...;  // 写操作
... = *address;  // 读操作

Address Sanitizer 的工作依赖编译器运行时库,当开启 Address Sanitizer 之后, 运行时库将会替换掉 mallocfree 函数,在 malloc 分配的内存区域前后设置“投毒”(poisoned)区域, 使用 free 释放之后的内存也会被隔离并投毒,poisoned 区域也被称为 redzone

这样对内存的访问,编译器会在编译期自动在所有内存访问之前做一下 check 是否被“投毒”。所以以上的代码,就会被编译器改成这样:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if (IsPoisoned(address)) {
  ReportError(address, kAccessSize, kIsWrite);
}
*address = ...;  // or: ... = *address;

这样的话,当我们不小心访问越界,访问到 poisoned 的内存(redzone),就会命中陷阱,在运行时 crash 掉,并给出有帮助的内存位置的信息,以及出问题的代码位置,方便开发者排查和解决。

Note: 从基本工作原理来看,我们可以获知,打开 Address Sanitizer 会增加内存占用,且因为所有的内存访问之前都会有 check 是否访问了“投毒”区域的内存,会有额外的运行开销,对运行性能造成一定的影响,因此通常只在 Debug 模式或测试场景下打开

更详细的原理参考第二篇 // TODO

如何开启 Address Sanitizer

默认 clang 是不打开 Address Sanitizer 的,需要增加 -fsanitize=address -g 参数,-g 用来在出现问题的报告中,增加有助于 debug 的信息,比如出问题的代码位置和行数等,非常建议带上。

如何使用我们在下个例子里进行展示。

分析一个 Use after free 的 case

来看一个简单的例子, test_use_after_free.c 文件有以下内容:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  int *p = malloc(sizeof(int));
  free(p);
  return *p;  // 访问了已经释放的内存地址
}

这段代码很简单,在堆上创建了一块 int 大小的内存,随后释放,然后 *p 来读取位于 p 内存地址的值,显然是有问题的。实际场景往往会更杂,free 的位置和访问的位置可能离得很远,不容易发现,而且编译期并不会提示错误。

编译:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
clang -fsanitize=address -g test_use_after_free.c -o use_after_free

运行之后crash,并提供给我们一些错误信息:

这些错误信息很重要,可以协助我们排查出现问题的位置。我们从上往下看,第一行告诉我们了内存地址访问错误类型为 heap-use-after-free,并给出了地址和寄存器的值:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
==65906==ERROR: AddressSanitizer: heap-use-after-free on address 0x000105000730 at pc 0x000102c57f48 bp 0x00016d1ab190 sp 0x00016d1ab188

接下来就是告诉我们是在 test_use_after_free.c 文件的 第 7 行 Read 时出的问题,也就是 return *p 时出现的问题。 接着就是该内存区域是在哪里释放的,就是第 6 行, 以及之前在哪里分配的,也就是第 5 行。 可以说非常清晰。

接下来就是 Shadow 的 bytes,具体这里先按下不表,放到下篇具体实现原理里来具体解释。从图上我标记的箭头可以看出访问的是一块已经释放的堆内存。

Heap buffer overflow 堆内存溢出的 case

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// heap-buffer-overflow.cpp
int main(int argc, char **argv) {
  int *array = new int[100];
  array[0] = 0;
  int res = array[100];  // 内存地址访问越界
  delete [] array;
  return res;
}

编译,这里用的是 C++,因此加上 -lc++ 来使用 libc++ 库

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
clang -fsanitize=address -g -lc++ test_heap_buffer_overflow.cpp -o heap_buffer_overflow

运行 & 错误信息:

分析: 第一行告诉我们错误类型为 heap-buffer-overflow,访问出错的内存地址为 0x00010613a7d4, 我们先记下来。

然后告诉我们是第 5 行的 读操作 导致的, 也就是 int res = array[100]; 这里。

接下来的信息是告诉我们出现错误读操作的内存地址 0x00010613a7d4 是位于 400 bytes 内存的右边 4 个 byte 的位置,根据代码,我们知道这 400bytes,其实就是代码中创建的 100 个 int 值所在的内存地址。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
0x00010403a7d4 is located 4 bytes to the right of 400-byte region [0x00010403a640,0x00010403a7d0)
allocated by thread T0 here:
    #0 0x1025de018 in wrap__Znam+0x74 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x4e018)
    #1 0x1021d3e6c in main test_heap_buffer_overflow.cpp:3
    #2 0x193e4be4c  (<unknown module>)

但实际中往往更复杂,访问的内存可能是距离很远的一块内存上,虽然也可以从这段错误信息里的 allocated by 的堆栈中找到实际分配这块的内存地址的位置,但是可能跟这个访问地址并没有什么关联,要注意辨别。

我们来这样模拟一下,在 array 后面再创建一个 array2,分配 100 个 int 的空间,然后访问 array 的时候,让其越界到 array2 的后面。为了方便查看,我们这里打印出来 array 和 array2 的内存地址范围。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <cstdio>
int main(int argc, char **argv) {
  int *array = new int[100];
  printf("array: %p\n", array);
  array[0] = 0;
  int *array2 = new int[100];
  printf("array2: %p\n", array2);
  int res = array[(array2-array + 100)];  // 首先肯定是越界了,甚至越界到 array2 的右边区域了
  delete [] array;
  return res;
}

我们来看下错误信息:

第二段错误信息里,相当于告诉我们访问的这块内存位于 array2 的紧挨着的右边的位置, 但是这个内存位置其实和访问出错并无关系,此时,这个位置信息价值就不大了,应该参考第一段错误信息(红框位置),根据出现访问问题的源代码位置来分析即可,第二段相当于一个辅助的信息。

Note: 到这里大家可能会思考一个问题,如果上面访问 array 的代码,正好越界到 array2 的地址合法范围内,比如,int res = array[(array2-array + 1)], 会不会被检测到并 crash 呢? 很遗憾,这种 case 虽然越界了,但根据前面的运行原理来看,访问的内存区域并未被“投毒”(poisoned),因此不会被检测到越界,也不会 crash。

最后我们再看一个检查内存泄漏的 case。

分析一个 Memory leak 的 case

我们在 test_memory_leak.cpp 模拟一个 leak:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <stdlib.h>

class BadClass {
public:
  BadClass(int value): value_(new int(value)) {}
  ~BadClass() {
    // 没有 delete value_ 导致泄漏
  }

private:
  int *value_;
};

int main() {
  BadClass *bad = new BadClass(10);
  delete bad;
  return 0;
}

Note: Memory leak 检测目前不支持 ARM,因此 M1 芯片的 MBP 也是不支持的, 运行时会出现以下的错误提示。 ASAN_OPTIONS=detect_leaks=1 ./test_memory_leak.out ==39355==AddressSanitizer: detect_leaks is not supported on this platform. [1] 39355 abort ASAN_OPTIONS=detect_leaks=1 ./test_memory_leak.out

这里我在 X86_64 的 Linux 机器上进行测试。

编译:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
clang -fsanitize=address -g -lstdc++ test_memory_leak.cpp -o test_memory_leak

运行:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# LeakSanitizer 在 X86 的 linux 上开启 Address Sanitizer 时默认打开的,因此直接运行即可
./test_memory_leak
# 如果是 Intel 版本的 macos,默认没有打开 LeakSanitizer,需要在运行前面增加一个环境变量来开启
ASAN_OPTIONS=detect_leaks=1 ./test_memory_leak

运行结果:

第一行告诉我们检测到了内存泄露,然后告诉我们泄漏了一个对象,共 4 个字节。泄漏的的位置是在 test_memory_leak.cpp 文件的第 15 行。

Summary

内存问题是 C/C++ 项目中比较头疼的问题,为了解决这类的问题,本篇文章主要介绍了 LLVM 的 Address Sanitizer 工具,以及基本的工作的原理;接着分析了 C/C++ 中几种常见的内存地址访问错误的 case,以及如何从错误信息中提取关键的信息进行排查问题。

其余的几种内存问题,大家可以自行模拟来尝试,非常建议在开发阶段 Debug 或者测试场景中打开 Address Sanitizer 提前暴露很多内存问题。

Ref & 扩展阅读

  1. Google AddressSanitizer Wiki
  2. Hardware-assisted AddressSanitizer
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-01-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
LLVM 工具系列 - Address Sanitizer 基本原理介绍及案例分析(1)
LLVM 提供了一系列的工具帮助 C/C++/Objc/Objc++ 开发者检查代码中可能的潜在问题,这些工具包括 Address Sanitizer,Memory Sanitizer,Thread Sanitizer,XRay 等等, 功能各异。
JoeyBlue
2023/01/08
2.7K0
LLVM 工具系列 - Address Sanitizer 基本原理介绍及案例分析(1)
LLVM-插桩
1.4 在llvm同级目录下新建llvm_build和llvm_release两个文件夹,llvm是编译起始文件夹,llvm_release则是编译结果文件夹
Helloted
2022/06/08
2.1K0
LLVM-插桩
某车联网App 通讯协议加密分析(四) Trace Code
之前我们已经通过Trace Block 来比对了Unidbg和App跑的结果。现在他们运行的流程都差不多了,但是结果还是不对,今天我们就要通过Trace Code进行更细致的对比。
奋飞安全
2022/09/21
1.3K1
ASAN和HWASAN原理解析
由于虚拟机的存在,Android应用开发者们通常不用考虑内存访问相关的错误。而一旦我们深入到Native世界中,原本面容和善的内存便开始凶恶起来。这时,由于程序员写法不规范、逻辑疏漏而导致的内存错误会统统跳到我们面前,对我们嘲讽一番。
Linux阅码场
2020/07/07
4K0
ASAN和HWASAN原理解析
使用 LLVM 实现一个简单编译器
作者:tomoyazhang,腾讯 PCG 后台开发工程师 1. 目标 这个系列来自 LLVM 的Kaleidoscope 教程,增加了我对代码的注释以及一些理解,修改了部分代码。现在开始我们要使用 LLVM 实现一个编译器,完成对如下代码的编译运行。 # 斐波那契数列函数定义 def fib(x)     if x < 3 then         1     else         fib(x - 1) + fib(x - 2) fib(40) # 函数声明 extern sin(arg)
腾讯技术工程官方号
2021/09/18
3.1K0
asan内存检测工具实例
GCC和CLANG都已经集成了功能,编译时加编译选项即可。主要是-fsanitize=address,其他便于调试。
mingjie
2023/10/13
6650
Android Address Sanitizer (ASan) 原理简介
前面介绍了 NDK 开发中快速上手使用 ASan 检测内存越界等内存错误的方法,现分享一篇关于 ASan 原理介绍的文章。
字节流动
2021/06/09
5.4K0
Android Address Sanitizer (ASan) 原理简介
自动更新基址原理与思路
需要注意的是,这里的机器码尽量多弄一下,这样来达到机器码是唯一的别的地方不能会重复。
用户8671053
2021/09/26
5850
AddressSanitizer算法及源码解析
AddressSanitizer是Google用于检测内存各种buffer overflow(Heap buffer overflow, Stack buffer overflow, Global buffer overflow)的一个非常有用的工具。该工具是一个LLVM的Pass,现已集成至llvm中,要是用它可以通过-fsanitizer=address选项使用它。AddressSanitizer的源码位于/lib/Transforms/Instrumentation/AddressSanitizer.cpp中,Runtime-library的源码在llvm的另一个项目compiler-rt的/lib/asan文件夹中。
Linux阅码场
2019/10/08
3.2K0
AddressSanitizer算法及源码解析
[2] 使用 LLVM 实现一门简单的语言
IR 指中间表达方式,介于高级语言和汇编语言之间。与高级语言相比,丢弃了语法和语义特征,比如作用域、面向对象等;与汇编语言相比,不会有硬件相关的细节,比如目标机器架构、操作系统等。
谛听
2022/03/06
2.6K0
为什么人人都该懂点LLVM
只要你和程序打交道,了解编译器架构就会令你受益无穷——无论是分析程序效率,还是模拟新的处理器和操作系统。通过本文介绍,即使你对编译器原本一知半解,也能开始用LLVM,来完成有意思的工作。
用户8710643
2021/06/09
1.7K0
理解go中空结构体的应用和实现原理
那为什么要这样使用空结构体呢?今天就跟大家一起来学习下空结构体的应用以及底层原理。
Go学堂
2023/01/31
3700
KASAN实现原理【转】
KASAN是一个动态检测内存错误的工具。KASAN可以检测全局变量、栈、堆分配的内存发生越界访问等问题。功能比SLUB DEBUG齐全并且支持实时检测。越界访问的严重性和危害性通过我之前的文章(SLUB DEBUG技术)应该有所了解。正是由于SLUB DEBUG缺陷,因此我们需要一种更加强大的检测工具。难道你不想吗?KASAN就是其中一种。KASAN的使用真的很简单。但是我是一个追求刨根问底的人。仅仅止步于使用的层面,我是不愿意的,只有更清楚的了解实现原理才能更加熟练的使用工具。不止是KASAN,其他方面我也是这么认为。但是,说实话,写这篇文章是有点底气不足的。因为从我查阅的资料来说,国内没有一篇文章说KASAN的工作原理,国外也是没有什么文章关注KASAN的原理。大家好像都在说How to use。由于本人水平有限,就根据现有的资料以及自己阅读代码揣摩其中的意思。本文章作为抛准引玉,如果有不合理的地方还请指正。
233333
2019/01/03
2.6K0
浅谈「内存调试技术」
内存问题在 C/C++ 程序中十分常见,比如缓冲区溢出,使用已经释放的堆内存,内存泄露等。
天存信息
2021/05/11
1K0
浅谈「内存调试技术」
C++ 中文周刊 2024-03-03 第150期
RSS https://github.com/wanghenshui/cppweeklynews/releases.atom
王很水
2024/07/30
1140
C++ 中文周刊 2024-03-03 第150期
Java NIO实现原理之Buffer
nio是基于事件驱动模型的非阻塞io,这篇文章简要介绍了nio,本篇主要介绍Buffer的实现原理。
Monica2333
2020/06/19
5380
libfuzzer 文档
就是变异,覆盖率那些都给你做好了,你只需要定义LLVMFuzzerTestOneInput,将编译的数据喂给要fuzz的目标函数就行
用户1423082
2024/12/31
650
深入解析 volatile 、CAS 的实现原理
在分析说明 volatile 和 CAS 的实现原理前,我们需要先了解一些预备知识,这将是对 volatile 和 CAS 有深入理解的基石。 预备知识 缓存 现代处理器为了提高访问数据的效率,在每个CPU核心上都会有多级容量小,速度快的缓存(分别称之为L1 cache,L2 cache,多核心共享L3 cache等),用于缓存常用的数据。 缓存系统中是以缓存行(cache line)为单位存储的。缓存行是 2 的整数幂个连续字节,一般为 32-256 个字节。最常见的缓存行大小是 64个字节。 因此当CP
tomas家的小拨浪鼓
2018/06/27
2.5K0
对X86汇编的理解与入门
本文描述基本的32位X86汇编语言的一个子集,其中涉及汇编语言的最核心部分,包括寄存器结构,数据表示,基本的操作指令(包括数据传送指令、逻辑计算指令、算数运算指令),以及函数的调用规则。个人认为:在理
Angel_Kitty
2018/04/09
2K0
对X86汇编的理解与入门
About Cache Coherence, Atomic Operation, Memory Ordering, Memory Barrier, Volatile
该文章介绍了CPU缓存以及多线程程序中CPU缓存一致性的问题,并给出了具体的例子和解决方案。文章指出,多线程程序中的CPU缓存不一致问题可能会导致性能下降,因此需要谨慎处理。通过使用原子操作、锁操作等技术,可以避免CPU缓存不一致问题,从而提高程序的性能。
s1mba
2017/12/28
1.7K0
 About Cache Coherence,  Atomic Operation,  Memory Ordering,  Memory Barrier, Volatile
相关推荐
LLVM 工具系列 - Address Sanitizer 基本原理介绍及案例分析(1)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验