自作OS(OS5)のUEFI+x86_64対応でやったこと/やっていること(そして-fPIEの謎挙動)
少し早いですが、この記事は「自作OS Advent Calendar 2017」の12/3(日)の記事です。
自作OS(OS5、GitHub)の「UEFI + x86_64」対応で
- やったこと(masterブランチへマージ済の内容)
- やっていること(作業ブランチで作業中の内容)、そして-fPIEの謎挙動
を紹介します。
OS5について
OS5はx86_32のQEMU(qemu-system-i386)を想定したフルスクラッチの自作OSです。CUIのみですが、ブートローダー・カーネル・ユーザーランド(アプリケーション群)を備え、すべて含めて3000行程度のOSです。
「自作OS Advent Calendar 2016」の12/13(火)の記事でOS5を紹介しました。
OS5については、今年、"Ohgami's Commentary on OS5"という「全コード + コメンタリー」の同人誌を作り、技術書典やオープンソースカンファレンス等で頒布しました。
PDF版は公開していますので、興味があれば見てもらえるとうれしいです。
その後、"UEFI + x86_64"の自機で動作させたく、「まずは下回りから」ということで、「UEFI」の勉強をしていました。
OS5で十分なブートローダーを作る程度であれば理解できてきたので、11月頃からOS5の「UEFI + x86_64対応」を行っています。
やったこと(masterブランチへマージ済の内容)
3f0b05dがHEADである現時点で、カーネル・ユーザーランド(アプリケーション群)をRAMへロードし、カーネルの先頭アドレスへジャンプするといった基本的な事はできます。
Makefileも修正済なので、OS5のソースディレクトリ直下で
$ make fs
を実行すると、UEFI+x86_64対応のブートローダーとブートローダーの動作確認程度のカーネル、x86_32のユーザーランド(x86_64対応未着手)の各バイナリが生成されます。
そして、
$ make run64
を実行すると、QEMU上で実行します。QEMUは"qemu-system-x86_64"コマンドを使用します。
また、QEMUにはUEFIファームウェアが入っていないので、加えてOVMFというオープンソースのUEFIファームウェアを使用します。Debian等APTが使用できる環境ならば、"qemu-system-x86"と"ovmf"パッケージをインストールしておいてください。
"make fs"ではfsディレクトリへ各バイナリを生成します。fsディレクトリはEFIシステムパーティションの構成になっているため、USBフラッシュメモリの第1パーティション(FATフォーマット済)へ
$ sudo cp -r fs/* /mnt/to/usb/
のようにコピーすれば、USBフラッシュメモリから起動することで実機で動作させることもできます。(ただし、Lenovo Thinkpad以外未確認)
なお、fsディレクトリの構成は以下の通りです。
fs ├── EFI │ └── BOOT │ └── BOOTX64.EFI ← ブートローダー ├── kernel.bin ← カーネル └── apps.img ← ユーザーランド
ブートローダーはユーザーランド(apps.img)が無い場合はロードをスキップする様に作っています。
そのため、kernel.binという名前で置いておけば任意のファイルをブートローダーにロード・実行させることができます。
ただし、ブートローダーはロードしたカーネルの先頭アドレスへジャンプするので、リンク時にはテキストセクションをバイナリ先頭へ配置し、なおかつエントリ関数をテキストセクション先頭に配置する必要があります。
現在のOS5カーネルでは以下のリンカスクリプトを使用しています。
■ os5/kernel/sys_64.ld
OUTPUT_FORMAT("binary"); SECTIONS { .text : {*(.text)} .rodata : { *(.strings) *(.rodata) *(.rodata.*) } .data : {*(.data)} .bss : {*(.bss)} }
そして、Makefileではオブジェクトファイルのリンク順でカーネル初期化関数がテキストファイル先頭に配置されるようにしています。
■ os5/kernel/Makefile
・・・省略・・・ ifneq ($(x86_64),true) CFLAGS += -m32 else CFLAGS += -fPIE ← (*1) endif LDFLAGS = -Map System.map -s -x ifneq ($(x86_64),true) LDFLAGS += -m elf_i386 -T sys.ld OBJS = sys.o cpu.o intr.o excp.o memory.o sched.o fs.o task.o \ syscall.o lock.o timer.o console_io.o queue.o \ common.o debug.o init.o kern_task_init.o else LDFLAGS += -T sys_64.ld OBJS = init_64.o fb.o ← "init_64.o"を先頭に endif kernel_64.bin: $(OBJS) ld $(LDFLAGS) -o $@ $+ kernel.bin: $(OBJS) ld $(LDFLAGS) -o $@ $+ %.o: %.S gcc $(CFLAGS) -o $@ $< %.o: %.c gcc $(CFLAGS) -o $@ $< ・・・省略・・・
(*1)について、PIEはPosition-Independent Executableの略で位置独立実行形式という意味です。
ブートローダーは、カーネル・ユーザーランドを配置可能なメモリ領域を検索し、見つけた場所へ配置します。
カーネルの実行バイナリがメモリ領域のどこに配置されても良いように、このオプションをつけています。
その後、ブートローダーは以下のようにカーネルへのジャンプを行います。
現状、カーネルのエントリ関数は、ブートローダーの動作確認程度で、以下の通りです。
■ os5/kernel/init_64.c
#include <efi.h> #include <fb.h> int kern_init(struct EFI_SYSTEM_TABLE *st __attribute__ ((unused)), struct fb *_fb) { unsigned int x, y; fb_init(_fb); for (y = 0; y < fb.vr; y++) for (x = 0; x < fb.hr; x++) draw_px(x, y, (x + y) % 256, y % 256, x % 256); while (1); return 0; }
やっていること(作業ブランチで作業中の内容)
作業ブランチはwip/support_uefi_x86_64_2です。(ブランチは移動するのでタグを打っておきました。)
masterブランチでせっかく対応したPIEなのですが、謎の挙動があり、作業ブランチではPIE対応を外しています。
問題は、-fPIEオプションをつけた場合に生成されるアセンブラが間違っているように見受けられる、というものです。
加えて、-fPIEオプションを付けずともPC(RIP)相対なアセンブラが生成され、実行にも今の所、問題無い、ということもあります。
"-fPIE"オプションをつけた場合の問題(謎の挙動)を以降で説明します。
(ただし、正直、コンパイラが間違うなんて事は考えにくく、自分の認識がどこか間違っているのではないかと思ってはいます。)
確認コード
- main.c
extern unsigned int foo; int main(void) { volatile unsigned int bar = foo; while (1); return 0; }
- foo.c
unsigned int foo = 0xbeefcafe;
確認手順
1. main.cとfoo.cをkernel/ディレクトリへ追加
2. 追加したファイルをビルドする様、kernel/Makefileを変更
■ kernel/Makefile(13〜16行目)
else LDFLAGS += -T sys_64.ld OBJS = main.o foo.o # 変更 endif
3. ビルド、実行
$ make run64
4. QEMUモニター[*2]で実行中のアセンブラとレジスタ値等を確認
※ kernel/MakefileのCFLAGSの-fPIEあり/なしそれぞれで3.と4.を行いました。
[*2] 2.6 QEMU Monitor - QEMU version 2.10.92 User Documentation
確認結果
【-fPIEあり】
- kernel/System.mapの抜粋
.data 0x0000000000000050 0x4 *(.data) .data 0x0000000000000050 0x0 main.o .data 0x0000000000000050 0x4 foo.o 0x0000000000000050 foo
- QEMUモニターでの確認結果
(qemu) xp/6i 0x100000 0x0000000000100000: push %rbp 0x0000000000100001: mov %rsp,%rbp 0x0000000000100004: mov 0x45(%rip),%rax # 0x100050 0x000000000010000b: mov (%rax),%eax 0x000000000010000d: mov %eax,-0x4(%rbp) 0x0000000000100010: jmp 0x100010 (qemu) info registers <= (*1) RAX=0000000000000000 RBX=0000000007a43f18 RCX=0000000000400000 RDX=0000000007a43f18 ... (qemu) xp/gx 0x100050 <= (*2) 0000000000100050: 0x00000003beefcafe (qemu) xp/wx 0x3beefcafe <= (*3) 00000003beefcafe: 0x00000000
- 説明
0x100000はmain.cとfoo.cから生成した実行バイナリ(kernel.bin)をブートローダーがロードしたアドレスです。
まず、main.cの
volatile unsigned int bar = foo;
0x000000000010000d: mov %eax,-0x4(%rbp)
ですので、グローバル変数fooの内容0xbeefcafeは、%eaxへ格納されていなければならないです。
しかし、レジスタ値を見てみると(*1)、RAXは全て0になっています。(EAXはRAXの下位32ビット)
%eaxへ値を格納している処理は以下の箇所です。
0x0000000000100004: mov 0x45(%rip),%rax # 0x100050 0x000000000010000b: mov (%rax),%eax
ここで、
0x0000000000100004: mov 0x45(%rip),%rax # 0x100050
は、"0x45(%rip)"のPC(%rip)相対アドレス計算結果のアドレス(0x100050)ではなく、アドレスが指す先を%raxへ格納します。
0x100050の内容を64ビット幅で確認すると(*2)、0x00000003beefcafe という値が格納されており、この値が %rax へ格納されます。
この後、
0x000000000010000b: mov (%rax),%eax
で、%rax(0x00000003beefcafe)が指す先の内容を%eaxへ格納しています。
0x3beefcafe の内容を見てみると(*3)、0が書かれており、
その結果、%eaxへは0が格納されてしまいます。
謎に思うのが、
0x0000000000100004: mov 0x45(%rip),%rax # 0x100050
の箇所です。
0x100050はグローバル変数fooのアドレスなので、
lea 0x45(%rip),%rax
であれば%raxに0x100050が格納され、次の
0x000000000010000b: mov (%rax),%eax
で、アドレス"0x100050(foo)"に格納されている内容が%eaxへ格納されるため、
無事変数fooの内容を%eaxへロードできるのですが、
なぜコンパイラは"lea"命令ではなく"mov"命令をしようしているのか、謎です。
【-fPIEなし】
- kernel/System.mapの抜粋
.data 0x0000000000000048 0x4 *(.data) .data 0x0000000000000048 0x0 main.o .data 0x0000000000000048 0x4 foo.o 0x0000000000000048 foo
- QEMUモニターでの確認結果
(qemu) xp/5i 0x100000 0x0000000000100000: push %rbp 0x0000000000100001: mov %rsp,%rbp 0x0000000000100004: mov 0x3e(%rip),%eax # 0x100048 0x000000000010000a: mov %eax,-0x4(%rbp) 0x000000000010000d: jmp 0x10000d (qemu) info registers RAX=00000000beefcafe RBX=0000000007a43f18 RCX=0000000000400000 RDX=0000000007a43f18 (qemu) xp/wx 0x100048 0000000000100048: 0xbeefcafe
- 説明
"-fPIE"を外していますが、
> 0x0000000000100004: mov 0x3e(%rip),%eax # 0x100048
のようにPC相対で値をロードしています。(こういうものなのか。。?)
この場合、"0x3e(%rip)"のアドレス計算結果0x100048が指す先を%eaxへ
格納しており、%eaxへfooの値 0xbeefcafe が格納されます。