
2026/09/04 2:57
Linux カーネルを新しいプラットフォームでビルドする手順
RSS: https://news.ycombinator.com/rss
要約▶
日本語翻訳:
本記事では、カスタム物理ハードウェアを使用せず、完全な Linux カーネルをユーザー空間で起動することに成功した、約 2,000 行の C++ コードで構築された最小限の RISC-V エミュレーターについて説明している。通常のセットアップが U-Boot などのブートローダーを使用してストレージからファイルをロードするのとは異なり、このエミュレーターは重要な周辺機器(RAM、UART8250)およびリソースを特定のアドレスに直接メモリマッピングする。これらは RV32IMA 命令セットをサポートし、タイマーについては SBI を介してインターフェースしており、MMU を備えた CPU、十分な RAM、およびシステムクロックタイマーが必要とされる。
Linux を起動するには、エミュレーターバイナリ内にカーネルイメージ、デバイスツリーブロッブ(DTB)、およびイニトラムフス(initramfs)の 3 つのコンポーネントを埋め込む。カーネルは不要な機能が無効化されたオプションを使用して crosstool-ng で最小限の 32 ビット RISC-V イメージとしてコンパイルされ、イニトラムフスには静的にリンクされた「Hello World」イニシャルプログラムおよび/dev/kmsg などの必須デバイスが含まれる。DTS ファイルから生成された DTB は初期コンソール出力を構成し、イニシャルプロセスの場所を指定する。
重要なのは、エミュレーター全体を WebAssembly にクロスコンパイル可能なことであり、これにより複雑なビルド環境や新しい PCB デザインなしに Linux ディストリビューションをブラウザ上で直接実行できる。生産用途には最適化されていない(コールドブートには最大 1 分かかる可能性がある)が、この軽量ツールは組み込み Linux 開発への参入障壁を下げ、RISC-V アーキテクチャの学習のためのアクセス可能なプラットフォームを提供する。
本文
Linux の稼働と RISC-V エミュレーター構築ガイド
概要
近年、組み込みシステムにおいてLinuxが主流のオペレーティングシステムとなっています。その魅力は柔軟性だけでなく、多数の開発者によるドライバ実装にあります。しかし、カスタムハードウェアでは当初、カーネルを自ら起動させる(ブリングアップ)作業が必要です。
本稿では、新規プラットフォームでの Linux 稼働に必要な最小限の設定手順について解説します。対象は新設計基板ではなく、RISC-V CPU のエミュレーターです。
Linux 稼働の要件
複雑なソフトウェアである Linux を動かすために必要なハードウェア要件は以下の 3 つだけ suffices です。
- **MMU(メモリ管理ユニット)**付きの CPU
- RAM: カーネル、デバイスツリー、initramfs を読み込む十分な容量
- システムクロックタイマー: 定期的な割り込み発生機能
エミュレーターの作成
上記要件を実装するため、約 2,000 行の C++ コードで以下の実装を行いました。
- CPU: RV32IMA インストラクションセットの実装(MMU 機能含む)
- RAM: シングル RAM パフォーマンスの実装
- タイマー: SBI(Supervisor Binary Interface)インタフェース
- UART (シリアル通信): WD8250 チップベースの実装
完全な実装コードは以下で入手可能です。
実際のハードウェアとの違い
本稿では自作パーフェリカルを使用しますが、実際の SoC ではデータシートを確認してマッピングアドレスを特定し、ブートローダー(u-boot など)を用いて不揮発性ストレージから起動します。
エミュレーターでの最終的なハードウェア定義例は以下の通りです。
struct Hardware { Hardware() { // パフォーマンスのアドレス空間にマッピング cpu.address_space().map(0x0000'0000, &ram); cpu.address_space().map(0xF400'0000, &uart8250); // RISC-V 互換の MMU をアドレス変換器として追加 cpu.address_space().add_address_translator(&riscv_mmu); // UART パフォーマンスの設定:出力をターミナルに印刷 uart8250.output_callback([](std::uint8_t c) { putchar(c); fflush(stdout); }); // カーネルを RAM の先頭に読み込む ram.write(0x00, LinuxKernel); // デバイストゥリーを RAM の末尾に読み込む constexpr static auto DeviceTreeBlobLoadAddress = 512_MiB - 1_MiB; ram.write(DeviceTreeBlobLoadAddress, DeviceTreeBlob); // ブートローダーと同様に、デバイスツリーのアドレスをレジスタ a1 に設定 emulator.cores()[0].a1() = DeviceTreeBlobLoadAddress; // 固定アドレスに initramfs を読み込み(後で参照可能) constexpr static auto InitRamFsLoadAddress = 0x1F700000; ram.write(InitRamFsLoadAddress, InitRamFs); // CPU の起動 cpu.power_up(); } auto step() -> void { cpu.step(); } private: riscv::Cpu<1> cpu; dev::riscv::MMU<std::uint32_t> riscv_mmu; dev::Ram ram(512_MiB); dev::UART8250 uart8250; }
ツールチェーンの構築
Linux を動かすには、まずプラットフォーム向けにカーネルを構成・コンパイルする必要があります。手作業での環境構築は複雑であるため、crosstool-ng プロジェクトを利用し、以下のタスクを自動化します。
- コンパイラとライブラリの入手
- 構成とコンパイルの実行
ターゲットの選択
32 ビット RISC-V Linux を動作させるため、
riscv32-unknown-linux-gnu のツールチェーンが必要です。これにより GCC コンパイラと glibc が提供されます。
$ ./bootstrap INFO :: *** Generating package version descriptions... INFO :: *** Done! $ make gmake[1]: Entering directory 'crosstool-ng'... GEN ct-ng $ ./ct-ng riscv32-unknown-elf ... Now configured for "riscv32-unknown-elf" $ ./ct-ng menuconfig
menuconfig を実行して設定メニューを開き、以下の項目を変更します。
- Target options: 「Use the MMU」を有効化
- Operating System: 「Target OS」をLinuxに設定
- C library: 「C library」をglibcに設定
- Enable C compiler: C++ コンパイラーを有効化
設定を保存し、
./ct-ng build を実行します。完了すると ~/x-tools/riscv32-unknown-linux-gnu にツールチェーンがインストールされます。
Linux カーネルのコンパイル
ツールチェーン準備ができたら、Linux カーネル(torvalds/linux)をコンパイルします。デフォルトの RISC-V 設定をベースにし、以下の点を変更します。
- Platform type: 「Allow configurations that result in non-portable kernels」を有効化
- Base ISA: RV32I に設定(64 ビットから 32 ビットへ)
- 拡張機能サポート: エミュレーター未実装のため、すべてを無効化
- Boot options: 「UEFI runtime support」を無効化
- ※エミュレーターは圧縮命令セットをサポートしていないため、「Emit compressed instructions」も無効化します。
以下の設定も行います。
- Target options:
- 「Generate code for the specific ABI」を
に設定ilp32 - 「Architecture Level」を
に設定rv32ima
- 「Generate code for the specific ABI」を
- Kernel Features:
- タイマー用の SBI v0.1 サポートを有効化
- リロケタブルカーネル(relocatable kernel)構築を有効化
- 肥大化要因である Virtualization, Loadable module support, Networking support, Cryptographic API を無効化
設定完了後、以下のコマンドでコンパイルを開始します。
$ make -j \ ARCH=riscv \ CROSS_COMPILE=/path/to/riscv32-unknown-linux-gnu-
make への回答はすべて
n で構いません。コンパイル完了後、以下に最終実行ファイルが存在します。
- ファイル場所:
arch/riscv/boot/Image
init プログラムのコンパイル
ユーザー空間到達直後に動作するシンプルな init プログラムを作成します。実際の組み込みでは busybox が一般的ですが、ここではテキスト出力用の簡易 C コードを使用します。
#include <stdlib.h> #include <stdint.h> #include <stdio.h> #include <fcntl.h> #include <unistd.h> int main() { // ログを直接カーネルログへ出力するため /dev/kmsg を開く int kmsg = open("/dev/kmsg", O_WRONLY); if (kmsg == -1) { return EXIT_FAILURE; } // メッセージの印刷 dprintf(kmsg, "Hello World from \033[32mLinux Userspace\033[0m!\n"); // init プロセスが終了しないように無限ループ while (true) { sleep(1); } // ファイルハンドルを閉じる close(kmsg); return EXIT_SUCCESS; }
共有ライブラリ依存を避け単独で動作するため、静的リンク(statically linked)します。
$ riscv32-unknown-linux-gnu-gcc -static init.c -o init
initramfs の作成
initramfs はシステム起動後に利用可能なファイルシステム(cpio アーカイブ)です。ルート
/ にマウントされ、通常 /bin, /dev, /proc などを含みます。
再現性のある構成を生成するために、以下のテキストファイルを
gen_init_cpio ツールで処理します。
initramfs 定義ファイル (
)initramfs.txt
# /bindir /bin 755 0 0file /bin/init init 755 0 0 # /devdir /dev 755 0 0nod /dev/kmsg 644 0 0 c 1 11nod /dev/initrd 644 0 0 b 1 250nod /dev/console 600 0 0 c 5 1nod /dev/null 666 0 0 c 1 3nod /dev/ram 644 0 0 b 1 0nod /dev/root 644 0 0 b 4 0nod /dev/ttyS0 660 0 0 c 4 64 # /procdir /proc 755 0 0
アーカイブを生成します。
$ gen_init_cpio initramfs.txt -i initramfs.cpio
デバイスツリーの作成
Linux カーネルはデバイスツリー(Device Tree)に基づいてドライバのロードや設定を行います。以下の DTS ファイルで最小限のハードウェアを定義します。
riscv-emulator.dts
/dts-v1/; / { #address-cells = <0x01>; #size-cells = <0x01>; model = "WerWolv's Emulator"; cpus { #address-cells = <0x01>; #size-cells = <0x00>; timebase-frequency = <65000000>; cpu@0 { device_type = "cpu"; compatible = "riscv"; reg = <0x00>; riscv,isa = "rv32ima"; mmu-type = "riscv,sv32"; interrupt_controller: interrupt-controller { #interrupt-cells = <0x01>; #address-cells = <0x00>; compatible = "riscv,cpu-intc"; interrupt-controller; }; }; }; memory@0 { device_type = "memory"; reg = <0x00 (512 * 1024 * 1024)>; }; }
パーフェリカルは
soc ノード下に配置します。
/ { // ... soc@F0000000 { #address-cells = <0x01>; #size-cells = <0x01>; compatible = "simple-bus"; ranges = <0x00 0xF0000000 0x10000000>; serial@4000000 { compatible = "ns8250", "ns16550"; reg = <0x4000000 0x100000>; interrupt-parent = <&interrupt_controller>; interrupts = <0x01>; no-loopback-test; clock-frequency = <5000000>; }; }; };
カーネル設定(
chosen ノード)を以下のように記述します。
/ { // ... aliases { serial0 = "/soc@F0000000/serial@4000000"; }; chosen { bootargs = "earlycon console=/dev/ttyS0 rdinit=/bin/init root=/dev/initrd"; stdout-path = "serial0"; linux,initrd-start = <0x1F700000>; linux,initrd-end = <0x1FEFFFFF>; }; };
重要なのは
chosen ノード内の値です。
- bootargs: earlycon で早期ログ出力を有効化し、UART(serial0)に割り当てます。また、ユーザー空間到達直後には
を起動させます。/bin/init - linux,initrd-start/end: initramfs のメモリアドレスを定義します。
DTS ファイルを DTB(Device Tree Blob)に変換します。
$ dtc -I dts -O dtb -o riscv-emulator.dtb riscv-emulator.dts
すべてを組み合わせて
作成した 3 つのファイルをエミュレーターに埋め込み、実行します。
constexpr std::uint8_t LinuxKernel[] = { #embed "Image"}; constexpr std::uint8_t DeviceTreeBlob[] = { #embed "device_tree.dtb"}; constexpr std::uint8_t InitRamFs[] = { #embed "initramfs.cpio"};
実行結果は以下の通りです。
$ ./emulator [ 0.000000] Booting Linux on hartid 0 [ 0.000000] Linux version 7.0.0-00166-g0f0013213293 (werwolv@fedora) ... [ 0.000000] Machine model: WerWolv's Emulator... [ 54.748992] Run /bin/init as init process [ 54.814251] Hello World from Linux Userspace!
ブラウザでの実行
作成したバイナリは WebAssembly(Wasm)へクロスコンパイル可能で、JavaScript から呼び出せるためブラウザ上で動作させることもできます。ただし、起動まで約 1 分かかる場合があり、これはエミュレータが最適化されていない環境下であることを示しています。
まとめ
以上により、RISC-V エミュレーター上で Linux システムを構築することができました。
- プロセス自体は実際のハードウェアでも同様ですが、メモリーマッピングは SoC データシートに基づき行う必要があります。
- 実際の組み込みシステムではブートローダー(u-boot など)がカーネルや initramfs を配置し、独自のファイル形式でバンドリングします(Android ブートローダーマニュアル参照)。
使用したコードとエミュレーターは GitHub で公開しています: