フルスクラッチで作る!
x86_64 自作 OS
パート 3
システムコールの薄い本

大神祐真

2018-12-30 版

著

へにゃぺんて 発行

1

はじめに
本書をお手に取っていただきありがとうございます。
本書はカーネルにシステムコールを実装してカーネルとアプリをバイナリレベルで分離してみる本です。
カーネル・アプリの分離によりできあがる構成は図 .1 の通りです。

▲図 1 本書まででできあがる OS のアーキテクチャ図

2

はじめに

本書の構成
本書は以下の 2 章構成です。
第 1 章で、カーネルにシステムコールを実装し、カーネルは初期化処理を終えると別バイナリとしてつくったアプリを呼び出すようにします。そして、呼び出されたアプリはシステムコールでカーネルの機能を呼び出すようにします。
第 2 章では、アプリから別バイナリのアプリを呼び出す仕組みを、フォアグラウンド実
行とバックグラウンド実行のそれぞれで実装します。そのため、第 1 章で実装したもの
を、ファイルシステムやスケジューラとの組み合わせで使えるようにしていきます。

開発環境・動作確認環境
筆者が開発や動作確認を行っている環境を以下に記載します。

• PC: Lenovo ThinkPad E450
• OS: Debian GNU/Linux 8.11 (コードネーム: Jessie)• コンパイラ: GCC 4.9.2– build-essential パッケージでインストールされるものです
言語としては、C 言語と一部アセンブラを使う程度なので、一般的な Linux 環境であれば問題ないと思います。

本書の PDF 版/HTML 版やソースコードの公開場所について
これまでの当サークルの同人誌同様、本書も PDF 版/HTML 版は以下のページで無料で公開しています。

• http://yuma.ohgami.jp
サンプルコードは以下の GitHub リポジトリで公開しています。

• https://github.com/cupnes/x86_64_jisaku_os_3_samples
サンプルコードのリポジトリ内はディレクトリで分かれています。各ディレクトリが以降の各項で作るものです。各項の冒頭で「この項のサンプルディレクトリは XX です。」と紹介しますので適宜参照してみてください。

3

はじめに

なお、本書開始時点のソースコードを「A00_kern_app_base」ディレクトリに置いていますので、本書で行う作業の差分を見たい場合は「diﬀ -r」等でディレクトリ差分をとってみてください。
また、これまで通り、本書の表紙も自作 OS の実行写真です。表紙のサンプルは「A01_cover」に置いていますので興味があれば見てみてください。私の手書きフォント
や何度か発表していた「縦書き日本語シェル」のソースコードが入っています。

4

目次

2

はじめに
本書の構成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

3

開発環境・動作確認環境 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

3

本書の PDF 版/HTML 版やソースコードの公開場所について . . . . . . . . . .

3

第1章

システムコールを実装し、カーネルとアプリを分離する

7

1.1

システムコールの仕組み . . . . . . . . . . . . . . . . . . . . . . . . . .

7

1.2

ソフトウェア割り込みを使えるようにする . . . . . . . . . . . . . . . .

8

1.2.1

割り込みハンドラ入口/出口処理 (アセンブラ) 実装 . . . . . . . .

8

1.2.2

割り込みハンドラの C 言語側の関数を実装 . . . . . . . . . . . .

9

1.2.3

実装した割り込みハンドラを登録する . . . . . . . . . . . . . . .

10

1.2.4

ソフトウェア割り込みを呼び出してみる. . . . . . . . . . . . .

11

1.2.5

実行する . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

13

カーネルから外部の実行バイナリ (アプリ) を起動する . . . . . . . . . .

13

1.3.1

exec 関数を実装する . . . . . . . . . . . . . . . . . . . . . . . .

13

1.3.2

実行ファイル (アプリ) を作る . . . . . . . . . . . . . . . . . . .

15

1.3.3

実行する . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

18

1.3

1.4

第2章

2.1

システムコールを実装する . . . . . . . . . . . . . . . . . . . . . . . . .

18

1.4.1

kernel: システムコールの処理を実装する . . . . . . . . . . . . .

19

1.4.2

app: システムコールをライブラリ化してみる . . . . . . . . . .

19

1.4.3

実行する . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

21

アプリから別のアプリを呼び出す

22

フォアグラウンド実行を実現する . . . . . . . . . . . . . . . . . . . . .

23

2.1.1

open と exec をシステムコール化 . . . . . . . . . . . . . . . . .

23

2.1.2

アプリからアプリを呼び出してみる . . . . . . . . . . . . . . . .

24

2.1.3

実行する . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

26

5

目次

2.2

バックグラウンド実行を実現する . . . . . . . . . . . . . . . . . . . . .

26

2.2.1

実験用の処理を削除し、スケジューラを有効化する. . . . . . .

27

2.2.2

アプリをランキューへ追加するシステムコールを作成する . . . .

30

2.2.3

アプリからランキュー追加を試す . . . . . . . . . . . . . . . . .

31

2.2.4

実行する . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

33
34

おわりに
表紙のフィギュアについて . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

34
35

参考情報
開発リポジトリ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

35

本シリーズの過去の著作 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

35

6

第1章

システムコールを実装し、カーネルとアプリを分離する
1.1 システムコールの仕組み
システムコールを実装するに当たり、まずはシステムコールの仕組みを説明します。
システムコールは一般的にソフトウェア割り込みで実現されます。システムコールを使ってアプリからカーネルの機能を呼び出す大まかな流れは図 1.1、図 1.2、図 1.3 の通りです。

▲図 1.1 システムコール実行の流れ (1、2)

7

第 1 章 システムコールを実装し、カーネルとアプリを分離する▲図 1.2 システムコール実行の流れ (3、4)▲図 1.3 システムコール実行の流れ (5、6)

別バイナリであるアプリとカーネル間の情報の受け渡しには汎用レジスタを使います。
後述しますが、関数の第 1 から第 4 引数に使われる RDI・RSI・RDX・RCX を使うこと
で、割り込みハンドラを通ってそのまま C 言語の関数へ渡るようにしています。

1.2 ソフトウェア割り込みを使えるようにする
まずはソフトウェア割り込みを使えるように割り込みハンドラを実装します。
この項のサンプルディレクトリは「011_softirq」です。

1.2.1 割り込みハンドラ入口/出口処理 (アセンブラ) 実装
まず handler.s に、割り込みハンドラの入口/出口となるアセンブリコード部分を実装
します (リスト 1.1)。

8

1.2 ソフトウェア割り込みを使えるようにする▼リスト 1.1

011_softirq/handler.s/* ・・・ 省略 ・・・ */.global kbc_handler
kbc_handler:/* ・・・ 省略 ・・・ *//* 追加 (ここから) */.global syscall_handler
syscall_handler:
push
%rcx
push
%rax
push
%rdx
push
%rbx
push
%rbp
push
%rsi
push
%rdi
call
do_syscall_interrupt
pop
%rdi
pop
%rsi
pop
%rbp
pop
%rbx
pop
%rdx
pop
%rcx /* RAX は戻り値の受け渡しに使うため退避したものは捨てる */
pop
%rcx
iretq
/* 追加 (ここまで) */"syscall_handler"というラベルを追加し、他の割り込みハンドラと同様に、call 命令でC 言語の関数 (割り込みのメイン処理) を呼び出す前後で汎用レジスタの退避 (push) と復帰 (pop) を行っています。
復帰処理 (pop 処理) の最後で、RCX を 2 回 pop している箇所がありますが、これは、

RAX をスタックに退避した分を読み捨てるためです*1 。
RAX はシステムコール呼び出し元へ戻り値を返すのに使うため、syscall_handler に入ってきたときの値を復帰せず、do_syscall_interrupt 関数内で設定されたものをそのまま返すようにしています。

1.2.2 割り込みハンドラの C 言語側の関数を実装
次に、"syscall.c"というファイルを追加し、"do_syscall_interrupt"関数を実装します(リスト 1.2)。

*1 「捨てるくらいなら最初から RAX を退避しなければ良いのでは？」という考えもありますが、C 言語の関数を呼び出す際のスタックのアライメント調整のためにこの回数のスタック退避を行っています (詳し
くは前著コラム参照)。

9

第 1 章 システムコールを実装し、カーネルとアプリを分離する▼リスト 1.2

011_softirq/syscall.c/* 追加 (ここから) */#include <pic.h>#include <fbcon.h>#define SYSCALL_INTR_NO

0x80

unsigned long long do_syscall_interrupt(
unsigned long long syscall_id, unsigned long long arg1,
unsigned long long arg2, unsigned long long arg3){
unsigned long long ret_val = 0xbeefcafe01234567;
puth(syscall_id, 16);puts("\r\n");puth(arg1, 16);puts("\r\n");puth(arg2, 16);puts("\r\n");puth(arg3, 16);puts("\r\n");/* PIC へ割り込み処理終了を通知 (EOI) */set_pic_eoi(SYSCALL_INTR_NO);
return ret_val;}/* 追加 (ここまで) */ソフトウェア割り込み時の処理としては、ここでは、渡された全ての引数の値をダンプし、戻り値としては実験用の値として適当な値を返すようにしています。
また、システムコールに使う割り込み番号は 0x80(128) 番とし*2 、return 直前で PIC
へ割り込み処理の終了を通知しています。

1.2.3 実装した割り込みハンドラを登録する
最後にシステムコールの初期化関数"syscall_init"を作成します。
この関数でやることは、ここまでで実装した割り込みハンドラを割り込みの機能へ登録するだけです (リスト 1.3)。
▼リスト 1.3

011_softirq/syscall.c*2 ハードウェアや例外の番号と被っていなければ何番でも良いです。本書では Linux 等でもよく使っている 0x80 番をシステムコールの割り込み番号としています。

10

1.2 ソフトウェア割り込みを使えるようにする#include <intr.h>#include <pic.h>#include <fbcon.h>/* 追加 */

#define SYSCALL_INTR_NO

0x80

unsigned long long do_syscall_interrupt(
unsigned long long syscall_id, unsigned long long arg1,
unsigned long long arg2, unsigned long long arg3){/* ・・・ 省略 ・・・ */}/* 追加 (ここから) */void syscall_handler(void);void syscall_init(void){
set_intr_desc(SYSCALL_INTR_NO, syscall_handler);enable_pic_intr(SYSCALL_INTR_NO);}/* 追加 (ここまで) */main.c の start_kernel 関数から呼び出せるように、syscall.h を用意しておきます (リスト 1.4)。
▼リスト 1.4

011_softirq/include/syscall.h/* 追加 (ここから) */#pragma once
void syscall_init(void);/* 追加 (ここまで) */1.2.4 ソフトウェア割り込みを呼び出してみる
ここまででソフトウェア割り込みを使うための準備は完了です。
あとは、main.c の start_kernel 関数からソフトウェア割り込みを呼び出してみるために以下を行います。

1. syscall_init 関数を呼び出す
2. 適当な値を汎用レジスタにセット
3. ソフトウェア割り込み 0x80 を実行
大した行数は無いので、書いてしまうとリスト 1.5 の通りです。
▼リスト 1.5

011_softirq/main.c11

第 1 章 システムコールを実装し、カーネルとアプリを分離する/* ・・・ 省略 ・・・ */#include <syscall.h>/* ・・・ 省略 ・・・ *//* 追加 */

void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
void *_fs_start){/* ・・・ 省略 ・・・ *//* 周辺 IC の初期化 */pic_init();
hpet_init();kbc_init();/* 追加 (ここから) *//* システムコールの初期化 */syscall_init();/* 実験として適当な値を引数にセットして呼び出してみる */unsigned long long softirq_ret;
asm volatile ("movq $1, %%rdi\n""movq $2, %%rsi\n""movq $3, %%rdx\n""movq $4, %%rcx\n""int $0x80\n""movq %%rax, %[softirq_ret]": [softirq_ret]"=r"(softirq_ret):);puth(softirq_ret, 16);
while (1)cpu_halt();/* 追加 (ここまで) *//* ・・・ 省略 ・・・ */HPET や KBC の初期化を行った直後にシステムコールの初期化を追加しています*3 。
そして、実験として適当な値を引数にセットして、ソフトウェア割り込み 0x80 番を呼び出してみています。
関数の引数は第 1 引数から順に RDI、RSI、RDX、RCX のレジスタを使います。ア
センブラ部分の割り込みハンドラ (syscall_handler) が呼び出されてから、C 言語関数

(do_syscall_interrupt 関数) が呼ばれるまでの間でこれらのレジスタを変更しては居ないので、ここで設定した値がそのまま do_syscall_interrupt 関数の第 1 から第 4 引数に渡ります。ここでは"1"、"2"、"3"、"4"という数値を第 1 から第 4 引数に渡しています。
ソフトウェア割り込みの呼び出しは"int"命令です。オペランドでは 0x80 番の割り込みを発生させることを指定しています。

int 命令により 0x80 番の割り込みが発生し、syscall_handler が呼び出されて、そこから戻ってくると、戻り値である RAX レジスタの内容を softirq_ret 変数へ格納します。
*3 同じ割り込みハンドラの設定なのでこの辺りに追加しました。

12

1.3 カーネルから外部の実行バイナリ (アプリ) を起動する

その後、softirq_ret をダンプして、その後は無限ループで固めています。

1.2.5 実行する
あとは、Makefile へ syscall.o を OBJS に追加すれば*4 、ビルドできます。
ビルドし*5 、実行すると、図 1.4 のように画面に表示されます。

▲図 1.4 011_softirq の実行結果

do_syscall_interrupt 関数内で渡された全ての引数のダンプ結果として第 1 から第 4引数に渡した値 (1、2、3、4) が出力され、do_syscall_interrupt 関数の戻り値をダンプした結果として"BEEFCAFE01234567"が出力されています。
意図通りに動いていて、良さそうです。

1.3 カーネルから外部の実行バイナリ (アプリ) を起動する
次に、カーネルから外部の実行バイナリをアプリとして起動してみます。そして、起動されたアプリからソフトウェア割り込みをシステムコールとして実行してみます。
この項のサンプルディレクトリは「012_exec_syscall」です。

1.3.1 exec 関数を実装する
ファイルシステムを用意しファイルを扱う方法は前々著「x86_64 自作 OS(パート 1)」
で紹介しました。ここでは、ファイルポインタ*6 を渡すとそのファイルを実行ファイルとして実行する"exec"関数を実装します。
*4 OBJS 変数へ syscall.o を追加するだけなので、コードを引用しての説明はしません。実装を見てみたい場合はサンプルコードを参照してください。
*5 ビルド・実行方法は前前著「x86_64 自作 OS(パート 1)」を参照してください。
*6 ファイルシステム上のファイルの実体へのポインタ。open 関数でファイルを開くと得られる。

13

第 1 章 システムコールを実装し、カーネルとアプリを分離する「実行ファイルとして実行する」と言っても、やることは単にそのファイルの先頭へ call
命令でジャンプするだけです (図 1.5)。本書で作る OS がサポートする実行ファイルは、「ファイルの先頭から機械語の実行バイナリ列が並んでいるもの」とします (後程そのようにアプリを作ります)。

▲図 1.5 ファイルの実行はファイル先頭へジャンプするだけ"proc.c"というソースファイルを新たに作成し、その中に exec 関数を実装してみます(リスト 1.6)。
▼リスト 1.6

012_exec_syscall/proc.c/* 追加 (ここから) */#include <fs.h>void exec(struct file *f){void *start_addr = (void *)f->data;
asm volatile ("call *%[start_addr]" :: [start_addr]"r"(start_addr));}/* 追加 (ここまで) */14

1.3 カーネルから外部の実行バイナリ (アプリ) を起動する

ファイルのデータ部分の先頭アドレスを start_addr に格納し、call 命令でそこへジャンプしているだけです。
そして、他のファイルから呼び出せるように exec 関数のプロトタイプ宣言を in-clude/proc.h に書いておきます。また、Makefile の"OBJS ="へも proc.o を追加しておいてください。(共にコードでの説明は省略)
この節の最後に main.c の start_kernel 関数から実験として"test"という実行バイナリを呼び出すようにしておきます (リスト 1.7)。
▼リスト 1.7

012_exec_syscall/main.c/* ・・・ 省略 ・・・ */#include <proc.h>/* ・・・ 省略 ・・・ *//* 追加 */

void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
void *_fs_start){/* ・・・ 省略 ・・・ *//* システムコールの初期化 */syscall_init();/* ファイルシステムの初期化 */fs_init(_fs_start);/* 追加 (ここから) *//* 実験として"test"という実行ファイルを実行してみる */exec(open("test"));while (1)cpu_halt();/* 追加 (ここまで) *//* ・・・ 省略 ・・・ */前節で追加したソフトウェア割り込みを実行する実験コードは削除しておきます (リスト 1.7)。

1.3.2 実行ファイル (アプリ) を作る
exec 関数で呼び出される実行ファイルを作ります。
アプリのソースコードを格納するディレクトリとして"apps"というディレクトリを作成します。アプリは実験用に複数作るので、その中にサブディレクトリとして"01_softirq"というディレクトリを作っておきます。
ここでは、apps/01_softirq/app.c に、実験用のアプリとして、適当な引数でソフト

15

第 1 章 システムコールを実装し、カーネルとアプリを分離する

ウェア割り込みを呼び出すだけのアプリを作成します (リスト 1.8)。
▼リスト 1.8

012_exec_syscall/apps/01_softirq/app.c/* 追加 (ここから) */int main(void){
unsigned long long softirq_ret;
asm volatile ("movq $1, %%rdi\n""movq $2, %%rsi\n""movq $3, %%rdx\n""movq $4, %%rcx\n""int $0x80\n""movq %%rax, %[softirq_ret]": [softirq_ret]"=r"(softirq_ret):);while (1);return 0;}/* 追加 (ここまで) */puth 関数や cpu_halt 関数が無い事を除いて、内容は start_kernel 関数に実装していたものと同じです。
そして、テキストセクションを先頭に持ってくるようにリンカスクリプトを作成します(リスト 1.9)。
▼リスト 1.9012_exec_syscall/apps/app.ld/* 追加 (ここから) */OUTPUT_FORMAT("binary");
SECTIONS{.body : {*(.text)*(.rodata)*(.data)__bss_start = .;*(.bss)__bss_end = .;*(.eh_frame)}}/* 追加 (ここまで) */リンカスクリプトは本書で登場するすべてのアプリで共通なので apps ディレクトリ直下に置いておきます。

16

1.3 カーネルから外部の実行バイナリ (アプリ) を起動する

この節の最後にこのアプリをビルドする Makefile を作成します (リスト 1.10)。
▼リスト 1.10

012_exec_syscall/apps/01_softirq/Makefile/* 追加 (ここから) */TARGET = test
CFLAGS = -Wall -Wextra -nostdinc -nostdlib -fno-builtin -fno-common-Iinclude -fPIE
LDFLAGS = -Map app.map -s -x -T ../app.ld -pie
OBJS = app.o\$(TARGET): $(OBJS)
ld $(LDFLAGS) -o $@ $+%.o: %.c
gcc $(CFLAGS) -c -o $@ $<deploy: $(TARGET)../../tools/create_fs.sh $(TARGET)
mv fs.img ../../../fs/
clean:
rm -f *~ *.o *.map $(TARGET).PHONY: deploy clean/* 追加 (ここまで) */deploy というターゲットを用意して、ファイルシステム作成スクリプトを呼び出すよ
うにしています。そのため、ソースディレクトリ直下に"tools"ディレクトリを作成し、その中に前前著で作成したスクリプト*7 を配置しておいてください。

*7 https://github.com/cupnes/x86_64_jisaku_os_samples/の 052_fs_create_fs/create_fs.sh17

第 1 章 システムコールを実装し、カーネルとアプリを分離する

CFLAGS や LDFLAGS の PIE とは
アプリの Makefile では、CFLAGS に"-fPIE"を、LDFLAGS に"-pie"を指定していました。PIE とは"Position Independent Executable(位置独立実行形式)"の略です。
ファイルシステム上の実行ファイルの先頭へジャンプするだけで実行できる理由
が、この PIE です。PIE 指定でコンパイルすることで、メモリ空間上のどこに配置されても実行できる実行ファイルになります。
それでは、PIE を使わない場合はどうするのかというと、普通は MMU(Memory

Management Unit、メモリ管理ユニット) にページングの設定を行い、アプリのバイナリが実際に配置されている物理アドレスをどのアプリでも共通のどこかの
仮想アドレスへマップするようにします。CPU は仮想アドレスを見て動作するので、どのアプリも仮想アドレス上は同じアドレスにマップされていれば、PIE で無くとも (この仮想アドレスを想定してリンクされていれば) 問題無い訳です。
本書のシリーズでも CPU 設定としてページングは有効です。ただ、UEFI が設定してくれた「物理アドレス=仮想アドレス」のマッピングを使っているので、実質、アドレスの変換は行われていません。

1.3.3 実行する
実行する際は、あらかじめ、apps/01_softirq ディレクトリで"make deploy"を実行してください。これにより、生成物を配置する fs ディレクトリにアプリの実行バイナリを含んだファイルシステムイメージ (fs.img) が配置されます。
実行結果として画面に出力される内容は、ソフトウェア割り込みからの戻り値の出力が無い事を除いて前項と同じですので、画面のスクリーンショットは省略します。

1.4 システムコールを実装する
ここまで作ってきたソフトウェア割り込みの仕組みを使ってシステムコールを実装します。ここでは渡された文字を画面に表示する putc をシステムコール化してみます。
この項のサンプルディレクトリは「013_syscall_putc」です。

18

1.4 システムコールを実装する

1.4.1 kernel: システムコールの処理を実装する
まず、カーネル側の syscall.c へ putc システムコールの処理を実装します。

putc システムコールの番号を決める定義を用意し、そのシステムコール番号で呼び出された場合に与えられた引数で putc 関数を呼び出すだけです (リスト 1.11)。
▼リスト 1.11

013_syscall_putc/syscall.c/* ・・・ 省略 ・・・ *//* 追加 (ここから) */enum SYSCCALL_NO {SYSCALL_PUTC,
MAX_SYSCALL_NUM};/* 追加 (ここまで) */unsigned long long do_syscall_interrupt(
unsigned long long syscall_id, unsigned long long arg1,
unsigned long long arg2 __attribute__((unused)),/* 変更 */
unsigned long long arg3 __attribute__((unused)))/* 変更 */{/* 変更 (ここから) */unsigned long long ret_val = 0;
switch (syscall_id) {case SYSCALL_PUTC:
putc((char)arg1);
break;}/* 変更 (ここまで) *//* PIC へ割り込み処理終了を通知 (EOI) */set_pic_eoi(SYSCALL_INTR_NO);
return ret_val;}/* ・・・ 省略 ・・・ */1.4.2 app: システムコールをライブラリ化してみる
この項で作るアプリのディレクトリは「02_putc」とします。
ライブラリとして使う関数は、ひとまず"lib.c"にまとめる事とし、リスト 1.12 のように

putc のシステムコールを呼び出すライブラリを用意します。
▼リスト 1.12

013_syscall_putc/apps/02_putc/lib.c19

第 1 章 システムコールを実装し、カーネルとアプリを分離する/* 追加 (ここから) */enum SYSCCALL_NO {SYSCALL_PUTC,
MAX_SYSCALL_NUM};
unsigned long long syscall(unsigned long long syscall_id __attribute__((unused)),unsigned long long arg1 __attribute__((unused)),unsigned long long arg2 __attribute__((unused)),unsigned long long arg3 __attribute__((unused))){unsigned long long ret_val;
asm volatile ("int $0x80\n""movq %%rax, %[ret_val]": [ret_val]"=r"(ret_val) :);return ret_val;}
void putc(char c){syscall(SYSCALL_PUTC, c, 0, 0);}/* 追加 (ここまで) */また、併せて"02_putc/include/lib.h"を作成し、リスト 1.12 で定義した putc 関数のプロトタイプ宣言を書いておいてください。
そして、app.c では、ライブラリ化した putc 関数を呼び出してみます (リスト 1.13)。
▼リスト 1.13

013_syscall_putc/apps/02_putc/app.c/* 追加 (ここから) */#include <lib.h>
int main(void){putc(’A’);
return 0;}/* 追加 (ここまで) */ビルドするためには、02_putc の Makefile には、"OBJS ="に"lib.o"を追加しておいて
ください。

20

1.4 システムコールを実装するアプリから return した時の戻り先は?
実はリスト 1.13 ではアプリの最後に無限ループを設置せず、return しています。
この場合、何処に return するのかというと、カーネル側の start_kernel 関数内でexec 関数を呼び出したところへ戻ります。そして、後続の while ループを実行し始め、cpu_halt 関数により CPU を休ませます。
これは exec 関数でインラインアセンブラによりジャンプする際に call 命令を使用しているためです。call 命令は戻り先のアドレスをスタックへ積んでからジャンプするため、return で戻ってくることができます。

1.4.3 実行する
ビルドし、実行すると、図 1.6 のように画面に’A’ が描画されることを確認できます。

▲図 1.6 013_syscall_putc の実行結果

結果は地味ですが、ここまでで、カーネルの機能をシステムコール化し、アプリ側ではシステムコール呼び出しを、ライブラリ化*8 することができました。
あとは同じ要領でシステムコールを増やしていけば、アプリ側で他のカーネル機能を呼び出せるようになります。

*8 ライブラリとはいっても別ファイルへ分けただけですが。静的ライブラリについては本シリーズの「UEFI ベアメタルプログラミング (ブートローダ編)」で扱っていますので、参考にしてみてください。
動的ライブラリについては、、、まだ扱ったことがないのでググってみてください。。

21

第2章

アプリから別のアプリを呼び出す
前章でカーネルとアプリを分離しました。ただ、アプリからアプリを呼び出す方法が無いので、このままでは単一のアプリしか実行できません。
この章では、以下の２つの仕組みを実装します。

1. 呼び出したアプリの終了後、呼び出し元へ戻る (フォアグラウンド実行)
2. アプリ呼び出し後、ただちに呼び出し元へ処理が戻り並列に実行される (バックグ
ラウンド実行)本書におけるフォアグラウンド実行とバックグラウンド実行のそれぞれを図示すると図2.1 の通りです。

▲図 2.1 本書におけるフォアグラウンド実行とバックグラウンド実行の動き

222.1 フォアグラウンド実行を実現する「フォアグラウンド」・「バックグラウンド」と言うには「どのアプリがキー入力等の入力を受け取るか」も関わりますが、本書ではそこまで扱わず、単に「バックグラウンド実
行の場合、呼びだされたアプリは並列で実行」ができれば良いものとします*1 。

2.1 フォアグラウンド実行を実現する
まずはフォアグラウンド実行をできるようにしてみます。
この項のサンプルディレクトリは「021_fg」です。

2.1.1 open と exec をシステムコール化
フォアグラウンド実行を実現するには、最低限、open と exec をアプリから呼び出すことができれば良いです。この節ではこれらをシステムコール化します。
編集するファイルは syscall.c だけです (リスト 2.1)。
▼リスト 2.1

021_fg/syscall.c#include <intr.h>#include <pic.h>#include <fbcon.h>#include <fs.h>#include <proc.h>/* 追加 *//* 追加 *//* ・・・ 省略 ・・・ */enum SYSCCALL_NO {SYSCALL_PUTC,SYSCALL_OPEN,/* 追加 */
SYSCALL_EXEC,/* 追加 */MAX_SYSCALL_NUM};
unsigned long long do_syscall_interrupt(
unsigned long long syscall_id, unsigned long long arg1,
unsigned long long arg2 __attribute__((unused)),unsigned long long arg3 __attribute__((unused))){unsigned long long ret_val = 0;
switch (syscall_id) {case SYSCALL_PUTC:*1 キー入力等の入力をフォアグラウンド/バックグラウンドにどのように実装するかは、キー入力等のシス

テムコールの実装次第です。例えばキーボード割り込みで呼び出されるユーザーハンドラを登録する機能をシステムコール化するようにすれば、コンテキストスイッチ時に呼び出すユーザーハンドラを変えることでキー入力値を渡すアプリを切り替える事ができます。ユーザーハンドラ登録のシステムコール実装は
表紙アプリのサンプルコード「A01_cover」に実装例がありますので興味があれば見てみてください。

23

第 2 章 アプリから別のアプリを呼び出す

putc((char)arg1);
break;/* 追加 (ここから) */case SYSCALL_OPEN:
ret_val = (unsigned long long)open((char *)arg1);
break;
case SYSCALL_EXEC:
exec((struct file *)arg1);
break;/* 追加 (ここまで) */}/* PIC へ割り込み処理終了を通知 (EOI) */set_pic_eoi(SYSCALL_INTR_NO);
return ret_val;}/* ・・・ 省略 ・・・ */システムコール割り込みハンドラの switch に条件を追加し、open や exec を呼び出すだけです。

2.1.2 アプリからアプリを呼び出してみる
open と exec のシステムコールを使ってアプリを呼び出してみます。
「03_call_fg」という名前で apps ディレクトリ内に作成することにします。
まず、lib.h と lib.c に open と exec を呼び出すための定義を追加します (リスト 2.2、
リスト 2.3)。
▼リスト 2.2

021_fg/apps/03_call_fg/include/lib.h#pragma once/* 追加 (ここから) */#define FILE_NAME_LEN 28
struct __attribute__((packed)) file {char name[FILE_NAME_LEN];
unsigned int size;
unsigned char data[0];};/* 追加 (ここまで) */void putc(char c);
struct file *open(char *file_name);
void exec(struct file *file);/* 追加 *//* 追加 */

24

2.1 フォアグラウンド実行を実現する▼リスト 2.3

021_fg/apps/03_call_fg/lib.cenum SYSCCALL_NO {SYSCALL_PUTC,SYSCALL_OPEN,/* 追加 */
SYSCALL_EXEC,/* 追加 */MAX_SYSCALL_NUM};/* ・・・ 省略 ・・・ */void putc(char c){syscall(SYSCALL_PUTC, c, 0, 0);}/* 追加 (ここから) */struct file *open(char *file_name){return (struct file *)syscall(
SYSCALL_OPEN, (unsigned long long)file_name, 0, 0);}
void exec(struct file *file){syscall(SYSCALL_EXEC, (unsigned long long)file, 0, 0);}/* 追加 (ここまで) */そして、リスト 2.4 の様に app.c を作成すると"puta"という実行バイナリを実行できます。
▼リスト 2.4

021_fg/apps/021_call_fg/app.c#include <lib.h>
int main(void){putc(’Z’);
exec(open("puta"));putc(’B’);
return 0;}

別バイナリを呼び出す前後が分かるように、リスト 2.4 では exec 関数の前後に putc 関
数で’Z’ の出力と’B’ の出力を置いてみました。

25

第 2 章 アプリから別のアプリを呼び出す

2.1.3 実行する
"02_putc"のアプリを"puta"という名前でファイルシステムへ入れておくことにします。
以下の様なコマンドを実行してファイルシステムイメージ fs.img を作成してください。

$ cd 021_fg/apps/02_putc/$ make$ cp test ../puta$ cd ../03_call_fg/$ make$ cp test ../$ cd ../$ ../tools/create_fs.sh test puta
$ ls fs.img
fs.img # 生成できていることを確認

この fs.img を使って実行すると、図 2.2 の結果となりました。

▲図 2.2 021_fg の実行結果’Z’ を出力した後で、puta 実行バイナリにより’A’ が出力され、puta から戻ってきた
後’B’ を出力しています。
これで、カーネル側でアプリを呼び出した時と同じようにアプリからアプリを呼び出せるようになりました。

2.2 バックグラウンド実行を実現する
次に、バックグラウンド実行を実現してみます。
この項のサンプルディレクトリは「022_bg」です。

26

2.2 バックグラウンド実行を実現する

2.2.1 実験用の処理を削除し、スケジューラを有効化する
バックグラウンド実行を実現するためには、前著で実装したスケジューラを使います。
しかし、前著の状態では、スケジュール対象のタスク等がカーネルのソースコードに
ハードコードされています。この節では、ハードコードされたタスクや実験コードの削除
や、実験用に作ったタスク生成機能を汎用化等を行い、次の節でこれらの機能を使いやす
いようにしておきます。
なお、この節で行う作業が済んだものはサンプルディレクトリ「022_bg_refactor」にあります。この節を飛ばす場合はこのディレクトリの状態から次の節を始めてみてくだ
さい。
編集するファイルは main.c(リスト 2.5) と sched.c(リスト 2.6) です。
▼リスト 2.5022_bg/main.c/* ・・・ 省略 ・・・ */struct __attribute__((packed)) platform_info {struct framebuffer fb;
void *rsdp;};/* void do_taskA(void); 削除 */#define INIT_APP"test"/* 追加 */

void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
void *_fs_start){/* ・・・ 省略 ・・・ *//* ファイルシステムの初期化 */fs_init(_fs_start);/* 削除 (ここから)/* 実験として"test"という実行ファイルを実行してみる */exec(open("test"));while (1)cpu_halt();
削除 (ここまで) *//* スケジューラの初期化 */sched_init();/* CPU の割り込み有効化 */enable_cpu_intr();/* スケジューラの開始 */sched_start();27

第 2 章 アプリから別のアプリを呼び出す/* 削除 (ここから)/* タスク A の開始 */do_taskA();
削除 (ここまで) *//* 追加 (ここから) *//* init アプリ起動 */exec(open(INIT_APP));/* 追加 (ここまで) *//* halt して待つ */while (1)cpu_halt();}/* 削除 (ここから)void do_taskA(void){
while (1) {putc(’A’);
volatile unsigned long long wait = 10000000;
while (wait--);}}削除 (ここまで) */▼リスト 2.6

022_bg/sched.c/* ・・・ 省略 ・・・ */#include <fbcon.h>#include <fs.h> /* 追加 */#define SCHED_PERIOD(5 * MS_TO_US)#define NUM_TASKS
1#define TASK_STASK_BYTES
4096/* 変更 *//* 変更 */

unsigned long long task_sp[MAX_TASKS]; /* 変更 */volatile unsigned int current_task = 0;
unsigned char task_stack[MAX_TASKS - 1][TASK_STASK_BYTES];
unsigned int num_tasks = 1;/* 追加 *//* 削除 (ここから)void do_taskB(void){
while (1) {putc(’B’);
volatile unsigned long long wait = 10000000;
while (wait--);}}削除 (ここまで) */void schedule(unsigned long long current_rsp){

28/* 変更 */

2.2 バックグラウンド実行を実現する

task_sp[current_task] = current_rsp;
current_task = (current_task + 1) % num_tasks; /* 変更 */set_pic_eoi(HPET_INTR_NO);asm volatile ("mov %[sp], %%rsp":: [sp]"a"(task_sp[current_task]));asm volatile ("pop%rdi\n""pop%rsi\n""pop%rbp\n""pop%rbx\n""pop%rdx\n""pop%rcx\n""pop%rax\n""iretq\n");}/* 変更 (ここから) */void sched_init(void){/* 5ms 周期の周期タイマー設定 */ptimer_setup(SCHED_PERIOD, schedule);}
void enq_task(struct file *f){unsigned long long start_addr = (unsigned long long)f->data;/* 予めタスクのスタックを適切に積んでおき、スタックポインタを揃える */unsigned long long *sp =(unsigned long long *)((unsigned char *)task_stack[num_tasks]+ TASK_STASK_BYTES);
unsigned long long old_sp = (unsigned long long)sp;/* push SS */--sp;*sp = 0x10;/* push old RSP */--sp;*sp = old_sp;/* push RFLAGS */--sp;*sp = 0x202;/* push CS */--sp;*sp = 8;/* push RIP */--sp;*sp = start_addr;/* push GR */unsigned char i;for (i = 0; i < 7; i++) {--sp;*sp = 0;29

第 2 章 アプリから別のアプリを呼び出す}
task_sp[num_tasks] = (unsigned long long)sp;
num_tasks++;}/* 変更 (ここまで) *//* ・・・ 省略 ・・・ */主に実験用の処理の削除です。
その他に、リスト 2.5 ではカーネルが起動するアプリを"INIT_APP"と定義してそれを初期化処理の最後に呼び出すようにしたり、リスト 2.6 では sched_init 関数内のタス
ク生成処理から汎用的なタスク生成関数 (enq_task 関数) を作ったりしています。

enq_task 関数は次節で使うので、include/sched.h にプロトタイプ宣言を追加しておいてください。
この節の変更を行ったあとは、ここまでで、ちゃんと変わらず実行できるか、一応ビルドして確認してみてください。

2.2.2 アプリをランキューへ追加するシステムコールを作成する
前節で、スケジューラを動作させながら、シングルタスクでカーネルから別アプリを起動できるようになりました。
あとは、ランキューへのアプリ追加をシステムコール化できれば、アプリが任意の実行
バイナリを並列で実行できます。
この節では、"SYSCALL_ENQ_TASK"というシステムコールとして実装してみます(リスト 2.7)。
▼リスト 2.7

022_bg/sched.c/* ・・・ 省略 ・・・ */#include <proc.h>#include <sched.h>/* 追加 */

#define SYSCALL_INTR_NO
enum SYSCCALL_NO {SYSCALL_PUTC,SYSCALL_OPEN,
SYSCALL_EXEC,SYSCALL_ENQ_TASK,MAX_SYSCALL_NUM
};

0x80

/* 追加 */

30

2.2 バックグラウンド実行を実現する

unsigned long long do_syscall_interrupt(
unsigned long long syscall_id, unsigned long long arg1,
unsigned long long arg2 __attribute__((unused)),unsigned long long arg3 __attribute__((unused))){unsigned long long ret_val = 0;
switch (syscall_id) {/* ・・・ 省略 ・・・ *//* 追加 (ここから) */case SYSCALL_ENQ_TASK:
enq_task((struct file *)arg1);
break;/* 追加 (ここまで) */}/* PIC へ割り込み処理終了を通知 (EOI) */set_pic_eoi(SYSCALL_INTR_NO);
return ret_val;}/* ・・・ 省略 ・・・ */前節で関数化した enq_task 関数をシステムコールで呼び出しています。

2.2.3 アプリからランキュー追加を試す
それでは、追加したシステムコールを試してみます。
まずは、並列で動作させるために呼び出される側のアプリを用意します。
「04_putb_inf」というディレクトリ名のアプリで、app.c の内容はリスト 2.8 の通りとします。
▼リスト 2.8

022_bg/apps/04_putb_inf/app.c#include <lib.h>#define WAIT_CLK

1000000

int main(void){
while (1) {unsigned long long wait = WAIT_CLK;
putc(’B’);
while (wait--);}return 0;}

31

第 2 章 アプリから別のアプリを呼び出す

文字’B’ を永遠に出し続けるだけです。
加えて、Makefile の"TARGET ="を"putb"へ変更し、生成される実行バイナリ名
が"putb"になるようにしておきます。
それ以外の内容は前回のアプリと同じです。
次に、この putb バイナリをランキューへ追加するアプリを「05_call_putb」という名前で作成します。
まず、SYSCALL_ENQ_TASK を呼び出す関数を lib.c に追加します (リスト 2.9)。
▼リスト 2.9022_bg/apps/05_call_putb/lib.cenum SYSCCALL_NO {SYSCALL_PUTC,SYSCALL_OPEN,
SYSCALL_EXEC,SYSCALL_ENQ_TASK,MAX_SYSCALL_NUM};/* 追加 *//* ・・・ 省略 ・・・ */void exec(struct file *file){syscall(SYSCALL_EXEC, (unsigned long long)file, 0, 0);}/* 追加 (ここから) */void enq_task(struct file *file){syscall(SYSCALL_ENQ_TASK, (unsigned long long)file, 0, 0);}/* 追加 (ここまで) */lib.h へ enq_task 関数のプロトタイプ宣言を追加すると app.c から呼び出せるようになります。
実験として、app.c はリスト 2.10 のように作ってみます。
▼リスト 2.10

022_bg/apps/05_call_putb/app.c#include <lib.h>#define WAIT_CLK

1000000

int main(void){
enq_task(open("putb"));while (1) {32

2.2 バックグラウンド実行を実現する

unsigned long long wait = WAIT_CLK;
putc(’A’);
while (wait--);}return 0;}"putb"バイナリを開き、enq_task でランキューへ追加すると、カーネルのスケジュー
ラにより並列で実行されます。
そのあとは、文字’A’ を永遠に出し続けるだけです。

2.2.4 実行する
実行結果の見た目は前著のスケジューラの実験と同じで、A と B それぞれの文字が画面に表示されれば成功です (図 2.3)。

▲図 2.3 022_bg の実行結果

33

おわりに
本書をお読みいただきありがとうございました！
本書では、システムコールでカーネルとアプリを分離し、フォアグラウンド/バックグ
ラウンドの仕組みも加え、アプリからアプリを呼び出せるようにしました。カーネルとア
プリをバイナリレベルで分離できたことで、ようやく「OS」と呼んでも良い感じになってきました。

# とはいえ、「ランキュー」がキューではなく単なる配列であったり、ロックを取る仕組みが無いので色々なものが並列で動き出すとリソースの取り合いが起こりそうであった
り、とまだまだ改善の余地がはありますが。
。
そんな訳で、まだまだ課題はありますが、「OS」と呼ぶための最低限の実装はひとまず完了かなと思います。ここまで読み進めた/試してみた方は、本当にお疲れさまでした。
この後は、色んなアプリを作るも良し、デバドラを充実させるも良し、で、この後もぜ
ひ自作 OS を楽しんでもらえればと思います。(むしろこれからが楽しいところなのカモ)

表紙のフィギュアについて
表紙にちょこっと写っているフィギュアは「x86_64 自作 OS」シリーズのマスコット
として「ぷちのこメーカー*2 」というサービスで作ってみたものです。
「参考情報」の章に書いていますが、このシリーズで作る OS の開発リポジトリ名は「yuaos(ゆあ OS)」で、このキャラの名前も「ゆあ」です。このシリーズで作る OS は「あなたの OS(Your OS)」である事から、この名前にしてみました。
ふたばちゃんねるという画像掲示板で昔盛り上がっていた「OS 娘 (OS 擬人化)」みた
く考えると、ゆあは大神家の四女ですね*3 。そう考えると、これまで結構色々と作ってきたなぁと思います。

*2 パ ー ツ を 選 ん で オ リ ジ ナ ル の 3D キ ャ ラ を 作 成 す る サ ー ビ ス で 、有 償 で フ ィ ギ ュ ア 化 で き ま す 。

URL:https://figure.uchinokomato.me/maker
OS5(Ohgami’s Commentary on OS5)、次女・三女: poiboot・poios(フルスクラッチで作る!UEFI ベアメタルプログラミングシリーズ)、という想定*3 長女:

34

参考情報開発リポジトリ
本にする前のネタ作り等、本書で扱った OS の開発は以下のリポジトリで行っています
ので、興味があれば見てみてください。
今は"next"ブランチが開発ブランチで、適当なタイミングで master へマージしています。

• ブートローダー– https://github.com/cupnes/poiboot• カーネル・アプリ– https://github.com/cupnes/yuaos

本シリーズの過去の著作
サークル「へにゃぺんて」の同人誌は、「フルスクラッチで作る!」シリーズ含め、すべて以下の URL から PDF 版/HTML 版は無料でダウンロード/閲覧できます• http://yuma.ohgami.jp「フルスクラッチで作る!」シリーズの著作としては以下があります。

• フルスクラッチで作る!UEFI ベアメタルプログラミング! パート 1、パート 2– ブートローダーを作る上で必要な UEFI を直接叩く方法を紹介• フルスクラッチで作る!x86_64 自作 OS(パート 1)– ハードウェアを抽象化しアプリへ提供するカーネルの基本的な枠組みを作り上げる– カーネルの機能を利用するアプリとして「画像ビューア」を作る• フルスクラッチで作る!x86_64 自作 OS パート 2– ACPI で HPET 取得してスケジューラを作る本
35

フルスクラッチで作る!x86 ＿ 64 自作 OS パート 3 システム
コールの薄い本2018 年 12 月 30 日 コミックマーケット 95 版 v1.0
著

者

大神祐真

発行所

へにゃぺんて連絡先

yuma@ohgami.jp

印刷所

日光企画

(C) 2018 へにゃぺんて36