---
title: UEFIによるELFバイナリの起動
tags:  #elf #bootloader #uefi #osdev_moku2  
author: [内田公太](https://convert.docswell.com/user/uchan_nos)
site: [Docswell](https://www.docswell.com/)
thumbnail: https://bcdn.docswell.com/page/2EVL2VRYJQ.jpg?width=480
description: Write your own bootloader that handles ELF binaries using UEFI.
published: September 18, 17
canonical: https://convert.docswell.com/s/uchan_nos/Z1J3W4-run-elf-binary-using-uefi
---
# Page. 1

![Page Image](https://bcdn.docswell.com/page/2EVL2VRYJQ.jpg)

UEFIによる
ELFバイナリの起動
@uchan_nos
2017年9月18日，第8回 自作OSもくもく会


# Page. 2

![Page Image](https://bcdn.docswell.com/page/57GXRLMWEL.jpg)

このストライドの目標
ELFバイナリ（自作OS）を
• Grubなどの既存ブートローダの力を借りず
UEFIでブートさせてみたい人が
• 実際のブートローダのコードを読めるようになる
こと．


# Page. 3

![Page Image](https://bcdn.docswell.com/page/4EQ8VYRQJP.jpg)

ELF : Executable and Linking Format
• a.out や COFF とかの仲間
• 現在の Unix, Linux での標準
• オブジェクトファイル，実行可能ファイルどちらもOK
• ツールチェーンで良くサポートされている
• a.out や COFF に比べ柔軟
• a.out には text, bss, data セクションしかない．
C++ のコンストラクタや例外テーブルとかサポートできない．
• COFF は a.out の拡張．でも，いろいろ制限がある．
• ELF は柔軟で，新しい言語とかのサポートがしやすい


# Page. 4

![Page Image](https://bcdn.docswell.com/page/KJ42MW8YE1.jpg)

ELF ファイルフォーマット
ELFヘッダ
• ELFヘッダ、SHT 、PHT 、セクション群からなる
• SHT：セクション・ヘッダ・テーブル
• PHT：プログラム・ヘッダ・テーブル
• セクション：.text, .data, .bss, .shstrtab など
• 詳しくは
『リンカ・ローダ実践開発テクニック』坂井弘亮, 2010
PHT
セクション
SHT


# Page. 5

![Page Image](https://bcdn.docswell.com/page/LE1M8Y2N7G.jpg)

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 セクション名格納用セクションの番号


# Page. 6

![Page Image](https://bcdn.docswell.com/page/GEW3ZG2ME2.jpg)

基本となるアイデア
• ELFファイルを適当なアドレス
（ADDR_FOO）に配置
• 初期化
• .bssを0クリアしたり
• グローバル変数のコンストラクタを呼
び出したり
• ADDR_FOO+e_entryにジャンプ
メモリ
ADDR_FOO
ELFファイル
+e_entry
.text


# Page. 7

![Page Image](https://bcdn.docswell.com/page/47ZV1LDME3.jpg)

問題：空きメモリはどこにある？
• マシンごとにいろいろ違う
• アーキテクチャ（x86，ARM）
• 搭載メモリ量
• UEFIファームウェアの実装・バージョン
• 当然，メモリマップは異なる
• メモリマップ：メモリのどこに，何があるか
• UEFI Memory Map メモ http://uchan.hateblo.jp/entry/2017/07/18/231528
• お行儀よくするためには
• メモリマップを取得して
• ELFファイルが乗る大きさの空き領域を探す


# Page. 8

![Page Image](https://bcdn.docswell.com/page/YJ65LW157V.jpg)

メモリマップ実例
使用用途
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()の呼出し後に自由に使えるメモリ領域


# Page. 9

![Page Image](https://bcdn.docswell.com/page/GJ5Y1M4GJ4.jpg)

実行可能ファイルとアドレス
• 一般に，オブジェクトファイルをリンクすると，アドレスが固
定化される
• というより，アドレスを固定化する作業＝リンク
• しかし，空き領域のアドレスは固定でない
• →困った！


# Page. 10

![Page Image](https://bcdn.docswell.com/page/LE351WGPJ5.jpg)

2つの解決策
• 常に空いてる固定領域があると仮定する
• 手持ちのマシンで確認した感じでは，
0x00100000（1MiB）から数MBは空いてるっぽい
• 位置非依存の実行可能ファイルを生成する
• Position Independent Executable
• どこに配置しても動く
• clang -fPIE -wl,-pie


# Page. 11

![Page Image](https://bcdn.docswell.com/page/8EDDXK13EG.jpg)

解決策：空き領域を仮定
• 手持ちのマシンで確認した感じでは，
0x00100000（1MiB）から数MBは空いてるっぽい
• どんなマシンでも空いているかは不明
• 0x00100000に配置する設定でリンク
• UEFIが提供する，指定アドレスにメモリを確保するAPIを使い，
0x00100000を先頭とするメモリ領域を割り当てる
• 割り当てられなかったらあきらめる


# Page. 12

![Page Image](https://bcdn.docswell.com/page/V7PXPKQPE8.jpg)

解決策：位置非依存実行可能ファイル
• PIEでコンパイル＆リンクすると，どこに配置しても動くよう
に，相対ジャンプとかを使った機械語になる
• どうしても無理なものもあり，リロケーションが必須
• .ctors セクション
• .ctors には，初期化関数のアドレスが格納されている
• PIEモードではリンク時にアドレスは決まらない
• データのアドレス
• 文字列リテラルのアドレス，グローバル変数のアドレスなど
• 関数じゃないので，相対ジャンプではどうしようもできない


# Page. 13

![Page Image](https://bcdn.docswell.com/page/2JVL2V5V7Q.jpg)

.ctorsについて
• ctors = Contructors の略
• dtors = Destructors
• コンストラクタとは，変数の初期化用関数のこと
• 変数を「組み立てる（construct）」もの
• std::vector&lt;int&gt; numbers(128);
• こう書くと，コンストラクタが引数128で呼ばれ，
要素数128の整数配列が生成される．
• グローバル変数のコンストラクタは
メイン関数を実行する前に呼ばないといけない
• グローバル変数の初期値をメイン関数実行前に設定するのと同じ理由．


# Page. 14

![Page Image](https://bcdn.docswell.com/page/5EGXRL61JL.jpg)

UEFIブートローダー


# Page. 15

![Page Image](https://bcdn.docswell.com/page/4JQ8VY4N7P.jpg)

UEFIとは
• Unified Extensible Firmware Interface
• Unified：各メーカーで統一された規格
• Extensible：あとあと拡張しやすい規格
• Firmware Interface：ファームウェアとOSのインターフェース
• BIOSを置き換えるファームウェアインターフェースの規格
• IntelとHPが開発したものが元となっている．
• インターフェースなので，プロセッサ非依存．
• 実装はいろいろ．「OVMF」はオープンソース実装
• 今のPCは既にUEFIに置き換わっている
• BIOSエミュレーションがある機種もある


# Page. 16

![Page Image](https://bcdn.docswell.com/page/K742MWQ3J1.jpg)

BIOS時代のブートローダー
• 512バイトのブートレコード
• 16ビット実アドレスモード＆アセンブラで頑張る
• 保護モード，IA32eモードの遷移は自力で．
• ※IA32eモード：64ビットモード
• ファイルシステムの解析も自力
• USBメモリが使えるかは機種依存


# Page. 17

![Page Image](https://bcdn.docswell.com/page/LJ1M8YPZEG.jpg)

UEFI時代のブートローダー
• サイズ制限はない
• 最初からIA32eモードで起動する
• UEFIが64ビットUEFIの場合．
• 最初からC言語で書ける！
• FATファイルシステムは標準装備
• USBメモリは標準サポート
OVMF搭載
MinnowBoard Turbot


# Page. 18

![Page Image](https://bcdn.docswell.com/page/GJW3ZG26J2.jpg)

UEFIアプリの自動起動
• UEFIアプリ
• UEFIの作法にのっとって作ったアプリ
• 中身はPEバイナリ
• ブートローダーもUEFIアプリとして作る
• 自動起動
• USBメモリをFATでフォーマット
• 固定パスにUEFIアプリを配置
• /EFI/BOOT/BOOTX64.EFI


# Page. 19

![Page Image](https://bcdn.docswell.com/page/4EZV1LDRJ3.jpg)

ブートローダーのコード解説
概要
• 参考文献：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：その他ソースコード


# Page. 20

![Page Image](https://bcdn.docswell.com/page/Y765LW11EV.jpg)

ブートローダーのコード解説
処理の流れ（本質のところだけ）
• グラフィックモードを取得（後でカーネルに渡すため）
• カーネルファイルを開く
• ファイルの大きさだけメモリを確保
• ファイルを読み込む
• グローバル変数のコンストラクタを呼ぶ
• メモリマップを得る
• ブートサービスを抜ける
• エントリポイントにジャンプ


# Page. 21

![Page Image](https://bcdn.docswell.com/page/G75Y1M4L74.jpg)

グラフィックモードを取得
EFI_STATUS EFIAPI UefiMain(…)
{
// コンソールを最大サイズにする
gST-&gt;ConOut-&gt;SetMode(gST-&gt;ConOut, FindLargestConMode());
// グラフィックモードを取得
struct GraphicMode GraphicMode;
GetGraphicMode(ImageHandle, &amp;GraphicMode);
• 沢山表示できるように，コンソールサイズを最大にする．
• カーネルに渡すためのグラフィックモードを得る．
（画面サイズやピクセルフォーマットなど）


# Page. 22

![Page Image](https://bcdn.docswell.com/page/9J2G19V3JR.jpg)

カーネルファイルを開く
// ルートディレクトリを開く
EFI_FILE_PROTOCOL *RootDir = NULL;
OpenFileProtocolForThisAppRootDir(ImageHandle, &amp;RootDir);
// カーネルファイルを開く
UINTN KernelFileSize;
EFI_FILE_PROTOCOL *KernelFile;
OpenFileForRead(RootDir, L&quot;\\kernel.elf&quot;, &amp;KernelFile, &amp;KernelFileSize);
• 起動パーティションのルートディレクトリを開く．
• ルートディレクトリ直下の kernel.elf を開く．
• ファイルサイズが KernelFileSize に書かれる．


# Page. 23

![Page Image](https://bcdn.docswell.com/page/DEYVZ438EM.jpg)

ファイルの大きさだけメモリを確保
// 0x100000からメモリを確保
EFI_PHYSICAL_ADDRESS KernelFileAddr = 0x00100000lu;
gBS-&gt;AllocatePages(
AllocateAddress,
EfiLoaderData,
(KernelFileSize + 4095) / 4096,
&amp;KernelFileAddr);
• 0x100000を先頭としたメモリ領域を確保．
• 確保するメモリタイプはEfiLoaderData
• (KernelFileSize + 4095) / 4096 でページ数を計算．


# Page. 24

![Page Image](https://bcdn.docswell.com/page/VJNM3YV978.jpg)

ファイルを読み込む
KernelFile-&gt;Read(KernelFile, &amp;KernelFileSize, (VOID*)KernelFileAddr);
// ELFのマジックナンバを確認
Elf64_Ehdr *Ehdr = (Elf64_Ehdr*)KernelFileAddr;
if (AsciiStrnCmp((CHAR8*)Ehdr-&gt;e_ident, &quot;\x7f&quot; &quot;ELF&quot;, 4) != 0) {
Print(L&quot;Kernel file is not elf.\n&quot;);
return EFI_LOAD_ERROR;
}
• 確保したメモリ領域にファイル全体を読み込む
• マジックナンバでEFLファイルであることを確認


# Page. 25

![Page Image](https://bcdn.docswell.com/page/YE9N9P63E3.jpg)

グローバル変数のコンストラクタを呼ぶ
typedef void (CtorType)(void);
Elf64_Shdr *CtorsSection = Elf64_FindSection(Ehdr, &quot;.ctors&quot;);
for (UINT64 *Ctor = (UINT64*)CtorsSection-&gt;sh_addr;
Ctor &lt; (UINT64*)(CtorsSection-&gt;sh_addr + CtorsSection-&gt;sh_size);
++Ctor)
{
CtorType *F = (CtorType*)*Ctor;
F();
}
• .ctorsセクションの内容は，64ビット整数の配列
• 配列の要素は初期化関数のアドレス


# Page. 26

![Page Image](https://bcdn.docswell.com/page/GE8K9DZL7D.jpg)

補足：ELFのセクションヘッダ
ELFヘッダ
ctor0のアドレス
PHT
ctor1のアドレス
セクション
セクションの
先頭アドレス
ctorNのアドレス
SHT
.ctors
sh_addr
sh_size
セクションの
大きさ（バイト）


# Page. 27

![Page Image](https://bcdn.docswell.com/page/LELZWMDQJR.jpg)

メモリマップを得る
// メモリマップを書き込むためのメモリを確保
struct MemoryMap MemoryMap;
AllocateMemoryMap(&amp;MemoryMap, 4096);
// メモリマップを取得
GetMemoryMap(&amp;MemoryMap);
• メモリマップ用には4KiBもあれば十分．
• メモリマップの1行につき40バイト程度必要．
• 余程のことがなければ100行を超えないだろう…


# Page. 28

![Page Image](https://bcdn.docswell.com/page/4JMV9YWKJW.jpg)

ブートサービスを抜ける
Status = gBS-&gt;ExitBootServices(ImageHandle, MemoryMap.MapKey);
if (EFI_ERROR(Status)) {
Status = GetMemoryMap(&amp;MemoryMap);
if (EFI_ERROR(Status)) { return Status; }
Status = gBS-&gt;ExitBootServices(ImageHandle, MemoryMap.MapKey);
if (EFI_ERROR(Status)) { return Status; }
}
• 前回のメモリマップの取得からExitBootServicesの呼び出しの
間にメモリマップが変わると，ExitBootServicesが失敗する
• 失敗したら，再度メモリマップを取得すればOK


# Page. 29

![Page Image](https://bcdn.docswell.com/page/PJR2G9Y6E9.jpg)

エントリポイントにジャンプ
// カーネルパラメータを準備
struct BootParam BootParam;
…
BootParam.graphic_mode = &amp;GraphicMode;
// エントリポイントにジャンプ
EntryPoint(&amp;BootParam);
• カーネルに渡す引数を準備して
• エントリポイントにジャンプ


