LLVM IR入门指南(7)——异常处理

2023-11-19

在这篇文章中,我主要介绍的是LLVM IR中的异常处理的方法。主要的参考文献是Exception Handling in LLVM

异常处理的要求

异常处理在许多高级语言中都是很常见的,在诸多语言的异常处理的方法中,trycatch块的方法是最多的。对于用返回值来做异常处理的语言(如C、Rust、Go等)来说,可以直接在高级语言层面完成所有的事情,但是,如果使用trycatch,就必须在语言的底层也做一些处理,而LLVM的异常处理则就是针对这种情况来做的。

首先,我们来看一看一个典型的使用trycatch来做的异常处理应该满足怎样的要求。C++就是一个典型的使用trycatch来做异常处理的语言,我们就来看看它的异常处理的语法:

// try_catch_test.cpp
struct SomeOtherStruct { };
struct AnotherError { };

struct MyError { /* ... */ };
void foo() {
	SomeOtherStruct other_struct;
	throw MyError();
	return;
}

void bar() {
	try {
		foo();
	} catch (MyError err) {
		// do something with err
	} catch (AnotherError err) {
		// do something with err
	} catch (...) {
		// do something
	}
}

int main() {
	return 0;
}

这是一串典型的异常处理的代码。我们来看看C++中的异常处理是怎样一个过程(可以参考throw expressiontry-block):

当遇到throw语句的时候,控制流会沿着函数调用栈一直向上寻找,直到找到一个try块。然后将抛出的异常与catch相比较,看看是否被捕获。如果异常没有被捕获,则继续沿着栈向上寻找,直到最终能被捕获,或者整个程序调用std::terminate结束。

按照我们上面的例子,控制流在执行bar的时候,首先执行foo,然后分配了一个局部变量other_struct,接着遇到了一个throw语句,便向上寻找,在foo函数内部没有找到try块,就去调用foobar函数里面寻找,发现有try块,然后通过对比进入了第一个catch块,顺利处理了异常。

这一过程叫stack unwinding,其中有许多细节需要我们注意。

第一,是在控制流沿着函数调用栈向上寻找的时候,会调用所有遇到的自动变量(大部分时候就是函数的局部变量)的析构函数。也就是说,在我们上面的例子里,当控制流找完了foo函数,去bar函数找之前,就会调用other_struct的析构函数。

第二,是如何匹配catch块。C++的标准中给出了一长串的匹配原则,在大多数情况下,我们只需要了解,只要catch所匹配的类型与抛出的异常的类型相同,或者是引用,或者是抛出异常类型的基类,就算成功。

所以,我们总结出,使用trycatch来处理异常,需要考虑以下要求:

  • 能够改变控制流
  • 能够正确处理栈
  • 能够保证抛出的异常结构体不会因为stack unwinding而释放
  • 能够在运行时进行类型匹配

LLVM IR的异常处理

下面,我们就看看在LLVM IR层面,是怎么进行异常处理的。

我们要指出,异常处理实际上有许多种形式。我这篇文章主要以Clang对C++的异常处理为例来说的。而这主要是基于Itanium提出的零开销的一种错误处理ABI标准,关于这个的详细的信息,可以参考Itanium C++ ABI: Exception Handling

首先,我们把上面的try_catch_test.cpp代码编译成LLVM IR:

clang++ -S -emit-llvm try_catch_test.cpp

然后,我们仔细研究一下错误处理。

关于上面的异常处理的需求,我们发现,可以分为两类:怎么抛,怎么接。

怎么抛

所谓怎么抛,就是如何抛出异常,主要需要保证抛出的异常结构体不会因为stack unwinding而释放,并且能够正确处理栈。

对于第一点,也就是让异常结构体存活,我们就需要不在栈上分配它。同时,我们也不能直接裸调用malloc等在堆上分配的方法,因为这个结构体也不需要我们手动释放。C++中采用的方法是运行时提供一个API:__cxa_allocate_exception。我们可以在foo函数编译而成的@_Z3foov中看到:

define void @_Z3foov() #0 {
	%1 = call i8* @__cxa_allocate_exception(i64 1) #3
	%2 = bitcast i8* %1 to %struct.MyError*
	call void @__cxa_throw(i8* %1, i8* bitcast ({ i8*, i8* }* @_ZTI7MyError to i8*), i8* null) #4
	unreachable
}

第一步就使用了@__cxa_allocate_exception这个函数,为我们异常结构体开辟了内存。

然后就是要处理第二点,也就是正确地处理栈。这里的方法是使用另一个C++运行时提供的API:__cxa_throw,这个API同时也兼具了改变控制流的作用。这个API开启了我们的stack unwinding。我们可以在libc++abi Specification中看到这个函数的签名:

void __cxa_throw(void* thrown_exception, struct std::type_info * tinfo, void (*dest)(void*));

其第一个参数,是指向我们需要抛出的异常结构体的指针,在LLVM IR的代码中就是我们的%1。第二个参数,std::type_info如果了解C++底层的人就会知道,这是C++的一个RTTI的结构体。简单来讲,就是存储了异常结构体的类型信息,以便在后面catch的时候能够在运行时对比类型信息。第三个参数,则是用于销毁这个异常结构体时的函数指针。

这个函数是如何处理栈并改变控制流的呢?粗略来说,它依次做了以下几件事:

  1. 把一部分信息进一步储存在异常结构体中
  2. 调用_Unwind_RaiseException进行stack unwinding

也就是说,用来处理栈并改变控制流的核心,就是_Unwind_RaiseException这个函数。这个函数也可以在我上面提供的Itanium的ABI指南中找到。

怎么接

所谓怎么接,就是当stack unwinding遇到try块之后,如何处理相应的异常。根据我们上面提出的要求,怎么接应该处理的是如何改变控制流并且在运行时进行类型匹配。

首先,我们来看如果bar单纯地调用foo,而非在try块内调用,也就是

void bar() {
	foo();
}

编译出的LLVM IR是:

define void @_Z3barv() #0 {
	call void @_Z3foov()
	ret void
}

和我们正常的不会抛出异常的函数的调用形式一样,使用的是call指令。

那么,如果我们代码改成之前的例子,也就是

void bar() {
	try {
		foo();
	} catch (MyError err) {
		// do something with err
	} catch (AnotherError err) {
		// do something with err
	} catch (...) {
		// do something
	}
}

其编译出的LLVM IR是一个很长很长的函数。其开头是:

define void @_Z3barv() #0 personality i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*) {
	%1 = alloca i8*
	%2 = alloca i32
	%3 = alloca %struct.AnotherError, align 1
	%4 = alloca %struct.MyError, align 1
	invoke void @_Z3foov()
		to label %5 unwind label %6
	; ...
}

我们发现,传统的call调用变成了一个复杂的invoketounwind的指令,这个指令是什么意思呢?

invoke指令就是我们改变控制流的另一个关键,我们可以在invoke instruction中看到其详细的解释。粗略来说,在我们上面编译出的LLVM IR代码中

invoke void @_Z3foov() to label %5 unwind label %6

这个代码的意思是:

  1. 调用@_Z3foov函数
  2. 判断函数返回的方式:
    • 如果是以ret指令正常返回,则跳转至标签%5
    • 如果是以resume指令或者其他异常处理机制返回(如我们上面所说的__cxa_throw函数),则跳转至标签%6

所以这个invoke指令其实和我们之前在跳转中讲到的phi指令很类似,都是根据之前的控制流来进行之后的跳转的。

我们的%5的标签很简单,因为原来C++代码中,在trycatch块之后啥也没做,就直接返回了,所以其就是简单的

5:
	br label %18
18:
	ret void

而我们的catch的方法,也就是在运行时进行类型匹配的关键,就隐藏在%6标签内。

我们通常称在调用函数之后,用来处理异常的代码块为landing pad,而%6标签,就是一个landing pad。我们来看看%6标签内是怎样的代码:

6:
	%7 = landingpad { i8*, i32 }
		catch i8* bitcast ({ i8*, i8* }* @_ZTI7MyError to i8*) ; is MyError or its derived class
		catch i8* bitcast ({ i8*, i8* }* @_ZTI12AnotherError to i8*) ; is AnotherError or its derived class
		catch i8* null ; is other type
	%8 = extractvalue { i8*, i32 } %7, 0
	store i8* %8, i8** %1, align 8
	%9 = extractvalue { i8*, i32 } %7, 1
	store i32 %9, i32* %2, align 4
	br label %10
10:
	%11 = load i32, i32* %2, align 4
	%12 = call i32 @llvm.eh.typeid.for(i8* bitcast ({ i8*, i8* }* @_ZTI7MyError to i8*)) #3
	%13 = icmp eq i32 %11, %12 ; compare if is MyError by typeid
	br i1 %13, label %14, label %19
19:
  	%20 = call i32 @llvm.eh.typeid.for(i8* bitcast ({ i8*, i8* }* @_ZTI12AnotherError to i8*)) #3
  	%21 = icmp eq i32 %11, %20 ; compare if is Another Error by typeid
  	br i1 %21, label %22, label %26

说人话的话,是这样一个步骤:

  1. landingpad将捕获的异常进行类型对比,并返回一个结构体。这个结构体的第一个字段是i8*类型,指向异常结构体。第二个字段表示其捕获的类型:
    • 如果是MyError类型或其子类,第二个字段为MyError的TypeID
    • 如果是AnotherError类型或其子类,第二个字段为AnotherError的TypeID
    • 如果都不是(体现在catch i8* null),第二个字段为null的TypeID
  2. 根据获得的TypeID来判断应该进哪个catch

我将上面代码中一些重要的步骤之后都写上了注释。

我们之前一直纠结的如何在运行时比较类型信息,landingpad帮我们做好了,其本质还是根据C++的RTTI结构。

在判断出了类型信息之后,我们会根据TypeID进入不同的标签:

  • 如果是MyError类型或其子类,进入%14标签
  • 如果是AnotherError类型或其子类,进入%22标签
  • 如果都不是,进入%26标签

这些标签内错误处理的框架都很类似,我们来看%14标签:

14:
	%15 = load i8*, i8** %1, align 8
	%16 = call i8* @__cxa_begin_catch(i8* %15) #3
	%17 = bitcast i8* %16 to %struct.MyError*
	call void @__cxa_end_catch()
	br label %18

都是以@__cxa_begin_catch开始,以@__cxa_end_catch结束。简单来说,这里就是:

  1. 从异常结构体中获得抛出的异常对象本身(异常结构体可能还包含其他信息)
  2. 进行异常处理
  3. 结束异常处理,回收、释放相应的结构体

在哪可以看到我的文章

我的LLVM IR入门指南系列可以在我的个人博客、GitHub:Evian-Zhang/llvm-ir-tutorial知乎CSDN中查看,本教程中涉及的大部分代码也都在同一GitHub仓库中。

本人水平有限,写此文章仅希望与大家分享学习经验,文章中必有缺漏、错误之处,望方家不吝斧正,与大家共同学习,共同进步,谢谢大家!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

LLVM IR入门指南(7)——异常处理 的相关文章

  • 使用 clang-llvm 编译器在 CUDA 中添加对类似于 __shared__ 的内存类型的支持

    我正在努力添加类似于的新内存类型 shared 在 CUDA 中称为 noc 需要使用 clang llvm 进行编译 以下是实现对新内存类型的解析的步骤 引用来自answer https stackoverflow com questio
  • Xcode 4 在使用 @property 时自动生成 iVar,在哪里可以找到此功能的官方文档?

    我已阅读 What s new in Xcode 但我找不到此功能的官方解释 哪里可以找到官方的解释 哪个文档 谢谢 假设你的意思是它会自动生成一个 ivar 以及 getter 和 setter 方法 即使你省略了 synthesize
  • Xcode“来自调试器的消息:对 k 数据包收到意外响应:正常”

    在模拟器上测试我的应用程序时收到此消息 来自调试器的消息 对 k 数据包收到意外响应 正常 这是什么意思 我的应用程序是否存在任何危险 使用 Xcode 6 4 和 7 2 如果你看一下文件进程GDBRemote cpp http llvm
  • 使用 llvm-prof 收集 LLVM 边缘分析

    我正在使用这些命令来编译下面的代码以收集 trunk llvm 中的边缘 块分析 clang emit llvm c sort c o sort bc opt insert edge profiling sort bc o sort pro
  • 有 libclang 的教程吗? [关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 我一直在寻找一些易于理解的 libclang 指南 我在这里或其他论坛上看到了一些帖子 但唯一推荐的信
  • 链接不支持异常处理的代码 (C++/LLVM)

    我正在尝试使用 llvm 作为我的软件的代码生成后端 并且刚刚意识到 llvm 的编译不支持 C 异常处理 为了提高效率 然而 在我的软件中 我广泛使用异常处理 如果我将所有回调函数包装在 try catch blocks 中 这样就不需要
  • 在 llvm 上运行 x86 程序

    是否可以使用llvm来运行x86程序 IE 我想使用 llvm 作为 x86 模拟器来运行 x86 程序 然后对 x86 程序进行检测 Thanks 我想你正在寻找LibCPU http LibCPU Org It has x86 前端 h
  • LoadInst 和 StoreInst 值和地址 LLVM

    我有一个文件 print c 它有两个功能 void printLoad print address and value of memory location from which value printf address value vo
  • 未优化的 clang++ 代码在简单的 main() 中生成不需要的“movl $0, -4(%rbp)”

    我创建了一个最小的 C 程序 int main return 1234 并使用 clang 5 0 禁用优化 默认 O0 得到的汇编代码是 https gcc godbolt org z OYLghAFBqd5QCxAYwPYBMCmBRd
  • 如何在 Xcode 中禁用一个文件的优化

    我的 Xcode 项目依赖于另一个库 当我使用以下命令构建项目时 这会导致项目出现错误 O3 option 这些错误仅存在于一个文件中 所以我想关掉 O3 该文件的选项 是否可以 打开目标 看下Build Phases 打开Compile
  • 如何从 LLVM 的中间表示中获取程序每个函数中执行的函数调用列表?

    我正在尝试使用 LLVM 构建一个简单版本的代码分析工具 我有一些 ll 文件 其中包含某些程序的中间 LLVM 表示 如何从 LLVM 的中间表示中获取程序每个函数中执行的函数调用列表 我的输入参数是 LLVM Module 类的一个实例
  • 使用 LLVM 内联特定函数调用

    给定一个llvm CallInst 我如何告诉内联器内联这个特定的调用 我可以将目标函数标记为AlwaysInline这将内联调用 但它也会内联每个调用 也许有某种方法可以在我发出特定调用时调用内联函数 内联基本块内的所有调用也可以 您可以
  • LLVM到底是什么?

    我一直听说 LLVM 它是 Perl 语言 然后是 Haskell 语言 然后有人用其他语言使用它 它是什么 它与 GCC 到底有什么区别 视角 安全等 LLVM 是一个用于构建 优化和生成中间和 或二进制机器代码的库 LLVM 可以用作编
  • LLVM 尾调用优化

    以下是我对事情的理解 当函数 f 调用自身是其最后一个动作时 它是尾递归的 通过形成循环而不是再次调用函数 可以显着优化尾递归 函数的参数已就地更新 并且函数体再次运行 这称为递归尾调用优化 LLVM 在使用 fastcc GHC 或 Hi
  • 使用 libclang 从内存中的 C 代码生成程序集

    我需要实现一个使用 LLVM Clang 作为后端将 C 代码编译为 eBPF 字节码的库 代码将从内存中读取 我也需要在内存中获取生成的汇编代码 到目前为止 我已经能够使用以下代码编译为 LLVM IR include
  • C++11 的 LLVM&Clang 支持

    我有一些为 MS VC 10 编写的代码 我使用 C 11 特别是像这样的表达式 std function
  • 字节码和位码有什么区别[重复]

    这个问题在这里已经有答案了 可能的重复 LLVM 和 java 字节码有什么区别 https stackoverflow com questions 454720 what are the differences between llvm
  • 如何让 clangd 转向 c++20

    当没有其他信息时 如何让 clangd 回退到 c 20 例如 在第一次构建之前 cmake 可以生成一个 这是在带有最新 LLVM 的 Arch Linux 上 这是通过 Emacs LSP 运行的 但这应该没有什么区别 你可以加 Com
  • 如何在 LLVM IR 中使用 RISC-V Vector (RVV) 指令?

    In 这个演示文稿 https llvm org devmtg 2019 04 slides TechTalk Kruppe Espasa RISC V Vectors and LLVM pdfKruppe 和 Espasa 概述了 RIS
  • 如何从 LLVM 指令获取文件名和目录?

    我需要在 llvm 过程中提取目录和文件名 当前版本的 llvm 已移动getFilename and getDirectory from DebugLoc to DebugInfoMetadata 我找不到班级成员getFilename直

随机推荐

  • Quartz框架多个trigger任务执行出现漏执行的问题分析

    一 问题描述 使用Quartz配置定时任务 配置了超过10个定时任务 这些定时任务配置的触发时间都是5分钟执行一次 实际运行时 发现总有几个定时任务不能执行到 二 示例程序 1 简单介绍 采用spring quartz整合方案实现定时任务
  • docker菜鸟入门

    菜鸟入门Docker 说明 一 什么是Docker 1 虚拟机和Linux容器 二 Docker用途 三 Docker安装 1 设置仓库 2 安装 Docker Engine Community 3 验证安装成功 四 Docker启动与停止
  • VoVNet论文解读

    摘要 1 介绍 2 高效网络设计的影响因素 2 1 内存访问代价 2 2 GPU计算效率 3 建议的方法 3 1 重新思考密集连接 3 2 One Shot Aggregation 3 3 构建 VoVNet 网络 4 实验 5 代码解读
  • 01背包(c++版)

    dp i j 表示从下标为 0 i 的物品里任意取 放进容量为j的背包 价值总和最大是多少 void test 2 wei bag problem1 vector
  • 上班族为何需要做副业?如何靠副业月入过万?

    网上统计某某城市平均工资8千以上 诶 一看自己3 4千 其实现在每个背井离乡在外上班的打工人都挺难的 城市很繁华 工资很现实 在工厂打螺丝的更是苦逼的生活 被资本家无情的压榨 在豪华的写字楼上班的白领 也过着996的生活 甚至是007的日子
  • 操作系统复习题

    一 选择题 在计算机系统中 操作系统是 核心系统软件 网络操作系统 不是基本的操作系统 实时性 不是分时系统的基本特征 关于操作系统的叙述 能方便用户编程的程序 是不正确的 操作系统的发展过程是 设备驱动程序组成的原始操作系统 管理程序 操
  • Response 456错误

    今天使用某share拉取股票数据时 遇到了Response 456错误 然而在网上查也没有查到 感觉是是较为少见的错误 http response code HTTP状态码对照表 t 332741160的专栏 CSDN博客 后来发现这个错误
  • 【Bun1.0】使用 Bun.js 构建快速、可靠和安全的 JavaScript 应用程序

    bun js Bun 是一个现代的JavaScript运行环境 如Node Deno 主要特性如下 启动速度快 更高的性能 完整的工具 打包器 转码器 包管理 官网 https bun sh 优点 与传统的 Node js 不同 Bun j
  • python数据分析与挖掘实战 -第四章数据预处理

    数据清洗 目的 删除原始数据集中的无关数据 重复数据 平滑噪声数据 筛选掉与挖掘主题无关的数据 处理缺失值 异常值等 缺失值处理方法 删除记录 数据插补和不处理 拉格朗日插值法 对于平面上已知的N个点 无两点在一条直线上 可以找到一个N 1
  • 解决vue中样式不起作用:样式穿透/深度选择器(/deep/)

    原因1 组件内部使用组件 添加了scoped属性 原因2 动态引入html 也添加了scoped属性 原因3 非以上两种 一 添加了scoped属性 Vue中的scoped属性的效果主要是通过PostCss实现的 以下是转译前的代码
  • 关于数据结构中的叶节点和二度节点的关系(通俗的理解)。

    简单记录一下自己的一些地方和对于这个问题我的一些见解 有说的不好的地方欢迎指正 这里只给出一种理解 另一种利用公式进行理解的 我就不写了 因为csdn里面太多了 先说结论 叶节点的数目 二度节点 1 首先来看这张图 可以看到这个图大体是包含
  • redis 基础概述与使用

    目录 一 redis 概述 redis 主从同步执行流程 redis 淘汰策略 缓存常见问题 KEYS指令与SCAN指令 SpringBoot 整合 redis StringRedisTemplate 与 RedisTemplate red
  • Android练手完整项目app(三)商品分类+流式布局Tag

    1 整体布局 结合项目 一 在FunctionFragment创建整体布局 搜索框布局应该include引入 这里我就没单独抽取
  • java springboot实现手机短信发送

    以下是一个使用Spring Boot实现手机短信发送的示例 首先添加pom依赖 需要引入阿里云的短信SDK和Spring Boot的web依赖
  • 【Linux】fork()

    目录 1 fork是什么 2 fork复制原理 3 逻辑地址与物理地址 4 计算fork 输出次数 1 fork是什么 linux下创建新进程的系统调用的是fork 其定义如下 include
  • 蓝桥杯获奖比例java_2019年第十届蓝桥杯省赛总结(JavaA组)

    update3 28 省一rank4 莫名进了国赛好神奇 记yzm10第一次体验A组 纯粹瞎水 早闻山东的JavaA组神仙打架 进国赛都成了奢望 往年只有五个名额 因此抱着做分母的心态来为学弟学妹试水 来到考场发现同组中光认识的大佬就不止五
  • Glide图片加载回调监听

    前两篇文章从源码的角度对Glide的加载流程进行了分析 这篇文章将对Glide的回调进行总结 1 方法一 设置图片中监听 方法一使用的是SimpleTarget类 他继承自BaseTarget 需要重写onResourceReady方法 o
  • 观点

    原文地址 https www sohu com a 315434322 672569 作者 中国工商银行业务研发中心 郝毅 霍嘉 肖烨 金石乔 本文笔者着重介绍了金融行业软件自动化测试的相关实践与思考 近两年来 多家金融机构和专业测试组织开
  • 超级完整 的 Maven 讲解 以及私服搭建

    第一章 Maven 简介 1 1 Maven 概述 Maven 是一款基于 Java 平台的项目管理和整合工具 它将项目的开发和管理过程抽象成一个项目对象模型 POM 开发人员只需要做一些简单的配置 Maven 就可以自动完成项目的编译 测
  • LLVM IR入门指南(7)——异常处理

    在这篇文章中 我主要介绍的是LLVM IR中的异常处理的方法 主要的参考文献是Exception Handling in LLVM 异常处理的要求 异常处理在许多高级语言中都是很常见的 在诸多语言的异常处理的方法中 try catch块的方