プログラミング
Swiftによる最小限のカーネルをQEMU上で実行する
A minimal kernel in Swift, running in QEMU (carette.xyz)
要約
この記事では、Swift言語を使って非常に基本的なカーネルを作成し、QEMUエミュレータ上で実行する実験について解説しています。Embedded Swiftの設定、ベアメタルターゲット向けのコンパイラとリンカの調整、アセンブリ言語との連携、そしてUARTへの直接書き込みやメモリ操作といった低レベル関数の実装方法が詳細に説明されています。
全文翻訳
更新(2026年9月28日)Max Desiatov氏から、_cdecl を使用して関数を宣言する代わりに、@c Swift属性が機能するはずだと指摘を受けました。@c属性は、Swiftで実装されたC関数としてグローバル関数をマークし、_cdecl属性を正式なものにします。提案へのリンクはこちらです。そのコメントに従って、ブログ記事とコードを更新しました。Maxさん、ありがとう!
私は小さな実験を始めました。それは、非常に基本的なカーネルをSwiftで書き、QEMU上で実行することです。目標はもちろんLinuxや他の一般的なカーネルを置き換えることではありませんが、単に楽しむことと、オペレーティングシステムなしでプログラムを実行するために何が必要かを理解することです。実際、私は10年前にarOSで同様の実験を行いました。
今のところ、カーネルはただ一つのことしか行いません。それは、QEMUを通してメッセージを印刷し、その後永遠に待機することです。これは最初の深い探求には十分です。
Swift Embedded
このプロジェクトでは、Embedded Swiftを使用しています。これは、オペレーティングシステムも標準ライブラリも(通常の意味では)利用できない環境向けに設計されたSwiftのサブセットです。
私の最初のPackage.swiftは非常に小さかったです。
```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ファイルはその単純さにおいてほとんど侮辱的でした。
```swift
// Sources/swift_kernel/test.swift
print("Hello... world?")
```
実行を試みました。
```bash
> swift run
<unknown>:0: error: unable to load standard library for target 'arm64-apple-macosx27.0.0'
```
おっと。私の現在のSwiftコンパイラには、まだEmbedded Swift標準ライブラリが同梱されていないようです。ドキュメントで見落としていました:
Embedded Swiftはまだ実験的であり、公開されているSwiftリリースではまだサポートされていないため、開発ツールチェーンを使用する必要があります。
そこで、開発ツールチェーンをインストールして再度テストしてみましょう。
```bash
> swiftly install main-snapshot && swiftly use main-snapshot
> swift run
[...] Hello... world?
```
成功しました!この時点で、Embedded Swiftが小さなプログラムをコンパイルして実行できることを確認できました。次のステップは、オペレーティングシステムが全くないマシン用にコンパイルすることでした。
QEMUセットアップ
このプロジェクトでは、ハッカーの間で最も有名なマシンエミュレータの一つであるQEMUというマシンエミュレータでテストします。私はApple Siliconマシン上でQEMUのvirtマシンを使用しています。したがって、ターゲットはaarch64-none-none-elfです。このターゲットは、トリプルとして設計されており、オペレーティングシステムがなく、Cランタイムによって提供される環境もない64ビットARMマシンを表します。
QEMUが起動すると、CPUは生の状態で始まります。Swiftコードを安全に実行できるようになる前に、スタックを設定し、プログラムがどこから開始するかを決定する必要があります。次の内容を含む最小限のアセンブリファイルが必要になります。
* _startシンボルを定義する
* 最初のコアのスタックを設定する
* Swiftのエントリ関数であるkernel_mainにジャンプする
* メインを実行し続けるために永遠に待機する
コンパイラセットアップ
コンパイラに、これが通常の実行可能ファイルではないことを伝える必要があります。コンパイラは通常の標準ライブラリのリンクを避ける必要があり、リンカはプラットフォーム固有のリンカスクリプトの代わりに私たちのリンカスクリプトを使用する必要があります。
このプロジェクトでは、Swift Package Manager (SPM) に渡される以下のtoolset.jsonを使用します。
```json
{
"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ネイティブビルドシステムを使用してプロジェクトをビルドします。
```bash
> swift build --triple aarch64-none-none-elf --toolset toolset.json --build-system native
```
残念ながら、最初のリンカ試行は失敗します。
```
ld.lld: error: section type mismatch for .strtab
>>> <internal>:(.strtab): SHT_STRTAB >>> output section .nonalloc: SHT_SYMTAB
ld.lld: error: section type mismatch for .shstrtab
>>> <internal>:(.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もありません。
セクションエラーは別の原因です。orphan handlingが有効になっていると、リンカはELFメタデータがどこに行くべきかを推測したくありません。したがって、リンカスクリプトは、保持しているセクションを明示的に配置する必要があります。
Swiftとアセンブリのブリッジング
main関数を持つ通常のSwiftファイルは使用できません。代わりに、@cを使用して指定されたカーネルエントリ関数を公開し、アセンブリファイルがC互換名でそれを呼び出せるようにします。
```swift
@c func kernel_main() {
print("Hello, Embedded Swift 😊")
while true {}
// Keep the program running
}
```
アセンブリのエントリポイントはSources/boot/boot.Sにあります。
```assembly
.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の実装は、少なくとも1文字を書き込む方法を必要とします。QEMUのvirtマシンでは、ARMのPL011 UARTは0x09000000にマッピングされています。したがって、putcharは直接そのメモリマップドレジスタに書き込むことができます。
```swift
// QEMU's PL011 UART register
let QEMU_UART_REGISTER: UInt = 0x0900_0000
@c @unsafe func putchar(_ c: CInt) -> CInt {
let uart = unsafe UnsafeMutablePointer<UInt8>(bitPattern: QEMU_UART_REGISTER)!
unsafe uart.pointee = UInt8(c)
return c
}
```
これはスクリーンではありませんが、シリアル出力ドライバーです。QEMUは、エミュレートされたUARTが受信したバイトをターミナルに表示します(-nographicオプションで実行するため)。
もう一つの不足している関数はmemmoveです。Swiftの低レベルコードは、通常の標準ライブラリを使用していない場合でも、基本的なメモリ操作を必要とします。この最初の実験では、ソースとデスティネーションが重複するかどうかによって、前方または後方にコピーする小さな実装を提供します。
```swift
@c @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 if unsafe d > s {
for i in (0..<n).reversed() {
unsafe d[i] = s[i]
}
}
return unsafe dest
}
```
これは、プロジェクトが通常のアプリケーションプログラミングから外れる最初のポイントです。ここでは、文字列を印刷するためにオペレーティングシステムのAPIを呼び出すのではなく、言語ランタイムが存在するために必要な低レベル関数を実装しています。
メモリレイアウトの説明
Swiftコードを実装したので、リンカはカーネルがどこにロードされ、実行可能ファイルの各部分がどこに配置されるかを知る必要があります。これはlinker.ldの役割です。
QEMUのvirtマシンは、このセットアップでAArch64カーネルを0x40080000にロードします。
```ld
ENTRY(_start)
. = 0x40080000;
/* Places the boot code first, followed by read-only data, initialized data, uninitialized data, and a stack */
.text : {
KEEP(*(.text.boot))
*(.text*)
}
.rodata : ALIGN(8) {
*(.rodata*)
}
.data : ALIGN(8) {
*(.data*)
}
.bss (NOLOAD) : ALIGN(16) {
__bss_start = .;
*(.bss*)
*(COMMON)
__
```