M1 Macでgnu-efiをビルドしてHello worldする

「ゼロからのOS自作入門」を読み進めているものの、UEFIアプリをビルドするために紹介されているEDK2はclangバージョンなどの都合でM1 Macで動作させることが難しかったことと、ビルド処理のブラックボックスが多いのが少し気にかかったので、gnu-efiというもう少しシンプルなライブラリを使ってやってみることにした。

とりあえずHello worldを表示させるまでで、本の内容が全部できるかどうかは今後。

準備

まずは、x86_64用のbinutilsをインストールする。しかし、macportsのx86_64-elf-binutilsのデフォルト設定ではobjcopyでefi向けの変換に対応していない。このため、macportsでソースをダウンロードしたあと、ビルド設定を手動編集してビルドを行った上、インストールする必要がある。とはいえ、macportsはこのような操作を行うことが考慮された設計になっている。

# 既にインストールを行ってしまった場合は、アンインストールしておく
sudo port uninstall x86_64-elf-binutils

# ソースのダウンロードを行う
sudo port patch x86_64-elf-binutils

# ソースディレクトリに移動
cd $(port dir x86_64-elf-binutils)

# Portfileを編集
vi Portfile

configure.args-appendがconfigureスクリプトに対する引数になるため、以下の記述を行末に追記する。

--enable-targets=all

インストールを行う。-sをつけないとビルドせずサーバーから持ってきたバイナリをインストールしようとするので注意。

sudo port -s -v install x86_64-elf-binutils

macportsで、gccをインストールする。gccはクロスコンパイル用(x86_64)と、ホスト用をインストールする。

sudo port selfupdate
sudo port install gcc10
sudo port install x86_64-elf-gcc # ← エラーになる

上記で、x86_64-elf-gccをインストールしようとするとエラーになる。M1でgccをビルドしようとするといつも出てくるhosthooksの修正が必要。Macportsの場合にはダウンロードされたソースファイルにパッチを当てる(編集する)ことが可能なので、それを行う。

cd $(port dir x86_64-elf-gcc)

gcc/config/host-darwin.c を探し、以下の編集を行う。簡単なのでpatchを作らずにて編集で行ってしまう。

#include行以下に以下の記述を行う。

#include "hosthooks.h"
#include "hosthooks-def.h"

最終行以下に次の記載を行う。

const struct host_hooks host_hooks = HOST_HOOKS_INITIALIZER;

参考: https://zenn.dev/tunefs/articles/80aa34dcce386e

終わったら、再度portのinstallを試みる。これで正常終了するはず。

sudo port install x86_64-elf-gcc

gnu-efiのビルド

git clone https://git.code.sf.net/p/gnu-efi/code gnu-efi
cd gnu-efi
# 

Make.defaultsに、macportsで入れたクロスコンパイラをセットする。対応する設定値を以下のように書き換える。CROSS_COMPILEのみ追加する(INSTALLの下など)。

PREFIX    := /opt/local/bin/
LIBDIR    := /opt/local/x86_64-elf/lib
HOSTCC    := $(prefix)gcc-mp-10

CROSS_COMPILE    := x86_64-elf-

ここまで来たら、makeでライブラリが生成されるはずです。

make

あとは、以下のサイトを参考にHello worldしてみます。私は以下のようなMakefileを用意しました。(gnu_efiのディレクトリは各自置き換えてください。)

https://wiki.osdev.org/GNU-EFI

PROGRAM     =main
CC          =/opt/local/bin/x86_64-elf-gcc
LD          =/opt/local/bin/x86_64-elf-ld
OBJCOPY     =/opt/local/bin/x86_64-elf-objcopy
SRCS        =main.c
OBJS        =$(SRCS:%.c=%.o)
GNU_EFI_DIR =/path/to/gnu-efi

INCLUDE_DIRS =-I$(GNU_EFI_DIR)/inc
CFLAGS      =-fpic -ffreestanding -fno-stack-protector -fno-stack-check -fshort-wchar -mno-red-zone -maccumulate-outgoing-args
LIBS        =-lgnuefi -lefi
LDFLAGS     =-shared -Bsymbolic -L$(GNU_EFI_DIR)/x86_64/lib -L$(GNU_EFI_DIR)/x86_64/gnuefi -T$(GNU_EFI_DIR)/gnuefi/elf_x86_64_efi.lds
OBJCOPYFLAGS = -j .text -j .sdata -j .data -j .dynamic -j .dynsym  -j .rel -j .rela -j .rel.* -j .rela.* -j .reloc --target efi-app-x86_64 --subsystem=10


all: $(PROGRAM).efi

$(OBJS): $(SRCS)
	$(CC) $(INCLUDE_DIRS) $(CFLAGS) -c $(SRCS)

$(PROGRAM).so: $(OBJS)
	$(LD) $(LDFLAGS) $(OBJS) $(GNU_EFI_DIR)/x86_64/gnuefi/crt0-efi-x86_64.o -o $(PROGRAM).so $(LIBS)

$(PROGRAM).efi: $(PROGRAM).so
	$(OBJCOPY) $(OBJCOPYFLAGS) $(PROGRAM).so $(PROGRAM).efi

clean:
	rm $(OBJS) $(PROGRAM).so $(PROGRAM).efi

main.cは、以下のようなものを用意します。上記サイトに書いてあったものを参考に、whileループだけ追加しています。こうしておかないとqemu で一瞬で実行が終了してしまい、文字の出力が見えなくなります。

#include <efi.h>
#include <efilib.h>
 
EFI_STATUS
EFIAPI
efi_main (EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
  InitializeLib(ImageHandle, SystemTable);
  Print(L"Hello, world!\n");
  while(1);
  return EFI_SUCCESS;
}

makeでmain.efiが生成されるはずです。

ちなみに、ホストはgccでなくても(Macデフォルトのclangへのリンクになっているgccでも)いけました。その場合はHOSTCCを/usr/bin/gccとします。