Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >LLVM 工具系列 - Address Sanitizer 实现原理(2)

LLVM 工具系列 - Address Sanitizer 实现原理(2)

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

上篇文章 「Address Sanitizer 基本原理介绍及案例分析」里我们简单地介绍了一下 Address Sanitizer 基础的工作原理,这里我们再继续深挖一下深层次的原理。

从上篇文章中我们也了解到,对一个内存地址的操作:

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

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

上面的内存地址访问的代码,编译器会帮我们修改为这样的代码:

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

这样对内存的访问,编译器会在编译期自动在所有内存访问之前通过判断 IsPoisoned(address) 做一下 check 是否被“投毒”。

那么实现且高效地实现 IsPoisoned(),并使得 ReportError() 函数比较紧凑就十分重要。

在深入了解之前,我们先了解 Shadow 内存,以及主应用内存区shadow 内存映射

Shadow 内存 & 主应用内存区shadow 内存间的映射

首先,虚拟内存地址被分配了两段不连续的区域:主应用内存区 和 shadow内存区域。

主应用内存区(Main Application Memory, or Mem for short),其实就是在应用里分配的常规内存。

Shadow 内存区,它包含了主内存区状态的 meta 信息,也称之为 shadow value(影子值)。主应用内存区和 shadow 内存区有一个映射关系,当应用内存被“投毒”(poisoned),会在 shadow 内存区记录一个值作为体现。这样就可以通过查询 shadow 内存区的值,来判断应用内存是否被“投毒”。

为了节省内存占用,AddressSanitizer 会把 8 bytes 的应用内存会映射到 1 byte 的 shadow 内存。这样的话,这 1byte 的 shadown 内存会有 9 种值对应应用内存的状态:

  • 负值,当 8 字节的应用内存全都被 poisoned 时;
  • 0 值,当且仅当 8 字节的应用内存都没有被 poisoned 时;
  • 1-7 值,为 k 的意思为 “前 k 个字节都没有被 poisoned,后 8-k 个字节被 poisoned”,这个是由 malloc 分配的内存总是 8 字节对齐作为前提来作为保证的。这样的话,当 malloc(13) 时,得到的是前一个 完整的 qword(8字节,未被 poisoned)加上后一个 qword 的前 5 个 byte(未被 poisoned)

如何检查是否在“投毒区”(poisoned/redzone)?

这样的话,我们就可以根据 shadow 内存的 9 种值来判断 引用内存的状态 了。

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

扩展为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 拿到主应用内存地址对应的 Shadow 内存地址
byte *shadow_address = MemToShadow(address);

// 检查 shadow 内存值,如果为 0,肯定没有被 poison,因为可以跳过
// 如果不为 0,需要进一步检查是否访问的字节是否被 poisoned
byte shadow_value = *shadow_address;
if (shadow_value) {
  // 进一步检查访问的内存大小是否被 poisoned
  if (SlowPathCheck(shadow_value, address, kAccessSize)) {
    ReportError(address, kAccessSize, kIsWrite);
  }
}

// Check the cases where we access first k bytes of the qword
// and these k bytes are unpoisoned.
bool SlowPathCheck(shadow_value, address, kAccessSize) {
  last_accessed_byte = (address & 7) + kAccessSize - 1;
  return (last_accessed_byte >= shadow_value);
}

SlowPathCheck() 里,检查是否当前访问的地址的前若干个字节是否被 poisoned 了,因为是 8bytes 的应用内存映射到 1byte 的 shadow 上,首先要知道偏移,偏移+长度就是最后一个字节的位置,shadow_value <= 这个位置 - 1,说明被投毒了。

来看个例子。

比如应用内存 0x1000 - 0x1007 对应 shadow 的 0xF000 的地址

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
0x1000, 0x1001, 0x1002, 0x1003, 0x1004, 0x1005, 0x1006, 0x1007,

如果 0xF000 的值为 2, 就说明 0x1000, 0x1001 未被 poisoned,0x1002 到 0x1007 是被 poisoned 的。

那么,如果有一个 int 值在 0x1002 上,长度是4字节,那么我就需要检查 0x1005 以及之前(也就是前6个字节)是否被投毒,也就是检查 shadow value 是否 <= 5,如果小于等于 5,就说明只有前 5 个或者更少未被 poisoned,第6个字节一定被 poisoned 了,也就是这个 int 值肯定是被 poisoned 了。

再来看计算公式:

last_accessed_byte = 0x1002 & 7 + 4 - 1 = 5,

如果 5 >= shadow value, 即认为被 poisoned,和上述解释是一致的。

LLVM 里的实现源码

实际上,LLVM 是通过自定义 LLVM Pass 来生成指令并配合运行时库来完成上面的操作的。

具体的源码可以参考 AddressSanitizer.cpp

源码超级长,我们只挑和上面相关的,首先定义了 static const uint64_t kDefaultShadowScale = 3;

, 1 << 3 == 8,因此就作为映射的粒度。

AddressSanitizerLegacyPass 继承自 FunctionPass,override 了 runOnFunction(Function &F),也就可以对所有的函数进行修改和操作。runOnFunction 实现内部,创建了 AddressSanitizer 的实例,并调用了其 instrumentFunction(F, TLI) 方法。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class AddressSanitizerLegacyPass : public FunctionPass {
public:
  static char ID;

  explicit AddressSanitizerLegacyPass(
      bool CompileKernel = false, bool Recover = false,
      bool UseAfterScope = false,
      AsanDetectStackUseAfterReturnMode UseAfterReturn =
          AsanDetectStackUseAfterReturnMode::Runtime)
      : FunctionPass(ID), CompileKernel(CompileKernel), Recover(Recover),
        UseAfterScope(UseAfterScope), UseAfterReturn(UseAfterReturn) {
    initializeAddressSanitizerLegacyPassPass(*PassRegistry::getPassRegistry());
  }

  // ...

  bool runOnFunction(Function &F) override {
    GlobalsMetadata &GlobalsMD =
        getAnalysis<ASanGlobalsMetadataWrapperPass>().getGlobalsMD();
    const StackSafetyGlobalInfo *const SSGI =
        ClUseStackSafety
            ? &getAnalysis<StackSafetyGlobalInfoWrapperPass>().getResult()
            : nullptr;
    const TargetLibraryInfo *TLI =
        &getAnalysis<TargetLibraryInfoWrapperPass>().getTLI(F);

    //️ ⬇️️️⬇️⬇️
    AddressSanitizer ASan(*F.getParent(), &GlobalsMD, SSGI, CompileKernel,
                          Recover, UseAfterScope, UseAfterReturn);
    return ASan.instrumentFunction(F, TLI);
  }

AddressSanitizer::instrumentFunction 内容很长,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
bool AddressSanitizer::instrumentFunction(Function &F,
                                          const TargetLibraryInfo *TLI) {
  ...

  // We want to instrument every address only once per basic block (unless there
  // are calls between uses).
  SmallPtrSet<Value *, 16> TempsToInstrument;
  SmallVector<InterestingMemoryOperand, 16> OperandsToInstrument;
  SmallVector<MemIntrinsic *, 16> IntrinToInstrument;
  SmallVector<Instruction *, 8> NoReturnCalls;
  SmallVector<BasicBlock *, 16> AllBlocks;
  SmallVector<Instruction *, 16> PointerComparisonsOrSubtracts;


  // Fill the set of memory operations to instrument.
  // 遍历 函数里的每一个 block
  for (auto &BB : F) {
    AllBlocks.push_back(&BB);
    TempsToInstrument.clear();
    int NumInsnsPerBB = 0;

    // 遍历 block 里的每一条指令 (Instruction)
    for (auto &Inst : BB) {
      if (LooksLikeCodeInBug11395(&Inst)) return false;
      SmallVector<InterestingMemoryOperand, 1> InterestingOperands;

      🌟🌟🌟
      // 寻找感兴趣的内存操作数(store/load,那他们的操作数当然也就是内存地址了)
      getInterestingMemoryOperands(&Inst, InterestingOperands);

      if (!InterestingOperands.empty()) {
        for (auto &Operand : InterestingOperands) {
          ...
          // 存到 vector 里
          OperandsToInstrument.push_back(Operand);
          NumInsnsPerBB++;
        }
      }
      ...
    }
  }
  ...
  // Instrument.
  int NumInstrumented = 0;
  for (auto &Operand : OperandsToInstrument) {
    if (!suppressInstrumentationSiteForDebug(NumInstrumented))
      // 对于找到的指令进行修改
      instrumentMop(ObjSizeVis, Operand, UseCalls,
                    F.getParent()->getDataLayout());
    FunctionModified = true;
  }

  ...

  LLVM_DEBUG(dbgs() << "ASAN done instrumenting: " << FunctionModified << " "
                    << F << "\n");

  return FunctionModified;
}

AddressSanitizer::instrumentMop()

Calls

void doInstrumentAddress()

Calls

AddressSanitizer::instrumentAddress() 是插入前面提到的内存判断的地方,函数比较长,这里省略掉不太影响理解的代码。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void AddressSanitizer::instrumentAddress(Instruction *OrigIns,
                                         Instruction *InsertBefore, Value *Addr,
                                         uint32_t TypeSize, bool IsWrite,
                                         Value *SizeArgument, bool UseCalls,
                                         uint32_t Exp) {
  Value *AddrLong = IRB.CreatePointerCast(Addr, IntptrTy);

  Type *ShadowTy =
      IntegerType::get(*C, std::max(8U, TypeSize >> Mapping.Scale));
  Type *ShadowPtrTy = PointerType::get(ShadowTy, 0);

  // 🌟🌟🌟
  Value *ShadowPtr = memToShadow(AddrLong, IRB);
  Value *CmpVal = Constant::getNullValue(ShadowTy);
  Value *ShadowValue =
      IRB.CreateLoad(ShadowTy, IRB.CreateIntToPtr(ShadowPtr, ShadowPtrTy));

  // 🌟🌟🌟
  // 创建比较指令,shadow_value != 0
  Value *Cmp = IRB.CreateICmpNE(ShadowValue, CmpVal);
  size_t Granularity = 1ULL << Mapping.Scale;
  Instruction *CrashTerm = nullptr;

  if (ClAlwaysSlowPath || (TypeSize < 8 * Granularity)) {
    // We use branch weights for the slow path check, to indicate that the slow
    // path is rarely taken. This seems to be the case for SPEC benchmarks.
    Instruction *CheckTerm = SplitBlockAndInsertIfThen(
        Cmp, InsertBefore, false, MDBuilder(*C).createBranchWeights(1, 100000));
    assert(cast<BranchInst>(CheckTerm)->isUnconditional());
    BasicBlock *NextBB = CheckTerm->getSuccessor(0);
    IRB.SetInsertPoint(CheckTerm);

    // 🌟🌟🌟
    // SlowPathCmp
    Value *Cmp2 = createSlowPathCmp(IRB, AddrLong, ShadowValue, TypeSize);
    if (Recover) {
      CrashTerm = SplitBlockAndInsertIfThen(Cmp2, CheckTerm, false);
    } else {
      BasicBlock *CrashBlock =
        BasicBlock::Create(*C, "", NextBB->getParent(), NextBB);
      CrashTerm = new UnreachableInst(*C, CrashBlock);
      BranchInst *NewTerm = BranchInst::Create(CrashBlock, NextBB, Cmp2);
      ReplaceInstWithInst(CheckTerm, NewTerm);
    }
  } else {
    CrashTerm = SplitBlockAndInsertIfThen(Cmp, InsertBefore, !Recover);
  }

  Instruction *Crash = generateCrashCode(CrashTerm, AddrLong, IsWrite,
                                         AccessSizeIndex, SizeArgument, Exp);
  Crash->setDebugLoc(OrigIns->getDebugLoc());
}

s

Value *AddressSanitizer::createSlowPathCmp()

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Value *AddressSanitizer::createSlowPathCmp(IRBuilder<> &IRB, Value *AddrLong,
                                           Value *ShadowValue,
                                           uint32_t TypeSize) {
  size_t Granularity = static_cast<size_t>(1) << Mapping.Scale;
  // Addr & (Granularity - 1)
  Value *LastAccessedByte =
      IRB.CreateAnd(AddrLong, ConstantInt::get(IntptrTy, Granularity - 1));
  // (Addr & (Granularity - 1)) + size - 1
  if (TypeSize / 8 > 1)
    LastAccessedByte = IRB.CreateAdd(
        LastAccessedByte, ConstantInt::get(IntptrTy, TypeSize / 8 - 1));
  // (uint8_t) ((Addr & (Granularity-1)) + size - 1)
  LastAccessedByte =
      IRB.CreateIntCast(LastAccessedByte, ShadowValue->getType(), false);
  // ((uint8_t) ((Addr & (Granularity-1)) + size - 1)) >= ShadowValue
  return IRB.CreateICmpSGE(LastAccessedByte, ShadowValue);
}

Ref & 扩展阅读

  1. AddressSanitizerAlgorithm
  2. LLVM AddressSanitizer source code
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-01-08,如有侵权请联系 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 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验