LWN:针对有验证功能的目标平台进行编译!

本文探讨了BPF程序在运行前必须通过内核验证器的挑战,GCC开发者何塞·马尔切西讲述了为验证架构编译面临的困难,包括限制、优化带来的问题和可能的解决方案,如禁用优化、针对性定制等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关注了就能看到更多这么棒的文章哦~

The challenge of compiling for verified architectures

By Jonathan Corbet
October 6, 2023
Cauldron
ChatGPT translation
https://lwn.net/Articles/946254/

在表面上来看,BPF虚拟机与许多其他计算机体系结构很相似;它具有寄存器,也有执行常规操作的指令。但存在一个关键差异:BPF程序必须在运行之前通过内核的验证器(verifier)的检查。验证器会施加一长串附加限制,以便能够确认任何一个收到的程序都是安全的。要通过这些检查可能会让BPF开发人员感到很痛苦。在2023年GNU工具大会上,何塞·马尔切西(José Marchesi)讨论了为经过验证的架构进行编译的问题,以及编译器如何生成能够通过验证的代码。

马尔切西一直在为GCC开发BPF后端,他首先表示,这个问题从本质上来说是无法解决的。BPF验证器试图做的事情在理论上就是不可能的。为了使问题可以得到处理,它对BPF程序强加了许多限制,因此许多正确的程序仍无法通过验证。验证器是一个随着每个内核版本而不断发展变动的编译目标平台,而且它不是唯一的验证器(Windows的BPF实现也有自己的验证器,施加了一些不一样的限制)。这是一项具有挑战性的任务,但也许实际上可以做一些事情来至少改善一下这个情况。

eb50fb9543e2affef79d021833157475.png

GCC里对BPF的“世界统治计划(world donimation plan)”有几个阶段,马尔切西介绍道。最初的目标是实现与LLVM达到平等水平,LLVM目前仍然是所有BPF程序都依赖的工具。他指出,LLVM BPF后端是由同时在开发BPF虚拟机的相同开发人员创建的;双方都可以进行更改从而来适应对方。但是,GCC开发人员则必须处理已经实际存在的虚拟机。因此,这个任务花了几年时间,但已经基本实现了。

下一步是能够编译和运行所有内核里已有的BPF tests;这是目前正在进行的工作。编译测试基本上已经完成,但由于上述原因运行它们要棘手得多:尽管已知这些程序是正确的,但GCC生成的代码会违反验证器的规则。马尔切西表示,一旦克服了这个问题,下一个目标将是编译和运行所有现存的BPF程序。

他说,像BPF虚拟机这样的要验证的目标是跟沙箱不同的。沙箱需要在代码周围包上一层监狱,从而阻止任何企图进行的恶意行为。验证器则相反,是要自己证明程序不会有害,然后在有特权的环境中运行它。验证器会施加许多“奇特性(peculiarities)”,编译器必须应对这些奇特性。例如,BPF程序具有不同的堆栈,每个frame都单独存储。但实际问题在于要生成可验证的代码。举例来说,验证器会对BPF代码中的后向跳转(backward jump)给出抱怨;开发人员可以设法避免它们,但优化编译器则会带来一些影响,并且有时候很难搞清楚后向跳转是在哪里引入的。还有许多其他方式都会被判定验证失败,这个列表会随着内核版本的更新而改变。

他表示,验证中出现的问题可分为两种主要类别。一种是某些类型的源代码(source constructs)根本就无法通过验证;这包括计算得出的goto跳转(computed gotos)和没有已知总次数限制的循环(loops without known bounds)。另一种是代码优化所引入的变动;开发人员可以避免有问题的源代码,也就是在编写代码时就考虑要通过验证的注意事项,但优化过的编译器可能混淆代码并引入问题。针对BPF这种编译目标平台的编译器要有额外的努力,才能生成能够通过验证的代码——这不是一个简单的目标。

Paths toward a solution

马尔切西然后提出了解决这个问题的一长串方法,从“方案零(approach zero)”开始:“也就是什么都不做”。他说,这是GCC的当前策略。它已经足可以完成编译DTrace BPF这个支持例程了,但远不足以用于通用情况。他表示,大约90%的BPF自检测目前在使用GCC编译时未能通过验证。因此,这一方法对于现有程序显然都不够,更不用说未来可能出现的程序了。

方法一,是完全禁用优化。这会降低性能,以及增加程序size;程序size很重要,因为验证器有大小限制。另一个问题似乎是某些程序无法在没有-O2优化的情况下工作;它们依赖于优化器提供的常量折叠和传播(constant folding and propagation)。因此,这似乎不是一个适用于现实编译器的可行选项。

方法二,是有选择性地禁用优化,找到会导致无法验证代码的那些优化并将其禁用。这是一个必须通过一些自动方式来完成的任务,他说。它可以通过将验证器施加的限制形式化(formalizing),然后在每个优化阶段后测试所生成的代码,以确保满足这些限制。如果引入了问题,那么该这组改动就可以被丢弃。然而,有的某一个优化阶段(pass)中可能会引入许多不同的优化;放弃这个阶段的话将使好的改动和坏的改动一起被丢弃。他说,还有一些问题是由于多个阶段组合才产生的。

另一种方法是方法三:“回退某个阶段的修改(antipasses)”,它需要明确地撤销导致问题的代码优化。这是LLVM目前使用的方法;它的优点在于只撤销有问题的改动,而不会影响编译过程的其他部分。但这种方案很脆弱,容易被编译器中其他地方的更改破坏;这导致了“维护是非常困难的”。他说,今年的LSFMM+BPF峰会上有开发人员表示,他们必须使用特定LLVM版本才能编译通过自己的BPF程序。因此,尽管LLVM开发人员正在添加新功能,但用户仍然坚持使用实际能工作且不具备这些新功能的旧版本。

方法四,是面向目标平台进行优化阶段的裁剪(target-driven pass tailoring),或通过直接在优化过程中插入hook来关闭相应的优化。LLVM试图朝这个方向发展,但其他编译器开发人员正在抵制这种做法。这会导致完全合法(从语言意义上)的优化变得非法,也就无法使用了。然而,这是一种更健壮的解决问题的方法。

还有一个变种是方法五:通用的阶段定制(generic pass tailoring)。编译器原有的-Osmall 或者-Ofast 之类的参数之外可以添加一个新选项(例如-Overifiable),用来指定仅对代码进行相对来说比较保守的转换来进行优化。这个选项不限于任何一个后端,也可以在其他场景下使用。他表示一些公司有安全团队会在二进制代码上运行静态分析器。这样的选项创建的更可预测的代码流(他甚至建议-Opredictable可能是一个更好的名称)会更容易供分析器使用。

方法六将是语言级别的支持;这可以采取例如提供有边界循环(loop bounds)的“#pragma”语句的形式。然而,很快就指出,验证器不能信任这种类型的源代码声明,因此这个方法很快被废除了。方法七是添加到汇编程序的支持,也被迅速放弃了。

马尔切西总结了演讲,最终可能需要结合上述方法来让GCC通常创建可以通过验证的BPF代码。他希望在进行下一步之前能先就要做什么的细节来建立广泛共识。为此,他正在与BPF标准化流程合作,希望能够让其他GCC开发人员也参与进来;他还正在与LLVM开发人员进行协调。从一般意义上说,这个问题可能是无法解决的,但对于BPF开发人员来说,确实可以做得更好。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

format,png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值