前言
这分析得不错
- ModuleAddressSanitizer关注全局变量
- AddressSanitizer关注栈变量
- 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存好的信息(如包括"<string literal>"
、待编译文件的全局变量、"<string literal>"
)。
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里的每个指令的操作符是不是感兴趣的,标准是:
- 是
LoadInst/StoreInst/AtomicRMWInst/AtomicCmpXchgInst
指令、涉及SIMD或者非指针传参的CallInst
,存在某个内存操作(MOP)。 - 是指针比较/减法指令(会导致指针传播)
- 是MemIntrinsic(memset/memcpy/memmove三种内存相关的操作)
- 是Alloca指令
- 是CallBase指令(包括Call指令和Invoke指令)
- 是Call指令
插桩
对内存操作指令插桩instrumentMop
计数一下Load/Store指令,最终进入AddressSanitizer::instrumentAddress
。
- 提取目标指令操作的目标内存地址
- 指定在目标指令前进行插桩。
- 创建指令,让目标地址右移三位
- 创建指令,让目标地址ADD ShadowMap偏移,得到ShadowPtr
- 创建指令,Load ShadowPtr的值,得到ShadowValue
- 创建指令,让ShadowValue和0值(0代表目标地址未被污染)比较,是否不相等。
- 创建指令,插入基于比较结果进行选择的分支
- 构建分支后新的基本块
- 创建指令,若ShadowValue不为零,需要检查最后一个访问Byte的长度是否超过ShadowValue
- 然后生成报告错误的指令
替换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处理
- 创建指令,在动态Alloca操作前Posion。
静态Alloca处理
- 将不感兴趣的静态Alloca(比如只有Loads/Stores/LifetimMarkers会用到这个Alloca)放到感兴趣的静态Alloca前面
- 围绕感兴趣的Alloca指令,画好包括Redzone在内的栈帧蓝图。其中也考虑了Left-most Redzone及对齐。
- 创建指令,按照栈帧蓝图开辟一块栈空间,在上面另外分配感兴趣的静态Alloca(剩下的Gap就是Redzone了),然后将原有栈变量的使用全都引到新的分配,。
- 创建指令,在最左边的Redzone插入ASAN信息,包括MagicValue、栈帧蓝图描述指针、PC。
- 在蓝图中画好ShadowByte分别应是什么值。对于局部生命周期的栈变量,在初始时候标记为
0xf8
,额外创建指令在llvm.lifetime.start
时标记有效部分位0x0,在llvm.lifetime.end
时再将有效部分标记为无效0xf8
。 - 创建指令,对于连续一样的ShadowByte调用
__asan_set_shadow_
函数来设置,否则,计算连续8字节应该设置怎么样的ShadowByte,通过插入一条store指令来完成8个ShadowByte的设置(避免逐个Byte设置导致开销过大)。 - 创建指令,在Return指令前解除对栈变量(包括Redzone)的污染。
- 对已经新分配栈内存的变量(被Redzone围绕),将原有的分配Alloca指令删除。