makeとgccだけでOSを作ってみる 

こんにちは．[だいみょーじん](https://twitter.com/Egh2Deywos)です．
この記事は，[自作OS Advent Calendar](https://adventar.org/calendars/6581)20日目の記事です．

対象読者:OS自作をやっていたり，興味がある人．特に[30日でできる! OS自作入門](https://book.mynavi.jp/ec/products/detail/id=22078)(以下「30日本」)を読んだことがある人．

# 30日本をやってみて感じたもやもや

これまで多くの人々をOS自作の沼に陥れた30日本．
私もこの本の影響でOS自作にとりつかれた者のひとりです．
OS自作界の住人は誰しも心のどこかにハードウェアを完全に支配したいという欲求があるのではないでしょうか？
いや絶対にあると思います．
みなさんは30日本でOSを書いているときにこう思ったことはないですか？

* なぜ著者が開発したツールを使わなければならないんだ！
* なぜCの標準ライブラリを使わなければならないんだ！
* なぜバッチファイルがあるんだ！（WindowsよりLinuxの方が好きなんで）

これではハードウェアを完全に支配しているとは言えない！
ということで，以下の条件でOSを作りたくなったわけですよ．

* ビルドの際にはGNUツール群以外使用禁止
* 自作OS内の命令列部分は全部自分で書く．要するにライブラリの使用禁止

# やったことリスト

上の条件を満たしつつOSを作るためにやったことは以下の通り

* マスターブートレコードを作成
* フロッピーディスクのイメージを出力するツールを自作
* Cのプログラムに飛ぶまでの部分をアセンブリで作成
* Cのプログラムを書く
* sprintfとかを自作
* フォントをOSに埋め込むためのツールを自作
* gdbでリアルモードデバッグ(おまけ)

ざっとこんな感じです．
順番に説明していきます．

## マスターブートレコードを作成

マスターブートレコードは30日本でも解説されている，最初に0x7c00番地に読み込まれるあれです．
[こんな感じ](https://github.com/TaiseiIto/hariboslinux/blob/main/src/bootsector.s)にアセンブリで書きました．
30日本との大きな違いは，GNUアセンブラはデフォルトでAT&T記法になっており，30日本で使われるIntel記法と異なる点です．
GNUアセンブラも`.intel_syntax`と書けばIntel記法で書けるそうですが，勉強のためにAT&T記法にチャレンジしてみました．
Intel記法とAT&T記法の一番の違いはオペランドの語順の違いです．
例えば「EAXにEDXを足す」はそれぞれ以下のようになります．

```:Intel記法
ADD EAX, EDX
```

```:AT&T記法
add %edx, %eax
```

今までIntel記法でしか書いたことがなかったので最初は面食らいましたが，Intel記法は`EAX += EDX`のように式として読む，AT&T記法は`add %edx to %eax`のように英語として読むように意識すれば簡単です．

あともうひとつ重要なこととして，リアルモードの機械語を出力するように指示する必要があります．
コードの中で`.code16`と書くと，それ以降の命令列はリアルモードの機械語で出力してくれます．

## フロッピーディスクのイメージを出力するツールを自作

`imagepacker`は30日本の`edimg.exe`に相当するやつです．
こいつにマスターブートレコードと，フロッピーディスクに入れたいファイルたちを食わせると，FATやルートディレクトリ内のディレクトリエントリ構造体の配列を書き出しつつファイルをクラスタに分解・配置していって，フロッピーディスクのイメージを吐き出します．
[こんな感じ](https://github.com/TaiseiIto/hariboslinux/blob/main/imagepacker/imagepacker.c)にCで書きました．
え？ライブラリ使ってるじゃんって？
いやこれは外部ツールであってOSに組み込まれるやつじゃないからいいの！（言い訳）
FATの仕様については30日本のほか，[これ](http://elm-chan.org/docs/fat.html)が参考になりました．
あと，Cでマスターブートレコードの構造体を扱う際に，構造体内の各要素のオフセットが仕様で決まっている関係で，構造体内にパディングを入れないよう構造体の末尾に`__attribute__((packed))`をつける必要があります．
この方法はosdev-jpの初心者質問相談でuchan-nosさんに教えていただきました．（感謝）
osdev-jpに入ればOS自作ガチ勢に気軽に質問できるのでみんな入信しましょう！

```c:マスターブートレコード構造体
typedef struct
{
	unsigned char jump_instructions[3];

	// The string doesn't end with '\0'.
	// Margin is filled with spaces.
	char product_name[8];

	unsigned short num_of_bytes_per_sector;
	unsigned char num_of_sectors_per_cluster;

	// Number of reserved sectors.
	// The boot record sectors are included in this value.
	// After the sectors, FAT sectors begin.
	unsigned short num_of_reserved_sectors;

	unsigned char num_of_FATs;
	unsigned short num_of_root_directory_entries;
	unsigned short num_of_sectors_in_disk;
	unsigned char media_type;
	unsigned short num_of_sectors_per_FAT;
	unsigned short num_of_sectors_per_track;
	unsigned short num_of_heads;
	unsigned int num_of_hidden_sectors;
	unsigned int large_num_of_sectors_in_disk;
	unsigned char drive_number;
	unsigned char reserved;

	// If it's 0x29, it's bootable;
	unsigned char boot_signature;

	unsigned int volume_serial_number;

	// The strings doesn't end with '\0'.
	// Margin is filled with spaces.
	char volume_label[11];
	char file_system_name[8];
} __attribute__((packed)) BootSector;
```

## Cのプログラムに飛ぶまでの部分をアセンブリで作成

Cのプログラムに飛ぶまでにアセンブリで作成した以下のバイナリを順番に実行します．

* マスターブートレコードの続きを主記憶にロードする[loaddisk.bin](https://github.com/TaiseiIto/hariboslinux/blob/main/src/loaddisk.s)
* BIOSでハードウェアのメモリマップを取得する[getmemmp.bin](https://github.com/TaiseiIto/hariboslinux/blob/main/src/getmemmp.s)
* 画面モードを設定する[initscrn.bin](https://github.com/TaiseiIto/hariboslinux/blob/main/src/initscrn.s)
* リアルモードからプロテクトモードに移行する[mv2prtmd.bin](https://github.com/TaiseiIto/hariboslinux/blob/main/src/mv2prtmd.s)
* 0x7c00番地を起点にロードされたディスクイメージを0x00100000番地から始まる領域に移してカーネルのメインプログラムに飛ぶ[dplydisk.bin](https://github.com/TaiseiIto/hariboslinux/blob/main/src/dplydisk.s)

ここはもう兎に角アセンブリでゴリゴリ書いてった感じです．
あと，リンカスクリプト（拡張子.ld）なるものを初めて書きました．
これはコンパイル済みのオブジェクトファイルをリンクする際の各オブジェクトの配置を記述するものです．
以下のようにメモリマップを[global.ld](https://github.com/TaiseiIto/hariboslinux/blob/main/src/global.ld)に記述して，

```:global.ld
LOADDEST = 0x00007c00;     /* この番地を始点にディスクを読み込む */
LOADDISKADDR = 0x00004200; /* ディスク内のloaddisk.binの位置 */
LOADDISKSIZE = 0x00000600; /* ディスク内のloaddisk.binのサイズ */
GETMEMMPSIZE = 0x00000600; /* ディスク内のgetmemmp.binのサイズ */
INITSCRNSIZE = 0x00000a00; /* ディスク内のinitscrn.binのサイズ */
MV2PRTMDSIZE = 0x00000400; /* ディスク内のmv2prtmd.binのサイズ */
DPLYDISKSIZE = 0x00000400; /* ディスク内のdplydisk.binのサイズ */
```

さらに`loaddisk.bin`内のオブジェクトの配置を[loaddisk.ld](https://github.com/TaiseiIto/hariboslinux/blob/main/src/loaddisk.ld)に記述します．

```:loaddisk.ld
OUTPUT_FORMAT("binary"); /* ELFヘッダを消す */

INCLUDE global.ld /* メモリマップの定義 */

BASE = LOADDEST + LOADDISKADDR; /* loaddisk.binの先頭番地 */

SECTIONS
{
	. = BASE;
	.text :
	{
		loaddisk.o(.text) /* 命令列 */
		loaddisk.o(.data) /* データ */
	}
	/DISCARD/:{*(.eh_frame)} /* 先頭に謎の領域が生成されないようにする*/
}
```

1行目でELFヘッダを消して先頭から.text領域が始まるようにするために`OUTPUT_FORMAT("binary");`と書いてあります．
起動時に0x7c00番地を起点にフロッピーディスクの内容を主記憶にロードしているので，「0x7c00+フロッピーディスク内における実行ファイルの先頭位置」がその実行ファイルの先頭の番地になるようにベースアドレスを設定します．
実行ファイル先頭に命令列`.text`セクションを配置し，そのあとにデータ`.data`セクションを配置します．
最後に，`/DISCARD/:{*(.eh_frame)}`と書いて`.eh_frame`という謎の領域が生成されないようにします．
あとは実行ファイルを`imagepacker`に食わせてフロッピーディスク上に配置するだけです．

下の図のように，フロッピーディスクにはマスターブートレコード，2つのFAT，ルートディレクトリ，loaddisk.binの命令列とデータ，その他のファイルが並んでいます．
起動すると0x7c00番地にマスターブートレコードを読み込んで実行し，ディスクの続きの領域を読み込んでloaddisk.binの命令列部分にジャンプします．
loaddisk.binではディスクのさらに続きの部分を読み込み，次のバイナリにジャンプし，カーネルのメインプログラムに到達するまでの各バイナリを実行していきます．

![boot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2354177/ff900d15-a10a-f8e8-891c-961fdfdc9eb7.png)

## Cのプログラムを書く

ここからカーネルの[main.c](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/main.c)を作り始めます．
ポイントは，一切ライブラリを使っていないことと，ELFヘッダを消したことです．

gccは何もオプションをつけていないと勝手に標準ライブラリをリンクしやがります．
それを抑制するために[Makefile](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/Makefile)でgccにオプション`-nostdlib`，`-fno-builtin`，`-fno-pie`を渡すようにしています．
また，64ビットな環境でも32ビットの機械語を出力させるために，オプション`-m32`を渡しています．

ELFヘッダを消すために，リンカスクリプト[kernel.ld](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/kernel.ld)を書いています．

```:kernel.ld
ENTRY(main); /* エントリポイントとなる関数を指定 */
OUTPUT_FORMAT("binary"); /* ELFヘッダを消去 */

BASE = 0x00106000; /* カーネルの先頭番地 */

SECTIONS
{
	. = BASE;
	.text :
	{
		main.o(.text)
                ... /* 以下各オブジェクトの.text領域，.rodata領域，.data領域，.bss領域を配置 */
	}
	/DISCARD/:{*(.eh_frame)}          /* 謎の領域が生成されないようにする */
	/DISCARD/:{*(.note.gnu.property)} /* 謎の領域が生成されないようにする */
}
```

先頭でエントリポイントの設定`ENTRY(main);`とELFヘッダの消去`OUTPUT_FORMAT("binary");`を指示し，各オブジェクトの`.text`領域，`.rodata`領域，`.data`領域，`.bss`領域を並べて，最後に謎の領域が生成されないように`/DISCARD/:{*(.eh_frame)}`，`/DISCARD/:{*(.note.gnu.property)}`と書いてあります．

## sprintfとかを自作

自作sprintf関数は[ここ](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/stdio.c)で実装しています．
sprintfの全ての機能を完全に実装しているわけではなく，必要な分だけ実装しています．
ポイントは，ライブラリの手を借りずに可変長引数を実現したことです．
呼び出し規約がcdeclであることを考慮して，アセンブリでスタックからn番目の引数を取得する関数を作りました．
sprintf関数の内部でこの`get_variadic_arg`を呼び出すことで，sprintfに渡されたn番目の引数を取得できます．

```:n番目の引数を取得する関数
				# // get nth arg in variadic arg function
				# // the first arg is 0th
get_variadic_arg:		# unsigned int get_variadic_arg(unsigned int n);
0:
	pushl	%ebp
	movl	%esp,	%ebp
	pushl	%esi
	movl	(%ebp),	%esi
	movl	0x08(%ebp),%edx
	movl	%ss:0x08(%esi,%edx,0x04),%eax
	popl	%esi
	leave
	ret
```

## フォントをOSに埋め込むためのツールを自作

最後に`makefont.exe`に相当するやつを作りました．
フォントのソース`hankaku.txt`を少しいじって[bitmap.txt](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/font/bitmap.txt)を作りました．
さてここからどうやってフォントをOSに組み込むかについてですが，1文字が縦16ピクセル横8ピクセルで，それがchar型で表現できる256種類あるわけで，それを1ピクセル1ビット，文字を16層にスライスしたときの1層が1バイト，1文字16バイト，256種類の文字で計4キビバイトのバイナリにまとめてカーネルにくっつければいいわけです．
ただ，このフォントは文字を表示するときにCのプログラムから参照するわけで，それならばバイナリにせずにC言語で扱えるような形にすれば使いやすくなると思いました．
そこで，[bitmap.txt](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/font/bitmap.txt)をC言語に変換するようなプログラム[translator.c](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/font/translator.c)を作ったわけです．
人生で初めてC言語を出力するC言語を書きました．
変換結果はこんな感じ

```c:fontdata.c
#include "font.h"

CharFont const font[0x100] =
{
	{{0000, 0x3c, 0x24, 0x24, 0x24, 0x24, 0x24, 0x3c, 0000, 0x3c, 0x24, 0x24, 0x24, 0x24, 0x24, 0x3c}},
	{{0000, 0x3c, 0x24, 0x24, 0x24, 0x24, 0x24, 0x3c, 0000, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20}},
	{{0000, 0x3c, 0x24, 0x24, 0x24, 0x24, 0x24, 0x3c, 0000, 0x3c, 0x20, 0x20, 0x3c, 0x04, 0x04, 0x3c}},
...
```

1ビットが1ピクセル，1バイトが文字の1層，1行が1文字のデザインを表していて，それが256行並んでいます．(長いので3行だけ載せてます．)

で，[font.h](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/font.h)からこのフォントバイナリにアクセスできるようにすれば，Cでフォントを扱えるようになるという仕組みです．

```c:font.h
#ifndef _FONT_H_
#define _FONT_H_

#define CHAR_WIDTH 0x08
#define CHAR_HEIGHT 0x10

#define TAB_LENGTH 4

// width 8 pixels
// height 16 pixels
typedef struct
{
	unsigned char row[0x10];
} CharFont;

extern CharFont const font[0x100];

// return value
// 0 means background color should be put at the pixel(x, y) of the character
// 1 means foreground color should be put at the pixel(x, y) of the character
unsigned char get_font_pixel(unsigned char character, unsigned char x, unsigned char y);

#endif
```

## gdbでリアルモードデバッグ(おまけ)

さてこうしてGNUツール群のみを使ってOSを作れるわけですが，オワコンレガシーBIOSを使ったOSはリアルモードで起動するため，開発の初期段階でリアルモードにおける動作をデバッグする必要があります．
QEMUをオプション`-gdb tcp::<gdbと通信するポート番号>`を付けてデバッガ待機状態で起動させ，続いてgdbを起動してgdbコマンド`target remote localhost:<QEMUと通信するポート番号>`を実行することでgdbからQEMU上で動くOSをデバッグできます．
リアルモード部分もこの方法で問題なくデバッグできますが，gdbの`x/i`コマンドでリアルモードの命令列を逆アセンブルする際に注意が必要です．
なんと，リアルモードの命令列であってもプロテクトモードの命令列として逆アセンブルしてしまい，正しいアセンブリを表示できないのです．
解決方法は[ここ](https://stackoverflow.com/questions/32955887/how-to-disassemble-16-bit-x86-boot-sector-code-in-gdb-with-x-i-pc-it-gets-tr)に記載されています．
具体的には，`.gdbinit`と`target.xml`と`i386-32bit.xml`を用意し，これらのファイルを置いた場所でgdbを起動するようにします．

```:.gdbinit
# tcp port
target remote localhost:<QEMUと通信するポート番号>

# real mode
set tdesc filename target.xml
```

```xml:target.xml
<?xml version="1.0"?>
<!DOCTYPE target SYSTEM "gdb-target.dtd">
<target>
 <architecture>i8086</architecture>
 <xi:include href="i386-32bit.xml"/>
</target>
```

`i386-32bit.xml`は[ここ](https://raw.githubusercontent.com/qemu/qemu/master/gdb-xml/i386-32bit.xml)から入手できます．

これら3つのファイルを同じディレクトリに配置し，そのディレクトリからgdbを起動することで，gdbで`x/i`コマンドを実行した際に命令列をリアルモードの命令列として逆アセンブルできるようになります．
ただし，プロテクトモードの命令列もリアルモードの命令列として逆アセンブルしてしまうので，プロテクトモード移行後のデバッグでは`.gdbinit`の中で`target.xml`を読み込んでいる部分をコメントアウトする必要があります．

# まとめ

今回の記事は作成中の自作OSである[hariboslinux](https://github.com/TaiseiIto/hariboslinux)を題材にしたものです．
linux上でビルドできるharibote OSという意味で，linuxではありません．（笑）
30日本でいえばまだ2週目で，これからどんどん実装していこうと思います．
使用するツールやライブラリに関して制限を設けることで，時間はかかっていますがとても勉強になっています．
また，最近はuchan-nosさんの[みかん本](http://zero.osdev.jp/)を参考にレガシーフリーなOSとして[THEOS](https://github.com/TaiseiIto/THEOS)（テオス）の開発にも挑戦してます．
なぜかFAT32のイメージを生成するプログラムから作ろうとしているのですが，こちらもゆっくり進めていく予定です．

# 参考文献

* [30日でできる！OS自作入門](https://book.mynavi.jp/ec/products/detail/id=22078)
* [FATファイルシステムのしくみと操作法](http://elm-chan.org/docs/fat.html)
* [リンカスクリプトの書き方](http://blueeyes.sakura.ne.jp/2018/10/31/1676/)
* [How to disassemble 16-bit x86 boot sector code in GDB with "x/i $pc"? It gets treated as 32-bit](https://stackoverflow.com/questions/32955887/how-to-disassemble-16-bit-x86-boot-sector-code-in-gdb-with-x-i-pc-it-gets-tr)