1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OS自作のためのGNU make

1
Posted at

はじめに

こんにちは.だいみょーじんです.
この記事は,第46回自作OSもくもく会で発表した内容をまとめ,自作OSアドベントカレンダー2025の22日目の記事として公開したものです.
この記事では,GNU makeの基本から,HeliOSという自作OSのビルドなどに使っているいくつかの小技までを紹介しています.

Make基本編

まずはMakeの一般的な使い方を紹介します.

Makeとは?

一言でいうとビルド自動化ツールです.
ビルド手順をMakefileというファイルに記述しておくと,makeコマンドでビルドできます.
commandを実行することによってsourceからtargetを生成するとしましょう.

rule.drawio.png

これは,Makefileで以下のように記述できます.

Makefile
target: source
    command

より具体的に,cchello.cをコンパイルしてa.outを生成したいとき,

example.drawio.png

これはMakefileで以下のように記述できます.

Makefile
a.out: hello.c
    cc hello.c

ひとつのターゲットに対して,ソースやコマンドを複数指定することも可能です.

multi_source_multi_command.drawio.png

これはMakefileで以下のように記述できます.

Makefile
target: source0 source1 source2
    command0
    command1
    command2

このようなルールの集まりをMakefileに記述しておくと,コマンドmake <target>でターゲットを生成できます.
また,コマンドmakeで,Makefileの一番上に記述されたターゲットを生成します.

Makeの利点

大規模な依存関係の解決

OSのように比較的大規模なものになってくると,ソースから最終生成物にいたるまでの依存関係が複雑になります.

dependency_tree.drawio.png

この複雑な依存関係をMakefileに全て書いておけば,makeコマンド一発でOSをビルドできます.

依存関係に基づいた最小限のビルド

makeコマンドは,ソースが変更されていない場合はターゲットを再生成しません.
例えば下の図のように,一番上のソースだけを書き換えた場合は,図の左上と右のコマンドのみが実行されます.

recompile.drawio.png

必要最小限のコマンドだけを呼び出すことにより,ビルド時間の短縮に繋がります.

階層化

以下の図のように,子ディレクトリのソースコードから中間生成物を生成し,その中間生成物と親ディレクトリのソースコードから最終生成物を生成することを考えましょう.

make_tree.drawio.png

この場合,まず子ディレクトリのMakefileで,子ディレクトリのソースコードから中間生成物を生成するというルールを書いておきます.
次に,親ディレクトリのMakefileで,子ディレクトリのソースコードからコマンドmake -C 子ディレクトリで子ディレクトリのMakefileを呼び出すことにより中間生成物を生成するというルールを書いておきます.
さらに親ディレクトリのソースコードと中間生成物から最終生成物を生成するルールも親ディレクトリのMakefileに書いておきます.
このように,makeコマンドの-Cオプションで別のディレクトリのMakefileを呼び出す機能を使うことで,Makefileをディレクトリ構造に合わせて階層化することができます.

マクロ

Makefileでは,マクロ名=文字列でマクロを定義できます.
Makefile内の$(マクロ名)が,定義された文字列に展開されます.
例えば,hello.cccでコンパイルしてhelloを生成するルール

Makefile
hello: hello.c
    cc hello.c -o hello

は,マクロを使ってこのように書き換えられます.

Makefile
SOURCE=hello.c
TARGET=hello

$(TARGET): $(SOURCE)
    cc $(SOURCE) -o $(TARGET)

また,以下のような暗黙的に定義済みであるマクロがあります.

  • $$$
  • $@はターゲット名
  • $<は最初のソース名
  • $^は全てのソース名

これらを使うと,上のルールは

Makefile
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マクロで指定できます.
この関数の機能を使うと,先程のルールは以下のように書き換えられます.

Makefile
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を生成するルールを以下のようにまとめて書けます.

Makefile
# ソースの列挙
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を使って依存先を列挙する関数です.

.make/header.mk
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から

Makefile
include $(shell git rev-parse --show-toplevel)/.make/header.mk

で取り込んでいます.
git rev-parse --show-toplevelは,リポジトリのトップディレクトリのパスを取得するコマンドで,どれだけ深い階層のMakefileからも,この1行で目的のファイルを取り込むことができます.
取り込んだ側のMakefile

Makefile
$(TARGET): $(call SOURCE_FILES, .)
    command

と書くことで,TARGETはこのMakefileが置かれているディレクトリ以下にあるすべてのソースファイルに依存します.

小技その2:中間生成物の場所の取得

これは親ディレクトリのMakefile$(call SUB_TARGET, 子ディレクトリ)と書くと,子ディレクトリのMakefileに中間生成物の場所を問い合わせられる機能です.
SUB_TARGET関数は,.make/header.mk

.make/header.mk
SUB_TARGET=$(shell make target -C $1 -s)

と定義します.
targetというターゲットは.make/footer.mk

.make/footer.mk
.PHONY: target
target:
	@echo $(abspath $(TARGET))

と定義され,子Makefileの末尾で,

Makefile
include $(shell git rev-parse --show-toplevel)/.make/footer.mk

により取り込まれます.
こうすることで,生成物の名前を必ずTARGETというマクロに入れておけば,親Makefileから子Makefileに生成物の場所を問い合わせられます.

小技その3:sudoの継承

sudomakeが実行されたときsudoという文字列に展開され,そうでない場合から文字列に展開されるマクロです.

Makefile
SUDO=$(shell if [ $$(id -u) -eq 0 ] && [ -n "$$(which sudo)" ]; then echo sudo; fi)

使いどころとしては,OSのディレクトリツリーをひとつのイメージファイルにまとめる時に,マウント,コピー,アンマウントの過程でsudo権限が必要になります.
ただし,Dockerコンテナのrootで実行するときはsudoが要らなかったりするので,sudoで実行されたかどうかで判断しています.

Makefile
$(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の動作確認ができるようにしています.

Screenshot from 2025-12-20 14-36-16.png

make runコマンドを実行すると画面が左右に分割され,左の画面にはOSがRS232CのCOM2に出力した内容が,右の画面にはターミナルが表示されます.
では,このmake runコマンドの中身を見てみましょう.
Makefilerunターゲットは,.tmux/Makefilerunターゲットを呼び出します.

Makefile
# Run the OS on QEMU.
# Usage: make run
.PHONY: run
run: $(TARGET)
	-make run -C .tmux -s

.tmux/Makefilerunターゲットは,tmuxで新しいセッションを作成し,そのセッションでrun.confを実行させます.
.PHONY: runは,runが偽のターゲットであり,コマンドを実行しても実際にrunというファイルが生成されるわけではないということを示します.
ターゲットを生成することが目的ではなく,コマンドを実行することが目的であるようなルールをMakefileに書きたいときは,このように.PHONYターゲットとして書きます.

.tmux/Makefile
# 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を動かすコマンドです.

.tmux/run.conf
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

Makefilerun_on_tmuxターゲットは,さらに.qemu/Makefilerunターゲットを呼び出します.

Makefile
# 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/Makefilerunターゲットは,QEMUの起動コマンドと各種オプションをマクロで組み立てて実行しています.
COM2マクロとして定義されたオプションにより,RS232CのCOM2への出力を標準出力に表示しつつ,COM2_LOGマクロで定義されたファイルにも保存しています.

.qemu/Makefile
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 debugmake debug_qemuで開いたtmuxのセッションを終了するときにも使えます.
Makefilestopターゲットは,.tmux/Makefilestopターゲットを呼び出します.

Makefile
# Stop the OS on QEMU.
# Usage: make stop
.PHONY: stop
stop:
	-make stop -C .tmux

.tmux/Makefilestopターゲットは,Makefilestop_on_tmuxターゲットを呼び出します.

.tmux/Makefile
# Stop the OS on QEMU.
# Usage: make stop
.PHONY: stop
stop:
	-make stop_on_tmux -C ..

Makefilestop_on_tmuxターゲットは,.qemu/Makefilestopターゲットを呼び出します.

Makefile
# 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/Makefilestopターゲットは,Telnet経由でQEMUを終了し,tmuxのセッションを終了します.

.qemu/Makefile
# 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にアタッチしてデバッグします.

debug.drawio.png

make debugコマンドを実行すると,こんな画面になります.

Screenshot from 2025-12-20 16-21-14.png

make runのときと同様に画面が左右に分割されます.
左側の画面ではQEMU上でOSが実行され,RS232CのCOM2の出力が表示されます.
右側の画面ではGDBが起動しOSにアタッチしています.
Makefiledebugターゲットは,.tmux/Makefiledebugターゲットを呼び出します.

Makefile
# Debug the OS on QEMU by GDB.
# Usage: make debug
.PHONY: debug
debug: $(TARGET)
	-make debug -C .tmux -s

.tmux/Makefiledebugターゲットは,tmuxで新しいセッションを立ち上げ,.tmux/debug.confを実行します.

.tmux/Makefile
# Debug the OS on QEMU by GDB
# Usage: make debug
.PHONY: debug
debug:
	-tmux new-session \; source-file debug.conf

.tmux/debug.confは,画面を左右に分割し,左側の画面でMakefiledebug_on_tmuxターゲットを,右側の画面で.gdb/Makefiledebugターゲットを呼び出します.

.tmux/debug.conf
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

左右それぞれの画面における実行の流れを見てみましょう.

左側の画面で実行されること

左側の画面ではMakefiledebug_on_tmuxターゲットが呼び出されます.
Makefiledebug_on_tmuxターゲットは,.qemu/Makefiledebugターゲットを呼び出します.

Makefile
# 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/Makefiledebugターゲットは,-gdb tcp::$(DEBUG_PORT)というオプションにより,TCPのDEBUG_PORTでGDBを待機する状態でQEMUを起動します.

.qemu/Makefile
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_PORTMakefileで2159と定義されています.

Makefile
DEBUG_PORT=2159

右側の画面で実行されること

右側の画面では.gdb/Makefiledebugターゲットが呼び出されます.
.gdb/Makefiledebugターゲットは,GDBを起動します.

.gdb/Makefile
.PHONY: debug
debug:
	gdb

GDBは起動時に.gdb/.gdbinitを実行します.
.gdb/.gdbinitはQEMUが待機している2159番ポートにアタッチします.

.gdb/.gdbinit
target remote localhost:2159

小技その6:QEMUのデバッグ

make debug_qemuコマンドで,GDBからOSを動かしているQEMUにアタッチしてデバッグします.

debug_qemu.drawio.png

make debug_qemuコマンドを実行すると,こんな画面になります.

Screenshot from 2025-12-20 21-15-53.png

make runmake debugのときと同様に画面が左右に分割されます.
左側の画面ではQEMUがGDBにアタッチされ,main関数の先頭で一時停止しています.
右側の画面はソースコードの確認などのための画面で,QEMUのソースコードの場所に移動しています.
これを実現するには,まずQEMUをデバッグ可能な形でビルドしておく必要があります.
私はOSの開発環境をDockerイメージとして構築しています.
なので,DockerfileにQEMUをデバッグ可能な形でビルドする手順を書いています.
QEMUのビルドを設定する際に,CFLAGSにCコンパイラに渡すオプションを,CXXFLAGSにC++コンパイラに渡すオプションを指定します.
これらに,最適化を抑制する-O0,デバッグ情報を埋め込む-g,関数のインライン化を抑制する-fno-inlineを指定します.

.docker/Dockerfile
# 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がインストールされます.
Makefiledebug_qemuターゲットは,.tmux/Makefiledebug_qemuターゲットを呼び出します.

Makefile
# Debug QEMU by GDB.
# Usage: make debug_qemu
.PHONY: debug_qemu
debug_qemu: $(TARGET)
	-make debug_qemu -C .tmux -s

.tmux/Makefiledebug_qemuターゲットは,tmuxで新しいセッションを開き,.tmux/debug_qemu.confを実行します.

.tmux/Makefile
# 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は,画面を左右に分割し,左側の画面ではMakefiledebug_qemu_on_tmuxターゲットを呼び出し,右側の画面ではQEMUのソースコードがある場所に移動します.

.tmux/debug_qemu.conf
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

Makefiledebug_qemu_on_tmuxターゲットは,.qemu/Makefiledebug_qemuターゲットを呼び出します.

Makefile
# 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/Makefiledebug_qemuターゲットは,GDBでQEMUをデバッグするコマンドを実行します.

.qemu/Makefile
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関数の先頭で一時停止している状態になります.

.qemu/.gdbinit
break main
run

小技その7:cargo clippyの自動化

cargo clippyは,Rustのコンパイル時にエラーや警告を出すほどのことではない指摘事項や,よりよい書き方を提案してくれるコマンドです.
OSは複数のバイナリからなり,HeliOSではひとつのcargoプロジェクトがひとつのバイナリを生成します.
いちいち各プロジェクトごとにcargo clippyを実行するのは面倒なので,Makefileclippyターゲットから,全てのプロジェクトに対してcargo clippyを呼び出すようにしています.

Makefile
# 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と同様に書くプロジェクトごとに実行するのが面倒なため,Makefilefmtターゲットから呼び出すようにしています.

Makefile
# 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ターゲットを定義することで,ビルドだけでなく実行やデバッグと行ったちょっとした操作の自動化に使える

参考文献

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?