用 Swift 编写的极简内核,在 QEMU 中运行
摘要
作者用 Embedded Swift 编写了一个极简内核并在 QEMU 的 aarch64 virt 机器上运行,记录了从工具链安装、Package.swift 配置到汇编入口与栈初始化的完整过程,并更新了 Swift 6.3 中新的 @c 属性替代 @_cdecl 的用法。
暂无内容
查看缓存全文
缓存时间: 2026/10/08 12:44
# 一个用 Swift 编写的极简内核,运行在 QEMU 上
来源:https://carette.xyz/posts/minimal_swift_kernel_on_qemu/
---
**更新(2026 年 9 月 28 日)**
Max Desiatov 告诉我(https://mastodon.social/@maxd/117348871405186390),`@c` 这个 Swift 属性(https://www.swift.org/blog/swift-6.3-released/#language-and-standard-library)应该可以用,而不必使用 `@_cdecl` 来声明函数。`@c` 属性将全局函数标记为用 Swift 实现的 C 函数,这使 `@_cdecl` 属性的形式化成为可能。该提案的链接在这里(https://github.com/swiftlang/swift-evolution/blob/main/proposals/0495-cdecl.md)。我已根据这个说明更新了博客文章和相关代码。谢谢 Max!
---
我开始了一个小实验:用 Swift 编写一个非常基础的内核,并在 QEMU(https://www.qemu.org/)上运行它。目标显然不是取代 Linux 或其他任何流行的内核,而只是为了好玩,并理解在没有操作系统支撑的情况下让一个程序运行需要什么。实际上,我在 10 年前曾用 arOS(https://carette.xyz/posts/im_writing_my_own_os/)做过类似的实验。
目前,这个内核只做一件事:通过 QEMU 打印一条消息,然后永远等待下去,这对第一次深入研究来说已经足够了。
## Embedded Swift
该项目使用的是 Embedded Swift(https://docs.swift.org/latest/documentation/embeddedswift/),它是 Swift 的一个子集,专为没有操作系统且(在通常意义上)没有标准库的环境而设计。我最初的那个 `Package.swift` 非常小:
```
// swift-tools-version: 6.4
import PackageDescription
let package = Package(
name: "swift-kernel",
platforms: [.macOS(.v14)],
targets: [
.executableTarget(
name: "swift_kernel",
swiftSettings: [
.enableExperimentalFeature("Embedded"),
.strictMemorySafety(),
.treatAllWarnings(as: .error),
]
)
]
)
```
而第一个 Swift 文件简单得近乎侮辱:
```
// Sources/swift_kernel/test.swift
print("Hello... world?")
```
我试着运行它:
```
> swift run
:0: error: unable to load standard library for target 'arm64-apple-macosx27.0.0'
```
哎呀。看起来我当前的 Swift 编译器还没有随附 Embedded Swift 的标准库。我在文档中漏看了这一点(https://docs.swift.org/latest/documentation/embeddedswift/install-embedded-swift):
> 由于 Embedded Swift 仍然处于实验阶段,且尚未在公开的 Swift 版本中获得支持,你需要使用开发工具链。
所以,我们来安装开发工具链,然后重新测试:
```
> swiftly install main-snapshot && swiftly use main-snapshot
> swift run
[...]
Hello... world?
```
成功了!到这里我已经确认了 Embedded Swift 能够编译并运行一个小程序。下一步是为一台完全没有操作系统的机器进行编译。
## QEMU 设置
在这个项目中,我将在一个名为 QEMU(https://www.qemu.org/)的机器模拟器上进行测试,它是黑客们最著名的机器模拟器之一。我在一台 Apple Silicon 机器上使用 QEMU 的 `virt` 机器。因此目标三元组是 `aarch64-none-none-none-elf`。这个目标以三元组(https://wiki.osdev.org/Target_Triplet)的形式描述了一台 64 位 ARM 机器:没有操作系统,也没有由 C 运行时提供的环境。
当 QEMU 启动时,CPU 处于原始状态。在它能安全执行 Swift 代码之前,我需要**配置一个栈**并**决定程序从哪里开始**。我需要一个最小的汇编文件,它要:
1. 定义 `_start` 符号(http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html),
2. 为第一个核心设置栈,
3. 跳转到我们的 Swift 入口函数 `kernel_main`,
4. 并且永远等待,以保持主流程运行。
## 编译器设置
我必须告诉编译器这不是一个普通的可执行文件:编译器必须避免链接通常的标准库,而链接器必须使用我们自己的链接脚本,而不是平台专用的脚本。该项目使用以下 `toolset.json`,它将传递给 Swift Package Manager(SPM):
```
{
"schemaVersion": "1.0",
"swiftCompiler": {
"extraCLIOptions": [
"-enable-experimental-feature",
"Embedded",
"-enable-experimental-feature",
"Volatile",
"-Xfrontend",
"-no-allocations",
"-Xfrontend",
"-function-sections",
"-Xfrontend",
"-disable-stack-protector",
"-Xlinker-driver",
"-nostdlib",
"-Xlinker-driver",
"-fuse-ld=lld"
]
},
"linker": {
"extraCLIOptions": [
"-nostdlib",
"-static",
"--gc-sections",
"--orphan-handling=error",
"-T",
"linker.ld",
"-Map",
".build/kernel.map"
]
}
}
```
现在,我们用新的目标、工具集和 Swift 的原生构建系统来构建项目:
```
> swift build --triple aarch64-none-none-elf --toolset toolset.json --build-system native
```
不幸的是,第一次链接尝试失败了:
```
ld.lld: error: section type mismatch for .strtab
>>> .(.strtab): SHT_STRTAB
>>> output section .nonalloc: SHT_SYMTAB
ld.lld: error: section type mismatch for .shstrtab
>>> .(.shstrtab): SHT_STRTAB
>>> output section .nonalloc: SHT_PROGBITS
ld.lld: error: undefined symbol: putchar
ld.lld: error: undefined symbol: memmove
```
嗯,这里发生了什么?因为我是在为裸机目标进行编译(提醒:`none-none-elf`),所以没有底层操作系统,也没有 C 标准库。因此既没有 `putchar`,也没有 `memmove`……
段(section)错误的来源不同。在启用了孤儿段处理(orphan handling)的情况下,链接器不想猜测 ELF 元数据应该放在哪里。因此,链接脚本必须显式地安排我要保留的那些段。
## 将 Swift 桥接到汇编
我不能使用带 `main` 函数的普通 Swift 文件。相反,我用 `@c` 暴露一个专门的内核入口函数,这样汇编文件就可以通过它兼容 C 的名称来调用它。
```
@c func kernel_main() {
print("Hello, Embedded Swift 😊")
while true {} // 保持程序运行
}
```
汇编入口点位于 `Sources/boot/boot.S`:
```
.section .text.boot
.global _start
_start:
/* Read the CPU ID. We only want Core 0 to run our Swift code. */
mrs x1, mpidr_el1
and x1, x1, #3
cbz x1, 2f
/* Park secondary cores in an infinite wait loop. */
1: wfe
b 1b
/* Set up the stack for Core 0. */
2: ldr x0, =__stack_top
mov sp, x0
/* Jump to our Swift entry point. */
bl kernel_main
/* Halt if we ever return. */
b 1b
```
QEMU 可以启动多个虚拟核心,但目前只有第一个核心应该执行我们的内核。其他核心则等待(使用 `wfe`),等待一个尚未发出的事件。
## 提供缺失的函数
Embedded Swift 使用的 `print` 实现需要一种至少能写入一个字符的方式。在 QEMU 的 `virt` 机器上,ARM 的 PL011 UART(https://krinkinmu.github.io/2020/11/29/PL011.html)被映射在 `0x09000000`。因此 `putchar` 可以直接写入那个内存映射寄存器:
```
// QEMU 的 PL011 UART 寄存器
let QEMU_UART_REGISTER: UInt = 0x0900_0000
@unsafe func putchar(_ c: CInt) -> CInt {
let uart = unsafe UnsafeMutablePointer(bitPattern: QEMU_UART_REGISTER)!
unsafe uart.pointee = UInt8(c)
return c
}
```
**这并不是一个屏幕驱动程序**,而是一个串口输出驱动程序。QEMU 随后会在终端中显示**由模拟 UART 接收到**的字节(因为我将使用 `-nographic` 选项来执行它)。
另一个缺失的函数是 `memmove`。即使我没有使用普通的标准库,Swift 的底层代码仍然需要基本的内存操作。对于这个第一次的实验,我提供了一个小型实现,它会根据源和目标是否重叠来决定向前或向后复制:
```
@unsafe func memmove(
_ dest: UnsafeMutableRawPointer,
_ src: UnsafeRawPointer,
_ n: Int
) -> UnsafeMutableRawPointer {
let d = unsafe dest.assumingMemoryBound(to: UInt8.self)
let s = unsafe src.assumingMemoryBound(to: UInt8.self)
if unsafe d < s {
for i in 0..<n {
unsafe d[i] = s[i]
}
} else {
for i in (0..<n).reversed() {
unsafe d[i] = s[i]
}
}
return unsafe dest
}
```
有了这些之后,让我们再次构建:
```
> swift build --triple aarch64-none-none-elf --toolset toolset.json --build-system native
```
然后用 QEMU 启动:
```
> qemu-system-aarch64 -machine virt -cpu cortex-a57 -nographic -kernel .build/aarch64-none-none-elf/debug/kernel
```
接下来……它会永远运行下去,因为内核被困在它的无限循环里。
这离一个有用的内核还差得远,但它确实是一个真实的 AArch64 ELF 可执行文件。它从一个汇编入口点开始,不链接任何标准库,并在手动配置的栈之上调用 Swift 代码。对于第一个 hello world 来说,这已经足够了。
## 接下来会怎样?
该项目是故意未完成的。下一步还没有实现,但它们为后续工作指明了方向:
1. 在启动期间清空 `.bss` 段,
2. 将 UART 寄存器封装在一个小型 Swift 抽象之后(为了简单起见,我把 QEMU 的硬编码了),
3. 弄清楚 Embedded Swift 能够支持哪些分配特性,
4. 研究中断和定时器,
5. 最终用能够调度实际工作的东西来替换这个无限循环。
在某个时候,我也可能想尝试帧缓冲区、真正的键盘驱动程序和一个最小的交互式环境。但在构建用户界面之前,我需要先理解它底下的机器。
## 与人类的交互
目前,这台机器在说"你好"。打印一条消息挺不错的,但仍然只是一种单向对话。PL011 UART 也有一个接收寄存器。在从它读取之前,我需要先检查它的标志寄存器。第 4 位,称为 `RXFE`,告诉我们接收 FIFO 是否为空。如果它是空的,我们就等待。
```
let QEMU_UART_FLAG_REGISTER: UInt = 0x0900_0018
@unsafe func readChar() -> UInt8 {
let uart = unsafe UnsafeMutablePointer(bitPattern: QEMU_UART_REGISTER)!
let flags = unsafe UnsafeMutablePointer(bitPattern: QEMU_UART_FLAG_REGISTER)!
// 在接收 FIFO 为空时,RXFE 位会被置位。
while unsafe (flags.pointee & 0x10) != 0 {}
return unsafe uart.pointee
}
```
这是**轮询**。CPU 会反复检查 UART,直到一个字符到来,而这是做这类事情的一种非常**低效**的方式。真正的内核最终会使用中断,但对于第一个实现来说,这样做已经足够简单了。
内核现在可以将字符回显到终端了:
```
@c func kernel_main() {
print("Swift kernel ready.")
let _ = unsafe putchar(62)
let _ = unsafe putchar(32)
var ignoreLineFeed = false
while true {
let character = unsafe readChar()
if character == 13 {
let _ = unsafe putchar(10)
let _ = unsafe putchar(62)
let _ = unsafe putchar(32)
ignoreLineFeed = true
} else if character == 10 {
if !ignoreLineFeed {
let _ = unsafe putchar(10)
let _ = unsafe putchar(62)
let _ = unsafe putchar(32)
}
ignoreLineFeed = false
} else {
let _ = unsafe putchar(CInt(character))
ignoreLineFeed = false
}
}
}
```
数字 `62` 和 `32` 只是 `>` 和一个空格的 ASCII 码。我可以创建一个小的字符串写入抽象,但目前我更愿意让机制保持可见。
现在运行相同的 QEMU 命令会给我们一个提示符:
```
Swift kernel ready.
> hello kernel
hello kernel
>
```
这仍然是一个非常小的功能。没有行编辑器,没有退格键处理,没有命令解析器……而且 CPU 因为使用轮询而非中断而一直忙于等待每个字符。
对于一个从 `print("Hello... world?")` 开始的程序来说,感觉这是一个很好的停下来的地方,并开始写一份 TODO 列表来改进所有这些。
完整代码可在我的私有 git 空间(https://carette.xyz/posts/free-of-github/)中找到:https://git.carette.xyz/k0pernicus/swift-kernel
玩得开心!
相似文章
Apple 内部:内核中的 Swift
Apple 已开始通过一项名为 KernelKit 的新工作将 Swift 集成到内核中,嵌入式 Swift 运行时出现在 macOS 和 iOS 中,标志着向内存安全内核扩展迈出了一步。
用地道 Rust 重写的 Linux 0.11,可在 QEMU 中启动
一个用现代 Rust 重写的 Linux 0.11 内核,可在 i386 上的 QEMU 中启动,包含完整的用户空间(shell 和 coreutils),支持一键构建镜像和 devcontainer 环境。
在 64GB Mac 上以 41–52 tok/s 运行 95.5 GiB 的 Qwen3.8-Flash-Next(比 llama.cpp 快 1.76 倍):Slipstream 发布,130k 上下文扩展,以及 Swift 变体
作者发布 Slipstream——一款针对 Apple Silicon 的 C++ Metal 推理引擎,支持原生 SSD 专家流式加载与投机草稿解码,可在 64GB Mac 上以 41–52 tok/s 运行 95.5 GiB 的 Qwen3.8-Flash-Next 模型,比 llama.cpp 快 1.76 倍,且 130k 长上下文下速度不衰减。
仅运行NES模拟器的内核
一个旨在无需任何操作系统的情况下运行NES模拟器的最小化内核,并附带像Mandelbrot分形可视化这样的额外程序。
@no_stp_on_snek:在自定义Rust内核和Swift调度领域忙碌的一晚。9个PR中有3个是真正新增或修改的Metal内核……
在自定义Rust内核和Swift调度逻辑的优化下,Qwen3.6-35b-A3B模型的预填速度在2k提示下从255 tok/s提升到1058 tok/s,实现了约4倍的加速,解码和困惑度未受影响。