ASAN Pass【源码分析】(五)——插桩

前言

这分析得不错

  1. ModuleAddressSanitizer关注全局变量
  2. AddressSanitizer关注栈变量
  3. ASAN Runtime关注堆变量

全局变量的准备

a. 提取全局变量类的初始化

执行前端任务前,读取源文件时会构建CodeGenModule类(生成跨函数的状态方便后续IR生成),进而创建并初始化SanitizerMetadata(生成元数据供后续Pass使用)。

// clang/lib/Frontend/CompilerInstance.cpp
bool CompilerInstance::ExecuteAction(FrontendAction &Act) {
	...
	if (Act.BeginSourceFile(*this, FIF)) {
		if (llvm::Error Err = Act.Execute()) {...}
    }
  	...
}

b. 处理声明时,提取所有全局变量

然后在处理转译单元前,会处理顶级声明。CodeGenModule解析时,每当遇到一个字符串文本/全局变量,就交由SanitizerMetadata插入到名为"llvm.asan.globals"的元数据中。

// clang/lib/Parse/ParseAST.cpp
void clang::ParseAST(Sema &S, bool PrintStats, bool SkipFunctionBodies) {
	...
	Consumer->HandleTopLevelDecl(ADecl.get());
	...
	Consumer->HandleTranslationUnit(S.getASTContext());
	...
}	

c. 初始化管理全局变量元数据的ASanGlobalsMetadataWrapperPass

这在《ASAN Pass【源码分析】(三)》就已经讲述

d. 准备全局元数据变量

ASanGlobalsMetadataWrapperPass::runOnModule时初始化生成全局元数据GlobalsMetadata供ASAN后续使用。具体说来,它会从"llvm.asan.globals"提取步骤b存好的信息(如包括""、待编译文件的全局变量、"")。

AddressSanitizer出动

初始化

AddressSanitizerLegacyPass::runOnFunction首先获取ASanGlobalsMetadataWrapperPass准备好的全局元数据GlobalsMetadata,然后将其交给刚初始化的AddressSanitizer进行具体插桩。

AddressSanitizer初始化时会保存当前函数所在模块的上下文信息,查询当前平台架构和位数,然后设置ShadowMap,包括offset。

Mapping.Offset = (kSmallX86_64ShadowOffsetBase & (kSmallX86_64ShadowOffsetAlignMask << Mapping.Scale));

分析需要插桩的指令

首先确保函数不是外部定义的,不是asan-debug-func,不是__asan_开头的ASAN Runtime函数。

(通过getOrInsertFunction函数)初始化一些回调函数。包括__asan_report_{exp_,}{load,store}{_n,N,2^n}{_noabort,}__asan_{exp_,}{load,store}{_n,N,2^n}{_noabort,}{__asan_,}memmove{__asan_,}memcpy{__asan_,}memset__asan_handle_no_return__sanitizer_ptr_cmp__sanitizer_ptr_sub,有些平台下会额外插入__asan_shadow数组变量以指向ShadowMap。

判断是否需要设置动态起址的(当前为否)。

llvm.localescape里的allocas标记为不感兴趣,即不对其插桩。

判断当前函数内的每个BasicBlock里的每个指令的操作符是不是感兴趣的,标准是:

  1. LoadInst/StoreInst/AtomicRMWInst/AtomicCmpXchgInst指令、涉及SIMD或者非指针传参的CallInst,存在某个内存操作(MOP)。
  2. 是指针比较/减法指令(会导致指针传播)
  3. 是MemIntrinsic(memset/memcpy/memmove三种内存相关的操作)
  4. 是Alloca指令
  5. 是CallBase指令(包括Call指令和Invoke指令)
  6. 是Call指令

插桩

对内存操作指令插桩instrumentMop

计数一下Load/Store指令,最终进入AddressSanitizer::instrumentAddress

  1. 提取目标指令操作的目标内存地址
  2. 指定在目标指令前进行插桩。
  3. 创建指令,让目标地址右移三位
  4. 创建指令,让目标地址ADD ShadowMap偏移,得到ShadowPtr
  5. 创建指令,Load ShadowPtr的值,得到ShadowValue
  6. 创建指令,让ShadowValue和0值(0代表目标地址未被污染)比较,是否不相等。
  7. 创建指令,插入基于比较结果进行选择的分支
  8. 构建分支后新的基本块
  9. 创建指令,若ShadowValue不为零,需要检查最后一个访问Byte的长度是否超过ShadowValue
  10. 然后生成报告错误的指令

替换MemIntrinsic

memset/memmove/memcpy替换成asan的wrapper(有__asan_前缀的)

污染函数栈

使用Alloca指令将函数传参分配为栈变量。

遍历函数,搜集alloca、ret、lifetime相关的指令。

初始化回调函数声明,包括__asan_stack_{malloc,free}_#__asan_{,un}poison_stack_memory__asan_set_shadow_{0x00, 0xf1, 0xf2, 0xf3, 0xf5, 0xf8}__asan_alloca_{,un}poison。它们具体内容在ASAN Runtime中。

动态Alloca处理

  1. 创建指令,在动态Alloca操作前Posion。

静态Alloca处理

  1. 将不感兴趣的静态Alloca(比如只有Loads/Stores/LifetimMarkers会用到这个Alloca)放到感兴趣的静态Alloca前面
  2. 围绕感兴趣的Alloca指令,画好包括Redzone在内的栈帧蓝图。其中也考虑了Left-most Redzone及对齐。
  3. 创建指令,按照栈帧蓝图开辟一块栈空间,在上面另外分配感兴趣的静态Alloca(剩下的Gap就是Redzone了),然后将原有栈变量的使用全都引到新的分配,。
  4. 创建指令,在最左边的Redzone插入ASAN信息,包括MagicValue、栈帧蓝图描述指针、PC。
  5. 在蓝图中画好ShadowByte分别应是什么值。对于局部生命周期的栈变量,在初始时候标记为0xf8,额外创建指令在llvm.lifetime.start时标记有效部分位0x0,在llvm.lifetime.end时再将有效部分标记为无效0xf8
  6. 创建指令,对于连续一样的ShadowByte调用__asan_set_shadow_函数来设置,否则,计算连续8字节应该设置怎么样的ShadowByte,通过插入一条store指令来完成8个ShadowByte的设置(避免逐个Byte设置导致开销过大)。
  7. 创建指令,在Return指令前解除对栈变量(包括Redzone)的污染。
  8. 对已经新分配栈内存的变量(被Redzone围绕),将原有的分配Alloca指令删除。

解毒NoReturnCalls

插桩指针比较/减法操作

ModuleAddressSanitizer出动

你可能感兴趣的:(内存安全,Sanitizer)