はじめに
こんにちは.だいみょーじんです.
この記事は,第46回自作OSもくもく会で発表した内容をまとめ,自作OSアドベントカレンダー2025の22日目の記事として公開したものです.
この記事では,GNU makeの基本から,HeliOSという自作OSのビルドなどに使っているいくつかの小技までを紹介しています.
Make基本編
まずはMakeの一般的な使い方を紹介します.
Makeとは?
一言でいうとビルド自動化ツールです.
ビルド手順をMakefileというファイルに記述しておくと,makeコマンドでビルドできます.
commandを実行することによってsourceからtargetを生成するとしましょう.
これは,Makefileで以下のように記述できます.
target: source
command
より具体的に,ccでhello.cをコンパイルしてa.outを生成したいとき,
これはMakefileで以下のように記述できます.
a.out: hello.c
cc hello.c
ひとつのターゲットに対して,ソースやコマンドを複数指定することも可能です.
これはMakefileで以下のように記述できます.
target: source0 source1 source2
command0
command1
command2
このようなルールの集まりをMakefileに記述しておくと,コマンドmake <target>でターゲットを生成できます.
また,コマンドmakeで,Makefileの一番上に記述されたターゲットを生成します.
Makeの利点
大規模な依存関係の解決
OSのように比較的大規模なものになってくると,ソースから最終生成物にいたるまでの依存関係が複雑になります.
この複雑な依存関係をMakefileに全て書いておけば,makeコマンド一発でOSをビルドできます.
依存関係に基づいた最小限のビルド
makeコマンドは,ソースが変更されていない場合はターゲットを再生成しません.
例えば下の図のように,一番上のソースだけを書き換えた場合は,図の左上と右のコマンドのみが実行されます.
必要最小限のコマンドだけを呼び出すことにより,ビルド時間の短縮に繋がります.
階層化
以下の図のように,子ディレクトリのソースコードから中間生成物を生成し,その中間生成物と親ディレクトリのソースコードから最終生成物を生成することを考えましょう.
この場合,まず子ディレクトリのMakefileで,子ディレクトリのソースコードから中間生成物を生成するというルールを書いておきます.
次に,親ディレクトリのMakefileで,子ディレクトリのソースコードからコマンドmake -C 子ディレクトリで子ディレクトリのMakefileを呼び出すことにより中間生成物を生成するというルールを書いておきます.
さらに親ディレクトリのソースコードと中間生成物から最終生成物を生成するルールも親ディレクトリのMakefileに書いておきます.
このように,makeコマンドの-Cオプションで別のディレクトリのMakefileを呼び出す機能を使うことで,Makefileをディレクトリ構造に合わせて階層化することができます.
マクロ
Makefileでは,マクロ名=文字列でマクロを定義できます.
Makefile内の$(マクロ名)が,定義された文字列に展開されます.
例えば,hello.cをccでコンパイルしてhelloを生成するルール
hello: hello.c
cc hello.c -o hello
は,マクロを使ってこのように書き換えられます.
SOURCE=hello.c
TARGET=hello
$(TARGET): $(SOURCE)
cc $(SOURCE) -o $(TARGET)
また,以下のような暗黙的に定義済みであるマクロがあります.
-
$$は$ -
$@はターゲット名 -
$<は最初のソース名 -
$^は全てのソース名
これらを使うと,上のルールは
SOURCE=hello.c
TARGET=hello
$(TARGET): $(SOURCE)
cc $^ -o $@
と書き換えられます.
関数
マクロをさらに発展させた関数の機能もあります.
$(関数名 第1引数, 第2引数,...)という書き方で関数を呼び出せます.
既存の関数
以下のような関数が事前に用意されています.
-
$(addprefix dir/, a b c)→dir/a dir/b dir/c -
$(addsuffix .c, a b c)→a.c b.c c.c -
$(basename a.c b.c c.c)→a b c -
$(dir /a/b/c /d/e/f)→/a/b/ /d/e/ -
$(notdir /a/b/c /d/e/f)→c f -
$(abspath paths)→絶対パスに展開 -
$(realpath paths)→シンボリックリンクの場合,参照先のパスに展開 -
$(wildcard *.c)→カレントディレクトリ直下の全ての.cファイル名の列に展開 -
$(shell command)→commandを実行し,その標準出力に展開
shell関数を実行するためのシェルは,SHELL=/bin/bashのようにSHELLマクロで指定できます.
この関数の機能を使うと,先程のルールは以下のように書き換えられます.
SOURCE=hello.c
TARGET=$(basename $(SOURCE))
$(TARGET): $(SOURCE)
cc $^ -o $@
自作の関数
関数を自作することもできます.
通常のマクロと同様の書き方で定義します.
第n引数は,$nと書きます.
自作関数を呼び出すときは,$(call 関数名, 第1引数, 第2引数, ...)と書きます.
例えば,MY_FUNC=hello $1という関数を定義すると,$(call MY_FUNC, world)はhello worldに展開されます.
拡張子に基づいたルール
これは以下の2つの機能により実現されます.
拡張子を変更する特殊なマクロ展開
例えば,FILES=a.x b.x c.xと定義されたマクロを,$(FILES:.x=.y)と呼び出すと,a.y b.y c.yに展開されます.
拡張子を変更したものを生成するルール
拡張子.xのソースから拡張子.yのターゲットを生成するルールは,具体的なファイルを個別に指定せずとも抽象的に%.y: %.xと書けます.
拡張子に基づいたルールの具体例
上の2つの機能を使うと,複数のアセンブリファイル*.sからオブジェクトファイル*.oを生成するルールを以下のようにまとめて書けます.
# ソースの列挙
SOURCES=$(wildcard *.s)
# ターゲットの列挙
OBJECTS=$(SOURCES:.s=.o)
# ターゲットからソースへの依存関係のみを記述
# 生成コマンドが書かれていないので,makeは該当するルールを探す
$(OBJECTS): $(@:.o=.s)
# 生成コマンドはこちらに書く
%.o: %.s
gcc $^ -c -nostdlib -Wall -Wextra -o $@
Make応用編
ここからは,私が開発しているHeliOSというOSのビルドで実際に使っている小技をいくつか紹介していきます.
小技その1:依存先の列挙
大規模なプロジェクトは大量のソースファイルを持っています.
それらのソースファイルを全部Makefileに書くのは大変です.
プロジェクトをGitでバージョン管理している場合,Gitを使ってこの問題を解決できます.
Gitはソースファイルのみを管理し,生成物は.gitignoreで無視します.
つまり,ソースファイルの一覧をGitで取得できるはずなので,それをmakeに渡す方法があります.
以下がMakefileでGitを使って依存先を列挙する関数です.
SOURCE_FILES=$(shell git ls-files -- $1; git ls-files --others --exclude-standard -- $1)
この関数は,第1引数で指定されてディレクトリ以下にあるソースファイルを全て列挙します.
git ls-files -- $1で,指定ディレクトリ以下でGitに管理されている全てのファイルを列挙します.
git ls-files --others --exclude-standard -- $1で,指定ディレクトリ以下のUntracked filesつまり新規のソースファイルを全て列挙します.
ただし,README.mdとかも列挙されるので,注意して使いましょう.
このコマンドは汎用性の高いコマンドなので,.make/header.mkで定義しておいて,複数のMakefileから
include $(shell git rev-parse --show-toplevel)/.make/header.mk
で取り込んでいます.
git rev-parse --show-toplevelは,リポジトリのトップディレクトリのパスを取得するコマンドで,どれだけ深い階層のMakefileからも,この1行で目的のファイルを取り込むことができます.
取り込んだ側のMakefileで
$(TARGET): $(call SOURCE_FILES, .)
command
と書くことで,TARGETはこのMakefileが置かれているディレクトリ以下にあるすべてのソースファイルに依存します.
小技その2:中間生成物の場所の取得
これは親ディレクトリのMakefileで$(call SUB_TARGET, 子ディレクトリ)と書くと,子ディレクトリのMakefileに中間生成物の場所を問い合わせられる機能です.
SUB_TARGET関数は,.make/header.mkで
SUB_TARGET=$(shell make target -C $1 -s)
と定義します.
targetというターゲットは.make/footer.mkで
.PHONY: target
target:
@echo $(abspath $(TARGET))
と定義され,子Makefileの末尾で,
include $(shell git rev-parse --show-toplevel)/.make/footer.mk
により取り込まれます.
こうすることで,生成物の名前を必ずTARGETというマクロに入れておけば,親Makefileから子Makefileに生成物の場所を問い合わせられます.
小技その3:sudoの継承
sudoでmakeが実行されたときsudoという文字列に展開され,そうでない場合から文字列に展開されるマクロです.
SUDO=$(shell if [ $$(id -u) -eq 0 ] && [ -n "$$(which sudo)" ]; then echo sudo; fi)
使いどころとしては,OSのディレクトリツリーをひとつのイメージファイルにまとめる時に,マウント,コピー,アンマウントの過程でsudo権限が必要になります.
ただし,Dockerコンテナのrootで実行するときはsudoが要らなかったりするので,sudoで実行されたかどうかで判断しています.
$(TARGET): $(call SOURCE_FILES, .)
rm -f $@
if mountpoint -q $(MOUNT_DIRECTORY); then umount -l $(MOUNT_DIRECTORY); fi
rm -rf $(MOUNT_DIRECTORY)
dd if=/dev/zero of=$@ ibs=$(BLOCK_SIZE) count=$(BLOCK_COUNT)
mkfs.fat $@
mkdir $(MOUNT_DIRECTORY)
$(SUDO) mount -o loop $@ $(MOUNT_DIRECTORY) # マウント
$(SUDO) make $(PROCESSOR_BOOT_LOADER_DESTINATION)
$(SUDO) make $(PROCESSOR_KERNEL_DESTINATION)
$(SUDO) make $(BOOTLOADER_DESTINATION) PROCESSOR_BOOT_LOADER=$(PROCESSOR_BOOT_LOADER) PROCESSOR_KERNEL=$(PROCESSOR_KERNEL) KERNEL=$(KERNEL)
$(SUDO) make $(KERNEL_DESTINATION)
for application in $(APPLICATIONS); do make -C $$application; done
$(SUDO) mkdir -p $(APPLICATION_DESTINATION_DIRECTORY)
$(SUDO) make $(APPLICATION_DESTINATIONS)
$(SUDO) umount $(MOUNT_DIRECTORY) # アンマウント
rm -rf $(MOUNT_DIRECTORY)
小技その4:QEMU上でOSを実行
HeliOSでは,make runコマンドでQEMU上でOSの動作確認ができるようにしています.
make runコマンドを実行すると画面が左右に分割され,左の画面にはOSがRS232CのCOM2に出力した内容が,右の画面にはターミナルが表示されます.
では,このmake runコマンドの中身を見てみましょう.
Makefileのrunターゲットは,.tmux/Makefileのrunターゲットを呼び出します.
# Run the OS on QEMU.
# Usage: make run
.PHONY: run
run: $(TARGET)
-make run -C .tmux -s
.tmux/Makefileのrunターゲットは,tmuxで新しいセッションを作成し,そのセッションでrun.confを実行させます.
.PHONY: runは,runが偽のターゲットであり,コマンドを実行しても実際にrunというファイルが生成されるわけではないということを示します.
ターゲットを生成することが目的ではなく,コマンドを実行することが目的であるようなルールをMakefileに書きたいときは,このように.PHONYターゲットとして書きます.
# Run the OS on QEMU.
# Usage: make run
.PHONY: run
run:
-tmux new-session \; source-file run.conf
.tmux/run.confは,画面を左右に分割し,それぞれの画面でいくつかのコマンドを実行します.
そして,左側の画面で実行されるmake run_on_tmux -sが,QEMU上でHeliOSを動かすコマンドです.
source-file ~/.tmux.conf
split-window -hc '#{pane_current_path}' # 画面を左右に分割
send-key -t 0 'cd ..' C-m # 左の画面で親ディレクトリに移動
send-key -t 0 'make run_on_tmux -s' C-m # 左の画面で"make run_on_tmux -s"を実行
send-key -t 1 'cd ..' C-m # 右の画面で親ディレクトリに移動
send-key -t 1 'make clippy' C-m # 右の画面で"make clippy"を実行
send-key -t 1 'make fmt' C-m # 右の画面で"make fmt"を実行
select-pane -t 1
Makefileのrun_on_tmuxターゲットは,さらに.qemu/Makefileのrunターゲットを呼び出します.
# Run the OS on QEMU.
# This target is called from .tmux/run.conf
# Don't execute this directly.
.PHONY: run_on_tmux
run_on_tmux:
-make run -C .qemu OS_PATH=$(abspath $(TARGET)) OS_NAME=$(PRODUCT) TELNET_PORT=$(TELNET_PORT) -s
.qemu/Makefileのrunターゲットは,QEMUの起動コマンドと各種オプションをマクロで組み立てて実行しています.
COM2マクロとして定義されたオプションにより,RS232CのCOM2への出力を標準出力に表示しつつ,COM2_LOGマクロで定義されたファイルにも保存しています.
QEMU=qemu-system-x86_64
COM1=-serial file:$(COM1_LOG)
COM1_LOG=../com1.log
COM2=-chardev stdio,id=com2,mux=on,logfile=$(COM2_LOG) -serial chardev:com2
COM2_LOG=../com2.log
CPUS = -smp 2
LOG=-d int,cpu_reset -D $(LOG_PATH)
LOG_PATH=../qemu.log
MEMORY_SIZE=-m 1G
MONITOR=-monitor telnet::$(TELNET_PORT),server,nowait
NO_REBOOT=--no-reboot
OS=-drive file=fat:rw:$(OS_PATH),format=raw,id=$(OS_NAME),if=none -device ide-hd,drive=$(OS_NAME),bootindex=1
OVMF_CODE=-drive file=$(OVMF_CODE_PATH),format=raw,if=pflash,readonly=on
OVMF_CODE_PATH=../../qemu/roms/edk2/Build/OvmfX64/DEBUG_GCC5/FV/OVMF_CODE.fd
OVMF_VARS=-drive file=$(OVMF_VARS_PATH),format=raw,if=pflash,readonly=on
OVMF_VARS_PATH=../../qemu/roms/edk2/Build/OvmfX64/DEBUG_GCC5/FV/OVMF_VARS.fd
VNC=-vnc :0
DEBUG=-S -gdb tcp::$(DEBUG_PORT)
XHCI=-device qemu-xhci
COMMAND=$(QEMU) $(COM1) $(COM2) $(CPUS) $(LOG) $(MEMORY_SIZE) $(MONITOR) $(NO_REBOOT) $(OS) $(OVMF_CODE) $(OVMF_VARS) $(VNC) $(XHCI)
# Run the OS on QEMU.
# Usage: $ make run OS_PATH=<os directory path> OS_NAME=<os name>
.PHONY: run
run:
-$(COMMAND)
実行が完了したら,make stopでtmuxのセッションを終了し,元の画面に戻ることができます.
このコマンドは,このあと説明するmake debugやmake debug_qemuで開いたtmuxのセッションを終了するときにも使えます.
Makefileのstopターゲットは,.tmux/Makefileのstopターゲットを呼び出します.
# Stop the OS on QEMU.
# Usage: make stop
.PHONY: stop
stop:
-make stop -C .tmux
.tmux/Makefileのstopターゲットは,Makefileのstop_on_tmuxターゲットを呼び出します.
# Stop the OS on QEMU.
# Usage: make stop
.PHONY: stop
stop:
-make stop_on_tmux -C ..
Makefileのstop_on_tmuxターゲットは,.qemu/Makefileのstopターゲットを呼び出します.
# Stop the OS on QEMU.
# This target is called from .tmux/Makefile
# Don't execute this directly.
.PHONY: stop_on_tmux
stop_on_tmux:
-make stop -C .qemu TELNET_PORT=$(TELNET_PORT)
.qemu/Makefileのstopターゲットは,Telnet経由でQEMUを終了し,tmuxのセッションを終了します.
# Stop the OS on QEMU.
# Usage: $ make stop
.PHONY: stop
stop:
-echo quit | nc localhost $(TELNET_PORT)
-tmux kill-server
小技その5:OSのデバッグ
make debugコマンドで,GDBからQEMU上で動いているOSにアタッチしてデバッグします.
make debugコマンドを実行すると,こんな画面になります.
make runのときと同様に画面が左右に分割されます.
左側の画面ではQEMU上でOSが実行され,RS232CのCOM2の出力が表示されます.
右側の画面ではGDBが起動しOSにアタッチしています.
Makefileのdebugターゲットは,.tmux/Makefileのdebugターゲットを呼び出します.
# Debug the OS on QEMU by GDB.
# Usage: make debug
.PHONY: debug
debug: $(TARGET)
-make debug -C .tmux -s
.tmux/Makefileのdebugターゲットは,tmuxで新しいセッションを立ち上げ,.tmux/debug.confを実行します.
# Debug the OS on QEMU by GDB
# Usage: make debug
.PHONY: debug
debug:
-tmux new-session \; source-file debug.conf
.tmux/debug.confは,画面を左右に分割し,左側の画面でMakefileのdebug_on_tmuxターゲットを,右側の画面で.gdb/Makefileのdebugターゲットを呼び出します.
source-file ~/.tmux.conf
split-window -hc '#{pane_current_path}' # 画面を左右に分割
send-key -t 0 'cd ..' C-m # 左側の画面で親ディレクトリに移動
send-key -t 0 'make debug_on_tmux -s' C-m # 左側の画面で"make debug_on_tmux -s"を実行
send-key -t 1 'cd ..' C-m # 右側の画面で親ディレクトリに移動
send-key -t 1 'make debug -C .gdb' C-m # 右側の画面で"make debug -C .gdb"を実行
select-pane -t 1
左右それぞれの画面における実行の流れを見てみましょう.
左側の画面で実行されること
左側の画面ではMakefileのdebug_on_tmuxターゲットが呼び出されます.
Makefileのdebug_on_tmuxターゲットは,.qemu/Makefileのdebugターゲットを呼び出します.
# Run the OS on QEMU.
# This target is called from .tmux/run.conf
# Don't execute this directly.
.PHONY: debug_on_tmux
debug_on_tmux:
-make debug -C .qemu OS_PATH=$(abspath $(TARGET)) OS_NAME=$(PRODUCT) DEBUG_PORT=$(DEBUG_PORT) TELNET_PORT=$(TELNET_PORT) -s
.qemu/Makefileのdebugターゲットは,-gdb tcp::$(DEBUG_PORT)というオプションにより,TCPのDEBUG_PORTでGDBを待機する状態でQEMUを起動します.
QEMU=qemu-system-x86_64
COM1=-serial file:$(COM1_LOG)
COM1_LOG=../com1.log
COM2=-chardev stdio,id=com2,mux=on,logfile=$(COM2_LOG) -serial chardev:com2
COM2_LOG=../com2.log
CPUS = -smp 2
LOG=-d int,cpu_reset -D $(LOG_PATH)
LOG_PATH=../qemu.log
MEMORY_SIZE=-m 1G
MONITOR=-monitor telnet::$(TELNET_PORT),server,nowait
NO_REBOOT=--no-reboot
OS=-drive file=fat:rw:$(OS_PATH),format=raw,id=$(OS_NAME),if=none -device ide-hd,drive=$(OS_NAME),bootindex=1
OVMF_CODE=-drive file=$(OVMF_CODE_PATH),format=raw,if=pflash,readonly=on
OVMF_CODE_PATH=../../qemu/roms/edk2/Build/OvmfX64/DEBUG_GCC5/FV/OVMF_CODE.fd
OVMF_VARS=-drive file=$(OVMF_VARS_PATH),format=raw,if=pflash,readonly=on
OVMF_VARS_PATH=../../qemu/roms/edk2/Build/OvmfX64/DEBUG_GCC5/FV/OVMF_VARS.fd
VNC=-vnc :0
DEBUG=-S -gdb tcp::$(DEBUG_PORT) # デバッグオプション
XHCI=-device qemu-xhci
COMMAND=$(QEMU) $(COM1) $(COM2) $(CPUS) $(LOG) $(MEMORY_SIZE) $(MONITOR) $(NO_REBOOT) $(OS) $(OVMF_CODE) $(OVMF_VARS) $(VNC) $(XHCI)
# Debug the OS on QEMU by GDB.
# Usage: $ make debug OS_PATH=<os directory path> OS_NAME=<os name> DEBUG_PORT=<debug port>
.PHONY: debug
debug:
-$(COMMAND) $(DEBUG)
DEBUG_PORTはMakefileで2159と定義されています.
DEBUG_PORT=2159
右側の画面で実行されること
右側の画面では.gdb/Makefileのdebugターゲットが呼び出されます.
.gdb/Makefileのdebugターゲットは,GDBを起動します.
.PHONY: debug
debug:
gdb
GDBは起動時に.gdb/.gdbinitを実行します.
.gdb/.gdbinitはQEMUが待機している2159番ポートにアタッチします.
target remote localhost:2159
小技その6:QEMUのデバッグ
make debug_qemuコマンドで,GDBからOSを動かしているQEMUにアタッチしてデバッグします.
make debug_qemuコマンドを実行すると,こんな画面になります.
make runやmake debugのときと同様に画面が左右に分割されます.
左側の画面ではQEMUがGDBにアタッチされ,main関数の先頭で一時停止しています.
右側の画面はソースコードの確認などのための画面で,QEMUのソースコードの場所に移動しています.
これを実現するには,まずQEMUをデバッグ可能な形でビルドしておく必要があります.
私はOSの開発環境をDockerイメージとして構築しています.
なので,DockerfileにQEMUをデバッグ可能な形でビルドする手順を書いています.
QEMUのビルドを設定する際に,CFLAGSにCコンパイラに渡すオプションを,CXXFLAGSにC++コンパイラに渡すオプションを指定します.
これらに,最適化を抑制する-O0,デバッグ情報を埋め込む-g,関数のインライン化を抑制する-fno-inlineを指定します.
# Install QEMU.
RUN git clone --branch v8.1.0 --depth 1 --recursive --shallow-submodules --single-branch https://gitlab.com/qemu-project/qemu.git
WORKDIR qemu
RUN ./configure --target-list=x86_64-softmmu CFLAGS="-O0 -g -fno-inline" CXXFLAGS="-O0 -g -fno-inline"
RUN make
RUN make install
WORKDIR roms/edk2
RUN ./OvmfPkg/build.sh -a X64
WORKDIR ../../..
これでデバッグ可能なQEMUがインストールされます.
Makefileのdebug_qemuターゲットは,.tmux/Makefileのdebug_qemuターゲットを呼び出します.
# Debug QEMU by GDB.
# Usage: make debug_qemu
.PHONY: debug_qemu
debug_qemu: $(TARGET)
-make debug_qemu -C .tmux -s
.tmux/Makefileのdebug_qemuターゲットは,tmuxで新しいセッションを開き,.tmux/debug_qemu.confを実行します.
# Debug QEMU by GDB
# Usage: make debug_qemu
.PHONY: debug_qemu
debug_qemu:
-tmux new-session \; source-file debug_qemu.conf
.tmux/debug_qemu.confは,画面を左右に分割し,左側の画面ではMakefileのdebug_qemu_on_tmuxターゲットを呼び出し,右側の画面ではQEMUのソースコードがある場所に移動します.
source-file ~/.tmux.conf
split-window -hc '#{pane_current_path}'
send-key -t 0 'cd ..' C-m
send-key -t 0 'make debug_qemu_on_tmux' C-m
send-key -t 1 'cat ../.qemu/.gdbinit' C-m
send-key -t 1 'cd ../../qemu/build' C-m
select-pane -t 0
Makefileのdebug_qemu_on_tmuxターゲットは,.qemu/Makefileのdebug_qemuターゲットを呼び出します.
# Run the OS on QEMU.
# This target is called from .tmux/run.conf
# Don't execute this directly.
.PHONY: debug_qemu_on_tmux
debug_qemu_on_tmux:
-make debug_qemu -C .qemu OS_PATH=$(abspath $(TARGET)) OS_NAME=$(PRODUCT) TELNET_PORT=$(TELNET_PORT) -s
.qemu/Makefileのdebug_qemuターゲットは,GDBでQEMUをデバッグするコマンドを実行します.
QEMU=qemu-system-x86_64
COM1=-serial file:$(COM1_LOG)
COM1_LOG=../com1.log
COM2=-chardev stdio,id=com2,mux=on,logfile=$(COM2_LOG) -serial chardev:com2
COM2_LOG=../com2.log
CPUS = -smp 2
LOG=-d int,cpu_reset -D $(LOG_PATH)
LOG_PATH=../qemu.log
MEMORY_SIZE=-m 1G
MONITOR=-monitor telnet::$(TELNET_PORT),server,nowait
NO_REBOOT=--no-reboot
OS=-drive file=fat:rw:$(OS_PATH),format=raw,id=$(OS_NAME),if=none -device ide-hd,drive=$(OS_NAME),bootindex=1
OVMF_CODE=-drive file=$(OVMF_CODE_PATH),format=raw,if=pflash,readonly=on
OVMF_CODE_PATH=../../qemu/roms/edk2/Build/OvmfX64/DEBUG_GCC5/FV/OVMF_CODE.fd
OVMF_VARS=-drive file=$(OVMF_VARS_PATH),format=raw,if=pflash,readonly=on
OVMF_VARS_PATH=../../qemu/roms/edk2/Build/OvmfX64/DEBUG_GCC5/FV/OVMF_VARS.fd
VNC=-vnc :0
DEBUG=-S -gdb tcp::$(DEBUG_PORT)
XHCI=-device qemu-xhci
COMMAND=$(QEMU) $(COM1) $(COM2) $(CPUS) $(LOG) $(MEMORY_SIZE) $(MONITOR) $(NO_REBOOT) $(OS) $(OVMF_CODE) $(OVMF_VARS) $(VNC) $(XHCI)
# Debug QEMU by GDB.
# Usage: $ make run OS_PATH=<os directory path> OS_NAME=<os name>
.PHONY: debug_qemu
debug_qemu:
gdb --args $(COMMAND)
GDBは.qemu/.gdbinitを自動的に実行します.
ここではmain関数にブレークポイントを貼って実行するので,main関数の先頭で一時停止している状態になります.
break main
run
小技その7:cargo clippyの自動化
cargo clippyは,Rustのコンパイル時にエラーや警告を出すほどのことではない指摘事項や,よりよい書き方を提案してくれるコマンドです.
OSは複数のバイナリからなり,HeliOSではひとつのcargoプロジェクトがひとつのバイナリを生成します.
いちいち各プロジェクトごとにcargo clippyを実行するのは面倒なので,Makefileのclippyターゲットから,全てのプロジェクトに対してcargo clippyを呼び出すようにしています.
# Clippy rust codes.
.PHONY: clippy
clippy:
make clippy -C $(BOOTLOADER_DIRECTORY) PROCESSOR_BOOT_LOADER=$(PROCESSOR_BOOT_LOADER) PROCESSOR_KERNEL=$(PROCESSOR_KERNEL) KERNEL=$(KERNEL)
make clippy -C $(KERNEL_DIRECTORY)
make clippy -C $(PROCESSOR_KERNEL_DIRECTORY)
for application in $(APPLICATIONS); do make clippy -C $$application; done
小技その8:cargo fmtの自動化
cargo fmtは,Rustのソースコードを整形してくれるコマンドで,これもcargo clippyと同様に書くプロジェクトごとに実行するのが面倒なため,Makefileのfmtターゲットから呼び出すようにしています.
# Format rust codes.
.PHONY: fmt
fmt:
make fmt -C $(BOOTLOADER_DIRECTORY) PROCESSOR_BOOT_LOADER=$(PROCESSOR_BOOT_LOADER) PROCESSOR_KERNEL=$(PROCESSOR_KERNEL) KERNEL=$(KERNEL)
make fmt -C $(KERNEL_DIRECTORY)
make fmt -C $(PROCESSOR_KERNEL_DIRECTORY)
for application in $(APPLICATIONS); do make fmt -C $$application; done
まとめ
- Makeは,ビルド自動化ツールである
- 「ソースからコマンドでターゲットを生成する」というルールを
Makefileに書き,makeコマンドでビルドする - 依存関係をルールとして
Makefileに書くことで,規模が大きくなってもmakeだけでビルドできる -
makeはファイルの最終更新時刻に基づき,無駄な再コンパイルを防ぎ,ビルド時間を短縮できる - マクロや関数が使える
-
.PHONYターゲットを定義することで,ビルドだけでなく実行やデバッグと行ったちょっとした操作の自動化に使える










