# 11.1 Linux 兼容层架构

FreeBSD 的 Linux 兼容层常被误认为虚拟机。

该兼容层不会引入显著的性能开销，某些场景下部分软件的性能甚至超过原生 Linux 环境。该兼容层既非指令集模拟器，也非二进制转译层，而是 Linux 应用程序二进制接口（Application Binary Interface，ABI）的系统级实现。

## 何为 Linuxulator

FreeBSD 对 Linux 应用程序的兼容机制依赖于 Linuxulator 这一核心组件。“Linuxulator”的字面含义为“Linux Emulator”，该名称容易与传统指令集模拟器相混淆。Linuxulator 并非传统模拟器，也不是独立的 FreeBSD 用户空间程序，仅为 FreeBSD 官方文档中对某一特定内核模块的非正式称谓，该内核模块的正式标识符为 `linux`。

Linuxulator 与 WSL2 的虚拟机方案和 Wine 的二进制兼容层解释机制截然不同。

具体而言，其核心原理如下：FreeBSD 内核能够识别并拦截 Linux 进程的系统调用请求，将其映射到功能等价的 FreeBSD 系统调用，并以 FreeBSD 内核的实现响应这些请求。

> **技巧**
>
> 换言之，Linuxulator 利用 FreeBSD 内核的系统调用来处理 Linux 进程的系统调用。

通过 Linuxulator 模块，FreeBSD 内核在系统调用接口上模拟 Linux 内核行为，但实际进程调度和执行仍由 FreeBSD 内核负责；处理系统调用的代码同样为 FreeBSD 原生实现。

通过 Linuxulator 运行的 Linux 进程在 FreeBSD 内核中按标准 FreeBSD 进程处理，与原生进程无异。

## 什么是 Linux 兼容层

FreeBSD 系统中不存在真实的 Linux 内核，FreeBSD 所声明的 Linux 内核版本号仅具有标识意义，而不具备实际的内核功能约束；该版本号甚至可设置为任意值，例如 `255.255`。

不同的 Linux 软件对内核版本的最低要求不同，其依赖和使用的系统调用接口也存在差异。例如，声明的 Linux 内核版本过低时，Arch Linux 的 chroot 环境可能无法正常初始化，并返回 `kernel too old` 的错误信息。

下文据 Terry Lambert（<tlambert@primenet.com>）发往 FreeBSD 邮件列表的邮件（邮件 ID：<199906020108.SAA07001@usr09.primenet.com>）整理而成，介绍 Linux 二进制兼容层的工作原理。原文可能已经佚失，本节进行了适当增补更新。

```sh
用户执行命令 (shell)
         |
         v
    +------------+
    | execve(2)  |
    +------------+
         |
         v
  +------------------------+
  | 检查文件幻数 Magic Number |
  +------------------------+
         |
         v
调用对应加载器 (ELF, a.out, PE...)
         |
         v
检查 ELF Note 段 Brand
         |
         v
Linux Brand
         |
         v
Linux ABI Loader
         |
         v
线程 PCB / syscall context 切换到 Linux 系统调用表
如 sys/amd64/linux/linux_sysent.c
         |
         v
查找依赖二进制 /compat/linux/... -> fallback /...
         |
         v
执行 Linux 二进制程序
```

FreeBSD 拥有抽象层“可执行类加载器（execution class loader）”，该抽象层直接嵌入在系统调用 execve(2) 的处理逻辑中。

传统上，UNIX 加载器依赖检查幻数（Magic Number）来判断文件格式。若无法匹配任何已知的二进制格式（如 ELF 或现已过时的 a.out），内核将返回 ENOEXEC 错误。此时，发起调用的 Shell 将接管该文件，并尝试将其作为该 Shell 类型的脚本进行解释执行。

> **思考题**
>
> ```sh
> $ file 701.pdf
> 701.pdf: PDF document, version 1.4
> $ hexdump -C -n 32 701.pdf
> 00000000 25 50 44 46 2d 31 2e 34 0d 0a 25 a1 b3 c5 d7 0d |%PDF-1.4..%.....|
> 00000010 0a 34 31 20 30 20 6f 62 6a 0d 0a 3c 3c 2f 44 65 |.41 0 obj..<</De|
> 00000020
> ```
>
> 结合上述信息，阅读源代码 `/contrib/file/magic/Magdir/pdf`，请思考 `file` 命令的工作原理。

**sys/sys/elf\_common.h** 示例片段，判断是否属于 ELF 文件：

```c
#define	ELFMAG0		0x7f
#define	ELFMAG1		'E'
#define	ELFMAG2		'L'
#define	ELFMAG3		'F'
#define	ELFMAG		"\177ELF"	/* ELF 幻数字符串 */
#define	SELFMAG		4		/* ELF 幻数字符串大小 */
```

FreeBSD 并非硬编码单一加载器，而是维护了一个加载器链表（execsw）。系统会按序尝试不同的加载器，包括用于处理脚本的 #!（Shebang）加载器。这意味着在现代 FreeBSD 中，绝大多数脚本的解析路径在内核态即已完成，而非依赖 Shell 的报错回退。

为了支持 Linux ABI，FreeBSD 的 ELF 加载器在识别出标准 ELF 幻数后（通过 **sys/sys/elf\_common.h** 中定义的幻数），会进一步校验 ELF Note 段中的专用 Brand 标记。由于 Linux 与原生 FreeBSD 程序共享相同的 ELF 基础格式，这一标记是区分 ABI 目标的决定性特征（SVR4/Solaris ELF 则不具备此类标记）。

**sys/compat/linux/linux\_elf.c** 示例片段，用于进一步判断该 ELF 是否是 Linux 程序。

```c
bool
linux_trans_osrel(const Elf_Note *note, int32_t *osrel)
{
    const Elf32_Word *desc;   // 指向 note 描述段的数据指针
    uintptr_t p;               // 用于计算描述段起始位置的临时指针

    // note + 1 指向 note 结构之后的位置（通常是 name 字段的起始）
    p = (uintptr_t)(note + 1);
    // 跳过 name 字段，按 Elf32_Addr 对齐
    p += roundup2(note->n_namesz, sizeof(Elf32_Addr));

    // desc 指向实际存放版本信息的描述段
    desc = (const Elf32_Word *)p;

    // 检查描述段第一个元素是否为 Linux ABI 标识
    if (desc[0] != GNU_ABI_LINUX)
        return (false);

    /*
     * 对于 Linux，我们使用以下方式编码操作系统版本号：
     *  (version << 16) | (major << 8) | minor
     * 具体宏定义在 linux_mib.h 中
     */
    *osrel = LINUX_KERNVER(desc[1], desc[2], desc[3]);

    return (true);  // 成功解析
}
```

**sys/compat/linux/linux\_mib.h** 示例：

```c
#define LINUX_KVERSION       5       // 内核主版本号（version），这里表示 Linux 内核主版本为 5
#define LINUX_KPATCHLEVEL    15      // 内核次版本号（major），这里表示次版本为 15
#define LINUX_KSUBLEVEL      0       // 内核修订号（minor），这里表示修订号为 0

// 将 Linux 内核版本号编码为单个整数
// 公式：高 16 位放 version，中间 8 位放 major，低 8 位放 minor
// 例如：LINUX_KERNVER(5,15,0) => (5 << 16) + (15 << 8) + 0 = 0x00050F00
#define LINUX_KERNVER(a,b,c) (((a) << 16) + ((b) << 8) + (c))
```

在确认 Linux Brand 后，加载器会通过修改进程执行上下文中的 sysentvec 指针，将默认的系统调用表切换为 Linux ABI 系统调用表。此后，该进程发起的所有系统调用均通过此表索引。相关的信号跳板（Signal Trampoline）和陷阱向量也随之切换。该系统调用表由内核模块提供，对于 amd64，其条目由 **sys/amd64/linux/linux\_sysent.c** 生成，负责将 Linux 系统调用号映射至相应的内核包装函数（Wrappers）。

**sys/compat/linux/linux\_util.c** 对默认根路径的定义代码：

```c
char linux_emul_path[MAXPATHLEN] = "/compat/linux";
// 定义字符串变量，存储 Linux 兼容环境的路径，初始值为 "/compat/linux"

SYSCTL_STRING(_compat_linux, OID_AUTO, emul_path, CTLFLAG_RWTUN,
    linux_emul_path, sizeof(linux_emul_path),
    "Linux runtime environment path");
// 注册 sysctl 节点，使该路径可以通过 sysctl 查看或修改
// 参数解释：
// _compat_linux      -> sysctl 所在的父节点
// OID_AUTO           -> 系统自动分配一个 OID
// emul_path          -> sysctl 名称
// CTLFLAG_RWTUN      -> 可读写，且可以在系统启动时通过 tunable 设置
// linux_emul_path    -> 绑定的变量
// sizeof(linux_emul_path) -> 变量长度
// "Linux runtime environment path" -> 描述信息
```

上述代码实际上是对 `sysctl compat.linux.emul_path` 的默认定义。

在文件系统层面，Linux 兼容层实现了一套备用根路径重定向机制。当 Linux 进程请求路径查找时，系统会优先尝试在 **/compat/linux/** 路径下定位文件；若未命中，则回退至宿主系统的原生路径。这使得 Linux 程序能无缝加载其专有的共享库，同时在必要时仍能访问 FreeBSD 的系统资源。结合 sysctl `compat.linux.osname`（默认值“Linux”）与 `compat.linux.osrelease`，再通过在 **/compat/linux** 中提供定制的 uname(1) 等工具，环境伪装得以完整实现。

**sys/compat/linux/linux\_util.c** 对备用根目录的设置代码：

```c
int
linux_pwd_onexec(struct thread *td)
{
    struct nameidata nd;  // 用于描述文件查找操作的数据结构
    int error;            // 存储函数调用返回的错误码

    NDINIT(&nd, LOOKUP, FOLLOW, UIO_SYSSPACE, linux_emul_path);  // 初始化 nd 查找 linux_emul_path
    error = namei(&nd);  // 执行查找
    if (error != 0) {
        pwd_altroot(td, NULL);  // 查找失败，调用 pwd_altroot 设置为 NULL
        return (0);
    }
    NDFREE_PNBUF(&nd);          // 释放 nd 中路径缓冲
    pwd_altroot(td, nd.ni_vp);  // 查找成功，将 ni_vp 传给 pwd_altroot
    vrele(nd.ni_vp);            // 释放 ni_vp 引用
    return (0);
}
```

**sys/sys/namei.h** 定义的部分回退机制：

```c
struct nameidata {
    // …省略一部分…

    /*
     * Arguments to lookup.
     */
    struct vnode *ni_startdir;  /* 起始目录 */
    struct vnode *ni_rootdir;   /* 逻辑根目录 */
    struct vnode *ni_topdir;    /* 逻辑顶目录 */
    int ni_dirfd;               /* *at 函数使用的起始目录文件描述符 */
    int ni_lcf;                 /* 本地调用标志 */

    // …省略一部分…
};

// …省略一部分…

#define namei_setup_rootdir(ndp, cnp, pwd) do {                      \
    if (__predict_true((cnp->cn_flags & ISRESTARTED) == 0))          \
        ndp->ni_rootdir = pwd->pwd_adir;  /* 如果不是重启调用，使用 pwd_adir 作为根目录 */ \
    else                                                               \
        ndp->ni_rootdir = pwd->pwd_rdir;  /* 否则使用 pwd_rdir 作为根目录 */ \
} while (0)
#endif

// …省略一部分…
```

FreeBSD 内核为 Linux ABI 提供了原生级的支持。绝大多数系统调用（如 VFS、VM、IPC 操作）在底层直接共享 FreeBSD 的内核实现。两者的差异仅在于系统调用边界的参数重组（Argument Marshaling）：FreeBSD 程序使用原生胶水函数，而 Linux 程序通过兼容层包装器接入。

严格来说，这是一种系统调用级的 ABI 翻译实现，而非指令集层面的“仿真”。CPU 直接执行 Linux 二进制文件的原生机器码，不存在中间指令翻译损耗。早期文献使用“仿真”一词，更多是受限于当时的技术术语体系，而非对其技术本质的准确描述。

## 为什么使用 Linux 兼容层并非“苦难哲学”

通过 `kldload linux` 加载模块的做法，不应受到质疑，正如荀子所言：“君子生非异也，善假于物也。”

使用 Linux 兼容层也是如此。类似的技术包括 Linux 上使用 Wine 或 CrossOver，乃至 [ReactOS](https://reactos.org/)；以及 Windows 平台上的 Linux 兼容层和 Android 兼容层，这些方案都已得到广泛应用。

## 参考文献

* 荀子. 荀子\[M]. 北京: 中华书局, 1954.
* FreeBSD Project. linux(4) -- Linux ABI support\[EB/OL]. \[2026-04-17]. <https://man.freebsd.org/cgi/man.cgi?query=linux&sektion=4>. Linux 二进制兼容层手册页。

## 课后习题

1. 阅读 FreeBSD 源代码中 **sys/compat/linux/** 目录下的 `linux_file.c` 文件，分析 Linux 系统调用到 FreeBSD 系统调用的映射机制，选取 3 个关键系统调用追踪其处理流程。
2. 修改 `compat.linux.osrelease` 为 2 个不同的 Linux 内核版本号，测试不同版本号下 Linux 软件的兼容性表现。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://book.bsdcn.org/di-11-zhang-linux-er-jin-zhi-jian-rong-ceng/di-11.1-jie-linux-jian-rong-ceng-jia-gou.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
