自作OSでファイルを保存

# はじめに

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

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

# 背景

私はharibote OSベースの自作OS [hariboslinux](https://github.com/TaiseiIto/hariboslinux) を開発しています．
30日本で作成されるharibote OSには起動時にディスクをメモリに読み込み，typeコマンドでファイルを表示する機能がありますが，一方で新たにファイルを作成してディスクに書き込む機能がありません．
そこで今回は [hariboslinux](https://github.com/TaiseiIto/hariboslinux) にその機能を実装した話を紹介します．

# 動作

![save1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2354177/ecc54f3b-b326-1d16-a314-dfc8ce579c96.png)

こんな感じでコマンドはLinuxに似せています．
`ls`でファイル一覧を出していますが，この時はまだ`test.txt`というファイルは存在していないので，`cat test.txt`しても何も表示されません．
そこで，`echo "Hello, World!" > test.txt`で"Hello, World!"という文字列を`test.txt`というファイルに書き込み（このときまだメモリ上に保存されているだけで，ディスクには保存されない），再度`cat test.txt`すると"Hello, World!"が表示されます．
最後に`savedisk`コマンドを実行すると，`test.txt`がディスクに保存されます．
一旦OSをシャットダウンして，再起動すると，

![save2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2354177/b3ab7da9-3844-9909-c7e1-4439828acb25.png)

`ls`でファイル一覧を確認すると，`test.txt`が残っており，`cat test.txt`でファイルの中身を確認すると，ちゃんと保存されていることが分かります．

# 実装

ここからはファイルの保存を実現する各機能の実装について解説していきます．

## リダイレクトによるファイルの生成

リダイレクトはシェルの機能の一つとして実装しました．
[shell.h](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/shell.h)にリダイレクトを表す構造体を定義し，シェル構造体にリダイレクト構造体を持たせています．

```c:リダイレクト構造体とシェル構造体
typedef struct _Redirection
{
	struct _Shell *shell;          // リダイレクトを行っているシェル
	struct _Task *task;            // リダイレクトを行っているシェルを動かしているタスク
	char *destination_file_name;   // リダイレクト先のファイル名
	struct _ChainString *output;   // リダイレクトされたバイト列のバッファ
	struct _Redirection *previous; // 双方向環状リスト構造におけるひとつ前のリダイレクト
	struct _Redirection *next;     // 双方向環状リスト構造におけるひとつ後のリダイレクト
} Redirection;

typedef struct _Shell
{
	struct _Console *console;
	struct _Queue *event_queue;
	Dictionary *variables;
	Redirection *redirections; // そのシェルで行われているリダイレクト
	struct _Shell *previous;
	struct _Shell *next;
	unsigned char type;
	#define SHELL_TYPE_CONSOLE	0x00
	#define SHELL_TYPE_SERIAL	0x01
	unsigned char flags;
	#define SHELL_FLAG_BUSY		0x01
	#define SHELL_FLAG_EXIT_REQUEST	0x02
} Shell;
```

そして，[shell.c](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/shell.c) のシェル上でコマンドを実行する関数`execute_command`において，`argv`の中に`>`と，それに続いてファイル名があればリダイレクトの準備をします．
リダイレクトに関連する部分だけ日本語でコメントしています．

```c:シェル上でコマンドを実行する関数
void *execute_command(Shell *shell, char const *command)
{
	unsigned int argc;
	char **argv;
	char *com_file_name;
	char *redirection_destination_file_name = NULL;
	void *com_file_binary;
	unsigned int com_file_size;
	unsigned char flags = 0;
	#define EXECUTE_COMMAND_FLAG_BACKGROUND	0x01
	if(shell->flags & SHELL_FLAG_BUSY)return NULL;
	// Create argv.
	argv = create_argv(shell, command); // commandを字句解析し，argvを生成する．
	if(!argv)return NULL;
	// Count argc.
	for(argc = 0; argv[argc]; argc++); // argvに含まれる文字列の個数argcを数える
	// Background flag
	if(!strcmp(argv[argc - 1], "&"))
	{
		flags |= EXECUTE_COMMAND_FLAG_BACKGROUND;
		free(argv[argc - 1]);
		argc--;
	}
	// Redirection
	if(2 < argc)if(!strcmp(argv[argc - 2], ">")) // argvの最後に">"とファイル名がある場合
	{
		redirection_destination_file_name = argv[argc - 1]; // リダイレクト先ファイル名
		free(argv[argc - 2]); // ">"は不要なので捨てる．
		argc -= 2; // ">"とリダイレクト先ファイル名はコマンドに渡すものではないのでargcを調整
	}
	// Load a file specified by argv[0].
	com_file_name = create_format_char_array("%s.com", argv[0]);
	com_file_binary = load_file(com_file_name);
	com_file_size = get_file_information(com_file_name)->size;
	if(com_file_binary) // The com file is found.
	{
		ConsoleEvent *console_event;
		Event new_event;
		// Execute the com file.
		CommandTaskArgument *command_task_argument = malloc(sizeof(*command_task_argument));
		Task *command_task = create_task(flags & EXECUTE_COMMAND_FLAG_BACKGROUND ? &main_task : get_current_task(), (void (*)(void *))command_task_procedure, 0x00010000, TASK_PRIORITY_USER);
		command_task->ldt = malloc(LDT_SIZE * sizeof(*command_task->ldt));
		command_task->task_status_segment.ldtr = alloc_global_segment(command_task->ldt, LDT_SIZE * sizeof(*command_task->ldt), SEGMENT_DESCRIPTOR_LDT);
		command_task_argument->com_file_name = com_file_name;
		command_task_argument->com_file_binary = com_file_binary;
		command_task_argument->com_file_size = com_file_size;
		command_task_argument->argc = argc;
		command_task_argument->argv = argv;
		command_task_argument->shell = flags & EXECUTE_COMMAND_FLAG_BACKGROUND ? serial_shell : shell;
		command_task_argument->task_return = malloc(sizeof(*command_task_argument->task_return));
		command_task_argument->task_return->task_type = TASK_TYPE_COMMAND;
		command_task_argument->task_return->task_return = malloc(sizeof(CommandTaskReturn));
		if(flags & EXECUTE_COMMAND_FLAG_BACKGROUND)switch(shell->type)
		{
		case SHELL_TYPE_CONSOLE:
			// Send prompt event.
			console_event = malloc(sizeof(*console_event));
			console_event->type = CONSOLE_EVENT_TYPE_PROMPT;
			new_event.type = EVENT_TYPE_SHEET_USER_DEFINED;
			new_event.event_union.sheet_user_defined_event.sheet = shell->console->text_box->sheet;
			new_event.event_union.sheet_user_defined_event.procedure = console_event_procedure;
			new_event.event_union.sheet_user_defined_event.any = console_event;
			enqueue(shell->console->text_box->sheet->event_queue, &new_event);
			break;
		case SHELL_TYPE_SERIAL:
			print_serial(prompt);
			break;
		default:
			ERROR(); // Invalid shell type
			break;
		}
		else shell->flags |= SHELL_FLAG_BUSY;
		if(redirection_destination_file_name) // コマンドの出力をリダイレクトする場合
		{
			create_redirection(shell, command_task, redirection_destination_file_name); // リダイレクト構造体を作成し，コマンドを実行するタスクと結び付けておく．
			free(redirection_destination_file_name);
		}
		start_task(command_task, command_task_argument, command_task_argument->task_return, 1); // コマンドを実行する．
	}
	else // The com file is not found.
	{
		ConsoleEvent *console_event;
		Event new_event;
		// Try interpreting the command as a shell variable assignment.
		interpret_shell_variable_assignment(shell, command);
		// Clean up com_file_name and argv.
		free(com_file_name);
		for(unsigned int argv_index = 0; argv_index < argc; argv_index++)free(argv[argv_index]);
		free(argv);
		switch(shell->type)
		{
		case SHELL_TYPE_CONSOLE:
			// Send prompt event.
			console_event = malloc(sizeof(*console_event));
			console_event->type = CONSOLE_EVENT_TYPE_PROMPT;
			new_event.type = EVENT_TYPE_SHEET_USER_DEFINED;
			new_event.event_union.sheet_user_defined_event.sheet = shell->console->text_box->sheet;
			new_event.event_union.sheet_user_defined_event.procedure = console_event_procedure;
			new_event.event_union.sheet_user_defined_event.any = console_event;
			enqueue(shell->console->text_box->sheet->event_queue, &new_event);
			break;
		case SHELL_TYPE_SERIAL:
			print_serial(prompt);
			break;
		default:
			ERROR(); // Invalid shell type
			break;
		}
	}
	return NULL;
}
```

そして，コマンドからWriteシステムコールが呼び出された場合，そのコマンドが出力をリダイレクトしているかどうかを確認し，リダイレクトされている場合は画面に出力するのではなく，リダイレクト構造体のバッファにバイト列を出力します．

```c:Writeシステムコール
int system_call_write(FileDescriptor *file_descriptor, void const *buffer, size_t count)
{
	Task *task = get_current_task();
	unsigned int counter = 0;
	unsigned int application_memory = (unsigned int)((CommandTaskAdditional *)task->additionals)->application_memory;
	SystemCallStatus *system_call_status = get_system_call_status();
	if(file_descriptor->flags & SYSTEM_CALL_OPEN_FLAG_WRITE)
	{
		Shell *shell = get_current_shell();
		switch((unsigned int)file_descriptor)
		{
			Redirection *redirection;
		case STDOUT: // 標準出力に出力する場合
		case STDERR: // 標準エラー出力に出力する場合
			redirection = get_redirection(task); // そのコマンドのタスクと結び付けられているリダイレクト構造体があるかどうか確認
			if(shell)for(void const *reader = buffer; reader != buffer + count; reader++)
			{
				if(redirection)put_char_redirection(redirection, *(char const *)reader); // リダイレクト構造体がある場合，その構造体のバッファにバイト列を出力する．
				else
				{
					Event event;
					event.type = EVENT_TYPE_SHELL_PUT_CHARACTER;
					event.event_union.shell_put_character_event.character = *(char const *)reader;
					event.event_union.shell_put_character_event.shell = shell;
					enqueue(shell->event_queue, &event);
				}
				counter++;
			}
			break;
...
```

そして，[shell.c](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/shell.c) でコマンドがExitシステムコールを呼び出して終了し，OSがアプリケーションの後片付けをする際に，リダイレクト構造体の終了処理を呼び出します．

```c:OSによるアプリケーションの後片付け
void clean_up_command_task(Task *command_task, CommandTaskArgument *command_task_argument)
{
	ConsoleEvent *console_event;
	Event new_event;
	char *return_value = create_format_char_array("%d", ((CommandTaskReturn *)command_task_argument->task_return->task_return)->return_value);
	set_dictionary_element(command_task_argument->shell->variables, "?", return_value);
	free(return_value);
	free(command_task_argument->com_file_binary);
	free(command_task_argument->com_file_name);
	for(unsigned int argv_index = 0; argv_index < command_task_argument->argc; argv_index++)free(command_task_argument->argv[argv_index]);
	free(command_task_argument->argv);
	free(command_task_argument->task_return->task_return);
	delete_redirection(command_task); // ここでリダイレクト構造体の終了処理を呼び出す．
...
```

リダイレクト構造体の終了処理は [shell.c](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/shell.c) にあります．
リダイレクト構造体のバッファから出力バイト列を取り出し，`save_file`関数でファイルに出力しています．

```c:リダイレクト構造体の終了処理
void delete_redirection(Task *command_task)
{
	Redirection *redirection = get_redirection(command_task); // ここで終了済みのコマンドに結び付けられているリダイレクトがあるかどうか確認
	if(redirection) // 終了済みのコマンドに結び付けられているリダイレクトがあった場合
	{
		unsigned char *output = (unsigned char *)create_char_array_from_chain_string(redirection->output); // リダイレクト構造体のバッファから出力バイト列を取り出す．
		save_file(redirection->destination_file_name, output, redirection->output->length); // 出力バイト列をファイルに保存する．
		redirection->previous->next = redirection->next;
		redirection->next->previous = redirection->previous;
		if(redirection->shell->redirections == redirection)redirection->shell->redirections = redirection->next;
		if(redirection->shell->redirections == redirection)redirection->shell->redirections = NULL;
		free(output);
		free(redirection->destination_file_name);
		delete_chain_string(redirection->output);
		free(redirection);
	}
}
```

リダイレクトによって出力されたバイト列をメモリ上のファイルに保存する`save_file`関数は [disk.c](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/disk.c) に記述されています．
ここではまずルートディレクトリエントリから，今から書き込むファイルと同名のファイルが存在するかどうかを調べます．
同名の既存のファイルがあった場合，そのファイルが変更可能であれば削除して上書きし，変更不能であればエラー出力して書き込みを諦めます．
次にルートディレクトリエントリに新たなファイルの情報を書き込み，ファイルの内容を1クラスタずつ書き込みます．
クラスタを書き込む際にFATの各クラスタのリスト構造の記述も書き換える必要があることと，ディスク全体の各セクタごとに変更されたかどうかを表すフラグを用意することで，ディスクに保存する際に必要最小限のセクタだけを書き込んで処理時間を減らしているのがポイントです．

```c:ファイルのメモリへの保存
void save_file(char const *file_name/*ファイル名*/, unsigned char const *content/*ファイルに保存するバイト列*/, unsigned int length/*ファイルに保存するバイト列の長さ*/)
{
	prohibit_switch_task(); // ファイルシステムは他のタスクからもアクセスできる共有オブジェクトなので，タスクスイッチを一時的に禁止することでロックをかけておく．
    // ディレクトリエントリに記述するファイル情報を書き込む場所を取得する
	FileInformation *file_information = get_file_information(file_name);
    // ファイルシステム上にファイルを書き込むのに必要なクラスタ数
	unsigned short number_of_necessary_clusters = (length + cluster_size - 1) / cluster_size;
	unsigned short cluster_number;
	unsigned short next_cluster_number;
	Time time = get_current_time();
	char const *dot = strchr(file_name, '.');
	char const *prefix_begin = file_name;
	char const *prefix_end = dot && (unsigned int)dot - (unsigned int)file_name <= _countof(file_information->name) ? dot : file_name + _countof(file_information->name);
	char const *suffix_begin = dot ? dot + 1 : file_name + _countof(file_information->name);
	char const *suffix_end = strlen(suffix_begin) <= _countof(file_information->extension) ? suffix_begin + strlen(suffix_begin) : suffix_begin + _countof(file_information->extension);
	if(file_information) // 同名のファイルが既に存在する場合
	{
		if(file_information->flags & FILE_INFORMATION_FLAG_READ_ONLY_FILE) // 既存の同名のファイルが読み込み専用の場合
		{
            // エラー出力して諦める
			ERROR(); // There is a read only file with same name.
			return;
		}
        // 既存の同名のファイルが変更できる場合，上書きしてしまう．
		delete_file(file_name);
	}
	else file_information = get_unused_file_information(); // 既存の同名のファイルが存在しない場合，ファイルを新規作成する．
    // ディレクトリエントリに新しいファイル情報を書き込む
	for(char *name = file_information->name; name != file_information->name + _countof(file_information->name); name++)*name = prefix_begin != prefix_end ? *prefix_begin++ : ' ';
	for(char *extension = file_information->extension; extension != file_information->extension + _countof(file_information->extension); extension++)*extension = suffix_begin != suffix_end ? *suffix_begin++ : ' ';
	file_information->flags = FILE_INFORMATION_FLAG_NORMAL_FILE;
	for(char *reserved = file_information->reserved; reserved != file_information->reserved + _countof(file_information->reserved); reserved++)*reserved = 0x00;
	file_information->time = ((unsigned short)time.hour << 11) + ((unsigned short)time.minute << 5) + (unsigned short)time.second / 2;
	file_information->date = ((time.year - 1980) << 9) + ((unsigned short)time.month << 5) + (unsigned short)time.day;
	file_information->cluster_number = get_unused_cluster_number();
	file_information->size = length;
	sector_flags[address2sector_number(file_information)] |= SECTOR_FLAG_CHANGED;
	cluster_number = file_information->cluster_number;
    // 1クラスタずつ書き込む
	for(unsigned short i = 0; i < number_of_necessary_clusters; i++)
	{
		void *cluster_address = get_cluster(cluster_number); // クラスタのアドレスを取得
		memcpy(cluster_address, content, cluster_size); // ファイル内容を書き込む
		sector_flags[address2sector_number(cluster_address)] |= SECTOR_FLAG_CHANGED; // 今書き込んだセクタが変更されたことを表すフラグを立てる．
		content += cluster_size;
		set_next_cluster_number(cluster_number, no_more_clusters); // 今書き込んだクラスタが今のところそのファイルの最後のクラスタであることをFATに書いておく
		if(i < number_of_necessary_clusters - 1){
			next_cluster_number = get_unused_cluster_number(); // 次の空きクラスタを取得する．
			set_next_cluster_number(cluster_number, next_cluster_number);　// FATのクラスタ番号のチェーン構造に新しいクラスタのつながりを書いておく．
			cluster_number = next_cluster_number;
		}
	}
	allow_switch_task(); // タスクスイッチを許可し，ロックを解除する
}
```

ファイル保存の際に同名のファイルが既に存在している場合，そのファイルを一度削除しています．
ファイルを削除する関数`delete_file`は [disk.c](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/disk.c) に記述されています．
ルートディレクトリエントリの中の削除対象ファイルの情報を削除し，さらにFATを編集することでそのファイルが使っていたすべてのクラスタを開放します．
最終的にはFATもディスクに書き込むので，FATの書き換え部分のセクタが変更されたというフラグを立てておきます．

```c:ファイルを削除する関数
// ファイルを削除する
void delete_file(char const *file_name)
{
	FileInformation *file_information = get_file_information(file_name); // ルートディレクトリエントリから，削除対象のファイル情報を探す
	if(!file_information)ERROR(); // ファイルが見つからなかったらエラー
	if(file_information->flags & FILE_INFORMATION_FLAG_READ_ONLY_FILE)ERROR(); // ファイルが読み込み専用の場合もエラー
	// Free the file information.
	file_information->name[0] = '\0'; // 不要になったファイル情報を捨てる
	sector_flags[address2sector_number(file_information)] |= SECTOR_FLAG_CHANGED; // 削除したファイル情報が含まれるセクタの変更フラグを立てておく．
	// 削除されたファイルのクラスタを開放する．
	free_cluster(file_information->cluster_number);
}

// ファイルを削除したことで不要になったクラスタを開放する．
void free_cluster(unsigned short cluster_number)
{
    // FATのクラスタリスト構造から，削除対象クラスタの次に接続されているクラスタを取得する．
	unsigned short next_cluster_number = get_next_cluster_number(cluster_number);
    // 「次のクラスタ」が存在する場合，再帰呼び出しで後続のクラスタを全て開放する．
	if(next_cluster_number != no_more_clusters)free_cluster(next_cluster_number);
    // FAT12は12ビットという中途半端なクラスタ番号によりこんな感じになる
    // 削除対象クラスタに使用可能な空きクラスタのしるしを書き込んでおく．
    // もちろん最終的にはFATもディスクに書き込むので，セクタ変更フラグを立てておく．
	if(cluster_number % 2)
	{
		((unsigned char **)file_allocation_tables)[0][(cluster_number - 1) / 2 * 3 + 2] = 0x00;
		((unsigned char **)file_allocation_tables)[0][(cluster_number - 1) / 2 * 3 + 1] &= 0x0f;
		sector_flags[address2sector_number(&((unsigned char **)file_allocation_tables)[0][(cluster_number - 1) / 2 * 3 + 2])] |= SECTOR_FLAG_CHANGED;
		sector_flags[address2sector_number(&((unsigned char **)file_allocation_tables)[0][(cluster_number - 1) / 2 * 3 + 1])] |= SECTOR_FLAG_CHANGED;
	}
	else
	{
		((unsigned char **)file_allocation_tables)[0][cluster_number / 2 * 3 + 1] &= 0xf0;
		((unsigned char **)file_allocation_tables)[0][cluster_number / 2 * 3] = 0x00;
		sector_flags[address2sector_number(&((unsigned char **)file_allocation_tables)[0][cluster_number / 2 * 3 + 1])] |= SECTOR_FLAG_CHANGED;
		sector_flags[address2sector_number(&((unsigned char **)file_allocation_tables)[0][cluster_number / 2 * 3])] |= SECTOR_FLAG_CHANGED;
	}
}
```

関数`set_next_cluster_number`では，ファイルの保存によって新たに書き込まれたクラスタ間のつながりをFATに書き込んでいます．
これも [disk.c](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/disk.c) に記述されています．
FATは大抵2つ存在するので，その両方にクラスタ間の繋がりを書き込んでおきます．
ここでもセクタの変更フラグを忘れずに立てておきます．

```c:新たに書き込まれたクラスタ間の繋がりのFATへの書き込み
// 第一引数で与えられた番号のクラスタの次に，第二引数で与えられた番号のクラスタが続くということを2つのFATに書き込む．
void set_next_cluster_number(unsigned short cluster_number, unsigned short next_cluster_number)
{
	if(cluster_number % 2)
	{
		((unsigned char **)file_allocation_tables)[0][(cluster_number - 1) / 2 * 3 + 2] = (unsigned char)(next_cluster_number >> 4);
		((unsigned char **)file_allocation_tables)[0][(cluster_number - 1) / 2 * 3 + 1] &= 0x0f;
		((unsigned char **)file_allocation_tables)[0][(cluster_number - 1) / 2 * 3 + 1] += (unsigned char)(next_cluster_number << 4);
		((unsigned char **)file_allocation_tables)[1][(cluster_number - 1) / 2 * 3 + 2] = (unsigned char)(next_cluster_number >> 4);
		((unsigned char **)file_allocation_tables)[1][(cluster_number - 1) / 2 * 3 + 1] &= 0x0f;
		((unsigned char **)file_allocation_tables)[1][(cluster_number - 1) / 2 * 3 + 1] += (unsigned char)(next_cluster_number << 4);
		sector_flags[address2sector_number(&((unsigned char **)file_allocation_tables)[0][(cluster_number - 1) / 2 * 3 + 2])] |= SECTOR_FLAG_CHANGED;
		sector_flags[address2sector_number(&((unsigned char **)file_allocation_tables)[0][(cluster_number - 1) / 2 * 3 + 1])] |= SECTOR_FLAG_CHANGED;
		sector_flags[address2sector_number(&((unsigned char **)file_allocation_tables)[1][(cluster_number - 1) / 2 * 3 + 2])] |= SECTOR_FLAG_CHANGED;
		sector_flags[address2sector_number(&((unsigned char **)file_allocation_tables)[1][(cluster_number - 1) / 2 * 3 + 1])] |= SECTOR_FLAG_CHANGED;
	}
	else
	{
		((unsigned char **)file_allocation_tables)[0][cluster_number / 2 * 3 + 1] &= 0xf0;
		((unsigned char **)file_allocation_tables)[0][cluster_number / 2 * 3 + 1] += (unsigned char)(next_cluster_number >> 8 & 0x0f);
		((unsigned char **)file_allocation_tables)[0][cluster_number / 2 * 3] = (unsigned char)next_cluster_number;
		((unsigned char **)file_allocation_tables)[1][cluster_number / 2 * 3 + 1] &= 0xf0;
		((unsigned char **)file_allocation_tables)[1][cluster_number / 2 * 3 + 1] += (unsigned char)(next_cluster_number >> 8 & 0x0f);
		((unsigned char **)file_allocation_tables)[1][cluster_number / 2 * 3] = (unsigned char)next_cluster_number;
		sector_flags[address2sector_number(&((unsigned char **)file_allocation_tables)[0][cluster_number / 2 * 3 + 1])] |= SECTOR_FLAG_CHANGED;
		sector_flags[address2sector_number(&((unsigned char **)file_allocation_tables)[0][cluster_number / 2 * 3])] |= SECTOR_FLAG_CHANGED;
		sector_flags[address2sector_number(&((unsigned char **)file_allocation_tables)[1][cluster_number / 2 * 3 + 1])] |= SECTOR_FLAG_CHANGED;
		sector_flags[address2sector_number(&((unsigned char **)file_allocation_tables)[1][cluster_number / 2 * 3])] |= SECTOR_FLAG_CHANGED;
	}
}
```

さて，これでメモリ上へのファイルの保存は完了です．
次は変更フラグが立ったセクタをディスクに書き込む必要があります．

## savediskコマンド

変更フラグが立ったセクタのディスクへの書き込みは，`savedisk`コマンドが行います．
`savedisk`コマンドは， [disk.c](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/disk.c) に記述された`write_entire_disk`関数を呼び出します．
全てのセクタについて，そのセクタの変更フラグを確認し，変更されている場合はディスクに書き込んで変更フラグをクリアします．

```c:全ての変更されたセクタをディスクに書き込む関数
void write_entire_disk(void)
{
    // 各シリンダ番号，各ヘッド番号，各セクタ番号について，
	for(unsigned char cylinder = 0; cylinder < boot_sector->number_of_sectors / (boot_sector->number_of_heads * boot_sector->number_of_sectors_per_track); cylinder++)for(unsigned char head = 0; head < boot_sector->number_of_heads; head++)for(unsigned char sector = 1; sector <= boot_sector->number_of_sectors_per_track; sector++)
	{
        // セクタ識別構造体
		SectorSpecifier sector_specifier;
		sector_specifier.cylinder = cylinder;
		sector_specifier.head = head;
		sector_specifier.sector = sector;
        // そのセクタが変更されているならば
		if(sector_flags[sector_specifier2sector_number(sector_specifier)] & SECTOR_FLAG_CHANGED)
		{
            // そのセクタをディスクへ書き込み，
			write_cluster(sector_specifier);
            // 楚のセクタの変更フラグをクリアする．
			sector_flags[sector_specifier2sector_number(sector_specifier)] &= ~SECTOR_FLAG_CHANGED;
		}
		else continue;
	}
}
```

ひとつのセクタをディスクに書き込む関数`write_cluster`は，同じく [disk.c](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/disk.c) に記述されています．
リアルモードのメモリアドレス空間は1MiBしかないため，リアルモードからアクセスできるところにバッファを作っておいて，まずはセクタをそのバッファに書き込んでおきます．
構造体`BIOSInterface`はBIOSに渡す引数やBIOSからの返り値を表し，BIOSを呼び出す際の各レジスタの値を設定しておきます．
最後に`call_bios`関数でBIOSを呼び出すことで，セクタがディスクに書き込まれます．

```c:セクタをディスクに書き込む関数
void write_cluster(SectorSpecifier sector_specifier)
{
    // BIOSに渡す引数を表す構造体
	BIOSInterface input;
    // メモリ上のセクタのアドレス
	unsigned char *source_address = sector_specifier2address(sector_specifier);
    // リアルモードからアクセスできるバッファ
	unsigned char *buffer_address = MEMORY_MAP_BIOS_BUFFER;
    // セクタの内容をバッファにコピー
	memcpy(buffer_address, source_address, boot_sector->sector_size);
    // 書き込む内容が正しいかどうか確認するためのログを出力
	printf_serial("Save cylinder %#04.2x, head %#04.2x, sector %#04.2x, source address %p\n", sector_specifier.cylinder, sector_specifier.head, sector_specifier.sector, source_address);
	for(unsigned char *byte = buffer_address; byte != buffer_address + boot_sector->sector_size; byte++)printf_serial("%02.2x%c", *byte, (unsigned int)(byte+ 1) % 0x10 ? ' ' : '\n');
    // BIOSを呼び出す際の各レジスタの値を設定
	input.ax = 0x0301;
	input.cx = (sector_specifier.cylinder << 8) | sector_specifier.sector;
	input.bx = (unsigned short)((unsigned int)buffer_address);
	input.dx = sector_specifier.head << 8;
	input.si = 0x0000;
	input.di = 0x0000;
	input.bp = 0x0000;
	input.es = (unsigned short)((unsigned int)buffer_address >> 4 & 0x0000f000);
	input.flags = 0x0202;
    // BIOS呼び出し
	call_bios(0x13/*割り込み番号*/, input);
}
```

## プロテクトモードからのBIOS呼び出し

さて，あとはプロテクトモードからBIOSを呼び出す機能です．
BIOSを呼び出すバイナリは`callbios.bin`というファイルですが，リアルモードでアクセスできるアドレス空間はメモリの先頭1MiBのみなので，この`callbios.bin`もその領域に配置する必要があります．
hariboslinuxの場合`0x7c00`に配置しています．
IPLはカーネルが起動してしまえばもう不要なので，上書きしても問題ないわけです．
この配置はカーネルの [main.c](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/main.c) で行っています．

```c:callbios.binの配置
	// Deploy callbios.bin
	char const * const call_bios_bin_name = "callbios.bin";
	unsigned int call_bios_bin_size = get_file_size(call_bios_bin_name);
	void *call_bios_bin = load_file(call_bios_bin_name);
	memcpy(MEMORY_MAP_CALL_BIOS, call_bios_bin, call_bios_bin_size);
	free(call_bios_bin);
```

そして，[bios.c](https://github.com/TaiseiIto/hariboslinux/blob/main/src/kernel/bios.c) においてアドレス`0x7c00`を指し示す関数ポインタ`_call_bios`を用意し，関数`call_bios`から`_call_bios`を呼び出しています．

```c:関数ポインタ_call_biosの呼び出し
// アドレス0x7c00を指し示す関数ポインタ
BIOSInterface *(* const _call_bios)(unsigned char interrupt_number, BIOSInterface *input) = (BIOSInterface *(* const)(unsigned char, BIOSInterface *))MEMORY_MAP_CALL_BIOS;

// プロテクトモードからBIOSを呼び出す関数
BIOSInterface call_bios(unsigned char interrupt_number, BIOSInterface input)
{
	BIOSInterface result;
	switch_polling_serial_mode();
    // 関数ポインタ_call_biosの呼び出し
	result = *_call_bios(interrupt_number, &input);
	switch_interrupt_serial_mode();
	printf_serial("result.ax = %#06.4x\n", result.ax);
	printf_serial("result.cx = %#06.4x\n", result.cx);
	printf_serial("result.bx = %#06.4x\n", result.bx);
	printf_serial("result.dx = %#06.4x\n", result.dx);
	printf_serial("result.flags = %#06.4x\n", result.flags);
	return result;
}
```

さて，ここからアセンブリに入ります！
`0x7c00`番地に配置される`callbios.bin`のソースは [callbios.s](https://github.com/TaiseiIto/hariboslinux/blob/main/src/callbios.s) です．
まずは32ビットプロテクトモードから16ビットプロテクトモードに移行し，再び32ビットプロテクトモードに戻ってくるということをします．
BIOS呼び出し後に元に戻ってこれるように，各種レジスタやGDT，IDTの設定などを保存しています．
コードセグメントの保存は少しトリッキーですが，元のコードセグメントに戻るためのジャンプ命令のセグメントセレクタ部分に元のコードセグメントセレクタを書き込んでおくことで，BIOSを呼び出し終わってこのジャンプ命令を実行することで元のコードセグメントに戻ってこれるということです．
次に，16ビットプロテクトモードのセグメントを含むGDTおよび16ビットリアルモードの割り込みベクタをIDTに設定します．
そして16ビットプロテクトモードのBIOS呼び出し関数`call_bios_16`を呼び出しています．
BIOSを呼び出し終わったらGDTとIDTを元に戻し，元のコードセグメントセレクタを書き込んでおいたジャンプ命令を実行することで元のコードセグメントに移行し，各種レジスタも復元し，関数を終了します．

```:16ビットプロテクトモードへの移行と32ビットプロテクトモードへの復帰
# この関数の先頭がちょうど0x7c00番地になってる
call_bios:			# BIOSInterface *call_bios(unsigned char interrupt_number, BIOSInterface *input, unsigned short task_status_segment_selector);
0:
	pushl	%ebp
	movl	%esp,	%ebp
	pushl	%ebx
	subl	$0x00000004,%esp
(省略)
3:
	pushal				# 汎用レジスタを保存しておく
	movl	%cr0,	%eax
	pushl	%eax			# CR0レジスタも保存しておく
	pushfl				# EFLAGSレジスタも保存しておく
	cli				# 割り込み禁止
	movl	%esp,	(esp_32)	# スタックポインタをメモリに保存
	movw	%cs,	(jmp_2_origin_cs + 0x05)# ジャンプ命令のセグメントセレクタ0xffffを，現在のコードセグメントで上書きしておく
    # 各セグメントセレクタを保存しておく
	movw	%ss,	(ss_32)		# save ss
	movw	%ds,	(ds_32)		# save ds
	movw	%es,	(es_32)		# save es
	movw	%fs,	(fs_32)		# save fs
	movw	%gs,	(gs_32)		# save gs
    # もとのGDTの保存
	sgdt	(gdtr_32)		# save GDT
    # 16ビットプロテクトモードに移行するためのGDTの読み込み
	lgdt	(gdtr_16)		# switch GDT
    # もとのIDTの保存
	sidt	(idtr_32)		# save IDT
    # 16ビットプロテクトモードに移行するためのIDTの読み込み
	lidt	(idtr_16)		# switch IDT
    # 16ビットプロテクトモードに移行し，BIOSを呼び出す
	ljmp	$0x20,	$call_bios_16
return_2_32: # 16ビットモードから32ビットモードへの復帰
0:
	lidt	(idtr_32)		# もとのIDTに戻す
	lgdt	(gdtr_32)		# もとのGDIに戻す
jmp_2_origin_cs:
0:
	ljmp	$0xffff,$origin_cs # コードセグメント0xffffは，元のコードセグメントに書き換わるので，もとのコードセグメントに戻ってこれる．
origin_cs:
0:
    # 各セグメントセレクタを元に戻す
	movw	(ss_32),%ss		# restore ss
	movw	(ds_32),%ds		# restore ds
	movw	(es_32),%es		# restore es
	movw	(fs_32),%fs		# restore fs
	movw	(gs_32),%gs		# restore gs
	movl	(esp_32),%esp		# スタックポインタを元に戻す
	popfl				# EFLAGSを元に戻す
	popl	%eax
	movl	%eax,	%cr0		# CR0を元に戻す
	popal				# 汎用レジスタを元に戻す
(省略)
	addl	$0x00000004,%esp
	popl	%ebx
	leave
	ret　# 呼び出し元に戻る

# GDT
gdt_16:         # 16ビットプロテクトモードのセグメントを含むGDT
				# null segment
				# selector 0x0000 null segment descriptor
	.word	0x0000		#  limit_low
	.word	0x0000		#  base_low
	.byte	0x00		#  base_mid
	.byte	0x00		#  access_right
	.byte	0x00		#  limit_high
	.byte	0x00		#  base_high

				# data segment for 32bit protected mode
				# selector 0x0008 whole memory is readable and writable
				# base	0x00000000
				# limit	0xffffffff
				# access_right 0x409a
	.word	0xffff		#  limit_low
	.word	0x0000		#  base_low
	.byte	0x00		#  base_mid
	.byte	0x92		#  access_right
	.byte	0xcf		#  limit_high
	.byte	0x00		#  base_high

				# code segment for 32bit protected mode
				# selector 0x0010 whole memory is readable and executable
				# base	0x00000000
				# limit	0xffffffff
				# access_right 0x4092
	.word	0xffff		#  limit_low
	.word	0x0000		#  base_low
	.byte	0x00		#  base_mid
	.byte	0x9a		#  access_right
	.byte	0xcf		#  limit_high
	.byte	0x00		#  base_high

				# data segment for 16bit protected mode
				# selector 0x0018 whole memory is readable and writable
				# base	0x00000000
				# limit	0xffffffff
				# access_right 0x409a
	.word	0xffff		#  limit_low
	.word	0x0000		#  base_low
	.byte	0x00		#  base_mid
	.byte	0x92		#  access_right
	.byte	0x0f		#  limit_high
	.byte	0x00		#  base_high

				# code segment for 16bit protected mode
				# selector 0x0020 whole memory is readable and executable
				# base	0x00000000
				# limit	0xffffffff
				# access_right 0x4092
	.word	0xffff		#  limit_low
	.word	0x0000		#  base_low
	.byte	0x00		#  base_mid
	.byte	0x9a		#  access_right
	.byte	0x0f		#  limit_high
	.byte	0x00		#  base_high

gdtr_16: # 16ビットプロテクトモードのセグメントを含むGDTの大きさと場所
	.word	(gdtr_16) - (gdt_16) - 1	# limit of GDT
	.long	gdt_16

gdtr_32: # 元のGDTの大きさと場所を保存しておく場所
	.word	0x0000
	.long	0x00000000

esp_32: # スタックポインタを保存しておく場所
	.long	0x00000000

idtr_16: # 16ビットリアルモードのIDTの大きさと場所
	.word	0x03ff
	.long	0x00000000

idtr_32: # 元のIDTの大きさと場所を保存しておく場所
	.word	0x0000
	.long	0x00000000

# 元のセグメントセレクタをここら辺に保存しておく
ss_32: 
	.word	0x0000
ds_32:
	.word	0x0000
es_32:
	.word	0x0000
fs_32:
	.word	0x0000
gs_32:
	.word	0x0000
```

これで16ビットプロテクトモードへの移行ができるようになりました．
次はさらに16ビットリアルモードへ移行し，BIOSを呼び出し，16ビットプロテクトモードに戻っていくということをしていきます．
16ビットプロテクトモードから16ビットリアルモードに移行するには，`CR0`レジスタの`PE`ビットをクリアします．
次にリアルモード用のスタックフレームを作成し，割り込みコントローラの設定をリアルモード用に変更します．
これでリアルモードにおける割り込みの準備が整ったので，再び割り込みを許可します．
そしてBIOSに渡す引数を各レジスタに設定し，`int`命令でBIOSを呼び出します．
`int`命令の割り込み番号が`0xff`になっていますがこれはダミーの割り込み番号で，実際にはこの`0xff`の部分が`int`命令実行前に指定された割り込み番号に書き換わるので，指定された割り込み番号の処理が実行されることになります．
事が済んだらあとは元に戻るだけです．
モード移行のため再び割り込みを禁止し，割り込みコントローラの設定をプロテクトモードの設定に戻し，リアルモードのスタックフレームを開放し，`CR0`レジスタの`PE`ビットを立ててジャンプ命令でCPUのパイプラインを遮断してプロテクトモードに移行し，32ビットプロテクトモード用のデータセグメントを設定し，最後にジャンプ命令で32ビットプロテクトモードのコードセグメントに復帰します．

```:16ビットリアルモードへの移行とBIOS呼び出し
call_bios_16:
0:
	# 16ビットプロテクトモードのデータセグメントを設定
	movw	$0x0018,%ax
	movw	%ax,	%ss
	movw	%ax,	%ds
	movw	%ax,	%es
	movw	%ax,	%fs
	movw	%ax,	%gs
	# CR0レジスタのPEビットをクリアし，16ビットリアルモードに移行する．
	movl	%cr0,	%eax
	andl	$0x7ffffffe,%eax
	movl	%eax,	%cr0
	jmp	1f # モード移行のためパイプライン実行を一時的に遮断
1:	# 16bit real mode # ここから16ビットリアルモード
	# 16ビットリアルモードのデータセグメントを設定する
	movw	$0x0000,%ax
	movw	%ax,	%ss
	movw	%ax,	%ds
	movw	%ax,	%es
	movw	%ax,	%fs
	movw	%ax,	%gs
	ljmp	$0x0000,$call_bios_16_real # ジャンプ命令でリアルモードのコードセグメントに移行
call_bios_16_real:	# リアルモード用のスタックを設定
0:
	movw	$call_bios - 0x200,%bp
	movw	%bp,	%sp
1:	# リアルモード用のスタックフレームを作成
	pushw	%bp
	movw	%sp,	%bp
	pushw	%bx
	subw	$0x0002,%sp
	movw	%sp,	%bx
(省略)
3:	# 汎用レジスタの保存
	pushaw
6:	# 割り込みコントローラの設定をリアルモード用に変更
	movb	$0x11,	%al
	outb	%al,	$0x0020
	movb	$0x08,	%al
	outb	%al,	$0x0021
	movb	$0x04,	%al
	outb	%al,	$0x0021
	movb	$0x01,	%al
	outb	%al,	$0x0021
	movb	$0x11,	%al
	outb	%al,	$0x00a0
	movb	$0x10,	%al
	outb	%al,	$0x00a1
	movb	$0x02,	%al
	outb	%al,	$0x00a1
	movb	$0x01,	%al
	outb	%al,	$0x00a1
	movb	$0xb8,	%al
	outb	%al,	$0x0021
	movb	$0xbf,	%al
	outb	%al,	$0x00a1
	sti # これでリアルモードにおける割り込みの環境が整ったので，割り込みを許可する
5:	# BIOSに渡す引数を各レジスタに設定
	movb	(interrupt_number),%dl
	movb	%dl,	(call_int + 1) # 割り込み番号をint命令に設定しておく
	movw	(input_ax),%ax
	movw	(input_cx),%cx
	movw	(input_bx),%bx
	movw	(input_dx),%dx
	movw	(input_si),%si
	movw	(input_di),%di
	movw	(input_bp),%bp
	movw	(input_es),%es
call_int:
0:
	int	$0xff # BIOS呼び出し
    # 0xffはダミーの割り込み番号で，この命令の実行時には指定された割り込み番号は書き換わっている．
2:	# BIOSからの返り値を保存しておく
	movw	%ax,	(output_ax)
	movw	%cx,	(output_cx)
	movw	%bx,	(output_bx)
	movw	%dx,	(output_dx)
	pushfw
	popw	%ax
	movw	%ax,	(output_flags)
1:	# 割り込みコントローラの設定を元に戻す
	cli # 割り込み禁止
	movb	$0x11,	%al
	outb	%al,	$0x0020
	movb	$0x20,	%al
	outb	%al,	$0x0021
	movb	$0x04,	%al
	outb	%al,	$0x0021
	movb	$0x01,	%al
	outb	%al,	$0x0021
	movb	$0x11,	%al
	outb	%al,	$0x00a0
	movb	$0x28,	%al
	outb	%al,	$0x00a1
	movb	$0x02,	%al
	outb	%al,	$0x00a1
	movb	$0x01,	%al
	outb	%al,	$0x00a1
	movb	$0xe8,	%al
	outb	%al,	$0x0021
	movb	$0xee,	%al
	outb	%al,	$0x00a1
2:	# restore registers
	popaw # 汎用レジスタを元に戻す
3:	# リアルモード用のスタックフレームを開放
	addw	$0x0002,%sp
	popw	%bx
	leave
4:	# 16ビットプロテクトモードに復帰
	# CR0レジスタのPEビットを立てる
	movl	%cr0,	%eax
	andl	$0x7fffffff,%eax
	orl	$0x00000001,%eax
	movl	%eax,	%cr0
	jmp	5f # モード移行のため，パイプラインを一時的に遮断する
5:  # ここから16ビットプロテクトモード
	# データセグメントを32ビットプロテクトモードのものに戻す
	movw	$0x0008,%ax
	movw	%ax,	%ss
	movw	%ax,	%ds
	movw	%ax,	%es
	movw	%ax,	%fs
	movw	%ax,	%gs
	# ジャンプ命令で32ビットプロテクトモードに戻る
	ljmp	$0x0010,$return_2_32
```

これでファイルをディスクに保存することができます．

# まとめ

いやー長かったですね．
なかなか手順が多くて大変ですが，大きな機能を一気に実装するのではなく，大きな機能を多数の小さな機能に分解し，小さな機能をひとつずつ実装していけばだいたいのことはできます．
自作OSに限らずあらゆるところでこの「問題分解力」を大切にしていきましょう！