はじめに
こんにちは.だいみょーじんです.
この記事は,自作OS Advent Calendar 2022の12日目の記事です.
対象読者::OS自作をやっていたり,興味がある人.特に30日でできる! OS自作入門(以下「30日本」)を読んだことがある人.
背景
私はharibote OSベースの自作OS hariboslinux を開発しています.
30日本で作成されるharibote OSには起動時にディスクをメモリに読み込み,typeコマンドでファイルを表示する機能がありますが,一方で新たにファイルを作成してディスクに書き込む機能がありません.
そこで今回は hariboslinux にその機能を実装した話を紹介します.
動作
こんな感じでコマンドは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をシャットダウンして,再起動すると,
ls
でファイル一覧を確認すると,test.txt
が残っており,cat test.txt
でファイルの中身を確認すると,ちゃんと保存されていることが分かります.
実装
ここからはファイルの保存を実現する各機能の実装について解説していきます.
リダイレクトによるファイルの生成
リダイレクトはシェルの機能の一つとして実装しました.
shell.hにリダイレクトを表す構造体を定義し,シェル構造体にリダイレクト構造体を持たせています.
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 のシェル上でコマンドを実行する関数execute_command
において,argv
の中に>
と,それに続いてファイル名があればリダイレクトの準備をします.
リダイレクトに関連する部分だけ日本語でコメントしています.
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システムコールが呼び出された場合,そのコマンドが出力をリダイレクトしているかどうかを確認し,リダイレクトされている場合は画面に出力するのではなく,リダイレクト構造体のバッファにバイト列を出力します.
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 でコマンドがExitシステムコールを呼び出して終了し,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 にあります.
リダイレクト構造体のバッファから出力バイト列を取り出し,save_file
関数でファイルに出力しています.
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 に記述されています.
ここではまずルートディレクトリエントリから,今から書き込むファイルと同名のファイルが存在するかどうかを調べます.
同名の既存のファイルがあった場合,そのファイルが変更可能であれば削除して上書きし,変更不能であればエラー出力して書き込みを諦めます.
次にルートディレクトリエントリに新たなファイルの情報を書き込み,ファイルの内容を1クラスタずつ書き込みます.
クラスタを書き込む際にFATの各クラスタのリスト構造の記述も書き換える必要があることと,ディスク全体の各セクタごとに変更されたかどうかを表すフラグを用意することで,ディスクに保存する際に必要最小限のセクタだけを書き込んで処理時間を減らしているのがポイントです.
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 に記述されています.
ルートディレクトリエントリの中の削除対象ファイルの情報を削除し,さらにFATを編集することでそのファイルが使っていたすべてのクラスタを開放します.
最終的にはFATもディスクに書き込むので,FATの書き換え部分のセクタが変更されたというフラグを立てておきます.
// ファイルを削除する
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 に記述されています.
FATは大抵2つ存在するので,その両方にクラスタ間の繋がりを書き込んでおきます.
ここでもセクタの変更フラグを忘れずに立てておきます.
// 第一引数で与えられた番号のクラスタの次に,第二引数で与えられた番号のクラスタが続くということを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 に記述されたwrite_entire_disk
関数を呼び出します.
全てのセクタについて,そのセクタの変更フラグを確認し,変更されている場合はディスクに書き込んで変更フラグをクリアします.
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 に記述されています.
リアルモードのメモリアドレス空間は1MiBしかないため,リアルモードからアクセスできるところにバッファを作っておいて,まずはセクタをそのバッファに書き込んでおきます.
構造体BIOSInterface
はBIOSに渡す引数やBIOSからの返り値を表し,BIOSを呼び出す際の各レジスタの値を設定しておきます.
最後にcall_bios
関数でBIOSを呼び出すことで,セクタがディスクに書き込まれます.
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 で行っています.
// 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 においてアドレス0x7c00
を指し示す関数ポインタ_call_bios
を用意し,関数call_bios
から_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 です.
まずは32ビットプロテクトモードから16ビットプロテクトモードに移行し,再び32ビットプロテクトモードに戻ってくるということをします.
BIOS呼び出し後に元に戻ってこれるように,各種レジスタやGDT,IDTの設定などを保存しています.
コードセグメントの保存は少しトリッキーですが,元のコードセグメントに戻るためのジャンプ命令のセグメントセレクタ部分に元のコードセグメントセレクタを書き込んでおくことで,BIOSを呼び出し終わってこのジャンプ命令を実行することで元のコードセグメントに戻ってこれるということです.
次に,16ビットプロテクトモードのセグメントを含むGDTおよび16ビットリアルモードの割り込みベクタをIDTに設定します.
そして16ビットプロテクトモードのBIOS呼び出し関数call_bios_16
を呼び出しています.
BIOSを呼び出し終わったらGDTとIDTを元に戻し,元のコードセグメントセレクタを書き込んでおいたジャンプ命令を実行することで元のコードセグメントに移行し,各種レジスタも復元し,関数を終了します.
# この関数の先頭がちょうど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ビットプロテクトモードのコードセグメントに復帰します.
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に限らずあらゆるところでこの「問題分解力」を大切にしていきましょう!