小手先でつくる自作OS – UEFI bootからkernelロードまで

自作 OS Advent Calendar 2017 25 日目の記事です。
メリークリスマス!

自作OSについては昔、「30日でできる! OS自作入門」を読みながら遊んでた時期があります。
時は流れて、今年の最近自作OSをもう一度考えてみようと思い参加いたしました。
でも、今回の記事はひどいです。
プログラムとか全部コピペで自分でまったく作ってません。汗汗汗汗汗

今日は、UEFIからkernelのbootについて書いていこうと思います。
理由は、自作OS入門の書籍はあるけどBIOSな環境でbootという内容が多くUEFIについてあまり触れられてないからです。また、実際に筆者自身がいきなり詰まりました。
やってる内容も少ないので、一緒に手を動かしながらというスタンスで行きます。
自作OSというより環境構築とかUEFIの動かし方的な感じになりました。

◆BIOSとUEFIの違いを簡単に
・BIOS
MBR=第一セクタ(512バイト)はブートセクタとなっており、ブートローダとパーティションテーブルを格納してあります。
そして、ブートローダーからOSを起動します。
ここの部分はアセンブラで書くのが一般的です。

・UEFI
ブートセクタはなく代わりにEFIパーティションをつくりFAT32でフォーマットし、UEFIイメージ(PEバイナリ)を配置します。この時、NVRAMに設定がない場合はデフォルトパスから実行します(x86_64な環境の場合は/efi/boot/bootx64.efi ←UEFIイメージ)
UEFIイメージはいきなりC言語などでかけます。

◆それでは、実際に動かしてみましょう
※筆者の環境がLinuxなのでLinuxな環境を想定しています。
必要なもの
・USBメモリ
インストールするもの
・Qemu
・gnu-efi (gnu-efi-libsとか?)
・ovmf
※恐らくこれらは全てaptやyum、dnfなどのパッケージマネージャでインストールすることができます。筆者の環境はArch Linuxなのでpacmanでインストールを行いました。

# pacman -S qemu
# pacman -S gnu-efi-libs
# pacman -S ovmf

次に、USBメモリを指して好きなようにパーティションを作りましょう。

# gdisk /dev/sdc ※sdcは環境によって変化 
GPT fdisk (gdisk) version 1.0.3

Partition table scan:
  MBR: protective
  BSD: not present
  APM: not present
  GPT: present

Found valid GPT with protective MBR; using GPT.

Command (? for help): d
Using 1

Command (? for help): n       
Partition number (1-128, default 1): 
First sector (34-3948509, default = 34) or {+-}size{KMGTP}: 
Last sector (34-3948509, default = 3948509) or {+-}size{KMGTP}: 512M
Current type is 'Linux filesystem'
Hex code or GUID (L to show codes, Enter = 8300): ef00
Changed type of partition to 'EFI System'
Command (? for help): n
Partition number (2-128, default 2): 
First sector (1048577-3948509, default = 1048578) or {+-}size{KMGTP}: 
Last sector (1048578-3948509, default = 3948509) or {+-}size{KMGTP}: 
Current type is 'Linux filesystem'
Hex code or GUID (L to show codes, Enter = 8300): 
Changed type of partition to 'Linux filesystem'

Command (? for help): w

Final checks complete. About to write GPT data. THIS WILL OVERWRITE EXISTING
PARTITIONS!!

Do you want to proceed? (Y/N): y
OK; writing new GUID partition table (GPT) to /dev/sdc.
Warning: The kernel is still using the old partition table.
The new table will be used at the next reboot or after you
run partprobe(8) or kpartx(8)
The operation has completed successfully.

今回は、512MBをEFIパーティションとしました。ここにUEFIイメージとkernelイメージなどを乗っけます。
第2パーティションはルートパーティションとしました。

まずは、第1パーティションをFAT32でフォーマットします。

# mkfs.vfat /dev/sdc1
mkfs.fat 4.1 (2017-01-24)
mkfs.vfat: /dev/sdc1 contains a mounted filesystem.

ここで、第2パーティションはext2に設定。
理由は基本的なファイルシステムだけど、パーミッション設定ができるから。
後々、ext3、ext4の差分を学習できるからの理由で採用しました。

# mkfs.ext2 /dev/sdc2 
mke2fs 1.43.7 (16-Oct-2017)
/dev/sdc2 contains a ext4 file system
        last mounted on /run/media/fal/57cca2c6-657f-4071-a14d-57f82c1409ff on Sat Dec 23 11:27:28 2017
Proceed anyway? (y,N) y
/dev/sdc2 is mounted; will not make a filesystem here!

これで、USBメモリの設定は終わりです。
あとは、実際のプログラミングに入っていきます。

bootx64.c

#include<efi.h>
#include<efilib.h>

EFI_STATUS
efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
    EFI_DEVICE_PATH *Path;
    EFI_LOADED_IMAGE *LoadedImageParent;
    EFI_LOADED_IMAGE *LoadedImage;
    EFI_HANDLE Image;
    CHAR16 *Options = L"root=/dev/sda2 rootfstype=ext2 rw quiet splash";
    EFI_STATUS Status=EFI_SUCCESS;

    InitializeLib(ImageHandle, SystemTable);

    Status = uefi_call_wrapper(BS->OpenProtocol, 6, ImageHandle, &LoadedImageProtocol, &LoadedImageParent, ImageHandle, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL);
    if (EFI_ERROR(Status)) {
        Print(L"Could not get LoadedImageProtocol handler %r\n", Status);
        return Status;
    }

    Path = FileDevicePath(LoadedImageParent->DeviceHandle, L"\\vmlinuz");
    if (Path == NULL) {
        Print(L"Could not get device path.");
        return EFI_INVALID_PARAMETER;
    }
    
    Status = uefi_call_wrapper(BS->LoadImage, 6, FALSE, ImageHandle, Path, NULL, 0, &Image);
    if (EFI_ERROR(Status)) {
        Print(L"Could not load %r", Status);
        FreePool(Path);
        return Status;
    }

    Status = uefi_call_wrapper(BS->OpenProtocol, 6, Image, &LoadedImageProtocol, &LoadedImage, ImageHandle, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL);
    if (EFI_ERROR(Status)) {
        Print(L"Could not get LoadedImageProtocol handler %r\n", Status);
        uefi_call_wrapper(BS->UnloadImage, 1, Image);
        FreePool(Path);
        return Status;
    }
    LoadedImage->LoadOptions = Options;
    LoadedImage->LoadOptionsSize = (StrLen(LoadedImage->LoadOptions)+1) * sizeof(CHAR16);

    Status = uefi_call_wrapper(BS->StartImage, 3, Image, NULL, NULL);
    uefi_call_wrapper(BS->UnloadImage, 1, Image);
    FreePool(Path);

    return EFI_SUCCESS;
}

Makefile

ARCH            = x86_64
EFIROOT         = /usr
HDDRROOT        = $(EFIROOT)/include/efi
INCLUDES        = -I. -I$(HDDRROOT) -I$(HDDRROOT)/$(ARCH)       -I$(HDDRROOT)/protocol

CRTOBJS         = $(EFIROOT)/lib/crt0-efi-$(ARCH).o
CFLAGS          = -O2 -fPIC -Wall -fshort-wchar -fno-strict-aliasing -fno-merge-constants -mno-red-zone
ifeq ($(ARCH),x86_64)
        CFLAGS += -DEFI_FUNCTION_WRAPPER
endif

CPPFLAGS        = -DCONFIG_$(ARCH)
FORMAT          = efi-app-$(ARCH)
INSTALL         = install
LDFLAGS         = -nostdlib
LDSCRIPT        = $(EFIROOT)/lib/elf_$(ARCH)_efi.lds
LDFLAGS    += -T $(LDSCRIPT) -shared -Bsymbolic -L$(EFIROOT)/lib $(CRTOBJS)
LOADLIBS        = -lefi -lgnuefi $(shell $(CC) -print-libgcc-file-name)

prefix          =
CC                      = $(prefix)gcc
AS                      = $(prefix)as
LD                      = $(prefix)ld
AR                      = $(prefix)ar
RANLIB          = $(prefix)ranlib
OBJCOPY         = $(prefix)objcopy

%.efi: %.so
        $(OBJCOPY) -j .text -j .sdata -j .data -j .dynamic -j .dynsym -j .rel \
                           -j .rela -j .reloc --target=$(FORMAT) $*.so $@

%.so: %.o
        $(LD) $(LDFLAGS) $^ -o $@ $(LOADLIBS)

%.o: %.c
        $(CC) $(INCLUDES) $(CFLAGS) $(CPPFLAGS) -c $< -o $@

TARGETS = bootx64.efi

all: $(TARGETS)

clean:
        rm -f $(TARGETS)

http://orumin.blogspot.jp/2014/12/4.html
http://orumin.blogspot.jp/2014/12/uefi.html
丸パクリです><
説明は、リンクの記事をご覧ください。

makeするとbootx64.efiというファイルができますのでこれをUSBメモリにコピーします。
USBファイルの第1パーティションの中に/efi/bootを作成し、bootx64.efiをコピー

次にkernelをvmlinuzという名前で配置するのですが もう何も作ってないのでLinuxにあるvmlinuz-linuxをvmlinuzにリネームして使うことにします。
USBファイルの第1パーティションの/ にvmlinuzとしてコピーします。
kernel panicが起こること前提で考えてます。

Qemuで動かしましょう。

# qemu-system-x86_64 --bios /usr/share/ovmf/ovmf_code_x64.bin --boot order=d /dev/sdc1

USBメモリからブートすれば自機でも同じようになることが確認できました。

コピペですが・・・
UEFIのおかげでブートの部分は前に比べて楽に実装できることが分かりました。
ドキュメントとwikiのリンクを共有します。
・ドキュメント
http://www.uefi.org/specifications
・phoenix wiki
http://wiki.phoenix.com/wiki/index.php/Main_Page

◆これからどうするの?
kernel部分はすごく面倒くさいイメージがありますが、
まずは、拘り過ぎず、ちまちま作っていくことにします。

今回のイベントに限らず、自作OSの記事は更新し、Twitterで共有いたします。
ツッコミ歓迎!

2 thoughts on “小手先でつくる自作OS – UEFI bootからkernelロードまで”

  1. 第2パーティションのフォーマットのところですが、出力メッセージが
    「/dev/sdc2 is mounted; will not make a filesystem here!」
    とあるように既にマウントされたパーティションのためext2でフォーマットできていないようです。
    上の手順で僕も試して同様のメッセージが出て駄目だったのですが、第2パーティションを一旦アンマウントしてからフォーマットすると無事にフォーマットできました。
    参考まで。

Leave a Reply

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください