learn-tech/专栏/WebAssembly入门课/07WASI:你听说过WebAssembly操作系统接口吗?.md
2024-10-16 06:37:41 +08:00

154 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
07 WASI你听说过 WebAssembly 操作系统接口吗?
你好,我是于航。
相信你在刚刚接触到 WebAssembly 这门技术的时候一定有所发现WebAssembly 这个单词实际上是由两部分组成,也就是 “Web” 和 “Assembly”。
“Web” 表明了 Wasm 的出身,也就是说它发明并最早应用于 Web 浏览器中, “Assembly” 则表明了 Wasm 的本质,这个词翻译过来的意思是 “汇编”,也就是指代它的 V-ISA 属性。
鉴于 Wasm 所拥有“可移植”、“安全”及“高效”等特性Wasm 也被逐渐应用在 Web 领域之外的一些其他场景中。今天我们将要讲解的,便是可以用于将 Wasm 应用到 out-of-web 环境中的一项新的标准 —— WASIWebAssembly System InterfaceWasm 操作系统接口。通过这项标准Wasm 将可以直接与操作系统打交道。
在正式讲解 WASI 之前,我们先来学习几个与它息息相关的重要概念。在了解了这些概念之后,相信甚至不用我过多介绍,你也能够感受到 WASI 是什么,以及它是如何与 Wasm 紧密结合的。
Capability-based Security
第一个我们要讲解的,是一个在“计算机安全”领域中十分重要的概念 —— “Capability-based Security”翻译过来为“基于能力的安全”。由于业界没有一个相对惯用的中文表达方式因此我还是保持了原有的英文表达来作为本节的标题在后面的内容中我也将直接使用它的英文表达方式以保证内容的严谨性。
Capability-based Security 是一种已知的、常用的安全模型。通常来讲,在计算机领域中,我们所提及的 capability 可以指代如 Token、令牌等概念。capability 是一种用于表示某种权限的标记,它可以在用户之间进行传递且无法被伪造。
在一个使用了 Capability-based Security 安全模型的操作系统中,任何用户对计算机资源的访问,都需要通过一个具体的 capability 来进行。
Capability-based Security 同时也指代了一种规范用户程序的原则。比如这些用户程序可以根据“最小特权原则”(该原则要求计算环境中的各个模块仅能够访问当下所必需的信息或资源)来彼此直接共享 capability这样可以使得操作系统仅分配用户程序需要使用的权限并且可以做到“一次分配多次使用”。
Capability-based Security 这个安全模型,通常会跟另外的一种基于“分级保护域”方式实现的安全模型形成对比。
基于“分级保护域”实现的安全模型,被广泛应用于类 Unix 的各类操作系统中,比如下图所示的操作系统 Ring0 层和 Ring3 层Ring1 / Ring2 一般不会被使用)便是“分级保护域”的一种具体实现形式。
在传统意义上Ring0 层拥有着最高权限,一般用于内核模式;而 Ring3 层的权限则会被稍加限制,一般用于运行用户程序。当一个运行在 Ring3 层的用户程序,试图去调用只有 Ring0 层进程才有权限使用的指令时,操作系统会阻止调用。这就是“分级保护域”的大致概念。
反观 Capability-based Securitycapability 通过替换在分级保护域中使用的“引用”,来达到提升系统安全性的目的。这里的“引用”是指用于访问资源的一类“定位符”,比如用于访问某个文件资源的“文件路径字符串”便是一个引用。
引用本身并没有指定实际对应资源的权限信息,以及哪些用户程序可以拥有这个引用。因此,每一次尝试通过该引用来访问实际资源的操作,都会经由操作系统来进行基于“分级保护域”的权限验证。比如验证发起访问的用户是否有权限持有该资源,这种方式便十分适合早期计算机系统的“多用户”特征(每个用户有不同的权限)。
在具有 capability 概念的操作系统中,只要用户程序拥有了这个 capability那它就拥有足够的权限去访问对应的资源。从理论上来讲基于 Capability-based Security 的操作系统甚至不需要如“权限控制列表ACL”这类的传统权限控制机制。
当然,为了实现上述我们提到的 capability 的能力,每一个 capability 不再是单一的由“字符串”组成的简单数据结构。并且我们还需要保障capability 的内部结构不会被用户程序直接访问和修改,以防止 capability 本身被伪造。
相对应的,用户程序只能够通过 capability 暴露出的特定“入口”,来访问对应的系统资源。我们可以用操作系统中常见的一个概念 —— “文件描述符File Descriptor”来类比 capability 的概念。如下图所示。
我们可以将文件描述符类比为 capability。举个例子当应用程序在通过 C 标准库中的 “fopen” 函数去打开一个文件时,函数会返回一个非负整数,来表示一个特定文件资源对应的文件描述符。
在拥有了这个描述符后,应用程序便可以按照在调用 “fopen” 函数时所指定的操作(比如 “w”来相应地对这个文件资源进行处理。当函数返回负整数时则表示无法获得该资源。在这些返回的错误代码中就包含有与“权限不足”相关的调用错误信息。
最为重要的一点是,拥有某个 capability 的用户程序,可以“任意地”处理这个 capability。比如可以访问其对应的系统资源、可以将其传递给其他的应用程序来进行使用或者也可以选择直接将这个 capability 删除。操作系统有义务确保某个特定的 capability 只能够对应系统中的某个特定的资源或操作,以保证安全策略的完备性。
系统调用System Call
第二个我们要讲解的概念叫做 “System Call”翻译成中文即“系统调用”或者也可称为 “操作系统调用”,这里我们使用简称 “系统调用”)。
还是回到我们在上一小节中曾提到过的一个场景:“使用 C 标准库中的 fopen 函数,来打开一个计算机本地文件”。请试想,当我们在调用这个 fopen 函数打开某个文件时实际上发生了什么fopen 函数是如何访问操作系统的文件资源的呢?带着这两个问题,我们一步步来看。
既然我们说 fopen 函数是 C 标准库中定义的一个函数,那么我们就从某个特定的 C 标准库实现所对应的源代码入手,来看看 fopen 函数的具体实现细节。这里我们以 musl 这个 libc 的实现为例。在它的源代码中,我们可以找到如下这段对 fopen 函数的定义代码(这里只列出了关键的部分)。
FILE *fopen(const char *restrict filename, const char *restrict mode) {
...
/* Compute the flags to pass to open() */
flags = __fmodeflags(mode);
fd = sys_open(filename, flags, 0666);
if (fd < 0) return 0;
...
}
代码的具体实现流程和细节我们不做深究唯一你需要注意的就是这句函数调用语句 _fd = sys_open(filename, flags, 0666);_在这行语句中musl 调用了一个名为 sys_open 的函数而在这个函数的背后就是我们本节内容的主角 系统调用
实际上任何其他需要与操作系统资源打交道的 C 甚至是 C++ 标准库函数包括 fopen 函数在内都需要通过 系统调用 来间接访问和使用这些系统资源sys_open 函数其实是对系统调用进行了封装在函数内部会使用内联的汇编代码去实际调用某个具体的系统调用这里 sys_open 对应的便是指用于打开本地文件资源的那个系统调用
每一个系统调用都对应着需要与操作系统打交道的某个特定功能并且有着唯一的系统调用 ID 与之相对应在不同的操作系统中对应同一系统调用的系统调用 ID 可能会发生变化
C/C++ 标准库的作用便是为我们提供了一个统一稳定的编程接口让我们的程序可以做到一次编写到处编译从某种程度上来讲标准库的出现为应用程序源代码提供了可移植性比如让我们不再需要随着操作系统类型的变化而硬编码不同的系统调用 ID
除此之外标准库还会帮助我们处理系统调用前后需要做的一些事情比如简化函数参数的传递对各种异常情况进行处理以及关闭文件之类的善后工作关于用户应用程序与操作系统调用之间的关系你可以参考我下面绘制的这幅图
WebAssembly 操作系统接口WASI
好了在讲解完 Capability-based Security 以及系统调用这两个概念之后我们再把目光移回到今天的主角 WASI其实从 WASI 对应的全称中我想你能够猜测到它肯定与我们在上一节中介绍的系统调用有着某种联系System Call System Interface没错那么接下来我们就一起看看 WASI 究竟是什么
我们从如何在 Web 场景之外使用 Wasm这个问题开始说起我们都知道Wasm 是一套新的 V-ISA也就是虚拟指令集架构其中的这些虚拟指令便无法被真实的物理 CPU 硬件直接执行
所以如果我们想要在浏览器之外使用 Wasm就需要提供一种基础设施来解释并执行这些被存放在 Wasm 二进制模块中的虚拟指令对于这样的基础设施我们通常称之为虚拟机Virtual Machine或者是 运行时引擎Runtime Engine
OK假设此时我们已经有了这样的一个虚拟机可以用于执行 Wasm 的虚拟字节码指令然后我们希望将这样一段 C/C++ 代码经过编译后 Wasm 的形式在这个虚拟机中运行在这段 C/C++ 代码中我们使用到了之前提到的 fopen 函数
但是问题来了我们知道在如 musl 这类 C 标准库的实现中类似 fopen 这样的函数最后会被编译为对某个特定平台IA32X86-64 系统调用的调用过程这对于 Wasm 来说会使自己丧失天生自带的可移植性
单纯对于某一个 Wasm 模块来讲由于我们并不知道这个模块将会被运行在什么类型的操作系统上因此我们无法将平台相关的具体信息放到 Wasm 模块中那如何解决这个问题呢WASI 给了我们答案
WASI Wasm 字节码与虚拟机之间增加了一层系统调用抽象层比如对于在 C/C++ 源码中使用的 fopen 函数当我们将这部分源代码与专为 WASI 实现的 C 标准库 wasi-libc 进行编译时源码中对 fopen 的函数调用过程其内部会间接通过调用名为 __wasi_path_open 的函数来实现这个 __wasi_path_open 函数便是对实际系统调用的一个抽象
__wasi_path_open 函数的具体实现细节会交由各个虚拟机自行处理也就是说虚拟机需要在其 Runtime 运行时环境中提供 Wasm 模块字节码所使用到的 __wasi_path_open 函数的解析和执行能力的支持而虚拟机在实际实现这些系统调用抽象层接口时也需要通过实际的系统调用来进行只不过这些细节上的处理对于 Wasm 二进制模块来讲是完全透明的
我们可以将上述提到的 wasi-libcWasm 二进制模块WASI 系统调用抽象层以及虚拟机基础设施之间的关系通过下图来直观地展示
实际上类似 __wasi_path_open 的这类以 __wasi 开头的用于抽象实际系统调用的函数便是 WASI 的核心组成部分WASI 根据不同系统调用所提供的不同功能将这些系统调用对应的 WASI 抽象函数接口分别划分到了不同的子集合中
如下图所示一个名为 wasi-core WASI 标准子集合包含有对应于文件操作网络操作等相关系统调用的 WASI 抽象函数接口其他如 cryptomultimedia 等子集合甚至可以包含与实际系统调用无关的一系列 WASI 抽象系统调用接口你可以理解为 WASI 所描述的抽象系统调用是针对 Wasm V-ISA 描述的抽象机器而言针对这部分抽象系统的具体实现则会依赖一部分实际的系统调用
WASI 在设计和实现时需要遵守 Wasm 可移植性安全性这两个基本原则那下面我们来分别看一看 WASI 及其相关的运行时/虚拟机基础设施是如何确保能够在设计和实现时满足这两个基本原则的
可移植性Portability
对于可移植性其实我们已经在讲解 WASI 时给出了答案WASI 通过在 Wasm 二进制字节码与虚拟机基础设施之间提供统一的系统调用抽象层来保证 Wasm 模块的可移植性这样一来上层的 Wasm 模块可以不用考虑平台相关的调用细节统一将对实际系统调用的调用过程转换为对抽象系统调用的调用过程
抽象系统调用的实现细节则由下层的相关基础设施来负责处理基础设施会根据其所在操作系统类型的不同将对应的抽象系统调用映射到真实的系统调用上当然并不是所有的抽象系统调用都需要被映射到真实的系统调用上因为对于某些抽象系统调用而言基础设施只是负责提供相应的实现即可
这样一个经过编译生成的 Wasm 二进制模块便可以在浏览器之外也同样保证其可移植性真正做到一次编译到处运行抽象便是解决这个问题的关键
安全性Security
对于安全性我们需要再次回到开头介绍的 Capability-based Security
实际上基础设施在真正实现 WASI 标准时便会采用 Capability-based Security 的方式来控制每一个 Wasm 模块实例所拥有的 capability
举个例子假设一个 Wasm 模块想要打开一个计算机本地文件而且这个模块还是由使用了 fopen 函数的 C/C++ 源代码编译而来那对应的虚拟机在实例化该 Wasm 模块时便会将 fopen 对应的 WASI 系统调用抽象函数 __wasi_path_open 以某种方式比如通过包装后的函数指针当做一个 capability 从模块的 Import Section 传递给该模块进行使用
通过这种方式基础设施掌握了主动权它可以决定是否要将某个 capability 提供给 Wasm 模块进行使用若某个 Wasm 模块偷偷使用了一些不为开发者知情的系统调用那么当该模块在虚拟机中进行实例化时便会露出马脚掌握这样的主动权正适合如今我们基于众多不知来源的第三方库进行代码开发的现状
对于没有经过基础设施授权的 capability 调用过程将会被基础设施拦截通过相应的日志系统进行收集这些隐藏的小伎俩便会在第一时间被开发者/用户感知并进行相应的处理
总结
好了讲到这今天的内容也就基本结束了最后我来给你总结一下
本节课我们主要讲解了什么是 WASIWASI 通过增加抽象层的方式解决了 Wasm 抽象机器V-ISA与实际操作系统调用之间的可移植性问题这可以保证我们基于 WASI 编写的 Wasm 应用模块真正做到一次编译到处运行抽象出的Wasm 系统调用层将交由具体的底层基础设施虚拟机/运行时来提供实现和支持
不仅如此基于 Capability-based Security 模型WASI 得以在最大程度上保证 Wasm 模块的运行时安全通过配合 Wasm 模块的 Import Section Export Section运行时便可以细粒度地控制模块实例所能够使用的系统资源这相较于传统的分级保护域模型来说无疑会更加灵活和安全每一个 Wasm 模块在运行时都仅能够使用被授权的 capability WASI 中定义的这些系统调用抽象接口便属于众多 capability 中的一种
另外你还需要知道的一点是无论是 Capability-based Security 模型还是分级保护域模型两者都是如今被广泛使用的安全模型只不过相对来说最小特权原则 使得 Capability-based Security 模型对权限的控制力度会更加精细分级保护域模型则是操作系统中广泛使用的一种安全策略
课后思考
最后我们来做一个思考题吧
你还能举出哪些场景是通过增加抽象层来解决了某个实际问题的
今天的课程就结束了希望可以帮助到你也希望你在下方的留言区和我参与讨论同时欢迎你把这节课分享给你的朋友或者同事一起交流一下