「作って理解するOS」を読んで「QEMUで電断できねーじゃんか！」になった人を救いたい

# 前書き

こんにちは．[だいみょーじん](https://twitter.com/Egh2Deywos)です．
最近，「作って理解するOS」(ISBN:9784297108472)(以下，「OS本」という)を読みました．
このOS本は，「30日でできる！OS自作入門」(ISBN:9784839919849)(以下，「30日本」という)よりもう一段ハイレベルな感じで，ページング，浮動小数演算，リアルモードへの移行，電断など，30日本では取り扱われなかった内容について学ぶことができます．
OSの機能の中でも特に難易度が高いと言われる電断ができるようになるのはとても嬉しいことですが，残念ながらBochsでは電断できてもQEMUでは電断できないという状態でOS本の内容は終わってしまっており，私の環境では実機でも電断できないという状態でした．
OS本を読まれた方ならもう分かっていると思いますが，QEMUや実機で電断できない原因はRSDT(Root System Description Table)が見つからないことです．
今回，QEMUや実機でRSDTを見つける方法を調べたので，ここに記事としてまとめることにしました．

# 前提知識

* OS本を読んでいること
* C言語

OS本はアセンブラのみを用いていますが，私がC言語でOSを作っているということと，C言語の方がわかりやすいと思うのでC言語を用いて解説します．
仕組みさえわかればC言語でもアセンブラでも同じことができるでしょう．

# RSDTとは

OS本でも解説されていますが，RSDTはACPI領域を解析する際のスタート地点であり，以下に示すACPIヘッダと，それに続く他のACPIテーブルへのポインタからなります．

```c:ACPIヘッダ
typedef struct _ACPITableHeader
{
        char signature[4];
        unsigned int length;
        unsigned char revision;
        unsigned char checksum;
        char oem_id[6];
        char oem_table_id[8];
        unsigned int oem_revision;
        unsigned int creater_id;
        unsigned int creater_revision;
} __attribute__((packed)) ACPITableHeader;
// RSDTの場合これ以降に他のACPIテーブルへのポインタが並ぶ
```

RSDTのACPIヘッダは`signature`が`"RSDT"`となっています．
`signature`はヌル終端でないことに注意しましょう．
また，`length`はヘッダを含むACPIテーブル全体の長さを意味します．
ヘッダを含むACPIテーブル全体をバイト単位に分割し，全バイトの総和が`0x00`となるように`checksum`が設定されているはずなので，以下のようにACPIテーブルを検証できるような関数を作っておくとよいでしょう．

```c:ACPIテーブル検証関数
bool acpi_table_is_correct(ACPITableHeader const *header)
{
        unsigned char sum = 0;
        for(unsigned char const *byte = (unsigned char const *)header; byte != (unsigned char const *)
        {
                header + header->length; byte++)sum += *byte;
        }
        if(sum == 0)
        {
                return true; // Correct ACPI table
        }
        else
        {
                return false;       // Incorrect ACPI table
        }
}
```

# RSDTを見つける3つの方法

私の知る限り，RSDTを見つける方法は以下3つがあります．

1. BIOSから取得したメモリマップから見つける
1. EBDA(Extended BIOS Data Area)から見つける
1. Main BIOS Areaから見つける．

1つ目の方法はOS本で解説されている方法ですが，これだけだとQEMUや実機ではRSDTを見つけられません．
1つ目の方法で見つからなかった場合は2つ目の方法を試し，それでも見つからなければ3つ目の方法を試すという流れでプログラムを組むことによって，だいたいの環境でRSDTを見つけることができると思います．
1つ目の方法はOS本で解説されている方法なので省略し，ここでは2つ目と3つ目の方法について解説します．

## RSDTを見つける方法その2

RSDTを見つける方法その2とその3は，RSDTへのポインタを保持するRSDPという領域を見つけるということが共通しています．以下にRSDPの構造を示します．

```c:RSDP
typedef struct _RSDP
{
        char signature[8];
        unsigned char checksum;
        char oemid[6];
        unsigned char revision;
        ACPITableHeader const *rsdt; // <= これがRSDTへのポインタ
        unsigned int length;
        unsigned long long xsdt_addr;
        unsigned char extended_checksum;
        unsigned char reserved[3];
} __attribute__((packed)) RSDP;
```

このRSDPが，EBDA(Extended BIOS Data Area)という領域に置かれています．
EBDAは，0x00000400番地に配置されたBDA(BIOS Data Area)から辿ることができます．

```c:BDA
typedef struct _BIOSDataArea
{
	unsigned short com_port_address[0x04];
	unsigned short lpt_port_address[0x03];
	unsigned short ebda_base_address; // <= EBDAのリアルモードにおけるセグメント
	unsigned short equipment_list_flags;
	unsigned char pcjr;
	unsigned short memory_size_before_ebda_kilobytes;
	unsigned char reserved_0;
	unsigned char ps2_bios_control_flags;
	unsigned short keyboard_flags;
	unsigned char storage_for_alternate_keypad_entry;
	unsigned short keyboard_buffer_head;
	unsigned short keyboard_buffer_tail;
	unsigned char keyboard_buffer[0x20];
	unsigned char drive_recalibration_status;
	unsigned char diskette_motor_status;
	unsigned char motor_shutoff_counter;
	unsigned char status_of_last_diskette_operation;
	unsigned char nec_diskette_comtroller_status[7];
	unsigned char current_video_mode;
	unsigned short number_of_screen_columns;
	unsigned short size_of_current_video_regen_buffer;
	unsigned short offset_of_current_vide_page;
	unsigned short cursor_position[8];
	unsigned char cursor_ending_scan_line;
	unsigned char cursor_starting_scan_line;
	unsigned char active_display_page_number;
	unsigned short base_port_address_for_active_crt_controller;
	unsigned char crt_mode_control_register_value;
	unsigned char cga_current_color_palette_mask_setting;
	unsigned int day_counter_0;
	unsigned char reserved_1;
	unsigned int daily_timer_control;
	unsigned char clocl_rollover_flag;
	unsigned char bios_break_flag;
	unsigned short soft_reset_flag;
	unsigned char status_of_last_hard_disk_operation;
	unsigned char number_of_hard_disks_attached;
	unsigned char xt_fixed_disk_drive_control_byte;
	unsigned char port_offset_to_current_fixed_disk_adapter;
	unsigned char time_out_value_for_lpt[4];
	unsigned char time_out_value_for_com[4];
	unsigned short keyboard_buffer_start_offset;
	unsigned short keyboard_buffer_end_offset;
	unsigned char rows_on_the_screen;
	unsigned short point_height_of_character_matrix;
	unsigned char video_mode_options;
	unsigned char ega_feature_bit_switches;
	unsigned char video_display_data_area;
	unsigned char display_combination_code_table_index;
	unsigned char last_diskette_data_rate_selected;
	unsigned char hard_disk_status_returned_by_controller;
	unsigned char hard_disk_error_returned_by_controller;
	unsigned char hard_disk_interrupt_control_flag;
	unsigned char combination_hard_floppy_disk_card;
	unsigned char drive_media_state[4];
	unsigned char track_currently_seeked_to_on_drive[2];
	unsigned char keyboard_mode_type;
	unsigned char keyboard_led_flags;
	unsigned int pointer_to_user_wait_complete_flag;
	unsigned int user_wait_time_out_value;
	unsigned char rtc_wait_function_flag;
	unsigned char lana_dma_channed_flags;
	unsigned char status_of_lana[2];
	unsigned int saved_hard_disk_interrupt_vector;
	void *bios_video_save_override_pointer_table;
	unsigned long long int reserved_2;
	unsigned char keyboard_nmi_control_flags;
	unsigned int keyboard_break_pending_flags;
	unsigned char port_60_single_byte_queue;
	unsigned char scan_code_of_last_key;
	unsigned char nmi_buffer_head_pointer;
	unsigned char nmi_buffer_tail_pointer;
	unsigned char nmi_scan_code_buffer[16];
	unsigned char reserved_3;
	unsigned short day_counter_1;
	unsigned long long int reserved_4[4];
	unsigned char intra_applications_communications_area[16];
	unsigned char print_screen_status_byte;
	unsigned char used_by_basic[3];
	unsigned char dos_single_diskette_mode;
	unsigned char post_word_area[10];
	unsigned char basic_shell_flag;
	unsigned short basics_default_ds_value;
	void *basic_int_1c_interrupt_handler;
	void *basic_int_23_interrupt_handler;
	void *basic_int_24_interrupt_handler;
	unsigned short reserved_5;
	unsigned short dos_dynamic_storage;
	unsigned char dos_diskette_initialization_table[14];
	unsigned int mode_command;
} __attribute__((packed)) BIOSDataArea;
```

かなり大きな構造体ですが，必要なのは3番目の`ebda_base_address`だけです．
これはEBDAのリアルモードにおけるセグメントを表しているので，4ビット左シフトすることでEBDAの先頭の物理アドレスが得られます．

```c:EBDA先頭物理アドレスの取得
#define MEMORY_MAP_BIOS_DATA_AREA       ((void*)0x00000400) // BDA先頭アドレス

BIOSDataArea const * const bios_data_area = MEMORY_MAP_BIOS_DATA_AREA; // BDA領域

BIOSDataArea const *get_bios_data_area(void)
{
        return bios_data_area; // BDA領域を取得
}

void const *get_extended_bios_data_area(void)
{
        return (void const *)((unsigned int)get_bios_data_area()->ebda_base_address << 4); // EBDA領域を取得
}
```

さて，RSDPはEBDA領域の先頭1KiB以内の16バイト境界のどこかに置かれています．
よって，EBDAの先頭1KiB以内を，16バイトごとに探索することになります．
RSDPは以下の条件を満たします．

* RSDPは`signature`が`"RSD PTR "`となります．ヌル終端でないことに注意しましょう．
* `checksum`があり，`rsdt`までの各バイトの総和が`0x00`となります．

これらの条件に従って，与えられたRSDPが正しいものかどうかを検証する関数を作成します．

```c:RSDP検証関数
bool rsdp_is_correct(RSDP const *rsdp)
{
        static char const * const rsdp_signature = "RSD PTR ";
         unsigned char sum = 0;
        if(strncmp(rsdp->signature, rsdp_signature, sizeof(rsdp->signature)))
        {
                return false;
        }
        for(unsigned char const *byte = (unsigned char const *)rsdp; byte != (unsigned char const *)&rsdp->length; byte++)
        {
                sum += *byte;
        }
        if(sum)
        {
                return false;
        }
        return true;
}
```

上記のRSDP検証関数を用い，EBDA領域内のRSDPを探します．

```c:EBDA領域内のRSDPを探す
RSDP const *get_rsdp(void)
{
        void const *extended_bios_data_area = get_extended_bios_data_area();
        for(char const *rsdp_candidate = extended_bios_data_area; (unsigned int)rsdp_candidate < (unsigned int)extended_bios_data_area + 0x00000400; rsdp_candidate += 0x10)
        {
                if(rsdp_is_correct((RSDP const *)rsdp_candidate))
                {
                        return (RSDP const *)rsdp_candidate;
                }
        }
        return NULL;
}
```

RSDPが見つかったら，あとはRSDTの先頭アドレスを取り出すだけです．

```c:RSDTの取得
ACPITableHeader const *get_rsdt_header(void)
{
        ACPITableHeader const *rsdt_header;
        RSDP const *rsdp = get_rsdp();
        if(!rsdp)
        {
                return NULL; // RSDP is not found!
        }
        rsdt_header = rsdp->rsdt;
        if(acpi_table_is_correct(rsdt_header))
        {
                return rsdt_header;
        }
        else return NULL; // RSDT is incorrect!
}
```

## RSDTを見つける方法その3

RSDTを見つける方法その2では，RSDPが見つからず，その結果RSDTも見つからない可能性があります．
その場合，RSDPはMain BIOS Area内の物理アドレス0x000e0000から0x000fffffまでの範囲の16バイト境界にある可能性があります．
この領域の探索も行うRSDP取得関数は，以下のようになります．

```c:Main BIOS Area内の探索も行うRSDP取得関数
RSDP const *get_rsdp(void)
{
        // RSDTを見つける方法その2におけるRSDP探索方法
        // EBDA内を探索する
        void const *extended_bios_data_area = get_extended_bios_data_area();
        for(char const *rsdp_candidate = extended_bios_data_area; (unsigned int)rsdp_candidate < (unsigned int)extended_bios_data_area + 0x00000400; rsdp_candidate += 0x10)
        {
                if(rsdp_is_correct((RSDP const *)rsdp_candidate))
                {
                        return (RSDP const *)rsdp_candidate;
                }
        }
        // RSDTを見つける方法その3におけるRSDP探索方法
        // main BIOS area内を探索する
        for(char const *rsdp_candidate = (char const *)0x000e0000; (unsigned int)rsdp_candidate < 0x00100000; rsdp_candidate += 0x10)
        {
                if(rsdp_is_correct((RSDP const *)rsdp_candidate))
                {
                        return (RSDP const *)rsdp_candidate;
                }
        }
        return NULL; // RSDP is not found.
}
```

一応見つからなかった場合にNULLを返していますが，私の経験ではこれでもRSDTが見つからなかったということはありません．

# 参考文献

* [BDA - BIOS Data Area - PC Memory Map](https://stanislavs.org/helppc/bios_data_area.html)
* [OSDev.org Memory map (x86)](https://wiki.osdev.org/Memory_Map_(x86))
* [OSDev.org RSDP](https://wiki.osdev.org/RSDP)
* [panda's tech note Advent Calendar 2018 Day 14 マルチコア化に向けた準備(ACPI)](https://ja.tech.jar.jp/ac/2018/day14.html)