前语
现在在做一些编译相关调研。先前写过篇《深入剖析 iOS 编译 Clang / LLVM》和《深入剖析 iOS 编译 Clang / LLVM 直播的 Slides》,内容偏理论。本篇着重对 LLVM 的运用,理论内容会很少,首要是说下怎样运用 llvm 来做些事情,会有具体的操作步骤和工程示例。本文对应的幻灯片见 《运用 LLVM 分享的幻灯片》。
代码推陈出新
昨日看了昨日和今日 WWDC22 的 session,看到了苹果为包体积也做了许多作业,甚至不惜改 C ABI的 call convention 来抵达此目的。
我很早前就做过一个计划,能够说是一个更优点理代码推陈出新的计划,那就先说下这个。
计划整体介绍
静态检查无法剖析实在运用场景里代码是不是真的用了,或用的是否多。
动态检查来说,曾经检查的办法有经过埋点检查相应代码是否有用到,还能够经过类的 isInitialized 办法来核算类是否被用到。第一个计划成本高,第二个计划规模太大,假如类都很大,那么检查成果的意义就没了。因而,需求一个能够动态检查函数和代码块等级是否运用的办法。
一些现有计划和其不行用的当地
下面列两个已有可检查比类更小粒度的计划。
gcov
clang 运用 -fprofile-instr-generate -fcoverage-mapping ,swiftc 运用 -profile-generate -profile-coverage-mapping 生成 .profraw 文件。llvm-profdata merge 转成 .profdata。编译时每个文件会用 GCOVProfiling 生成 .gcno 包括计数和源码的映射联系,运转时用的是 GCDAProfiling 处理回调记载运转时履行了哪些代码。最终 llvm-cov 转成陈述,生成东西是 gcov,生成的陈述能够看到哪些代码有用到,哪些没有用。
gcov 关于线下测验够用,但无法放到线上运用。
SanitizerCoverage 插桩回调函数
SanitizerCoverage 是 libfuzzer 运用的代码掩盖技能,运用 -fsanitize-coverage=trace-pc-guard
这个编译 flag 刺进不同等级的桩,会在程序操控流图的每条边刺进__sanitizer_cov_trace_pc_guard
。
假如只对函数插桩,运用 -fsanitize-coverage=func
,trace-pc-guard
,只对基本块用 -fsanite-coverage=bb
,no-prune
,trace-pc-guard
。swift 运用 -sanitize-coverage=func
和 -sanitize=undefined
编译 flags。
在回调函数 __sanitizer_cov_trace_pc_guard_init
和 __sanitizer_cov_trace_pc_guard
里完结自己要干的事情,比方对当时插桩地址符号化,运转后就能够得到运转时调用了哪些办法。
运用 SanitizerCoverage 插桩,一个是编译会很慢,另一个是刺进规模难操控,上线后各方面影响不行控。SanitizerCoverage 本是用于 fuzzing 测验的一个 llvm pass,因而能够了解 SanitizerCoverage 运用的技能,自建一个专门用于代码推陈出新的 pass 用来处理 SanitizerCoverage 和 gcov 不好用的问题。
克己可刺进指令的 Pass
之所以在编译中间层刺进指令而不在编译 frontend 刺进代码的原因是,这样做的话能用类似 llvm-mctoll 二进制转中间层 IR 代码的办法,可对第三方这样没有 frontend 源码而只有生成的二进制产品的库进行剖析。
在函数中刺进履行指令履行自定功用的办法是,用 IRBuilder 运用 SetInsertPoint 设置方位,CreateCall 刺进指令,刺进在块的初始方位,用的是 dyn_cast<BinaryOperator>(&I)
。CreateCall 调用 LLVMContextFunctionCallee 来自 F.getParent()->getOrInsertFunction
,其第一个参数便是要履行咱们自界说函数的函数名,第二个参数 FunctionType 是经过 paramTypes
和 Type::getVoidTy
依据 LLVMContext 而来。 运用编译特色能够指定要操控的函数,pass 可用 getGlobalVariable 取到 llvm.global.annotations
,也便是一切编译特色。
F.getName().front()
为 \x01
标明的是 OC 办法,去掉这个前缀可得到办法名,.contains("_block")
是闭包函数。F.getName().startswith("_Z")
是 C++ 函数(_Z
、__Z
、___Z
都是)。运用 F.getName()
判读读取一个映射表进行比照,也能够抵达经过编译特色设置操控指定函数的作用。映射表里设置需求线上验证的函数集合。然后,处理函数和块计数与源码的映射联系,编译加入处理克己 pass 记载运转时代码履行情况的回调。
运用
pass 代码编译生成 dylib 后,在 Xcode 中运用需求替换 clang 为编译 pass 的 clang,编译 pass 的版别也要对应上。在 xconfig 中设置构建指令选项 OTHER_CFLAGS OTHER_CPLUSPLUSFLAGS 是 -Xclang -load -Xclang $pass,CC CXX 设置为替换的 clang。调试是用的 opt,可换成 opt scheme,在 Edit Scheme 里设置 opt 的发动参数。
llvm 14 后只能运用 new pm,legcy pm(pass manager) 经过 Xlang 给 clang 传参,而 new pm 不行,new pm 的 pass 让 clang 加载,一种办法是运用 -fpass-plugin,另一种是把 pass 加到 clang 的 pipeline 里,从头构建对应版别的 clang。具体来说便是 PassBuilder 的回调 registerPipelineStartEPCallback 答应 ModulePassManager 运用 addPass 增加咱们的 pass。
计划是这样,接下来的内容是偏实践的一些操作,你也能够跟着实践下,究竟本篇是说怎样运用 LLVM 嘛。
先看看 gcov 的用法。
生成代码掩盖率陈述
指令行中开启代码掩盖率的编译选项,参看官方攻略:Source-based Code Coverage 。
经过一个比方实践下。
建个 C 代码文件 main.m :
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("hi there!\n");
return 0;
}
void foo() {
return;
}
加上代码掩盖率的编译参数进行编译。
xcrun clang -fprofile-instr-generate -fcoverage-mapping main.m -o mainCoverage
运转生成的 mainCoverage 会生成 default.profraw 文件,自界说文件名运用 LLVM_PROFILE_FILE=”my.profraw” ./mainCoverage 指令。
关于 Swift 文件也没有问题,建一个 swift 文件 hi.swift
hi()
func hi() {
print("hi")
}
func f1() {
doNothing()
func doNothing() {}
}
经过 swiftc 来编译
swiftc -profile-generate -profile-coverage-mapping hi.swift
从上面 clang 和 swiftc 的指令能够看出,clang 运用的是 -fprofile-instr-generate 和 -fcoverage-mapping 编译 flags,swiftc 运用的是 -profile-generate 和 -profile-coverage-mapping 编译 flags。
编译出的可履行文件 mainCoverage 和 hi 都会多出
生成代码掩盖率前建立索引,也便是生成 .profdata 文件。经过 xcrun 调用 llvm-prodata 指令。指令如下:
xcrun llvm-profdata merge -sparse my.profraw -o my.profdata
合并多个 .profdata 文件运用下面的指令:
llvm-profdata merge one.profdata two.profdata -output all.profdata
运用 llvm-cov 指令生成行的陈述
xcrun llvm-cov show ./mainCoverage -instr-profile=my.profdata
输出:
1| |#include <stdio.h>
2| |
3| |int main(int argc, char *argv[])
4| 1|{
5| 1| printf("hi there!\n");
6| 1| return 0;
7| 1|}
8| |
9| 0|void foo() {
10| 0| return;
11| 0|}
上面的输出能够看到,9到11行是没有履行的。
从文件层面看掩盖率,能够经过下面的指令:
xcrun llvm-cov report ./mainCoverage -instr-profile=my.profdata
输出的陈述如下:
Filename Regions Missed Regions Cover Functions Missed Functions Executed Lines Missed Lines Cover Branches Missed Branches Cover
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
/Users/mingdai/Downloads/PTest/main.m 2 1 50.00% 2 1 50.00% 7 3 57.14% 0 0 -
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TOTAL 2 1 50.00% 2 1 50.00% 7 3 57.14% 0 0 -
生成 JSON 的指令如下:
xcrun llvm-cov export -format=text ./mainCoverage -instr-profile=my.profdata > my.json
从生成的 json 文件能够看到这个生成的陈述有5个核算项,分别是函数、实例化、行、区域和分支。
更多陈述生成选型参看 llvm-cov 官方阐明 。
Xcode 配置生成代码掩盖率陈述
在 Xcode 里开启代码掩盖率,先挑选”Edit Scheme…”,再在 Test 中的 Options 里勾上 Gather coverage for all targets 或 some targets。如下图
在 Build Setting 中进行设置,增加 -profile-generate 和 -profile-coverage-mapping 编译 flags。
调用 llvm profile 的 c 函数生成 .profraw 文件。代码见:
// MARK: - 代码掩盖率
func codeCoverageProfrawDump(fileName: String = "cc") {
let name = "\(fileName).profraw"
let fileManager = FileManager.default
do {
let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
let filePath: NSString = documentDirectory.appendingPathComponent(name).path as NSString
__llvm_profile_set_filename(filePath.utf8String)
print("File at: \(String(cString: __llvm_profile_get_filename()))")
__llvm_profile_write_file()
} catch {
print(error)
}
}
codeCoverageProfrawDump 函数放到 applicationWillTerminate 里履行,就能够生成在本次操作完后的代码掩盖率。
经过 llvm-cov report 指令将 .profraw 和生成的 Mach-O 文件相关输出代码掩盖率的陈述,完好完结和调试看,参看 DaiMingCreationToolbox 里的 FundationFunction.swift 和 SwiftPamphletAppApp.swift 文件。
Fuzzing 介绍
别的,llvm 还供给另一种掩盖率输出,编译参数是 -fprofile-arcs -ftest-coverage 和链接参数 -lgcov,运转程序后会生成 .gcda 和 .gcno 文件,运用 lcov 或 gcovr 就能够生成一个 html 来检查掩盖率。
之所以能够输出代码掩盖率,首要是 llvm 在编译期间给函数、基本块(IDA 中以指令跳转当分界线的每块代码)和鸿沟(较基本块多了履行鸿沟信息)插了桩。插桩的函数也有回调,假如想运用插桩函数的回调,有源码能够运用 SanitizerCoverage, 官方阐明见:SanitizerCoverage。
SanitizerCoverage 用的是 ModulePass,是 llvm 供给的 ModulePass、CallGraphSCCPass、FunctionPass、LoopPass、RegionPass 这几个插桩 pass 中的一种。SanitizerCoverage 还运用在 llvm 的 Fuzz 生成器 libfuzzer 上,libfuzzer 能够从硬件和 IR 层面进行插桩获取程序的掩盖率。
Fuzzing 生成器的概念最早是威斯康星大学 Barton Miller 教授在他的课上提出的,后运用于安全测验范畴,比方 PROTOS 测验集项目、网络协议安全测验 SPIKE、最遍及运用的文件 Fuzzing 技能 Peach、语法模板 funfuzz 和 Dom fuzz 的 Domato、剖析 llvm IR 符号履行渠道 Klee、源码插桩和 QEMU 办法完结代码掩盖 fuzzing 的 AFL 和刚才我提到的 llvm 自带依据 SanitizerCoverage 的 libfuzzer、挖掘体系内核缝隙的体系函数调用模板 Fuzzing 库 syzkaller 和依据 libfuzzer 和 protobuf 做的 libprotobuf-mutator、组合了 libFuzzer,AFL++ 和 Honggfuzz 还有 ClusterFuzz 的渠道 OSS-Fuzz。
其间 Spike 是网络协议开源 Fuzzing 东西,由 Dave Aitel 编写的,Dave Aitel 是《the Hacker’s Handbook》(《黑客防范手册》)和《the Shellcoder’s Handbook》(《黑客攻防技能宝典:体系实战篇》)的作者。网络协议剖析东西首要是 WireShark 和运用层的 SockMon(特定进程、协议、IP、函数抓包),和 IDA、OD 等东西结合找到软件履行的网络指令剖析数据包的处理进程。Spike 能够对数据发包收包,还能够结构数据包主动化做掩盖更大的测验。
QEMU 是 2003 年 Fabrice Bellard 做的虚拟机,包括许多架构和硬件设备的模仿履行,原理是 qemu TCG 模块把机器代码转成 llvm IR,这个进程叫做反编译,关于反编译能够参考这篇论文《An In-Depth Analysis of Disassembly on Full-Scale x86/x64 Binaries》。之所以能够做到反编译是由于机器指令和汇编指令是一一对应的,能够先将机器指令翻译成机器对应的汇编,IR 实践上便是一个不遵循硬件规划的指令集,和硬件相关的汇编会按照 IR 的规划翻译成机器无关的 IR 指令。这样做的优点便是无论是哪个机器上的可履行二进制文件都能够一致成一份规范的指令标明。IR 也能够规划成 DSL,比方 Ghidra 的 Sleigh 言语。
反编译后,再将得到的 IR 转成目标硬件设备可履行机器言语,IDA Pro 也是用的这个原理,IDA 的 IR 叫 microcode,IDA 的插件 genmc 专门用来显现 microcode,HexRaysDeob 是运用 microcode 来做混杂的库。
qemu 做的是没有源码的二进制程序的剖析,是一个完好的虚拟机东西,其间只有 tcg 模块的一部分功用就能够完结模仿 CPU 履行,履行进程中刺进剖析的代码就能够便利的拜访寄存器,对地址或指令 hook,完结这些功用的库是 Unicorn,还有功用更多些的 Qiling。Qiling 和 Unicorn 不同的是 Unicorn 只完结了 CPU 指令的仿真,而 Qiling 能够处理更高层次的动态库、体系调用、I/O 处理或 Mach-O 加载等,Qiling 还能够经过 Python 开发自己动态剖析东西,运转时进行 hotpatch,支撑 macOS。依据 qemu 还有能够拜访履行的一切代码和数据做回放程序履行进程的 PANDA、虚拟地址消毒剂 QASan、组合 Klee 和 qemu 的 S2E。
能够运用 js 来开发免编译功用的 Frida 也能够用于 Fuzzing,在 iOS 渠道上的 Fuzzing 参看1、2、3,运用东西见 iOS-messaging-tools。
更多 Fuzzing 材料能够参看 GitHub 上一份整理好的 Awesome-Fuzzing。
可见 Fuzzing 生成器运用规模十分广,除了获取代码掩盖率,还能够进行网络安全剖析和安全缝隙剖析。本文首要是依据源码插桩,源码插桩库首要是 libfuzzer、AFL++、honggfuzz、riufuzz(honggfuzz 二次开发)。
AFL++ 在有源码情况下原理和 libfuzzer 差不多,仅仅底层不是用的 SanitizerCoverage,而是自完结的一个 pass,没有源码时 AFL++ 用的便是 qemu 中 TCG 模块的代码,在反编译为 IR 时进行插桩。更多 AFL++ 运用拜见《What is AFL and What is it Good for?》
Fuzzing 除了代码掩盖率,还需求又能够创立更多输出条件,记载履行途径,目标和方向是找出程序运转时在什么输入条件和途径下会有问题。但仅是检测哪些代码有用到,实践上只需用上 Fuzzing 的代码掩盖率就能够了。
SanitizerCoverage 插桩回调函数
那接下来实践下 libfuzzer 中完结代码掩盖率的 SanitizerCoverage 技能。
指令行履行
xcrun clang -fembed-bitcode main.m -save-temps -v -fsanitize-coverage=trace-pc-guard
运用 -fsanitize-coverage=trace-pc-guard 这个编译 flag 刺进不同等级的桩,会在程序操控流图的每条边刺进:
__sanitizer_cov_trace_pc_guard(&guard_variable)
假如只对函数插桩,运用 -fsanitize-coverage=func
,trace-pc-guard
,只对基本块用 -fsanite-coverage=bb
,no-prune
,trace-pc-guard
。swift 运用 -sanitize-coverage=func
和 -sanitize=undefined
编译 flags。
运用插桩函数回调,先在 Xcode 的 Other C Flags 里增加 -fsanitize-coverage=trace-pc-guard
。swift 便是在 Other Swift Flags 里增加 -sanitize-coverage=func
和 -sanitize=undefined
。
在回调函数里完结自己要干的事情,比方对当时插桩地址符号化,代码如下:
#import <dlfcn.h>
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N;
if (start == stop || *start) return;
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
,*x = ++N;
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
printf("调用了办法: %s \n", info.dli_sname);
}
运转后就能够得到运转时调用了哪些办法。
有了这些数据就能够核算哪些办法调用了,调用了多少次。经过和全源码比照,取差集能够找到运转中没有履行的办法和代码块。其实运用 Fuzzing 的概念还能够做许多剖析的作业,全面数据化观测代码履行情况。能够到我的 GCDFetchFeed 工程中,翻开 AppDelegate.m 里的两个插桩回调办法的注释来试用。
中止试用插桩,能够用 __attribute__((no_sanitize("coverage")))
编译特色。或许经过黑名单或白名单,分别是 -fsanitize-coverage-ignorelist=blocklist.txt
和 -fsanitize-coverage-allowlist=allowlist.txt
,规模能够试文件夹、单个文件或许单个办法。
allowlist.txt 示例:
# 答应文件夹里一切文件
src:bar/*
# 特定源文件
src:foo/a.cpp
# 答应文件中一切函数
fun:*
blocklist.txt 示例:
# 禁用特定源文件
src:bar/b.cpp
# 禁用特定函数
fun:*myFunc*
上线前检查出的没有用到的代码,并不标明上线后用户不会用到,比方 AB 试验、用户特别设置、不常见 Case 等。这就能够运用 allowlist.txt 将部分不确认的代码放到线上去检测,或许经过主动刺进埋点灰度检测,这些不确认的代码不是主链路的,因而检测影响规模会很低。
SanitizerCoverage 本身是一个 llvm pass,代码在 llvm 工程的 llvm-project/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp 途径下,那么怎样完结一个自界说的 pass 呢?
先把 llvm 装到本地。
装置 LLVM
运用 homebrew,指令如下:
brew install llvm@13
@13
标明 llvm 的版别。装置后运用途径在是 /usr/local/opt/llvm/
,比方 cmake 构建编译环境能够运用下面的指令:
$LLVM_DIR=/usr/local/opt/llvm/lib/cmake/llvm cmake ..
能够用 Visual Studio Code 开发 pass,装置微软的 C/C++ 的 extension,在 C/C++ Configurations 里把 /usr/local/opt/llvm/include/
加入到包括途径中。
llvm 的更新运用 brew upgrade llvm
llvm 也能够经过源码来装置,履行如下指令即可:
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout release/14.x
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_TARGETS_TO_BUILD=host -DLLVM_ENABLE_PROJECTS=clang ../llvm
cmake --build .
这儿的 cmake 参数 -DLLVM_ENABLE_PROJECTS=clang
标明也会构建 clang 东西。假如还要加上 lld 以在构建时能够用自己的 pass,能够直接加成 -DLLVM_ENABLE_PROJECTS="clang;lld"
。
自界说装置目录的话,增加 -DCMAKE_INSTALL_PREFIX=/home/user/custom-llvm
。然后在设置途径 export PATH=$PATH:/home/user/custom-llvm/bin
。
-G 编译选项挑选 Ninja 编译速度快。
各种设置整到一起:
cmake -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DLLVM_TARGETS_TO_BUILD=host -DLLVM_ENABLE_PROJECTS="clang;lld" -DCMAKE_INSTALL_PREFIX=/Users/mingdai/Downloads/PTest/my-llvm-bin ../llvm
克己 Pass
Pass 介绍
llvm 属于 multi-pass 编译器,LLVM Pass 管理器是处理 pass 履行的注册和时序组织。曾有两个 pass 管理器,一个是 New Pass 管理器也叫 Pass 管理器,另一个是 Legacy Pass 管理器。New Pass 现在是默许的管理器,Legacy Pass 在 LLVM 14 中被废弃。Legacy 和 New 两个 pass 管理器在运用上最大的差异便是,Legacy 会注册一个新的指令选项,而 New Pass 只用界说一个 pass。别的 Legacy 需求完结 print 成员办法来打印,需求在经过 opt 经过传递 -analyze 指令行选项来运转,而 New Pass 管理器是不必的,只需求完结 printing pass。
总的来说 Legacy
- 依据承继性
- 剖析和打印 pass 之间没有差异
- 注册时加载一切需求的 pass
- 不变的 pass 履行调度
- Transformation passes 界说了它们在履行前保证保存的内容
Legacy 的 pass 类
-
llvm::Pass
llvm::ModulePass
llvm::FunctionPass
llvm::PassRegistry
New
- 依据 CRTP、mixin 和 concept-model 的 idiom-based
- 在履行进程中,依据需求有条件的加载依靠的 pass(更快、更有效)
- Transformation passes 在履行后回来它们所保存的内容
New 的 pass 类
llvm::PassInfoMixin<DerivedT>
llvm::AnalysisInfoMixin<DerivedT>
-
llvm::FunctionAnalysisManager
- 别号类型
llvm::AnalysisManager<llvm::Function>
- 别号类型
-
llvm::ModuleAnalysisManager
- 别号类型
llvm::AnalysisManager<llvm::Module>
- 别号类型
llvm::PreservedAnalysis
LLVM Pass 能够对 LLVM IR 进行优化。优化表现在 Pass 能够对 IR 进行剖析和转化,因而 Pass 首要也是分为剖析(analysis)和转化(transform)两类。
剖析里有数据流剖析技能,分为以下三种:
- Reaching-Definition Analysis 抵达定值剖析
- Live-Variable Analysis 活跃变量剖析
- Available-Expression Analysis 可用表达式剖析
一些常用的优化办法,比方删去核算成果不会运用的句子、删去归纳变量、删去公共子表达式、进入循环前就对不管循环多少次都是相同成果的表达式进行求值、快的操作替换慢操作、用可推导出值是常量的表达式来替代表达式等。
编写优化的几个办法。完好代码参看这儿。
刺进新指令:
- 直接经过类或命名的结构函数。
- 运用 llvm::IRBuilder<> 模板类。
删去指令:
- llvm::Instruction::eraseFromParent() 成员函数
替换存在指令:
- llvm::ReplaceInstWithInst() 函数
#include “llvm/Transforms/Utils/BasicBlockUtils.h”
直接改指令
- llvm::User::setOperand() 成员函数
Value ⇒ ConstantInt 类型转化:
Type _t;
ConstantInt* val = dyn_cast<ConstantInt>(_t);
获取 ConstantInt 类的值
ConstantInt* const_int;
uint64_t val = const_int->getZExtValue();
替换某个指令
Instruction inst;
// 替换,仅仅替换了引证,并没删
inst.replaceAllUsesWith(val);
// 删去
if(inst->isSafeToRemove())
inst->eraseFromParent();
对应的 IR 代码
; 履行前
%12 = load i32, i32* %2, align 4
%13 = add nsw i32 %12, 0
store i32 %13, i32* %3, align 4
; 只替换指令引证
%12 = load i32, i32* %2, align 4
%13 = add nsw i32 %12, 0
store i32 %12, i32* %3, align 4
%12 = load i32, i32* %2, align 4
store i32 %12, i32* %3, align 4
Instruction referencing instruction not embedded in a basic block!
%12 = load i32, i32* %2, align 4
<badref> = add nsw i32 %12, 0
建立新指令
// 取出第一个操作数
Value* val = inst.getOperand(0);
// 确认新指令的刺进方位
IRBuilder<> builder(&inst);
// val << 1
Value* newInst = builder.CreateShl(val, 1);
// 替换指令
inst.replaceAllUsesWith(newInst);
Analysis pass 的 print pass 是依据一个 Transformation pass,会请求原始 pass 剖析的成果,并打印这些成果。会注册一个指令行选项 print<analysis-pass-name>
。
完结 pass 要挑选是 Analysis 仍是 Transformation,也便是要对进行输入 IR 的剖析仍是进行转化来决议选用哪种。挑选 Transformation 一般承继 PassInfoMixin。Analysis 承继 AnalysisInfoMixin。
pass 生成的插件分为动态和静态的。静态插件不需求在运转时用 -load-pass-plugin 选项进行加载,但需求在 llvm 工程中设置 CMake 从头构建 opt。
做自己 pass 前能够先了解下 llvm 内部的 pass 示例,能够先从两个最基本的 Hello 和 Bye 来。比较实用的是一些做优化的 pass,这些 pass 也是学习写 pass ,了解编译器怎样作业的重要资源。许多 pass 都完结了编译器开发理论中著名的概念。比方优化 memcpy 调用(比方用 memset 替换)的 memcpyopt 、简化 CFG IRTransforms、总是内联用 alwaysinline 润饰的函数的 always-inline 、死代码消除的 dce 和删去未运用的循环的 loop-deletion。
克己刺进指令 pass
接下来,怎样在运转时刺进指令来获取咱们需求代码运用情况。完好代码能够在这儿 MingPass 拉下代码参考进行修正调试。
个 pass 功用是在运转时环境直接在特定方位履行指定的函数。先写个要履行的函数,新建个文件 loglib.m,代码如下:
#include <stdio.h>
void runtimeLog(int i) {
printf("核算成果: %i\n", i);
}
再到 MingPass.cpp 中包括模块头文件
#include "llvm/IR/Module.h"
会用到 Module::getOrInsertFunction 函数来给 loglib.m 的 runtimeLog 做声明。
更改 runOnFunction 函数,代码如下:
virtual bool runOnFunction(Function &F) {
// 从运转时库中获取函数
LLVMContext &Context = F.getContext();
std::vector<Type*> paramTypes = {Type::getInt32Ty(Context)};
Type *retType = Type::getVoidTy(Context);
FunctionType *funcType = FunctionType::get(retType, paramTypes, false);
FunctionCallee logFunc = F.getParent()->getOrInsertFunction("runtimeLog", funcType);
for (auto &BB : F) {
for (auto &I : BB) {
if (auto *op = dyn_cast<BinaryOperator>(&I)) {
IRBuilder<> builder(op);
// 在 op 后边加入新指令
builder.SetInsertPoint(&BB, ++builder.GetInsertPoint());
// 在函数中刺进新指令
Value* args[] = {op};
builder.CreateCall(logFunc, args);
return true;
} // end if
}
}
return false;
}
在 build 目录下 make 出 pass 的 so 后,链接 main.m 和 loglib.m 的产品成可履行文件,指令如下:
clang -c loglib.m
/usr/local/opt/llvm/bin/clang -flegacy-pass-manager -Xclang -load -Xclang build/src/libMingPass.so -c main.m
clang main.o loglib.o
./a.out
输入数字4后,打印如下:
4
核算成果: 6
6
更多克己 pass
能够在这儿检查,代码里有具体注释。这儿先留个白,后边再增加内容。
IR
你会发现开发 pass 需求更多的了解 IR,才能够更好的操控 LLVM 前端处理的高档言语。接下来我会说下那些高档言语的特性是怎样在 IR 里表现的。先介绍下 IR。
IR 介绍
LLVM IR(Intermediate Representation) 能够称为中间代码,是 LLVM 整个编译进程的中间标明。
llvm ir 的根底块里的指令是不行跳转到根底块的中间或尾部,只能从根底块的第一个指令进入根底块。
下面是 ir 的几个特色:
- llvm ir 不是机器代码而是生成机器代码之前的一种有些看起来像高档言语的,比方函数和强类型,有些看起来像低级程序集,比方分支和基本块。
- llvm ir 是强类型。
- llvm 没有 sign 和 unsign 整数差异。
- 大局符号用 @ 符号最初。
- 本地符号用 % 符号最初。
- 有必要界说和声明一切符号。
IR 指令
常用指令
- alloca:分配栈空间
- load:从栈和大局内存读值
- store:将值写到栈或大局内存
- br:分支(条件或非条件)
- call:调用函数
- ret:从一个函数回来,可能会带上一个回来值
- icmp/fcmp:比较整型或浮点值
- add/sub/mul:整数二进制算术运算
- fadd/fsub/fmul:浮点二进制算术运算
- sdiv/udiv/fdiv:有符号位整数/无符号位整数/浮点除法
- shl/shr:位向左/向右
- lshr/ashr:逻辑/算术右移
- and/or/xor:位逻辑运算(没有
not
!)
常用特别 ir 指令
- select:依据一个没有 IR 等级分支的条件挑选一个值。
- phi:依据当时基本块前身挑选一个值。
- getelementpointer:获取数组或结构体里子元素的地址(不是值)。官方阐明[[llvm.org/docs/GetEle… Often Misunderstood GEP Instruction]]。
- extractvalue:从一个数组或结构体中提取一个成员字段的值(不是地址)。
- insertvalue:将一个值增加给数组或结构体的成员字段。
ir 转化指令
- bitcast:将一个值转成给定类型而不改动它的位。
- trunc/fptrunc:将一个类型的整数/浮点值切断为一个更小的整数/浮点类型。
- zext/sext/fpext:将一个值扩展到一个更大的整数/浮点类型上。
- fptoui/fptosi:将一个浮点值转化为无符号/有符号位的整数类型。
- uitofp/sitofp:将一个无符号/有符号位整数值转化为浮点类型。
- ptrtoint:将指针转成整数。
- inttoptr:将整数值转成指针类型。
ir 库的 header 地址在 include/llvm/IR ,源文件在 lib/IR ,文档 llvm Namespace Reference。一切类和函数都在 llvm 命名空间里。
首要根底类的阐明如下:
- llvm::Module:ir 的容器类的最高档。
- llvm::Value:一切可作为其他值或指令操作数的基类。
- llvm::Constant
- llvm::ConstantDataArray (Constants.h)
- llvm::ConstantInt (Constants.h)
- llvm::ConstantFP (Constants.h)
- llvm::ConstantStruct (Constants.h)
- llvm::ConstantPointerNull (Constants.h)
- llvm::Function
- llvm::GlobalVariable
- llvm::BasicBlock
- llvm::Instruction
- Useful X-macro header: Instruction.def
- llvm::BinaryOperator (InstrTypes.h)
- add, sub, mul, sdiv, udiv, srem, urem
- fadd, fsub, fmul, fdiv, frem
- shl, lshr, ashr, and, or, xor
- llvm::CmpInst (InstrTypes.h)
- llvm::ICmpInst (Instructions.h)
- llvm::FCmpInst (Instructions.h)
- llvm::UnaryInstruction (InstrTypes.h)
- llvm::CastInst (Instrtypes.h)
- llvm::BitCastInst (Instructions.h)
- llvm::Constant
- llvm::Type:代表一切的 IR 数据类型,包括原始类型,结构类型和函数类型。
大局变量
下面 c 代码:
int variable = 21;
int main()
{
variable = variable * 2;
return variable;
}
对应的 ir 代码如下:
@variable = global i32 21
define i32 @main() {
%1 = load i32, i32* @variable ; load the global variable
%2 = mul i32 %1, 2
store i32 %2, i32* @variable ; store instruction to write to global variable
ret i32 %2
}
如上代码,你能够看到大局变量是 @ 字符前缀,main 函数也是 @ 符号作为前缀,因而 main 函数也是 llvm 中的大局变量。llvm 将大局变量当作指针,因而拜访大局变量时,有必要用 load 指令显现撤销对大局变量的引证,相同的,你要用 store 指令显现存储大局变量的值。
本地变量
本地变量有两种,一种是暂时变量,也能够说是寄存器,另一种是仓库分配的部分变量。
暂时变量或寄存器是经过为变量引进一个新的符号来创立的:
%reg = add i32 4, 2
仓库分配的部分变量是经过在堆上分配变量来创立的。
%stack = alloca i32
几乎每条指令都会回来一个值,该值一般分配给一个暂时变量。由于 llvm ir 的 SSA 办法,暂时变量只能分配一次。因而下面的代码就会犯错:
%tmp = add i32 4, 2
%tmp = add i32 4, 1 ; Error here
为契合 SSA,一般会是下面的代码:
%tmp.0 = add i32 4, 2
%tmp.1 = add i32 4, 1 ; fine now
简化为:
%0 = add i32 4, 2
%1 = add i32 4, 1
这种部分变量的数量基本是无限的。由于实在机器的寄存器数量有限,因而编译器后端可能需求将其间一些暂时寄存器放在仓库上。
alloca 产生一个指向已分配类型的指针。必需求显现的运用 load 或 store 指令来分别读取和写入值。
常量
有两种常量,一种是不占用分配内存的常量,另一种是占用分配内存的常量。
不占用分配内存的常量没有等效 llvm ir,他们是在编译前端将常量值刺进到运用他们的方位。
%1 = add i32 %0, 17 ; 17 便是内联的常量
占用分配内存的常量运用 constant 关键字界说:
@hello = internal constant [6 x i8] c"hello\00"
%struct = type { i32, i8 }
@struct_constant = internal constant %struct { i32 16, i8 4 }
能够看出常量实践也是个大局变量,可见性能够用 private 和 internal 来约束,这样它在当时办法之外是不行见的。
字符串
在 LLVM 中有两种完结字符串类型的办法:
- 在 LLVM IR 中编写完结
- 用生成 IR 的高档言语编写完结
LLVM IR 中有一个简略但有用的字符串类型。
咱们将创立一个动态的、可变的字符串类型,它能够被增加,也能够刺进,转化巨细写等等,这取决于界说了哪些支撑函数来操作字符串类型。
这一切都归结为为类制造适宜的类型界说,然后界说一组丰厚的函数来对类型界说进行操作
store i8* %output, i8** %1
;this->maxlen = %value (value that was passed into @malloc is the new maxlen)
%4 = getelementptr %String* this, i32 0, i32 2
store i32 %value, i32* %4
ret void
}
define fastcc void @String_Add_Char(%String* %this, i8 %value) {
; Check if we need to grow the string.
%1 = getelementptr %String* %this, i32 0, i32 1
%length = load i32* %1
%2 = getelementptr %String* %this, i32 0, i32 2
%maxlen = load i32* %2
; if length == maxlen:
%3 = icmp eq i32 %length, %maxlen
br i1 %3, label %grow_begin, label %grow_close
grow_begin:
%4 = getelementptr %String* %this, i32 0, i32 3
%factor = load i32* %4
%5 = add i32 %maxlen, %factor
call void @String_Resize(%String* %this, i32 %5)
br label %grow_close
grow_close:
%6 = getelementptr %String* %this, i32 0, i32 0
%buffer = load i8** %6
%7 = getelementptr i8* %buffer, i32 %length
store i8 %value, i8* %7
%8 = add i32 %length, 1
store i32 %8, i32* %1
ret void
}
结构体
下面的 c 代码:
struct Foo
{
size_t x;
double y;
};
对应的 ir 代码如下:
%Foo = type {
i64, ; index 0 = x
double ; index 1 = y
}
能够看到 struct 的结构成员是从0开端的数字进行索引。
下面是嵌套结构的比方:
struct FooBar
{
Foo x;
char* c;
Foo* y;
}
对应的 ir 代码:
%FooBar = type {
%Foo, ; index 0 = x
i8*, ; index 1 = c
%Foo* ; index 2 = y
}
不完好结构类型,关于躲藏结构细节十分有用。比方下面的 c 代码:
void Bar(struct Foo *);
对应的 ir 代码:
%Foo = type opaque
declare void @Bar(%Foo)
ir 的结构成员是经过索引而不是称号来记载的,getelementptr(GEP) 是专门用来核算指向任何结构成员的指针。比方下面 c++ 代码:
struct Foo
{
int a;
char *b;
double c;
};
对应 ir 是:
%Foo = type {
i32, ; 0: a
i8*, ; 1: b
double ; 2: c
}
GEP 索引如 ir 代码中的注释所示。现在拜访 b 成员,c 代码:
Foo foo;
char **bptr = &foo.b;
先在仓库上运用 alloca 指令分配目标,拜访 b 成员,运用 GEP 指令核算指向内存方位的指针。
%foo = alloca %Foo
; char **bptr = &foo.b
%1 = getelementptr %Foo, %Foo* %foo, i32 0, i32 1
假如创立一个 Foo 目标数组,如下:
Foo bar[100];
bar[17].c = 0.0;
会转成以下 ir 代码:
; Foo bar[100]
%bar = alloca %Foo, i32 100
; bar[17].c = 0.0
%2 = getelementptr %Foo, %Foo* %bar, i32 17, i32 2
store double 0.0, double* %2
如上面代码所示,它首要会分配一个指向 100 个 Foo 目标的指针。然后用 GEP 指令检索数组中第17个条目的第二个元素。
类型转化
有九种不同类型的转化
- Bitwise casts (type casts)
- Zero-extending casts (unsigned upcasts).
- Sign-extending casts (signed upcasts).
- Truncasting casts (signed and unsigned downcasts).
- Floating-point extending casts (float upcasts).
- Floating-point truncasting casts (float downcasts)
- Pointer-to-integer casts.
- Integer-to-pointer casts.
- Address-space casts (pointer casts).
Bitwise Casts bitwise cast 是按位强制转化。比方能够将指向字节的指针位转化为指向某个结构的指针。
typedef struct
{
int a;
} Foo;
extern void *malloc(size_t size);
extern void free(void *value);
void allocate()
{
Foo *foo = (Foo *) malloc(sizeof(Foo));
foo.a = 12;
free(foo);
}
转化成对应的 ir:
%Foo = type { i32 }
declare i8* @malloc(i32)
declare void @free(i8*)
define void @allocate() nounwind {
%1 = call i8* @malloc(i32 4)
%foo = bitcast i8* %1 to %Foo*
%2 = getelementptr %Foo, %Foo* %foo, i32 0, i32 0
store i32 12, i32* %2
call void @free(i8* %1)
ret void
}
Zero-Extending Casts(Unsigned Upcasts) 比方下面的 c 代码:
uint8 byte = 117;
uint32 word;
void main()
{
/* The compiler automatically upcasts the byte to a word. */
word = byte;
}
运用 zext 指令:
@byte = global i8 117
@word = global i32 0
define void @main() nounwind {
%1 = load i8, i8* @byte
%2 = zext i8 %1 to i32
store i32 %2, i32* @word
ret void
}
Sign-Extending Casts (Signed Upcasts) 将 zext 替换成 sext 指令即可。
@char = global i8 -17
@int = global i32 0
define void @main() nounwind {
%1 = load i8, i8* @char
%2 = sext i8 %1 to i32
store i32 %2, i32* @int
ret void
}
Truncating Casts (Signed and Unsigned Downcasts) signed 和 unsigned 整数都运用相同的指令 trunc 来减少相关数字的巨细。这是由于 llvm ir 假设一切有符号整数值都是二进制补码格局,因而 turn 留意处理这两种情况:
@int = global i32 -1
@char = global i8 0
define void @main() nounwind {
%1 = load i32, i32* @int
%2 = trunc i32 %1 to i8
store i8 %2, i8* @char
ret void
}
Floating-Point Extending Casts (Float Upcasts) 浮点数能够运用 fpext 指令进行扩展,比方下面的 c 代码:
float small = 1.25;
double large;
void main()
{
/* The compiler inserts an implicit float upcast. */
large = small;
}
会变成:
@small = global float 1.25
@large = global double 0.0
define void @main() nounwind {
%1 = load float, float* @small
%2 = fpext float %1 to double
store double %2, double* @large
ret void
}
Floating-Point Truncating Casts (Float Downcasts) 浮点数能够切断为更小的巨细:
@large = global double 1.25
@small = global float 0.0
define void @main() nounwind {
%1 = load double, double* @large
%2 = fptrunc double %1 to float
store float %2, float* @small
ret void
}
Pointer-to-Integer Casts 运用 ptrtoint 指令将指针类型转化为整数类型。
Integer-to-Pointer Casts 运用 inttoptr 指令将整数转化成指针。
函数和声明
函数界说取决于 calling convention、excption-aware 和模块是否对外揭露。
下面是一个简略的 c 函数:
int Bar(void)
{
return 17;
}
会转化成
define i32 @Bar() nounwind {
ret i32 17
}
私有函数界说如下:
define private i32 @Foo() nounwind {
ret i32 17
}
ir 的私有函数仅仅 llvm 模块级的私有函数,并不是和高档言语 private 关键字界说的函数对应。
函数原型在 ir 里运用 declare 来声明:
int Bar(int value);
对应运用 declare 声明为:
declare i32 @Bar(i32 %value)
可变参数的函数,要用省略号界说或声明它,然后需求运用特别的函数调用语法。示例如下:
declare i32 @printf(i8*, ...) nounwind
@.textstr = internal constant [20 x i8] c"Argument count: %d\0A\00"
define i32 @main(i32 %argc, i8** %argv) nounwind {
; printf("Argument count: %d\n", argc)
%1 = call i32 (i8*, ...) @printf(i8* getelementptr([20 x i8], [20 x i8]* @.textstr, i32 0, i32 0), i32 %argc)
ret i32 0
}
函数重载不是在 ir 里处理的,是在源言语上处理的。重载的函数称号不同,示例如下:
int function(int a, int b) {
return a + b;
}
double function(double a, double b, double x) {
return a*b + x;
}
上面 c 代码对应 ir 是:
define i32 @_Z8functionii(i32 %a, i32 %b) #0 {
; [...]
ret i32 %5
}
define double @_Z8functionddd(double %a, double %b, double %x) #0 {
; [...]
ret double %8
}
可见在 ir 里,重载函数的称号和功用都是不同的。
类或结构体一般是按值传递,在传递目标时隐式克隆目标。比方下面的 c 代码:
struct Point {
double x;
double y;
double z;
};
Point add_points(Point a, Point b) {
Point p;
p.x = a.x + b.x;
p.y = a.y + b.y;
p.z = a.z + b.z;
return p;
}
对应 ir 是:
%struct.Point = type { double, double, double }
define void @add_points(%struct.Point* noalias sret %agg.result,
%struct.Point* byval align 8 %a,
%struct.Point* byval align 8 %b) #0 {
; there is no alloca here for Point p;
; p.x = a.x + b.x;
%1 = getelementptr inbounds %struct.Point, %struct.Point* %a, i32 0, i32 0
%2 = load double, double* %1, align 8
%3 = getelementptr inbounds %struct.Point, %struct.Point* %b, i32 0, i32 0
%4 = load double, double* %3, align 8
%5 = fadd double %2, %4
%6 = getelementptr inbounds %struct.Point, %struct.Point* %agg.result, i32 0, i32 0
store double %5, double* %6, align 8
; p.y = a.y + b.y;
%7 = getelementptr inbounds %struct.Point, %struct.Point* %a, i32 0, i32 1
%8 = load double, double* %7, align 8
%9 = getelementptr inbounds %struct.Point, %struct.Point* %b, i32 0, i32 1
%10 = load double, double* %9, align 8
%11 = fadd double %8, %10
%12 = getelementptr inbounds %struct.Point, %struct.Point* %agg.result, i32 0, i32 1
store double %11, double* %12, align 8
; p.z = a.z + b.z;
%13 = getelementptr inbounds %struct.Point, %struct.Point* %a, i32 0, i32 2
%14 = load double, double* %13, align 8
%15 = getelementptr inbounds %struct.Point, %struct.Point* %b, i32 0, i32 2
%16 = load double, double* %15, align 8
%17 = fadd double %14, %16
%18 = getelementptr inbounds %struct.Point, %struct.Point* %agg.result, i32 0, i32 2
store double %17, double* %18, align 8
; there is no real returned value, because the previous stores directly wrote
; to the caller allocated value via %agg.result
ret void
}
能够看到 add_points 函数回来的是 void,别的增加了一个参数,这个参数是指向回来成果的指针,有调用者来分配。这个指针是 noalias 特色,sret 特色标明这是回来值。参数是 byval 特色,标明他们是按值传递的结构。
下面代码是展现怎样调用 add_points 函数。
int main() {
Point a = {1.0, 3.0, 4.0};
Point b = {2.0, 8.0, 5.0};
Point c = add_points(a, b);
return 0;
}
编成 ir 代码如下:
define i32 @main() #1 {
; these are the a, b, c in the scope of main
%a = alloca %struct.Point, align 8
%b = alloca %struct.Point, align 8
%c = alloca %struct.Point, align 8
; these are copies, which are passed as arguments
%1 = alloca %struct.Point, align 8
%2 = alloca %struct.Point, align 8
; copy the global initializer main::a to %a
%3 = bitcast %struct.Point* %a to i8*
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %3, i8* bitcast (%struct.Point* @main.a to i8*), i64 24, i32 8, i1 false)
; copy the global initializer main::b to %b
%4 = bitcast %struct.Point* %b to i8*
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %4, i8* bitcast (%struct.Point* @main.b to i8*), i64 24, i32 8, i1 false)
; clone a to %1
%5 = bitcast %struct.Point* %1 to i8*
%6 = bitcast %struct.Point* %a to i8*
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %5, i8* %6, i64 24, i32 8, i1 false)
; clone b to %1
%7 = bitcast %struct.Point* %2 to i8*
%8 = bitcast %struct.Point* %b to i8*
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %7, i8* %8, i64 24, i32 8, i1 false)
; call add_points with the cloned values
call void @add_points(%struct.Point* sret %c, %struct.Point* byval align 8 %1, %struct.Point* byval align 8 %2)
; [...]
}
能够看到调用者为回来值 %c 分配空间,而且保证在实践经过引证传递参数前克隆参数 a 和 b。
反常处理函数,能够回来一个指向反常实例的指针,创立一个 setjmp/longjmp 帧,或许简略指定 uwtable 特色。
函数指针的表达办法和 c 差不多,比方下面 c 代码:
int (*Function)(char *buffer);
对应 ir 为:
@Function = global i32(i8*)* null
Unions
llvm 不支撑 unions,下面的 c++ 代码:
union Foo
{
int a;
char *b;
double c;
};
Foo Union;
对应 ir 为:
%union.Foo = type { double }
@Union = %union.Foo { 0.0 }
其它成员没有了,ir 要拜访他们要用 bitcast 指令将指向 union 的指针转化为你想要的指针。
%1 = bitcast %union.Foo* @Union to i32*
store i32 1, i32* %1
%2 = bitcast %union.Foo* @Union to i8**
store i8* null, i8** %2
实践上 unions 只不过是一块运用不同隐式指针强制转化拜访的内存。处理 unions 没有类型安全。前端言语需求对 unions 做支撑,能够简略的分配 unions 的总巨细,也便是最大成员的巨细,然后依据需求生成代码来从头解说分配的内存。
if-then-else 分支
llvm ir 是按次序履行的指令序列组成。这些指令组合在一起构成基本块,每个基本块都以改动程序操控流的指令完毕。
下面是个简略的 if-then-else 分支:
int max(int a, int b) {
if (a > b) {
return a;
} else {
return b;
}
}
在 ir 中,操控流是经过在基本块之间跳转完结的。这些基本块包括不改动操控流的指令序列。每个基本块都以改动程序操控流的指令完毕。最常见的分支指令是 br。br 能够带上条件,然后它完结了一个简略的 if-then-else 。如下:
br i1 %cond, label %iftrue, label %iffalse
br 也能够完结无条件跳转到某个目的地:
br label %dest
前面的 c 函数对应的 ir 代码如下:
define i32 @max(i32 %a, i32 %b) {
entry:
%retval = alloca i32, align 4
%0 = icmp sgt i32 %a, %b
br i1 %0, label %btrue, label %bfalse
btrue: ; preds = %2
store i32 %a, i32* %retval, align 4
br label %end
bfalse: ; preds = %2
store i32 %b, i32* %retval, align 4
br label %end
end: ; preds = %btrue, %bfalse
%1 = load i32, i32* %retval, align 4
ret i32 %1
}
如上代码所示,共有4个基本块,第一个是函数入口,运用 alloca 在仓库上分配空间,用作较大的暂时存储,然后运用 icmp 指令比较两个参数 %a 和 %b。成果是一个布尔标志 i1,将其用于 br 指令的条件。然后依据所选用的分支,将 %a 或 %b 存储到暂时 %retval 变量中。每个分支都以无条件分支到最终一个基本块 %end 完毕。来自 %retval 的值被加载并回来。
经过 opt -dot-cfg input.ll 能够获得 CFG 流程图。
select 指令能够进行更高等级优化,不生成分支,缩短代码,比方上面的 ir 代码优化后为:
define i32 @max(i32 %a, i32 %b) {
%1 = icmp sgt i32 %a, %b
%2 = select i1 %1, i32 %a, i32 %b
ret i32 %2
}
静态单一赋值表(SSA Form)和 PHI
下面是一个 c 函数:
int max(int a, int b) {
if (a > b) {
return a;
} else {
return b;
}
}
对应的 llvm ir 代码如下:
define i32 @max(i32 %a, i32 %b) {
entry:
%retval = alloca i32, align 4
%0 = icmp sgt i32 %a, %b
br i1 %0, label %btrue, label %bfalse
btrue: ; preds = %2
store i32 %a, i32* %retval, align 4
br label %end
bfalse: ; preds = %2
store i32 %b, i32* %retval, align 4
br label %end
end: ; preds = %btrue, %bfalse
%1 = load i32, i32* %retval, align 4
ret i32 %1
}
能够看到 max 函数运用 alloc 在仓库上分配空间,其间存储了较大的值。在一个分支中,%a 被存储,而在另一个分支中,%b 被存储到仓库分配的内存中。尽可能防止运用内存 load/store 操作,而是更多的运用寄存器。所以按下面办法来写:
define i32 @max(i32 %a, i32 %b) {
entry:
%0 = icmp sgt i32 %a, %b
br i1 %0, label %btrue, label %bfalse
btrue:
%retval = %a
br label %end
bfalse:
%retval = %b
br label %end
end:
ret i32 %retval
}
这不是有效的 LLVM IR,由于 LLVM IR 是静态单已分配办法,也便是 SSA。SSA 办法要求每个变量只分配一次。SSA 办法支撑并简化了许多的编译器优化,而且是指令式编程言语解说器中中间标明的实践上的规范。
那怎样用 SSA 办法 LLVM IR 完结上述代码?答案是神奇的 phi 指令。phi 指令以 SSA 理论中运用的 函数命名。这个函数会依据操控流神奇的挑选正确的值。在 LLVM 中,你有必要手动指定值的称号和前一个基本块。
end:
%retval = phi i32 [%a, %btrue], [%b, %bfalse]
这儿咱们指示 phi 指令在前一个基本块为 %btrue 时挑选 %a。假如之前的基本块是 %bfalse,那么将运用 %b。然后将该值分配给一个新变量 %retval。
define i32 @max(i32 %a, i32 %b) {
entry:
%0 = icmp sgt i32 %a, %b
br i1 %0, label %btrue, label %bfalse
btrue: ; preds = %2
br label %end
bfalse: ; preds = %2
br label %end
end: ; preds = %btrue, %bfalse
%retval = phi i32 [%a, %btrue], [%b, %bfalse]
ret i32 %retval
}
后端的 PHI
让咱们看看 @max 函数现在怎样映射到实践的机器代码。检查的是 x86 64 位生成的代码,用不同优化等级进行编译。非优化后端指令 llc -O0 -filetype=asm。得到的成果是:
max: # @max
# %bb.0: # %entry
cmpl %esi, %edi # %edi = %a, %esi = %b
jle .LBB0_2
# %bb.1: # %btrue
movl %edi, -4(%rsp) # mov src, dst
jmp .LBB0_3
.LBB0_2: # %bfalse
movl %esi, -4(%rsp) # mov src, dst
jmp .LBB0_3
.LBB0_3: # %end
movl -4(%rsp), %eax # return value in eax
retq
参数 %a 和 %b 分别在 %edi 和 %esi 中传递。咱们能够看到编译后端生成的代码运用仓库来存储更大的值。因而,当咱们编写 LLVM IR 时,编译器后端生成的代码并不是咱们所想的。原因是编译器后端需求用真机指令来完结 phi 指令。一般这是经过分配给一个寄存器或存储到一个公共仓库方位来完结的。一般编译器后端将运用仓库来完结 phi 指令。可是,假如咱们在后端运用更多优化,也便是 llc -O1,能够获得更优化的版别:
max: # @max
# %bb.0: # %entry
cmpl %esi, %edi
jg .LBB0_2
# %bb.1: # %bfalse
movl %esi, %edi
.LBB0_2: # %end
movl %edi, %eax
retq
这儿的 phi 函数是经过运用 %edi 寄存器完结的。在一个分支中,%edi 现已包括所需的值,所以什么都没发生。在另一个分支中,%esi 被复制到 %edi。在 %end 基本块中,%edi 包括来自两个分支的所需值。这更像是咱们的想法。咱们能够看到优化是需求在整个编译管道中的运用的东西。
Lambda 函数
lambda 函数是一个匿名函数,它能够自在引证包括函数中的部分变量,也包括参数变量。Lambda 的完结和 Pascal 的嵌套函数相同,仅仅编译器负责为 lambda 函数生成内部称号。有几种不同的办法能够完结 lambda 函数。
int foo(int a)
{
auto function = [a](int x) { return x + a; };
return function(10);
}
这儿的问题是 lambda 函数引证了调用者的一个部分变量,即 a,即使 lambda 函数是它自己的函数。这能够经过将部分变量作为隐式参数传递给 lambda 函数来轻松处理:
define internal i32 @lambda(i32 %a, i32 %x) {
%1 = add i32 %a, %x
ret i32 %1
}
define i32 @foo(i32 %a) {
%1 = call i32 @lambda(i32 %a, i32 10)
ret i32 %1
}
或许,假如 lambda 函数运用多个变量,你能够将他们包装在一个结构中,然后将指针传递给 lambda 函数:
extern int integer_parse();
int foo(int a, int b)
{
int c = integer_parse();
auto function = [a, b, c](int x) { return (a + b - c) * x; };
return function(10);
}
对应 IR 是:
; ModuleID = 'lambda_func_1_cleaned.ll'
source_filename = "lambda_func_1_cleaned.ll"
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"
%lambda_args = type { i32, i32, i32 }
declare i32 @integer_parse()
define i32 @lambda(%lambda_args* %args, i32 %x) {
%1 = getelementptr %lambda_args, %lambda_args* %args, i32 0, i32 0
%a = load i32, i32* %1
%2 = getelementptr %lambda_args, %lambda_args* %args, i32 0, i32 1
%b = load i32, i32* %2
%3 = getelementptr %lambda_args, %lambda_args* %args, i32 0, i32 2
%c = load i32, i32* %3
%4 = add i32 %a, %b
%5 = sub i32 %4, %c
%6 = mul i32 %5, %x
ret i32 %6
}
define i32 @foo(i32 %a, i32 %b) {
%args = alloca %lambda_args
%1 = getelementptr %lambda_args, %lambda_args* %args, i32 0, i32 0
store i32 %a, i32* %1
%2 = getelementptr %lambda_args, %lambda_args* %args, i32 0, i32 1
store i32 %b, i32* %2
%c = call i32 @integer_parse()
%3 = getelementptr %lambda_args, %lambda_args* %args, i32 0, i32 2
store i32 %c, i32* %3
%4 = call i32 @lambda(%lambda_args* %args, i32 10)
ret i32 %4
}
显然,这个主题有一些可能的改变:
- 你能够将一切隐式作为参数参数当成参数来传递。
- 你能够将结构中一切隐式参数作为显现参数传递。
- 你能够传入一个指向调用者帧的指针,并让 lambda 函数从输入帧中提取参数和部分变量。
生成器 Generators
生成器是一个函数,它以这样的一种办法重复生成一个值,使函数的状况在函数的重复调用中坚持不变,包括函数在生产量时的部分偏移量。
完结生成器最直接的办法便是将其一切状况变量,包括参数、部分变量和回来值都包装到一个 ad-hoc 结构中,然后将该结构的地址传递给生成器。
某种程度上说,你需求在每次调用时盯梢你正在履行的生成器是哪个块。这能够经过多种办法完结。咱们在这展现的办法是运用 LLVM 的 blockaddress 指令来保存下一个应该履行的本地代码块的地址。其它完结运用简略的状况变量,然后依据状况变量的值进行类似开关的调度。在这两种情况下,最终成果是相同的,也便是为生成器中的每个本地块履行不同的代码块。
重要的是将迭代器视为一种微线程,每逢再次调用迭代器时,它就会康复。换句话说,咱们需求保存迭代器每次经过多远的地址,以便它能够康复,就好像发生了微观线程切换相同。所以咱们在回来指令之后保存指令的地址,这样咱们就能够持续运转,就好像咱们一开端就没有回来相同。
下面是一段简化的伪代码。
#include <stdio.h>
generator int foo()
{
yield 1;
yield 2;
yield 3;
}
int main()
{
foreach (int i in foo())
printf("Value: %d\n", i);
return 0;
}
对应的 IR 如下:
%foo_context = type {
i8*, ; 0: block (state)
i32 ; 1: value (result)
}
define void @foo_setup(%foo_context* %context) nounwind {
; set up 'block'
%1 = getelementptr %foo_context* %context, i32 0, i32 0
store i8* blockaddress(@foo_yield, %.yield1), i8** %1
ret void
}
; The boolean returned indicates if a result was available or not.
; Once no more results are available, the caller is expected to not call
; the iterator again.
define i1 @foo_yield(%foo_context* %context) nounwind {
; dispatch to the active generator block
%1 = getelementptr %foo_context* %context, i32 0, i32 0
%2 = load i8** %1
indirectbr i8* %2, [ label %.yield1, label %.yield2, label %.yield3, label %.done ]
.yield1:
; store the result value (1)
%3 = getelementptr %foo_context* %context, i32 0, i32 1
store i32 1, i32* %3
; make 'block' point to next block to execute
%4 = getelementptr %foo_context* %context, i32 0, i32 0
store i8* blockaddress(@foo_yield, %.yield2), i8** %4
ret i1 1
.yield2:
; store the result value (2)
%5 = getelementptr %foo_context* %context, i32 0, i32 1
store i32 2, i32* %5
; make 'block' point to next block to execute
%6 = getelementptr %foo_context* %context, i32 0, i32 0
store i8* blockaddress(@foo_yield, %.yield3), i8** %6
ret i1 1
.yield3:
; store the result value (3)
%7 = getelementptr %foo_context* %context, i32 0, i32 1
store i32 3, i32* %7
; make 'block' point to next block to execute
%8 = getelementptr %foo_context* %context, i32 0, i32 0
store i8* blockaddress(@foo_yield, %.done), i8** %8
ret i1 1
.done:
ret i1 0
}
declare i32 @printf(i8*, ...) nounwind
@.string = internal constant [11 x i8] c"Value: %d\0A\00"
define void @main() nounwind {
; allocate and initialize generator context structure
%context = alloca %foo_context
call void @foo_setup(%foo_context* %context)
br label %.head
.head:
; foreach (int i in foo())
%1 = call i1 @foo_yield(%foo_context* %context)
br i1 %1, label %.body, label %.tail
.body:
%2 = getelementptr %foo_context* %context, i32 0, i32 1
%3 = load i32* %2
%4 = call i32 (i8*, ...)* @printf(i8* getelementptr([11 x i8]* @.string, i32 0, i32 0), i32 %3)
br label %.head
.tail:
ret void
}
下面是一个涉及部分变量的稍微杂乱的示例:
#include <stdio.h>
generator int foo(int start, int after)
{
for (int index = start; index < after; index++)
{
if (index % 2 == 0)
yield index + 1;
else
yield index - 1;
}
}
int main(void)
{
foreach (int i in foo(0, 5))
printf("Value: %d\n", i);
return 0;
}
生成 IR 代码为:
%foo_context = type {
i8*, ; 0: block (state)
i32, ; 1: start (argument)
i32, ; 2: after (argument)
i32, ; 3: index (local)
i32 ; 4: value (result)
}
define void @foo_setup(%foo_context* %context, i32 %start, i32 %after) nounwind {
; set up 'block'
%1 = getelementptr %foo_context* %context, i32 0, i32 0
store i8* blockaddress(@foo_yield, %.init), i8** %1
; set up 'start'
%2 = getelementptr %foo_context* %context, i32 0, i32 1
store i32 %start, i32* %2
; set up 'after'
%3 = getelementptr %foo_context* %context, i32 0, i32 2
store i32 %after, i32* %3
ret void
}
define i1 @foo_yield(%foo_context* %context) nounwind {
; dispatch to the active generator block
%1 = getelementptr %foo_context* %context, i32 0, i32 0
%2 = load i8** %1
indirectbr i8* %2, [ label %.init, label %.loop_close, label %.end ]
.init:
; copy argument 'start' to the local variable 'index'
%3 = getelementptr %foo_context* %context, i32 0, i32 1
%start = load i32* %3
%4 = getelementptr %foo_context* %context, i32 0, i32 3
store i32 %start, i32* %4
br label %.head
.head:
; for (; index < after; )
%5 = getelementptr %foo_context* %context, i32 0, i32 3
%index = load i32* %5
%6 = getelementptr %foo_context* %context, i32 0, i32 2
%after = load i32* %6
%again = icmp slt i32 %index, %after
br i1 %again, label %.loop_begin, label %.exit
.loop_begin:
%7 = srem i32 %index, 2
%8 = icmp eq i32 %7, 0
br i1 %8, label %.even, label %.odd
.even:
; store 'index + 1' in 'value'
%9 = add i32 %index, 1
%10 = getelementptr %foo_context* %context, i32 0, i32 4
store i32 %9, i32* %10
; make 'block' point to the end of the loop (after the yield)
%11 = getelementptr %foo_context* %context, i32 0, i32 0
store i8* blockaddress(@foo_yield, %.loop_close), i8** %11
ret i1 1
.odd:
; store 'index - 1' in value
%12 = sub i32 %index, 1
%13 = getelementptr %foo_context* %context, i32 0, i32 4
store i32 %12, i32* %13
; make 'block' point to the end of the loop (after the yield)
%14 = getelementptr %foo_context* %context, i32 0, i32 0
store i8* blockaddress(@foo_yield, %.loop_close), i8** %14
ret i1 1
.loop_close:
; increment 'index'
%15 = getelementptr %foo_context* %context, i32 0, i32 3
%16 = load i32* %15
%17 = add i32 %16, 1
store i32 %17, i32* %15
br label %.head
.exit:
; make 'block' point to the %.end label
%x = getelementptr %foo_context* %context, i32 0, i32 0
store i8* blockaddress(@foo_yield, %.end), i8** %x
br label %.end
.end:
ret i1 0
}
declare i32 @printf(i8*, ...) nounwind
@.string = internal constant [11 x i8] c"Value: %d\0A\00"
define i32 @main() nounwind {
; allocate and initialize generator context structure
%context = alloca %foo_context
call void @foo_setup(%foo_context* %context, i32 0, i32 5)
br label %.head
.head:
; foreach (int i in foo(0, 5))
%1 = call i1 @foo_yield(%foo_context* %context)
br i1 %1, label %.body, label %.tail
.body:
%2 = getelementptr %foo_context* %context, i32 0, i32 4
%3 = load i32* %2
%4 = call i32 (i8*, ...)* @printf(i8* getelementptr([11 x i8]* @.string, i32 0, i32 0), i32 %3)
br label %.head
.tail:
ret i32 0
}
履行上述操作的另一种可能办法是为每个状况生成一个 LLVM IR 函数,然后存储一个函数指针。
在上下文结构中,每逢需求调用新的状况或函数时都会更新。
面向目标结构
接下来研究各种面向目标的结构,看怎样映射到 LLVM IR。
类
一个类只不过是一个结构,它具有一组相关函数,这些函数接受一个隐式的第一个参数,即指向该结构的指针。因而将一个类映射到 LLVM IR 很简略:
#include <stddef.h>
class Foo
{
public:
Foo()
{
_length = 0;
}
size_t GetLength() const
{
return _length;
}
void SetLength(size_t value)
{
_length = value;
}
private:
size_t _length;
};
首要将这段代码转化成两个独立的部分:
- 结构界说
- 办法列表,包括结构函数
; The structure definition for class Foo.
%Foo = type { i32 }
; The default constructor for class Foo.
define void @Foo_Create_Default(%Foo* %this) nounwind {
%1 = getelementptr %Foo, %Foo* %this, i32 0, i32 0
store i32 0, i32* %1
ret void
}
; The Foo::GetLength() method.
define i32 @Foo_GetLength(%Foo* %this) nounwind {
%1 = getelementptr %Foo, %Foo* %this, i32 0, i32 0
%2 = load i32, i32* %1
ret i32 %2
}
; The Foo::SetLength() method.
define void @Foo_SetLength(%Foo* %this, i32 %value) nounwind {
%1 = getelementptr %Foo, %Foo* %this, i32 0, i32 0
store i32 %value, i32* %1
ret void
}
然后咱们保证调用了结构函数 Foo_Create_Default。
每逢创立结构的实例时:
Foo foo;
%foo = alloca %Foo
call void @Foo_Create_Default(%Foo* %foo)
虚拟办法
虚办法只不过是编译器操控的函数指针。每个虚办法都记载在 vtable 中,它是给定类所需的一切函数指针的结构:
class Foo {
public:
virtal int GetLengthTimesTwo() const {
return _length * 2;
}
void SetLength(size_t value) {
_length = value;
}
private:
int _length;
};
int main() {
Foo foo;
foo.setLength(4);
return foo.GetLengthTimesTwo();
}
这变成:
%Foo_vtable_type = type { i32(%Foo*)* }
%Foo = type { %Foo_vtable_type*, i32 }
define i32 @Foo_GetLengthTimesTwo(%Foo* %this) nounwind {
%1 = getelementptr %Foo, %Foo* %this, i32 0, i32 1
%2 = load i32, i32* %1
%3 = mul i32 %2, 2
ret i32 %3
}
@Foo_vtable_data = global %Foo_vtable_type {
i32(%Foo*)* @Foo_GetLengthTimesTwo
}
define void @Foo_Create_Default(%Foo* %this) nounwind {
%1 = getelementptr %Foo, %Foo* %this, i32 0, i32 0
store %Foo_vtable_type* @Foo_vtable_data, %Foo_vtable_type** %1
%2 = getelementptr %Foo, %Foo* %this, i32 0, i32 1
store i32 0, i32* %2
ret void
}
define void @Foo_SetLength(%Foo* %this, i32 %value) nounwind {
%1 = getelementptr %Foo, %Foo* %this, i32 0, i32 1
store i32 %value, i32* %1
ret void
}
define i32 @main(i32 %argc, i8** %argv) nounwind {
%foo = alloca %Foo
call void @Foo_Create_Default(%Foo* %foo)
call void @Foo_SetLength(%Foo* %foo, i32 4)
%1 = getelementptr %Foo, %Foo* %foo, i32 0, i32 0
%2 = load %Foo_vtable_type*, %Foo_vtable_type** %1
%3 = getelementptr %Foo_vtable_type, %Foo_vtable_type* %2, i32 0, i32 0
%4 = load i32(%Foo*)*, i32(%Foo*)** %3
%5 = call i32 %4(%Foo* %foo)
ret i32 %5
}
请留意,一些 C++ 编译器将 _vtable 存储在结构中的负偏移量处,这样 memeset(this, 0, sizeof(*this))之类的东西就能够作业,即使在 OOP 上下文中应始终防止运用此类指令。
Rust 特征和 VTable 与 C++ 比较,Rust 的确具有彻底不同的目标模型。可是,当涉及到动态调度的低级细节时,他们十分类似。咱们将讨论 rust 文档中的一个示例,以及 rustc 编译器宣布什么样的 llvm IR。rust 和 C++ 都运用虚拟办法进行动态调度。可是,在 rust 中,高档言语中没有虚拟办法之类的东西。相反,咱们能够为咱们的数据类型完结 trait,然后完结一个接口,该接口接受一切完结 trait 的数据类型并动态分派到正确的 trait 完结,也便是下面示例中 dyn Trait 语法。
编译器有必要在运转时动态决议运用哪个函数。编译器只知道存储在 components 向量中的目标的确满意特征 Draw。作为对不太熟悉 rust 的人的附注:将目标包装在 Box 中本质上是将目标放在堆上,有点类似于 C++ 中的 unique_ptr 并有效地答应咱们放置 trait 目标,也便是本例中为 dyn Drawable 在向量中。
; test::Screen::run
; Function Attrs: nonlazybind uwtable
define void @"Screen::run"(%Screen* %self) {
start:
;; (omitting the initial prologue and setup code)
;; this is the start of the for loop in Screen::run calling the next method
;; on the iterator for the first time and checking whether it is None (or
;; null in llvm here)
;; %5 contains the pointer to the first component in the vector here
%6 = icmp eq i64* %5, null
br i1 %6, label %end, label %forloop
end: ; preds = %forloop, %start
ret void
forloop: ; preds = %start, %forloop
%7 = phi i64* [ %next_component, %forloop ], [ %5, %start ]
;; here the boxed pointer is retrieved and dereferenced to retrieve the
;; vtable pointer
%8 = bitcast i64* %7 to {}**
%self_ptr = load {}*, {}** %8
%9 = getelementptr inbounds i64, i64* %7, i64 1
%vtable_ptr = bitcast i64* %9 to void ({}*)***
%vtable = load void ({}*)**, void ({}*)*** %vtable_ptr
;; 3 is the index into the vtable struct, which refers to the draw implementation for this particular struct
%trait_method_ptr = getelementptr inbounds void ({}*)*, void ({}*)** %vtable, i64 3
%trait_method = load void ({}*)*, void ({}*)** %vmethod
;; indirect call to trait method
call void %trait_method({}* %self_ptr)
;; retrieve the next object
%next_component = call i64* @"<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next"({ i64*, i64* }* %iter)
%14 = icmp eq i64* %next_component, null
br i1 %14, label %end, label %forloop
}
在 llvm 模块的大局变量中,咱们能够看到如下所示的 vtable。Button 和 SelectBox 都有相关的 vtable。
@vtable.screen = private unnamed_addr constant
;; the Type of the constant vtable structure
{ void (%SelectBox*)*, i64, i64, void (%SelectBox*)* }
{
;; first entry is the function to drop the object
void (%SelectBox*)* @"core::ptr::drop_in_place<test::SelectBox>", ;; destructor
i64 32, ;; size
i64 8, ;; alignment
;; last in the vtable is the pointer to the SelectBox::draw implementation
void (%SelectBox*)* @"<test::SelectBox as test::Draw>::draw"
}
;; the vtable for Button is structured basically the same
@vtable.button = private unnamed_addr constant
{ void (%Button*)*, i64, i64, void (%Button*)* }
{
void (%Button*)* @"core::ptr::drop_in_place<test::Button>",
i64 32, i64 8,
void (%Button*)* @"<test::Button as test::Draw>::draw"
}
这篇博文 Exploring Dynamic Dispatch in Rust 更具体的解说 vtable 和动态调度以及他们在 rust 和 C++ 中的差异。
单一承继
单承继十分简略:每个结构体或类在内存中按声明次序依次摆放。
class Base {
public:
void SetA(int value) {
_a = value;
}
private:
int _a;
};
class Derived: public Base {
public:
void SetB(int value) {
SetA(value);
_b = value;
}
protected:
int _b;
}
在这儿 a 和 b 将在内存中互相跟随,因而从一个类承继仅仅一个简略的问题。
将基类声明为承继类中的第一个成员。
%Base = type {
i32 ; '_a' in class Base
}
define void @Base_SetA(%Base* %this, i32 %value) nounwind {
%1 = getelementptr %Base, %Base* %this, i32 0, i32 0
store i32 %value, i32* %1
ret void
}
%Derived = type {
i32, ; '_a' from class Base
i32 ; '_b' from class Derived
}
define void @Derived_SetB(%Derived* %this, i32 %value) nounwind {
%1 = bitcast %Derived* %this to %Base*
call void @Base_SetA(%Base* %1, i32 %value)
%2 = getelementptr %Derived, %Derived* %this, i32 0, i32 1
store i32 %value, i32* %2
ret void
}
因而,基类简略的成为派生类的类型声明的一般成员。
然后编译器有必要在派生类被引证为其基类时刺进适当的类型转化,如上所示,运用 bitcast 运算符。
多重承继
多重承继也不是很难,仅仅在每个派生类内部按次序摆放多个承继的结构,同时考虑到屡次承继的数据成员的重复性。
下面的 c++ 代码:
class BaseA
{
public:
void SetA(int value)
{
_a = value;
}
private:
int _a;
};
class BaseB: public BaseA
{
public:
void SetB(int value)
{
SetA(value);
_b = value;
}
private:
int _b;
};
class Derived:
public BaseA,
public BaseB
{
public:
void SetC(int value)
{
SetB(value);
_c = value;
}
private:
// Derived now has two '_a' members and one '_b' member.
int _c;
};
等效 LLVM IR:
%BaseA = type {
i32 ; '_a' from BaseA
}
define void @BaseA_SetA(%BaseA* %this, i32 %value) nounwind {
%1 = getelementptr %BaseA, %BaseA* %this, i32 0, i32 0
store i32 %value, i32* %1
ret void
}
%BaseB = type {
i32, ; '_a' from BaseA
i32 ; '_b' from BaseB
}
define void @BaseB_SetB(%BaseB* %this, i32 %value) nounwind {
%1 = bitcast %BaseB* %this to %BaseA*
call void @BaseA_SetA(%BaseA* %1, i32 %value)
%2 = getelementptr %BaseB, %BaseB* %this, i32 0, i32 1
store i32 %value, i32* %2
ret void
}
%Derived = type {
i32, ; '_a' from BaseA
i32, ; '_a' from BaseB
i32, ; '_b' from BaseB
i32 ; '_c' from Derived
}
define void @Derived_SetC(%Derived* %this, i32 %value) nounwind {
%1 = bitcast %Derived* %this to %BaseB*
call void @BaseB_SetB(%BaseB* %1, i32 %value)
%2 = getelementptr %Derived, %Derived* %this, i32 0, i32 2
store i32 %value, i32* %2
ret void
}
然后,只需将 baseB 作为 BaseB 的实例引证,编译器就会供给所需的类型转化和指针算术。请留意,他所需求的仅仅从一个类到另一个类的位转化以及及时 getelementptr 的最终一个参数的调整。
虚拟承继
虚拟承继实践上十分简略,由于它要求将相同的基类合并到一个实例中。比方下面的 c++ 代码:
class BaseA
{
public:
int a;
};
class BaseB: public BaseA
{
public:
int b;
};
class BaseC: public BaseA
{
public:
int c;
};
class Derived:
public virtual BaseB,
public virtual BaseC
{
int d;
};
Dervied 将只包括一个 BaseA 实例,即使它的承继图规则它应该有两个承继。
成果如下:
class Derived
{
public:
int a;
int b;
int c;
int d;
};
所以 a 的第二个实例被默许的忽略,由于它会导致 BaseA 的多个实例存在于 Derived 中。
接口 Interface
接口只不过是没有数据成员的基类,其间一切办法都是纯虚拟的,也便是没有主体。因而转化 IR 的办法和将虚拟成员函数转化为 LLVM IR 的办法是相同的。
Boxing 和 Unboxing
Boxing 是将非目标原始值转化成目标的进程。创立一个包装类,你能够用非目标值实例化和初始化它。
Unboxing 是 boxing 的逆进程。你经过从 box 目标中检索 box 的值,将一个完好的目标降级为一个纯标量值。
下面是 Boxing 和 Unboxing 对应的 IR 代码:
@Boxee = global i32 17
%Integer = type { i32 }
define void @Integer_Create(%Integer* %this, i32 %value) nounwind {
; you might set up a vtable and associated virtual methods here
%1 = getelementptr %Integer, %Integer* %this, i32 0, i32 0
store i32 %value, i32* %1
ret void
}
define i32 @Integer_GetValue(%Integer* %this) nounwind {
%1 = getelementptr %Integer, %Integer* %this, i32 0, i32 0
%2 = load i32, i32* %1
ret i32 %2
}
define i32 @main() nounwind {
; box @Boxee in an instance of %Integer
%1 = load i32, i32* @Boxee
%2 = alloca %Integer
call void @Integer_Create(%Integer* %2, i32 %1)
; unbox @Boxee from an instance of %Integer
%3 = call i32 @Integer_GetValue(%Integer* %2)
ret i32 0
}
New 运算符
new 运算符一般只不过是 C malloc 函数的类型安全版别,在 C++ 的某些完结中,它们能够交换调用,而不会导致看不见或不需求的副作用。
实例的 new 操作符 new X 对应的 llvm ir 代码如下:
declare i8* @malloc(i32) nounwind
%X = type { i8 }
define void @X_Create_Default(%X* %this) nounwind {
%1 = getelementptr %X, %X* %this, i32 0, i32 0
store i8 0, i8* %1
ret void
}
define void @main() nounwind {
%1 = call i8* @malloc(i32 1)
%2 = bitcast i8* %1 to %X*
call void @X_Create_Default(%X* %2)
ret void
}
new X(Y,Z) 办法的调用是相同的,除了 X 和 Z 作为参数传递给结构函数。
数组的 new 运算符 代码 new X[100] 被映射到一个循环中,一次初始化每个数组元素:
declare i8* @malloc(i32) nounwind
%X = type { i32 }
define void @X_Create_Default(%X* %this) nounwind {
%1 = getelementptr %X, %X* %this, i32 0, i32 0
store i32 0, i32* %1
ret void
}
define void @main() nounwind {
%n = alloca i32 ; %n = ptr to the number of elements in the array
store i32 100, i32* %n
%i = alloca i32 ; %i = ptr to the loop index into the array
store i32 0, i32* %i
%1 = load i32, i32* %n ; %1 = *%n
%2 = mul i32 %1, 4 ; %2 = %1 * sizeof(X)
%3 = call i8* @malloc(i32 %2) ; %3 = malloc(100 * sizeof(X))
%4 = bitcast i8* %3 to %X* ; %4 = (X*) %3
br label %.loop_head
.loop_head: ; for (; %i < %n; %i++)
%5 = load i32, i32* %i
%6 = load i32, i32* %n
%7 = icmp slt i32 %5, %6
br i1 %7, label %.loop_body, label %.loop_tail
.loop_body:
%8 = getelementptr %X, %X* %4, i32 %5
call void @X_Create_Default(%X* %8)
%9 = add i32 %5, 1
store i32 %9, i32* %i
br label %.loop_head
.loop_tail:
ret void
}
C 调用 LLVM 接口
项目在:CLLVMCase
这是代码:
/*
int sum(int a, int b) {
return a + b;
}
,*/
void csum() {
LLVMModuleRef module = LLVMModuleCreateWithName("sum_module");
LLVMTypeRef param_types[] = {LLVMInt32Type(), LLVMInt32Type()};
// 函数参数依次是函数的类型,参数类型向量,函数数,标明函数是否可变的布尔值。
LLVMTypeRef ftype = LLVMFunctionType(LLVMInt32Type(), param_types, 2, 0);
LLVMValueRef sum = LLVMAddFunction(module, "sum", ftype);
LLVMBasicBlockRef entry = LLVMAppendBasicBlock(sum, "entry");
LLVMBuilderRef builder = LLVMCreateBuilder();
LLVMPositionBuilderAtEnd(builder, entry);
// IR 的表现办法有三种,一种是内存中的目标集,一种是文本言语,比方汇编,一种是二进制编码字节 bitcode。
LLVMValueRef tmp = LLVMBuildAdd(builder, LLVMGetParam(sum, 0), LLVMGetParam(sum, 1), "tmp");
LLVMBuildRet(builder, tmp);
char *error = NULL;
LLVMVerifyModule(module, LLVMAbortProcessAction, &error);
LLVMDisposeMessage(error);
// 可履行引擎,假如支撑 JIT 就用它,否则用 Interpreter。
LLVMExecutionEngineRef engine;
error = NULL;
LLVMLinkInMCJIT();
LLVMInitializeNativeTarget();
if (LLVMCreateExecutionEngineForModule(&engine, module, &error) != 0) {
fprintf(stderr, "Could not create execution engine: %s\n", error);
return;
}
if (error)
{
LLVMDisposeMessage(error);
return;
}
long long x = 5;
long long y = 6;
// LLVM 供给了工厂函数来创立值,这些值能够被传递给函数。
LLVMGenericValueRef args[] = {LLVMCreateGenericValueOfInt(LLVMInt32Type(), x, 0), LLVMCreateGenericValueOfInt(LLVMInt32Type(), y, 0)};
LLVMInitializeNativeAsmPrinter();
LLVMInitializeNativeAsmParser();
// 函数调用
LLVMGenericValueRef result = LLVMRunFunction(engine, sum, 2, args);
printf("%lld\n", LLVMGenericValueToInt(result, 0));
// 生成 bitcode 文件
if (LLVMWriteBitcodeToFile(module, "sum.bc") != 0) {
fprintf(stderr, "Could not write bitcode to file\n");
return;
}
LLVMDisposeBuilder(builder);
LLVMDisposeExecutionEngine(engine);
}
Swift 调用 LLVM 接口
llvm 的接口还能够经过 swift 来调用。
先创立一个 module.modulemap 文件,创立 LLVMC.h 和 LLVMC.c 文件,主动生成 SwiftLLVMCase-Bridging-Header.h。设置 header search paths 为 llvm 地点途径 /usr/local/opt/llvm/include ,library search paths 设置为 /usr/local/opt/llvm/lib 。将 /usr/local/opt/llvm/lib/libLLVM.dylib 加到 Linked Frameworks and Libraries 里。
module.modulemap 内容
module llvm [extern_c] {
header "LLVMC.h"
export *
}
LLVMC.h 里设置要用到的 llvm 的头文件,比方:
#ifndef LLVMC_h
#define LLVMC_h
#include <stdio.h>
#include <llvm-c/Analysis.h>
#include <llvm-c/BitReader.h>
#include <llvm-c/BitWriter.h>
#include <llvm-c/Core.h>
#include <llvm-c/Disassembler.h>
#include <llvm-c/ExecutionEngine.h>
#include <llvm-c/IRReader.h>
#include <llvm-c/Initialization.h>
#include <llvm-c/Linker.h>
#include <llvm-c/Object.h>
#include <llvm-c/Support.h>
#include <llvm-c/Target.h>
#include <llvm-c/TargetMachine.h>
#include <llvm-c/Transforms/IPO.h>
#include <llvm-c/Transforms/PassManagerBuilder.h>
#include <llvm-c/Transforms/Scalar.h>
#include <llvm-c/Transforms/Vectorize.h>
#include <llvm-c/lto.h>
#endif /* LLVMC_h */
在 swift 中写如下代码试试
import Foundation
import llvm
func hiIR() {
let module = LLVMModuleCreateWithName("HiModule")
LLVMDumpModule(module)
LLVMDisposeModule(module)
}
hiIR()
履行成果如下:
; ModuleID = 'HiModule'
source_filename = "HiModule"
下面一个简略的 c 函数
int sum(int a, int b) {
return a + b;
}
运用 llvm 的接口写对应的 IR 代码如下:
func cSum() {
let m = Module(name: "CSum")
let bd = IRBuilder(module: m)
let f1 = bd.addFunction("sum", type: FunctionType([IntType.int32, IntType.int32], IntType.int32))
// 增加基本块
let entryBB = f1.appendBasicBlock(named: "entry")
bd.positionAtEnd(of: entryBB)
let a = f1.parameters[0]
let b = f1.parameters[1]
let tmp = bd.buildAdd(a, b)
bd.buildRet(tmp)
m.dump()
}
dump 出对应 IR 如下:
; ModuleID = 'CSum'
source_filename = "CSum"
define i32 @sum(i32 %0, i32 %1) {
entry:
%2 = add i32 %0, %1
ret i32 %2
}
关于操控流函数,比方下面的 swift 函数:
func giveMeNumber(_ isBig : Bool) -> Int {
let re : Int
if !isBig {
// the fibonacci series (sort of)
re = 3
} else {
// the fibonacci series (sort of) backwards
re = 4
}
return re
}
运用 llvm 接口编写 IR,代码如下:
func controlFlow() {
let m = Module(name: "CF")
let bd = IRBuilder(module: m)
let f1 = bd.addFunction("calculateFibs", type: FunctionType([IntType.int1], FloatType.double))
let entryBB = f1.appendBasicBlock(named: "entry")
bd.positionAtEnd(of: entryBB)
// 给本地变量分配空间 let retVal : Double
let local = bd.buildAlloca(type: FloatType.double, name: "local")
// 条件比较 if !backward
let test = bd.buildICmp(f1.parameters[0], IntType.int1.zero(), .equal)
// 创立 block
let thenBB = f1.appendBasicBlock(named: "then")
let elseBB = f1.appendBasicBlock(named: "else")
let mergeBB = f1.appendBasicBlock(named: "merge")
bd.buildCondBr(condition: test, then: thenBB, else: elseBB)
// 指到 then block
bd.positionAtEnd(of: thenBB)
let thenVal = FloatType.double.constant(1/89)
bd.buildBr(mergeBB) // 到 merge block
// 指到 else block
bd.positionAtEnd(of: elseBB)
let elseVal = FloatType.double.constant(1/109)
bd.buildBr(mergeBB) // 到 merge block
// 指到 merge block
bd.positionAtEnd(of: mergeBB)
let phi = bd.buildPhi(FloatType.double, name: "phi_example")
phi.addIncoming([
(thenVal, thenBB),
(elseVal, elseBB)
])
// 赋值给本地变量
bd.buildStore(phi, to: local)
let ret = bd.buildLoad(local, type: FloatType.double, name: "ret")
bd.buildRet(ret)
m.dump()
}
输出对应 IR 代码:
; ModuleID = 'CF'
source_filename = "CF"
define double @giveMeNumber(i1 %0) {
entry:
%local = alloca i32, align 4
%1 = icmp eq i1 %0, false
br i1 %1, label %then, label %else
then: ; preds = %entry
br label %merge
else: ; preds = %entry
br label %merge
merge: ; preds = %else, %then
%phi_example = phi i32 [ 3, %then ], [ 4, %else ]
store i32 %phi_example, i32* %local, align 4
%ret = load i32, i32* %local, align 4
ret i32 %ret
}
这儿有完好代码 SwiftLLVMCase。
解说履行 bitcode(IR)
IR 的表现办法有三种,一种是内存中的目标集,一种是文本言语,一种是二进制编码字节 bitcode。
关于 Intel 芯片能够经过 Pin,arm 架构能够用 DynamoRIO,现在 DynamoRIO 只支撑 Window、Linux 和 Android 体系,对 macOS 的支撑还在进行中。另一种办法是经过依据 llvm 的 interpreter 开发来完结解说履行 bitcode,llvm 用许多 C++ 的接口在内存中操作,将可读的文本文件解析到内存中,编译进程文本的 IR 不会生成,只会生成一种紧凑的二进制标明,也便是 bitcode。下面具体说下怎样做。
先构建一个支撑 libffi 的 llvm。编译 llvm 源码时加上 libffi 的选项来翻开 DLLVM_ENABLE_FFI 的选项翻开 libffi,编译指令如下:
cmake -G Ninja -DLLVM_ENABLE_FFI:BOOL=ON ../llvm
创立一个项目。cmake 文件里留意设置自己的编译生成的 llvm 途径,还有 llvm 源码途径,设置这个途径首要是为了用装置 llvm 时没有包括的 ExecutionEngine/Interpreter/Interpreter.h 头文件。
完结办法是经过拜访 llvm 的 ExcutionEngine 进行 IR 指令解说履行。声明一个可拜访 ExcutionEngine 内部的类 PInterpreter,代码如下:
// 运用 public 拜访内部
class PInterpreter : public llvm::ExecutionEngine,
public llvm::InstVisitor<llvm::Interpreter> {
public:
llvm::GenericValue ExitValue;
llvm::DataLayout TD;
llvm::IntrinsicLowering *IL;
std::vector<llvm::ExecutionContext> ECStack;
std::vector<llvm::Function*> AtExitHandlers;
};
然后声明要用的办法。
class MInterpreter : public llvm::ExecutionEngine {
public:
llvm::Interpreter *interp;
PInterpreter *pItp;
llvm::Module *module;
explicit MInterpreter(llvm::Module *M);
virtual ~MInterpreter();
virtual void run();
virtual void execute(llvm::Instruction &I);
// 入口
virtual int runMain(std::vector<std::string> args,
char * const *envp = 0);
// 遵循 ExecutionEngine 接口
llvm::GenericValue runFunction(
llvm::Function *F,
const std::vector<llvm::GenericValue> &ArgValues
);
void *getPointerToNamedFunction(const std::string &Name,
bool AbortOnFailure = true);
void *recompileAndRelinkFunction(llvm::Function *F);
void freeMachineCodeForFunction(llvm::Function *F);
void *getPointerToFunction(llvm::Function *F);
void *getPointerToBasicBlock(llvm::BasicBlock *BB);
};
如上面代码所示,由于要履行 IR,所以用到获取 IR 的函数和基本块地址的办法,getPointerToFunction 和 getPointerToBasicBlock。最终再履行指令时,先打印出指令,然后进行履行,代码如下:
class MingInterpreter : public MInterpreter {
public:
MingInterpreter(Module *M) : MInterpreter(M) {};
virtual void execute(Instruction &I) {
I.print(errs());
MInterpreter::execute(I);
}
};
完好代码参看 MingInterpreter。
项目是依据 c 言语,能够运用 llvm include 里的 llvm-c/ExecutionEngine.h 接口头文件,运用 c 来编写。OC 和 Swift 项目还需求依据各自言语特性进行开发完善解说功用。