借り初めのひみつきち

仮ブログです。

ACPI で電源を切るの補足

前回の ACPI 記事が当初の予定より長くなってしまって説明を省略した部分の補足したいと思います。

neriring.hatenablog.jp

FADT

FADT というのは Fixed ACPI Description Table の略で、 ACPI のもっとも基本的で重要なテーブルのひとつで、電源制御などの多くの情報が記述されています。 ACPI ではこのようなテーブルが他にも多数登場します。

FADT を実際に取得するにはまず RSDP (RSD PTR) という構造体を探します。

f:id:neriring16:20190311202535p:plain

RSDP の取得方法は efi_mainの引数の EFI System Tableの中にある ConfigurationTable という配列をスキャンします。
ここには ACPI 以外のファームウェアのテーブルも含まれており ACPI 1.x 系と ACPI 2.x 系でも内容が異なるので、例えば以下のように GUID が一致するかどうか調べながらスキャンします。
なお、 ExitBootServices API を呼び出すと ConfigurationTable の内容は無効になります。ExitBootServices API を呼び出す前に RSDP のポインターを保存しておきましょう。

#define EFI_ACPI_TABLE_GUID \
    {0x8868e871,0xe4f1,0x11d3, {0xbc,0x22,0x00,0x80,0xc7,0x3c,0x88,0x81}}

void* efi_find_config_table(EFI_SYSTEM_TABLE *st, CONST EFI_GUID* guid) {
    for (int i = 0; i < st->NumberOfTableEntries; i++) {
        EFI_CONFIGURATION_TABLE* tab = st->ConfigurationTable + i;
        if (IsEqualGUID(&tab->VendorGuid, guid)) {
            return tab->VendorTable;
        }
    }
    return NULL;
}

EFI_STATUS EFIAPI efi_main(IN EFI_HANDLE image, IN EFI_SYSTEM_TABLE *st) {
    rsdp = efi_find_config_table(st, &efi_acpi_20_table_guid);
}

レガシーBIOSのやり方はここでは省略します。(UEFI の方が簡単です!)

RSDP を取得できたらその中にある RSDT または XSDT のポインターを取得します。 RSDP は以下のような構造になっています。

typedef struct {
    char       signature[8];
    uint8_t    checksum;
    char       oemid[6];
    uint8_t    revision;
    uint32_t   rsdtaddr;
    uint32_t   length;
    uint64_t   xsdtaddr;
    uint8_t    checksum2;
    uint8_t    reserved[3];
} __attribute__((packed)) acpi_rsd_ptr_t;

レガシーBIOSの時代はメモリを広範囲にスキャンする必要があったので signature を確認したあと checksum や revision を確認する必要がありましたが、 UEFI では最初から GUID で ACPI のバージョンを特定しているので細かいチェックは省略して構いません。
rsdtaddr が RSDT のアドレス、 xsdtaddr が XSDT のアドレスとなります。

RSDT というのは Root System Description Table の略で、他のテーブルへのポインターの配列が格納された ACPI のすべてのテーブルの起点となるテーブルです。
XSDT は RSDT を64ビットに拡張したもので、以下のような構造になります。
これ以降のほとんどの ACPI テーブルは先頭に Header という共通構造があります。

typedef struct {
    char        signature[4];
    uint32_t    length;
    int8_t      revision;
    uint8_t     checksum;
    char        oemid[6];
    char        oem_table_id[8];
    uint32_t    oem_rev;
    uint32_t    creator_id;
    uint32_t    creator_rev;
} __attribute__((packed)) acpi_header_t;

typedef struct {
    acpi_header_t   Header;
    uint64_t    Entry[];
} __attribute__((packed)) acpi_xsdt_t;

古い ACPI 1.x の時代は RSDT しかサポートされていませんでしたが、現代の PC は ACPI 2.x になり XSDT をサポートしています。特別な事情がない限り通常は XSDT を使いましょう。

XSDT が見つかったら Entry の中には ACPI Header を持ったテーブルのポインターが並んでいるので、必要なテーブルのシグネチャをチェックしながら探します。

void* acpi_find_table(const char* signature) {
    for (int i = 0; i < n_entries_xsdt; i++) {
        acpi_header_t* entry = (acpi_header_t*)xsdt->Entry[i];
        if (is_equal_signature(entry->signature, signature)) {
            return entry;
        }
    }
    return NULL;
}

fadt = acpi_find_table("FACP");

注意点として Entry の要素数は XSDT では直接定義されておらず、 Header にある Length フィールドからオフセット (36) や要素のサイズ (64bitなので8バイト) を考慮して計算する必要があります。また、 64bit 値の配列ですが直前のヘッダーが 64bit でアラインされていないので注意が必要な場合があります。
なお、 ACPI のテーブルはほとんどが名称とシグネチャが一致しますが、 FADT は歴史的な事情で名称が「FADT」でもシグネチャは「FACP」という異なる名前になるので注意が必要です。

ここまでの手順で FADT が取得できました。

S5 オブジェクト

S5 オブジェクトには SLP_TYPx に出力するべき値が格納されたパッケージになっているというのはすでに書いた通りですが、これを AML にすると「08 5F 53 35 5F 12」 という並びから始まるバイト列になります。 ACPI では名前に「_」(0x5F) を詰めて強引に4文字にするルールがあるようです。
FADT にある DSDT というテーブルに格納されている AML から該当するバイト列パターンを検索します。ほとんどのオブジェクトはもっと複雑なツリー構造になっているのでこのような手段でアクセスできるオブジェクトは限られています。
なお、 0x08 と 「_」 (0x5F) の間に「\」(0x5C) が入ることもあるようです。(筆者はまだ見たことないです)

S5 のパースに必要な AML バイトコードは 0x08 (NameOP) 0x12 (PackageOP) 0x00 (ZeroOP) 0x0A (BytePrefix) の4種類です。

いくつかの S5 オブジェクトの例を見てみましょう。

実機では以下のパターンがよく見られるようです。

08 5F 53 35 5F 12 07 04 0A 07 00 00 00

これを展開すると以下のようになります。

08 (NameOP)
  5F 53 35 5F (名前 "_S5_")
12 (PackageOP)
  07 (以下の長さ)
  04 (要素数)
    0A (BytePrefix)
      07 (値1)
    00 (ZeroOP)
    00 (ZeroOP)
    00 (ZeroOP)

オブジェクト名「S5」で { 0x07, 0x00, 0x00, 0x00 } のパッケージということがわかります。

QEMU では以下のような内容になります。

08 5F 53 35 5F 12 06 04 00 00 00 00

これを展開すると以下のようになります。

08 (NameOP)
  5F 53 35 5F (名前 "_S5_")
12 (PackageOP)
  06 (以下の長さ)
  04 (要素数)
    00 (ZeroOP)
    00 (ZeroOP)
    00 (ZeroOP)
    00 (ZeroOP)

オブジェクト名「S5」で { 0x00, 0x00, 0x00, 0x00 } のパッケージということがわかります。

VirtualBox では以下のような内容になります。

08 5F 53 35 5F 12 06 02 0A 05 0A 05

これを展開すると以下のようになります。

08 (NameOP)
  5F 53 35 5F (名前 "_S5_")
12 (PackageOP)
  06 (以下の長さ)
  02 (要素数)
    0A (BytePrefix)
      05 (値1)
    0A (BytePrefix)
      05 (値2)

オブジェクト名「S5」で { 0x05, 0x05 } のパッケージということがわかります。
なぜか要素数が2になっています。

このように環境によってたった3ビットの SLP_TYPx の値は異なることがわかります。

tl;dr

たったふたつの OUT 命令にたどり着くまでにはこんなに色々あるのでした。