retrage.github.io

EFI stubなLinux kernelのヘッダ部分を見てみる

EFI環境においてLinux kernelの起動方法には ブートローダを用いる方法とEFI stubの2通りがある. EFI stubではbzImageに対してEFI Application相当のヘッダを付加することで EFIから直接kernelを起動する. ここでは,EFI stubなLinux kernelのヘッダが実際に見ることで どのように直接起動できるようにしているかを見ていく. 実際の記事を書いたのが相当前なので,ここではLinux kernel 4.5を対象としている.

最初にCONFIG_EFI_STUB=yとしてビルドしたときのbzImageのhexdumpの一部を以下に示す.

00000000: 4d5a ea07 00c0 078c c88e d88e c08e d031  MZ.............1
00000010: e4fb fcbe 4000 ac20 c074 09b4 0ebb 0700  ....@.. .t......
00000020: cd10 ebf2 31c0 cd16 cd19 eaf0 ff00 f000  ....1...........
00000030: 0000 0000 0000 0000 0000 0000 8200 0000  ................
00000040: 5573 6520 6120 626f 6f74 206c 6f61 6465  Use a boot loade
00000050: 722e 0d0a 0a52 656d 6f76 6520 6469 736b  r....Remove disk
00000060: 2061 6e64 2070 7265 7373 2061 6e79 206b   and press any k
00000070: 6579 2074 6f20 7265 626f 6f74 2e2e 2e0d  ey to reboot....
00000080: 0a00 5045 0000 6486 0400 0000 0000 0000  ..PE..d.........
00000090: 0000 0100 0000 a000 0602 0b02 0214 4020  ..............@ 
000000a0: 5f00 0000 0000 c0ad e800 1040 0000 0002  _..........@....
000000b0: 0000 0000 0000 0000 0000 2000 0000 2000  .......... ... .
000000c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000d0: 0000 00d0 4701 0002 0000 0000 0000 0a00  ....G...........
000000e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000100: 0000 0000 0000 0600 0000 0000 0000 0000  ................
00000110: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000120: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000130: 0000 0000 0000 0000 0000 2e73 6574 7570  ...........setup
00000140: 0000 e03b 0000 0002 0000 e03b 0000 0002  ...;.......;....
00000150: 0000 0000 0000 0000 0000 0000 0000 2000  .............. .
00000160: 5060 2e72 656c 6f63 0000 2000 0000 e03d  P`.reloc.. ....=

EFI ApplicationはPEフォーマットを採用している. PEフォーマットではDOSヘッダが最初にあり,次にPEヘッダが置かれている. それぞれのシグネチャを手がかりに先のhexdumpの結果を見ていく. 最初に0x00に0x4d5a(=“MZ”)があり,DOSヘッダであることを示している. 次に0x3cを見ると,0x0082があり,これがPEヘッダの先頭位置を表している. 0x0082には0x5045(=“PE”)があり.確かにこれがPEヘッダの始まりであることがわかる. 以下、PEヘッダの定義が続くが、ここでは深追いしない. 以上のように,確かにbzImageがEFI Applicationの形式となっていることがわかる.

ところで.bzImageはgccなどの通常のツールチェーンを用いてビルドされるため, 直接PEフォーマットを出力することはできない. 次にどのようにしてPEフォーマットを出力するのかを見ていく. 最初にarch/x86/boot/Makefileの一部を示す.

sed-zoffset := -e 's/^\([0-9a-fA-F]*\) [ABCDGRSTVW] \(startup_32\|startup_64\|efi32_stub_entry\|efi64_stub_entry\|efi_pe_entry\|input_data\|_end\|z_.*\)$$/\#define ZO_\2 0x\1/p'

quiet_cmd_zoffset = ZOFFSET $@
      cmd_zoffset = $(NM) $< | sed -n $(sed-zoffset) > $@

targets += zoffset.h
$(obj)/zoffset.h: $(obj)/compressed/vmlinux FORCE
        $(call if_changed,zoffset)

ここではsedを用いてarch/x86/boot/compressed/vmlinuxのnmの出力結果から エントリポイントとなるシンボルのアドレスをzoffset.hに出力していることがわかる. 次に出力されたzoffset.hの一部を示す.

#define ZO__end 0x0000000000609000
#define ZO_efi64_stub_entry 0x0000000000000390
#define ZO_efi_pe_entry 0x0000000000000210
#define ZO_input_data 0x00000000000003b4
#define ZO_startup_32 0x0000000000000000
#define ZO_startup_64 0x0000000000000200
#define ZO_z_extract_offset 0x0000000000e74000
#define ZO_z_extract_offset_negative 0xffffffffff18c000
#define ZO_z_input_len 0x00000000005e4c23
#define ZO_z_output_len 0x00000000014468b8
#define ZO_z_run_size 0x0000000001553000

ここで特に重要なのはZO_efi_pe_entryである.これは後で利用される. 次にsrc/arch/x86/boot/tools/build.c(以下,build.c)を利用する. これはbzImageを作成するためのツールである. build.cは以下の4つの引数をとる.

  • setup.bin
  • vmlinux.bin
  • zoffset.h
  • 出力先

build.cの処理を実際にみていく. ここではPEヘッダのエントリポイントの設定に注目する. build.cの一部を示す.

static void update_pecoff_text(unsigned int text_start, unsigned int file_sz)
{
        unsigned int pe_header;
        unsigned int text_sz = file_sz - text_start;

        pe_header = get_unaligned_le32(&buf[0x3c]);

        /*
         * Size of code: Subtract the size of the first sector (512 bytes)
         * which includes the header.
         */
        put_unaligned_le32(file_sz - 512, &buf[pe_header + 0x1c]);

        /*
         * Address of entry point for PE/COFF executable
         */
        put_unaligned_le32(text_start + efi_pe_entry, &buf[pe_header + 0x28]);

        update_pecoff_section_header(".text", text_start, text_sz);
}

bufはsetup.binをベースにbzImageの最初の512byteとなる部分である. pe_headerはbufの0x3cにある32bitを取得する.先のバイナリでは0x0082となる. 次にPEヘッダのSize of codeをbufに書き込む.

次にPEヘッダのAddress Of Entry Pointをbufに書き込む. text_startはrelocを考慮したときのsetup.binのサイズであり, efi_pe_entryは先のzoffset.hから得たZO_efi_pe_entryの値,0x0210となる. 書き込む位置は[pe_header + 0x28] = 0x82 + 0x28 = 0xaaとなる. 最初に示したhexdumpの0xaaをみると、0x4010が書き込まれている. ここから,EFIがbzImageを0x4010から実行されることがわかる.

まとめると,EFIはEFI Applicationをロードすると, PEヘッダに記載されているAddress Of Entry Pointで指定されているアドレスから実行する. EFI stubなLinux kernelのbzImageの場合,efi_pe_entryから実行されるため, efi_pe_entryのアドレスがAddress Of Entry Pointに入っている必要がある. build.cはこのアドレスをsetup.bin,vmlinux.binから算出し,埋め込んでいることがわかった.

最後にbzImageの0x4010以降とefi_pe_entryとを比較してみる.

bzImageのhexdumpの結果を示す.

00004010: 4889 0d81 e15e 0048 8915 82e1 5e00 488d  H....^.H....^.H.
00004020: 0573 e15e 0048 8905 64e1 5e00 e800 0000  .s.^.H..d.^.....
00004030: 005d 4881 ed31 0200 0048 012d b0e1 5e00  .]H..1...H.-..^.
00004040: 4889 c7e8 a8a1 5e00 4883 f800 743f 4889  H.....^.H...t?H.
00004050: c648 8d05 a8fd ffff 8986 1402 0000 eb18  .H..............

arch/x86/boot/compressed/head64.oのobjdumpの結果を示す.

0000000000000210 <efi_pe_entry>:
 210:   48 89 0d 00 00 00 00    mov    %rcx,0x0(%rip)        # 217 <efi_pe_entry+0x7>
 217:   48 89 15 00 00 00 00    mov    %rdx,0x0(%rip)        # 21e <efi_pe_entry+0xe>
 21e:   48 8d 05 00 00 00 00    lea    0x0(%rip),%rax        # 225 <efi_pe_entry+0x15>
 225:   48 89 05 00 00 00 00    mov    %rax,0x0(%rip)        # 22c <efi_pe_entry+0x1c>
 22c:   e8 00 00 00 00          callq  231 <efi_pe_entry+0x21>
 231:   5d                      pop    %rbp
 232:   48 81 ed 00 00 00 00    sub    $0x0,%rbp
 239:   48 01 2d 00 00 00 00    add    %rbp,0x0(%rip)        # 240 <efi_pe_entry+0x30>
 240:   48 89 c7                mov    %rax,%rdi
 243:   e8 00 00 00 00          callq  248 <efi_pe_entry+0x38>
 248:   48 83 f8 00             cmp    $0x0,%rax
 24c:   74 3f                   je     28d <fail>
 24e:   48 89 c6                mov    %rax,%rsi
 251:   48 8d 05 00 00 00 00    lea    0x0(%rip),%rax        # 258 <efi_pe_entry+0x48>
 258:   89 86 14 02 00 00       mov    %eax,0x214(%rsi)
 25e:   eb 18                   jmp    278 <handover_entry+0x18>

おまけ

参考までにEDK2でビルドしたHello WorldをUEFI Shell上で出力するEFI Applicationのhexdumpの結果を以下に示す.

00000000: 4d5a 0000 0000 0000 0000 0000 0000 0000  MZ..............
00000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000030: 0000 0000 0000 0000 0000 0000 8000 0000  ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000060: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000070: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000080: 5045 0000 6486 0300 0000 0000 0000 0000  PE..d...........

ここからわかるように,EFIではDOSヘッダについてはSignatureと0x3c位置の部分しか見ないようである. そのため,先のbzImageではこの他の領域をLegacy Bootのために有効活用しているようである.

参考文献