執筆者 : 金沢工業大学 工学部 情報工学科 橘 真吾 (インターン生)
監修者:高橋 浩和
※ 「ARM64 OSを作ろう」連載記事一覧はこちら
※ 「ARM64 OS」のコードはGitHubにて公開しています。
はじめに
ARMはRISC系CPU(固定長命令)を代表する、最も広く普及した命令セットの一つです。 主にモバイル端末やマイコン分野でも利用されてきましたが、近年ではApple社のMシリーズに代表されるPC向けのプロセッサーのアーキテクチャとしても存在感を高めています。
このオペレーティングシステムは「RISC-V OSを作ろう」シリーズの流れを汲んだ ARM64版 となっています。 ARM64はRISC-Vと同じRISCのCPUでありますが、根本的なレジスタの命名規則の違いや、システムレジスタの扱い方の違いがあり、本連載ではこれらの違いを学びながらOSを実装していきます。
このOSでは以下の単純な実装を目指すことにします。
- Raspberry Pi 4Bをターゲットとした実行環境の構築
- U-Bootでのカーネルイメージの起動
- UART通信で他PCへの文字列送信を用いた実装
- ARMアセンブリでのコンテキストスイッチの実装
- タイマー割り込みのためのトラップベクタの登録とタイマーハンドラの用意
- ラウンドロビンアルゴリズムの実装
- タイムスライスを用いたタイムシェアリングスケジューリング
- タスク状態の保持と時限待ち
- セマフォによる排他制御の実装と「食事をする哲学者」問題を用いた検証アプリケーションの作成
※本記事(連載第1回)では、上記のうち「UART通信で他PCへの文字列送信を用いた実装」までの実装と実行を行います。
環境の用意
環境の説明
この記事ではRaspberry Pi 4B単体で開発できるようにしています。 Raspberry Pi 4Bで開発する場合、快適にコーディングするためにターミナルで使えるエディタを使用することをおすすめします。(NanoやVimなど)
Visual Studio Codeなどのエディタで開発したい場合は他のPCで開発することをおすすめします。 *1
ソフトウェアでの準備
Linuxディストリビューションは、Ubuntu 24.04 LTSを使用してビルドしました。必要とするツール群の殆どが標準パッケージとして提供されており、苦労することなくARM64用のバイナリを生成し、動作させることができます。
まず、Ubuntu標準で用意されているARM関連パッケージとU-Bootのビルドに必要なパッケージをインストールします。
| パッケージ | 説明 | 用途 |
|---|---|---|
| gcc | コンパイラ | OSのビルド |
| bison | 構文解析器生成 | U-Bootのビルド |
| flex | 字句解析器生成 | U-Bootのビルド |
| libssl-dev | 暗号・ハッシュ・証明書関連の開発用ライブラリ | U-Bootのビルド |
以下のコマンドをRaspberry Pi 4Bで実行しましょう。
1. 必要なパッケージのインストール
OSのコンパイル環境と、ブートローダー(U-Boot)をビルドするために必要なツール群をシステムに追加します。
sudo apt install git make gcc bison flex libssl-dev
x86-64のコンピュータのOSでコンパイルする場合はgccを以下のように変更してください
aarch64-linux-gnu-gcc
2. ソースコードのファイルの作成
本連載で作成するOSのソースコードを書くためのファイルを作成します。以下のコマンドを実行してください。
mkdir arm64-os cd arm64-os touch main.c start.S boot.txt test.ld Makefile
作成が完了したらMakefileを先に書いてしまいましょう。このMakefileはこれから同じものを使い続けます。
nano Makefile
ARCH_PREFIX =
CC = $(ARCH_PREFIX)gcc
LD = $(ARCH_PREFIX)ld
NM = $(ARCH_PREFIX)nm
OBJCOPY = $(ARCH_PREFIX)objcopy
CFLAGS = -O2 -ffreestanding -mgeneral-regs-only -g
ASFLAGS = -g
LDFLAGS = -T test.ld -Map=testout.map
OBJ_DIR=obj
C_SRC := $(wildcard *.c)
S_SRC := $(wildcard *.S)
OBJ_FILES := $(C_SRC:%.c=$(OBJ_DIR)/%.o) \
$(S_SRC:%.S=$(OBJ_DIR)/%.o)
.phony: all clean
all: arm64os.elf
$(OBJ_DIR):
mkdir -p $(OBJ_DIR)
$(OBJ_DIR)/%.o :%.c | $(OBJ_DIR)
@echo Compiling $<...
$(CC) -c $(CFLAGS) -c $< -MD -MF $@.d -o $@
$(OBJ_DIR)/%.o : %.S | $(OBJ_DIR)
@echo Compiling $<...
$(CC) -c $(ASFLAGS) -c $< -MD -MF $@.d -o $@
arm64os.elf: $(OBJ_FILES)
@echo Linking $@
$(LD) $(LDFLAGS) -o $@ $(OBJ_FILES)
$(NM) -n $@ > $(@:elf=sym)
$(OBJCOPY) -O binary -R .note -R .note.gnu.build-id -R .comment -S $@ $(@:elf=bin)
mkimage -a 0x4000000 -e 0x4000000 -A arm64 -T standalone -O u-boot -C none -d $(@:elf=bin) $(@:elf=img)
@echo Done.
$(OBJ_FILES) arm64os.elf: Makefile
clean:
rm -f $(OBJ_DIR)/* *.elf *.bin *.img *.sym *.map
x86-64のコンピュータのOSでクロスコンパイルする場合はMakefileの先頭の内容を以下のように変更してください
ARCH_PREFIX = aarch64-linux-gnu- CC = $(ARCH_PREFIX)gcc LD = $(ARCH_PREFIX)ld NM = $(ARCH_PREFIX)nm OBJCOPY = $(ARCH_PREFIX)objcopy
※nanoでは保存がctrl + oで、終了がctrl + x
3. U-Bootの準備
自作OSを起動するためには、柔軟な設定が可能なブートローダーが必要です。ここではデファクトスタンダードであるU-Bootを利用するため、ソースコードを取得します。
cd .. git clone --depth=1 --branch u-boot-2023.07.y https://source.denx.de/u-boot/u-boot.git
4. U-Bootのビルド
Raspberry Pi 4B向けのデフォルト設定( rpi_4_defconfig )を読み込んだ上で、コンパイラでビルドを実行します。-j20 は並列ビルドの指定です(環境に合わせて調整してください)。
cd u-boot make rpi_4_defconfig make -j20
5. U-Bootの配置
ビルドによって生成されたバイナリ(u-boot.bin )を、Raspberry Pi 4Bが起動時に読み込むファームウェアディレクトリへコピーします。
sudo cp u-boot.bin /boot/firmware/
6. 起動設定( config.txt )の変更
Raspberry Pi 4Bの起動時の設定ファイルを編集し、Linuxカーネルではなく、先ほど配置したU-Bootが起動するように変更します。また、シリアル通信でのデバッグを可能にするための設定も行います。
sudo nano /boot/firmware/config.txt
変更点は以下のとおりです
- 既存の
kernel=vmlinuzをコメントアウト(Linuxの起動を無効化) - 新しく
kernel=u-boot.binを追加(U-Bootをカーネルとして指定) enable_uart=1を追加(UARTシリアル通信を有効化)
#kernel=vmlinuz kernel=u-boot.bin enable_uart=1
7. ブートスクリプトの作成
U-Bootが起動した後に「何を読み込んでどう実行するか」を記述したスクリプト( boot.txt )を、U-Bootが解釈できるバイナリ形式( .scr )に変換します。
cd ../arm64-os nano boot.txt
fatload mmc 0:1 0x4000000 arm64os.img fatload mmc 0:1 0x3000000 bcm2711-rpi-4-b.dtb fdt addr 0x3000000 fdt resize 0x1000 bootm 0x4000000 - 0x3000000 go 0x4000000
各コマンドの説明:
fatload:MMCデバイス(SDカード)の1番パーティション(OSパーティション)から指定のアドレスにOSイメージをロードfatload(2行目):MMCデバイス(SDカード)の1番パーティション(OSパーティション)から指定のアドレスにデバイスツリーをロードfdt addr:デバイスツリーファイルを配置するメモリアドレスの指定fdt resize:デバイスツリーファイル用のメモリ領域を拡張bootm:OSイメージとデバイスツリーファイルを使用してブートgo:bootmで起動できなかった場合、OSイメージで起動する
sudo mkimage -C none -A arm64 -T script -d boot.txt /boot/firmware/boot_arm64os.scr
これで環境構築は終了です。 ARM Cortex-Aアーキテクチャマニュアルも入手しておきましょう。
https://developer.arm.com/documentation/ddi0487/latest/
ハードウェアでの準備
ハードウェアでは以下のような構成を想定してARM64OSを実行します。
----------------- -------------------- --------------------- | Raspberry Pi 4B | → | シリアルコンソール | → | 開発PC| TeraTerm等 | ----------------- -------------------- ---------------------
1. シリアル変換アダプタ(シリアルコンソール)への接続
シリアル変換アダプタにジャンパー線を接続します。 このとき、GNDピンには黒色の線を使用しておくと、後の配線確認で基準(グランド)が分かりやすくなります。

2. Raspberry Pi 4Bへの接続
シリアル変換アダプタとRaspberry Pi 4Bを接続します。ここでのポイントは、送信(TX)と受信(RX)を交差(クロス)させて接続することです。
- GND同士を接続: アダプタのGND(黒線)を、Raspberry Pi 4Bの6番ピン(GND)へ接続
- RXとTXを接続: アダプタの受信ピン(RXD)を、Raspberry Pi 4Bの送信ピン(8番ピン / GPIO14 TXD0)へ接続
- TXとRXを接続: アダプタの送信ピン(TXD)を、Raspberry Pi 4Bの受信ピン(10番ピン / GPIO15 RXD0)へ接続
(写真の配線例では、黒色を6番(GND)へ、緑色を8番(TXD0)へ、青色を10番(RXD0)へ接続しています)

接続するピンを間違うと Raspberry Pi 4B 本体、
またはシリアルコンソール本体が故障します。
配線間違いがないよう十分注意してください。
(筆者は一度Raspberry Pi 4B側の接続を間違えて、シリアルコンソールを破壊しました)
3. シリアルコンソール本体のUSB部を他PCに差し込む
コードを書く
test.ld (リンカスクリプト)
まず、大前提としてコードを実行するにはメモリに変数や関数を配置しなければなりませんが、どこに何の変数や関数をおいてよいのかを定義しているのがリンカであり、リンカスクリプトです。
以下のリンカスクリプトでは次のことを定義しています。
1. メモリマップ
- 物理メモリアドレス
0x4000000から128MBをRAMとして利用 - エントリポイントは
_start
2. 起動コードの配置
.textセクションの先頭に*(.reset)を配置し、メモリ先頭で後述のstart.Sのコードが確実に実行されるように配置できるようにします
3. BSS領域の初期化対策
.bssを配置し、その両端を_bss_startと_bss_endで囲みます- これにより、C言語のスタートアップコードで一括してゼロクリア状態で実行することが可能になります
4. スタック領域の確保
- バイナリの末尾に4KB(
0x1000)の領域をスタックとして確保し、その末尾(高位アドレス)を_stack_endとしています start.Sでこの値をスタックポインタに設定します
OUTPUT_ARCH( "aarch64" ) ENTRY( _start ) MEMORY { ram (wxa!ri) : ORIGIN = 0x4000000, LENGTH = 128M } SECTIONS { .text : { PROVIDE(_text_start = .); _stext = .; *(.reset) *(.text .text.*) PROVIDE(_text_end = .); } >ram AT>ram _etext = .; .rodata : { PROVIDE(_rodata_start = .); *(.rodata .rodata.*) *(.note.* ) PROVIDE(_rodata_end = .); } >ram AT>ram .data : { . = ALIGN(4096); PROVIDE(_data_start = .); *(.data .data.*) PROVIDE(_data_end = .); } >ram AT>ram .bss :{ . = ALIGN(16); PROVIDE(_bss_start = .); *(.bss .bss.*) . = ALIGN(16); PROVIDE(_bss_end = .); } >ram AT>ram .stack :{ . = ALIGN(16); PROVIDE(_stack_start = .); . = . + 4096; PROVIDE(_stack_end = .); } >ram AT>ram }

start.S (ARM64アセンブリ)
OSイメージが起動すると最初にstart.Sの処理が行われます。
start.Sは、U-Bootからの移行直後の何もない状態からC言語のコード(main関数)が実行できるようにする処理を行います。
1. セクション定義とエントリポイント
.section .reset:このコードを.resetという名前のセクションに配置します。リンカスクリプトで指定した通り、このセクションはバイナリの先頭(メモリのロード位置)に配置されるため、起動直後に必ず実行されます.globl _start:_startラベルを外部(リンカ)から見えるようにします。リンカスクリプトのENTRY(_start)によってここがプログラム全体の開始地点として登録されます
2. スタックポインタの設定
- C言語の関数を実行するには、ローカル変数や関数の戻りアドレスを保持するための「スタック領域」が必要です
- リンカスクリプトで定義したスタックの末尾アドレス
_stack_endを読み込み、スタックポインタを設定しています - ARM64のポイント:ARM64では、即値やメモリアドレスを直接
spレジスタにロードする命令が使えません。そのため、一度汎用レジスタ(ここではx4)を経由してspに値を移しています
3. main関数への移行
bl main(Branch with Link):戻り先アドレスをリンクレジスタ(x30 / lr)に保存して、C言語のmain関数へジャンプしますb .は通常、OSのmain関数が終了することはありませんが、万が一returnして戻ってきた場合に備えて配置しています。b .は自分自身にジャンプし続けることで、ループ処理をしています
.section .reset,"ax",@progbits .globl _start _start: ldr x4, =_stack_end mov sp, x4 bl main b .
main.c (C言語プログラム)
start.S内でmain関数を呼び出すようになりました。
次にmain.cのコードです。
シリアルコントローラの仕様
Raspberry Pi 4Bに搭載されているシリアルコントローラ(Mini UART)は、「8250 UART」と呼ばれる古くからのデファクトスタンダードなチップとほぼ互換があります。そのため、制御手順やステータスフラグの意味などは、多数の仕様書がインターネットで公開されています。
しかし、レジスタへのアクセス方法(メモリマップ) に決定的な違いがあります。
| 特徴 | 標準的な8250(x86等) | Raspberry Pi 4B(この環境) |
|---|---|---|
| レジスタ長 | 1バイト(8bit) | 4バイト(32bit) |
| アドレス単位 | 1バイトずつ(+0, +1, +2...) | 4バイトずつ(+0, +4, +8...) |
コードでは以下のように実装しています。
| レジスタ | 番地 | 説明 |
|---|---|---|
| UART_BASE | 0xFE215040U | Mini UARTの物理ベースアドレス |
| UART_TX | UART_BASE + 0 | 送信用レジスタ (書き込み用) |
| UART_LSR | UART_BASE + 20 | ラインステータスレジスタ (状態確認用) |
| THR_EMPTY | 0x20U | 送信バッファ空きフラグ (LSRの第5ビット) |
1. 文字送信関数( uart_putchar )
UARTのハードウェアFIFO(バッファ)の状態を確認しながら、1文字を送信します。
- ポーリング待機:
while文でLSRレジスタの第5ビット(THR_EMPTY)を監視します。このビットが0の間はバッファが満杯であることを示すため1になるまで待機します。 - 送信: バッファに空きができたら
UART_TXレジスタに文字を書き込みます。
2. 文字列出力と数値変換 ( print_message )
デバッグ用に、文字列と16進数を表示できる簡易的な printf 互換関数です。
- 可変長引数:
stdarg.hのva_list等を使用し、任意の数の引数を受け取ります。 - 16進数変換 (
%x):- 64bit値 (
unsigned long) を扱います。 - ビットシフトとマスク操作
(v >> i*4) & 0xFを利用して、上位ビットから4ビット(16進数1桁)ずつ取り出します。 - ゼロサプレッション: 上位の不要な
0を表示せず、値が0の場合のみ0を表示するロジックを組み込んでいます。
- 64bit値 (
- 改行コード:
\nを検出した際、ターミナル表示用に\r(CR) を付与して出力します。
各処理の構成要素
| 処理ブロック | 内容説明 |
|---|---|
| 引数解析(while) | 文字列を1文字ずつ走査し、終端 \0 まで繰り返す |
| 数値変換(%x) | 0-9 なら '0'+x 、 10-15 なら 'a'+x-10 でASCII文字に変換する |
| 通常文字の出力 | uart_putchar を呼び出し、1文字ずつ送信レジスタにセットする |
#include <stdarg.h> #include <stdint.h> #define TRUE 1 #define FALSE 0 #define UART_BASE 0xFE215040U #define UART_TX (UART_BASE + 0U*4U) #define UART_LSR (UART_BASE + 5U*4U) #define THR_EMPTY 0x20U void uart_putchar(char c){ volatile uint32_t * const uart = (uint32_t *)UART_TX; volatile uint32_t * const status = (uint32_t *)UART_LSR; while ( !(*status & THR_EMPTY) ) ; *uart = c; } void print_message(const char* s, ...){ va_list ap; va_start (ap, s); while (*s) { if (*s == '%' && *(s+1) == 'x') { unsigned long v = va_arg(ap, unsigned long); _Bool print_started = FALSE; int i; s += 2; for (i = 15; i >= 0; i--) { unsigned long x = (v & 0xFUL << i*4) >> i*4; if (print_started || x != 0U || i == 0) { print_started = TRUE; uart_putchar(x < 10 ? ('0' + x) : ('a' + x - 10)); } } } else { if (*s == '\n') { uart_putchar('\r'); } uart_putchar(*s++); } } va_end (ap); } void main(void){ while(1){ print_message("Hello World!\n"); } }
コンパイル
コードが完成したのでビルドをします。
ビルドをするには以下のコマンドを実行してください。
makeコマンドを実行して生成されたarm64os.imgを以下のコマンドで/boot/firmware/にコピーしてください。
コピーが完了したらsudo rebootで再起動をします。
make sudo cp arm64os.img /boot/firmware/ sync sudo reboot
makeコマンドを実行すると以下のようなログが表示されますが、Done.と最後に表示されたらビルドに成功しています。
$make make: Warning: File 'main.c' has modification time 4214 s in the future Compiling main.c... gcc -c -O2 -ffreestanding -mgeneral-regs-only -g -c main.c -MD -MF obj/main.o.d -o obj/main.o Compiling primitive.S... gcc -c -g -c primitive.S -MD -MF obj/primitive.o.d -o obj/primitive.o Compiling start.S... gcc -c -g -c start.S -MD -MF obj/start.o.d -o obj/start.o Linking arm64os.elf ld -T test.ld -Map=testout.map -o arm64os.elf obj/main.o obj/primitive.o obj/start.o ld: warning: arm64os.elf has a LOAD segment with RWX permissions nm -n arm64os.elf > arm64os.sym objcopy -O binary -R .note -R .note.gnu.build-id -R .comment -S arm64os.elf arm64os.bin mkimage -a 0x4000000 -e 0x4000000 -A arm64 -T standalone -O u-boot -C none -d arm64os.bin arm64os.img Image Name: Created: Tue Dec 16 06:16:42 2025 Image Type: AArch64 U-Boot Standalone Program (uncompressed) Data Size: 4096 Bytes = 4.00 KiB = 0.00 MiB Load Address: 04000000 Entry Point: 04000000 Done.
動かしてみる
上記のコマンドを実行するとRaspberry Pi 4Bのファームウェアが起動し、U-Bootが実行されますが、U-Bootの画面が出たときにHit any key to stop autoboot:の値が0になるまでにEnterキーを押すことでU-Bootでコマンドを実行できるようになります。
U-Boot 2023.07.02 (Dec 13 2025 - 21:44:31 +0900)
DRAM: 948 MiB (effective 7.9 GiB)
RPI 4 Model B (0xd03115)
Core: 211 devices, 17 uclasses, devicetree: board
MMC: mmcnr@7e300000: 1, mmc@7e340000: 0
Loading Environment from FAT... Unable to read "uboot.env" from mmc0:1...
In: serial
Out: vidconsole
Err: vidconsole
Net: eth0: ethernet@7d580000
PCIe BRCM: link up, 5.0 Gbps x1 (SSC)
starting USB...
Bus xhci_pci: Register 5000420 NbrPorts 5
Starting the controller
USB XHCI 1.00
scanning bus xhci_pci for devices... 4 USB Device(s) found
scanning usb for storage devices... 0 Storage Device(s) found
Hit any key to stop autoboot: 0
これが表示されたらEnterキーを押す。
そこで以下のコマンドを入力してください。
U-Boot> fatload mmc 0:1 0x1000 boot_arm64os.scr U-Boot> source 0x1000
上記のコマンドの説明は以下のとおりです。
fatload mmc 0:1 0x1000 boot_arm64os.scr
fatload:FATファイルシステムからファイルを読み込むmmc 0:1:MMCデバイス(ここではMicro SDカード)の0番の第1パーティションを指定0x1000:メモリアドレスの0x1000番地にファイルを読み込むboot_arm64os.scr:読み込むスクリプトファイル
source 0x1000
source:指定されたメモリアドレスにあるU-Bootスクリプトを順次実行する0x1000:スクリプトが格納されているメモリアドレス
これら2つのコマンドを実行することにより、以下のような実行結果が表示されるはずです。
## Executing script at 00001000 4160 bytes read in 88 ms (45.9 KiB/s) 54874 bytes read in 24 ms (2.2 MiB/s) Working FDT set to 3000000 ## Booting kernel from Legacy Image at 04000000 ... Image Name: Image Type: AArch64 U-Boot Standalone Program (uncompressed) Data Size: 4096 Bytes = 4 KiB Load Address: 04000000 Entry Point: 04000000 Verifying Checksum ... OK Loading Standalone Program ## Starting application at 0x04000000 ... Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! : :
これで環境構築から実行までができました。
最後に
今回は環境構築からU-Bootで独自のOSイメージ(arm64os.img)の実行をし、UART通信でHello World!を繰り返し表示するところまでを実装しました。
今回作成したプログラムはGitHubにて公開しています。自由にカスタマイズしてみてください。 https://github.com/Tachi-Shin/Arm64-OS/tree/No1/arm64-os
次回は、タスク切り替えを実装します。お楽しみに。
*1:筆者もVisual Studio Codeで普段からコーディングをしている関係で別PCで開発しています。筆者はx86-64のPCのWindows11上でWSL2のUbuntu 24.04を使い、ビルドして作成したカーネルイメージをRaspberry Pi 4BにUSBで転送して実行していました。