1.5K Views
September 18, 17
スライド概要
Write your own bootloader that handles ELF binaries using UEFI.
サイボウズ・ラボ株式会社で教育向けのOSやCPU、コンパイラなどの研究開発をしています。
UEFIによる ELFバイナリの起動 @uchan_nos 2017年9月18日,第8回 自作OSもくもく会
このストライドの目標 ELFバイナリ(自作OS)を • Grubなどの既存ブートローダの力を借りず UEFIでブートさせてみたい人が • 実際のブートローダのコードを読めるようになる こと.
ELF : Executable and Linking Format • a.out や COFF とかの仲間 • 現在の Unix, Linux での標準 • オブジェクトファイル,実行可能ファイルどちらもOK • ツールチェーンで良くサポートされている • a.out や COFF に比べ柔軟 • a.out には text, bss, data セクションしかない. C++ のコンストラクタや例外テーブルとかサポートできない. • COFF は a.out の拡張.でも,いろいろ制限がある. • ELF は柔軟で,新しい言語とかのサポートがしやすい
ELF ファイルフォーマット ELFヘッダ • ELFヘッダ、SHT 、PHT 、セクション群からなる • SHT:セクション・ヘッダ・テーブル • PHT:プログラム・ヘッダ・テーブル • セクション:.text, .data, .bss, .shstrtab など • 詳しくは 『リンカ・ローダ実践開発テクニック』坂井弘亮, 2010 PHT セクション SHT
ELF64ヘッダフォーマット ELFヘッダ PHT セクション SHT off field 意味 0x00 e_ident 先頭4バイトは 0x7f, ‘E’, ‘L’, ‘F’ 0x10 e_type ファイルタイプ ET_EXEC, ET_REL など 0x12 e_machine EM_386 など 0x14 e_version ファイルバージョン 0x18 e_entry エントリポイントのアドレス 0x20 e_phoff プログラムヘッダテーブルのファイル位置 0x28 e_shoff セクションヘッダテーブルのファイル位置 0x30 e_flags 未使用 0x34 e_ehsize ELF ヘッダサイズ 0x36 … 0x3e e_shstrndx セクション名格納用セクションの番号
基本となるアイデア • ELFファイルを適当なアドレス (ADDR_FOO)に配置 • 初期化 • .bssを0クリアしたり • グローバル変数のコンストラクタを呼 び出したり • ADDR_FOO+e_entryにジャンプ メモリ ADDR_FOO ELFファイル +e_entry .text
問題:空きメモリはどこにある? • マシンごとにいろいろ違う • アーキテクチャ(x86,ARM) • 搭載メモリ量 • UEFIファームウェアの実装・バージョン • 当然,メモリマップは異なる • メモリマップ:メモリのどこに,何があるか • UEFI Memory Map メモ http://uchan.hateblo.jp/entry/2017/07/18/231528 • お行儀よくするためには • メモリマップを取得して • ELFファイルが乗る大きさの空き領域を探す
メモリマップ実例 使用用途 0 EfiBootServicesCode 1 EfiConventionalMemory 開始アドレス 00000000 00001000 ページ数 1 9F 2 EfiConventionalMemory 3 EfiACPIMemoryNVS 4 EfiConventionalMemory 00100000 00800000 00808000 700 8 8 5 6 7 8 00810000 00818000 00820000 00900000 8 8 E0 A00 EfiACPIMemoryNVS EfiConventionalMemory EfiACPIMemoryNVS EfiBootServicesData … 太字:ExitBootServices()の呼出し後に自由に使えるメモリ領域
実行可能ファイルとアドレス • 一般に,オブジェクトファイルをリンクすると,アドレスが固 定化される • というより,アドレスを固定化する作業=リンク • しかし,空き領域のアドレスは固定でない • →困った!
2つの解決策 • 常に空いてる固定領域があると仮定する • 手持ちのマシンで確認した感じでは, 0x00100000(1MiB)から数MBは空いてるっぽい • 位置非依存の実行可能ファイルを生成する • Position Independent Executable • どこに配置しても動く • clang -fPIE -wl,-pie
解決策:空き領域を仮定 • 手持ちのマシンで確認した感じでは, 0x00100000(1MiB)から数MBは空いてるっぽい • どんなマシンでも空いているかは不明 • 0x00100000に配置する設定でリンク • UEFIが提供する,指定アドレスにメモリを確保するAPIを使い, 0x00100000を先頭とするメモリ領域を割り当てる • 割り当てられなかったらあきらめる
解決策:位置非依存実行可能ファイル • PIEでコンパイル&リンクすると,どこに配置しても動くよう に,相対ジャンプとかを使った機械語になる • どうしても無理なものもあり,リロケーションが必須 • .ctors セクション • .ctors には,初期化関数のアドレスが格納されている • PIEモードではリンク時にアドレスは決まらない • データのアドレス • 文字列リテラルのアドレス,グローバル変数のアドレスなど • 関数じゃないので,相対ジャンプではどうしようもできない
.ctorsについて • ctors = Contructors の略 • dtors = Destructors • コンストラクタとは,変数の初期化用関数のこと • 変数を「組み立てる(construct)」もの • std::vector<int> numbers(128); • こう書くと,コンストラクタが引数128で呼ばれ, 要素数128の整数配列が生成される. • グローバル変数のコンストラクタは メイン関数を実行する前に呼ばないといけない • グローバル変数の初期値をメイン関数実行前に設定するのと同じ理由.
UEFIブートローダー
UEFIとは • Unified Extensible Firmware Interface • Unified:各メーカーで統一された規格 • Extensible:あとあと拡張しやすい規格 • Firmware Interface:ファームウェアとOSのインターフェース • BIOSを置き換えるファームウェアインターフェースの規格 • IntelとHPが開発したものが元となっている. • インターフェースなので,プロセッサ非依存. • 実装はいろいろ.「OVMF」はオープンソース実装 • 今のPCは既にUEFIに置き換わっている • BIOSエミュレーションがある機種もある
BIOS時代のブートローダー • 512バイトのブートレコード • 16ビット実アドレスモード&アセンブラで頑張る • 保護モード,IA32eモードの遷移は自力で. • ※IA32eモード:64ビットモード • ファイルシステムの解析も自力 • USBメモリが使えるかは機種依存
UEFI時代のブートローダー • サイズ制限はない • 最初からIA32eモードで起動する • UEFIが64ビットUEFIの場合. • 最初からC言語で書ける! • FATファイルシステムは標準装備 • USBメモリは標準サポート OVMF搭載 MinnowBoard Turbot
UEFIアプリの自動起動 • UEFIアプリ • UEFIの作法にのっとって作ったアプリ • 中身はPEバイナリ • ブートローダーもUEFIアプリとして作る • 自動起動 • USBメモリをFATでフォーマット • 固定パスにUEFIアプリを配置 • /EFI/BOOT/BOOTX64.EFI
ブートローダーのコード解説 概要 • 参考文献:EDK II で UEFI アプリケーションを作る • http://osdev-jp.readthedocs.io/ja/latest/2017/create-uefi-app-with-edk2.html • ソース:https://github.com/uchan-nos/edk2/tree/bootloader/MyBootLoader • ファイル構成 • • • • • MyBootLoader.dsc:パッケージ記述ファイル MyBootLoader.dec:パッケージ宣言ファイル Loader.inf:モジュール定義ファイル Loader.c:メイン関数を含むソースコード *.c:その他ソースコード
ブートローダーのコード解説 処理の流れ(本質のところだけ) • グラフィックモードを取得(後でカーネルに渡すため) • カーネルファイルを開く • ファイルの大きさだけメモリを確保 • ファイルを読み込む • グローバル変数のコンストラクタを呼ぶ • メモリマップを得る • ブートサービスを抜ける • エントリポイントにジャンプ
グラフィックモードを取得 EFI_STATUS EFIAPI UefiMain(…) { // コンソールを最大サイズにする gST->ConOut->SetMode(gST->ConOut, FindLargestConMode()); // グラフィックモードを取得 struct GraphicMode GraphicMode; GetGraphicMode(ImageHandle, &GraphicMode); • 沢山表示できるように,コンソールサイズを最大にする. • カーネルに渡すためのグラフィックモードを得る. (画面サイズやピクセルフォーマットなど)
カーネルファイルを開く // ルートディレクトリを開く EFI_FILE_PROTOCOL *RootDir = NULL; OpenFileProtocolForThisAppRootDir(ImageHandle, &RootDir); // カーネルファイルを開く UINTN KernelFileSize; EFI_FILE_PROTOCOL *KernelFile; OpenFileForRead(RootDir, L"\\kernel.elf", &KernelFile, &KernelFileSize); • 起動パーティションのルートディレクトリを開く. • ルートディレクトリ直下の kernel.elf を開く. • ファイルサイズが KernelFileSize に書かれる.
ファイルの大きさだけメモリを確保 // 0x100000からメモリを確保 EFI_PHYSICAL_ADDRESS KernelFileAddr = 0x00100000lu; gBS->AllocatePages( AllocateAddress, EfiLoaderData, (KernelFileSize + 4095) / 4096, &KernelFileAddr); • 0x100000を先頭としたメモリ領域を確保. • 確保するメモリタイプはEfiLoaderData • (KernelFileSize + 4095) / 4096 でページ数を計算.
ファイルを読み込む
KernelFile->Read(KernelFile, &KernelFileSize, (VOID*)KernelFileAddr);
// ELFのマジックナンバを確認
Elf64_Ehdr *Ehdr = (Elf64_Ehdr*)KernelFileAddr;
if (AsciiStrnCmp((CHAR8*)Ehdr->e_ident, "\x7f" "ELF", 4) != 0) {
Print(L"Kernel file is not elf.\n");
return EFI_LOAD_ERROR;
}
• 確保したメモリ領域にファイル全体を読み込む
• マジックナンバでEFLファイルであることを確認
グローバル変数のコンストラクタを呼ぶ
typedef void (CtorType)(void);
Elf64_Shdr *CtorsSection = Elf64_FindSection(Ehdr, ".ctors");
for (UINT64 *Ctor = (UINT64*)CtorsSection->sh_addr;
Ctor < (UINT64*)(CtorsSection->sh_addr + CtorsSection->sh_size);
++Ctor)
{
CtorType *F = (CtorType*)*Ctor;
F();
}
• .ctorsセクションの内容は,64ビット整数の配列
• 配列の要素は初期化関数のアドレス
補足:ELFのセクションヘッダ ELFヘッダ ctor0のアドレス PHT ctor1のアドレス セクション セクションの 先頭アドレス ctorNのアドレス SHT .ctors sh_addr sh_size セクションの 大きさ(バイト)
メモリマップを得る // メモリマップを書き込むためのメモリを確保 struct MemoryMap MemoryMap; AllocateMemoryMap(&MemoryMap, 4096); // メモリマップを取得 GetMemoryMap(&MemoryMap); • メモリマップ用には4KiBもあれば十分. • メモリマップの1行につき40バイト程度必要. • 余程のことがなければ100行を超えないだろう…
ブートサービスを抜ける
Status = gBS->ExitBootServices(ImageHandle, MemoryMap.MapKey);
if (EFI_ERROR(Status)) {
Status = GetMemoryMap(&MemoryMap);
if (EFI_ERROR(Status)) { return Status; }
Status = gBS->ExitBootServices(ImageHandle, MemoryMap.MapKey);
if (EFI_ERROR(Status)) { return Status; }
}
• 前回のメモリマップの取得からExitBootServicesの呼び出しの
間にメモリマップが変わると,ExitBootServicesが失敗する
• 失敗したら,再度メモリマップを取得すればOK
エントリポイントにジャンプ // カーネルパラメータを準備 struct BootParam BootParam; … BootParam.graphic_mode = &GraphicMode; // エントリポイントにジャンプ EntryPoint(&BootParam); • カーネルに渡す引数を準備して • エントリポイントにジャンプ