makeコマンドによる省力化

例えば演習1.6-3において,作成した sort.s の動作を確認するために sort.s, print_eax.s, test_sort.s を結合する場合,以下の手順を実行する必要がある。

$ nasm sort.s                           -- sort.oを生成
$ nasm print_eax.s                      -- print_eax.oを生成
$ nasm test_sort.s                      -- test_sort.oを生成
$ ld sort.o print_eax.o test_sort.o     -- 結合してa.outを生成
$ ./a.out
1
9

ソースファイルを修正するたびに毎回上記のコマンドを入力するのは面倒だ。

コンパイルやアセンブルやリンクを必要なだけ行って実行可能ファイルを作り出す作業をビルドと言う。ビルドを自動化するツールを使えば,「修正 → ビルド → 動作テスト → 修正 → ビルド → 動作テスト → …」という作業サイクルの手間が大きく減る。 また,バージョン管理システムのリポジトリにはソースファイルのみ置き,コンパイル結果やアセンブル結果は置かないのが原則だが,代わりにビルドツールの設定ファイルを置いておけば,コンパイル結果やアセンブル結果はビルドツールを使って容易に得られる。

makeはよく知られた(古典的な)ビルドツールだ。特徴をまとめると以下のようになる。

  • カレントディレクトリの Makefile(先頭が大文字)というファイルに,自動化したい作業内容を記述する。
  • make ターゲット名」を実行すると,指定したターゲットを作るための作業が実行される。

    • ターゲットは基本的に生成したいファイル名(例えば make print_eax.o を実行すると print_eax.o を生成するコマンドが実行される)だが,ファイル名ではないターゲット(例えば make test を実行すると自動テストが行われるなど)も定義できる。
    • 引数なしで make を実行すると,Makefile中で最初に定義したターゲット(デフォルトターゲット)が生成される。
  • 「ターゲット ○○ を作るためには先に △△ が必要」というターゲット間の依存関係を考慮して,必要な作業をすべて行う。また,「△△.s より △△.o の方が新しい(前回生成した後,ソースファイルを更新していない)ので △△.o は作り直さなくてよい」といった判断も行う。

参考資料:

  • GNU make(マニュアル)
  • GNU Make 第3版(O'Reilly社の書籍のPDF版)
  • info makeコマンド (上記のWeb上のマニュアルと同内容)

Makefileの書き方

Makefileの中身は基本的に「生成規則(ルール)の集合」だ。 生成規則は以下の形式で記述する。#から行末まではコメントとして扱われる。 コマンド記述行の行頭は空白ではなく水平タブ(Tabキーで入力される制御文字。文字コード9)なので注意。

# 生成規則の記述形式
target : sourcefiles ...
        command
        command
        ...
# commandの行頭は空白ではなく水平タブ
# 記述例
# (この例は4つの生成規則からなる)
# (似た記述が多くあって冗長だが,後述の機能を使えばもっと簡潔に書ける)
# (シェルのalias機能は効かないので,nasmのオプション引数も書く必要がある)

# 実行可能ファイル名を a.out ではなく test_sort とした.
# ldコマンドの -o オプションで出力ファイル名を指定できる.
test_sort: test_sort.o sort.o print_eax.o
        ld -m elf_i386 test_sort.o sort.o print_eax.o -o test_sort

test_sort.o: test_sort.s
        nasm -felf test_sort.s

sort.o: sort.s
        nasm -felf sort.s

print_eax.o: print_eax.s
        nasm -felf print_eax.s

一つの生成規則は,「このターゲットを生成するためには,指定したソースファイルをすべて生成した後,コマンドを上から順に実行すればよい」という意味を表す。

生成規則を書く順序は自由だが,トップダウン式(最終的に欲しいターゲットが上,それのソースファイルの規則が下)で書くのが慣わしだ(「最初に定義したターゲットがデフォルトターゲット」ということにも合致する)。 上記の例の場合,引数なしで make を実行した場合,実行可能ファイル test_sort が生成される。 まず test_sort のソースファイルである test_sort.o, sort.o, print_eax.o が生成された後,test_sort を生成するコマンドが実行される(下記)。

$ make
nasm -felf test_sort.s
nasm -felf sort.s
nasm -felf print_eax.s
ld -m elf_i386 test_sort.o sort.o print_eax.o -o test_sort

マクロ

Makefile中で繰り返し使う文字列は,マクロとして定義すれば,記述が簡潔になるし,修正も容易になる。

# マクロ定義
AS = nasm -felf
LD = ld
LDFLAGS = -m elf_i386
OBJS_SORT = test_sort.o sort.o print_eax.o

# 生成規則
test_sort: $(OBJS_SORT)
        $(LD) $(LDFLAGS) $+ -o $@

test_sort.o: test_sort.s
        $(AS) $<

sort.o: sort.s
        $(AS) $<

print_eax.o: print_eax.s
        $(AS) $<
  • マクロの定義: 名前 = 置換文字列
  • マクロの使用: $(マクロ名)

アセンブラを表すマクロ名 AS,リンカを表すマクロ名 LD,リンカのオプション引数を表すマクロ名 LDFLAGS は,慣習としてよく用いられる。 他に,Cコンパイラを表すマクロ名 CC などがある。 GNU makeマニュアルの10.3節「Variables used by implicit rules」を参照。

$@, $+, $< は,自動的に定義される特別なマクロだ。

  • $@ はターゲット名(英語のatに見立てて目標を表す)。
  • $+ はソースファイルの列全体。
  • $< はソースファイル列の先頭の1ファイル名(左向き矢印に見立てて入力を表す)。

型規則(パタン規則)

上記のMakefile記述例で,.s ファイルから .o ファイルを生成する手順は,test_sort.o も sort.o も print_eax.o も同じだ。 型規則を使うと,これらの生成規則を一つにまとめることができる。

# .sから.oを生成する型規則
%.o: %.s
        $(AS) $<
# マクロ定義
AS = nasm -felf
LD = ld
LDFLAGS = -m elf_i386
OBJS_SORT = test_sort.o sort.o print_eax.o

# .sから.oを生成する型規則
%.o: %.s
        $(AS) $<

# 生成規則
# デフォルトターゲットは test_sort
test_sort: $(OBJS_SORT)
        $(LD) $(LDFLAGS) $+ -o $@

% が「任意の名前」を表し,「%.o: %.s という型に合致するすべてのファイル対にこの規則を適用する」という意味になる。

型規則を書く位置は自由だが,通常の生成規則より前に書く慣習がある。型規則はデフォルトターゲットの定義として扱われない。

疑似ターゲット

make test を実行すると動作テストを行う」というように,makeに「ファイルの生成」以外の仕事をさせることもできる。 この場合の test のような,ファイル名ではないターゲットのことを,疑似ターゲット (phony target) という。

以下のような疑似ターゲットが慣習的に用いられる。

  • 自動テストを行う test
  • 不要なファイルを削除する clean
  • ビルド結果を適切なディレクトリに配備する install
  • 複数のターゲットをすべて生成する all

基本的には,通常のターゲットと同じように生成規則を書けばよい。

# test_sortの生成規則など
-- 略 --

# 疑似ターゲットであることを明示
.PHONY: test clean

# 自動テスト (test_sortの出力がanswer.txtと一致するか)
test: test_sort answer.txt
        ./test_sort | diff - answer.txt

# .oファイルやバックアップファイル等を削除
clean:
        rm -f *.o *~ a.out
$ make clean
rm -f *.o *~ a.out

上記の例で,.PHONY: から始まる行は,:の右辺の名前が疑似ターゲットであることを示している。 疑似ターゲットと同じ名前のファイル(例えば clean という名前のファイル)がカレントディレクトリにあると,.PHONY の指定がなければ,「cleanはすでに存在している」と出力されるだけで何もしない。 .PHONY の指定があれば,同じ名前のファイルとは無関係に,その生成規則を実行する。

参考: ターゲットの集合を表す疑似ターゲット

make all を実行すれば実行可能ファイル test_print と 10primes と test_sort を生成するようにしたい場合:

.PHONY: all

# make all で3つのファイルを生成する.
# 他の生成規則より上に書いてあればallがデフォルトターゲットになる.
all: test_print 10primes test_sort

# 各ターゲットの生成規則
-- 略 --

make all を実行すると,ターゲット all のソースファイルとして指定されている3つのターゲットが生成される。 3つのターゲットが生成されたら仕事は終わりなので,all に対するコマンド列は必要ない。

以下のターゲット test も同様だ。

.PHONY: test test-1 test-2 test-3

# make test で3つのテストを行う
test: test-1 test-2 test-3

# test-1, test-2, test-3の生成規則
-- 略 --

補足: 正しいMakefileの書き方

疑似ターゲットは「ファイルを生成しない仕事」に限って使うべきだ。 ファイルを生成する仕事は,そのファイルをターゲットとする生成規則として記述する方が,自然だし間違いも少ない。

# 悪い例

all: step1 step2 step3 step4

step1: test_sort.s
        $(AS) $<
step2: sort.s
        $(AS) $<
step3: print_eax.s
        $(AS) $<
step4: test_sort.o sort.o print_eax.o
        $(LD) $(LDFLAGS) $+ -o test_sort

all は本来,「単独でも生成できる複数のターゲット」をまとめて生成したいときに使うものだが,この例はそうなっていない。また,「step4の実行前にstep1〜step3を実行しなければならない」ことが all の右辺の順序で表されているが,このような「実行順序の制御」は下記の「よい例」のように書けば不要だし,わざわざ記述すると間違いの原因になる。

疑似ターゲットを濫用していることも問題だ。 この例では例えば make test_sort を実行してもファイル test_sort が得られない。 正しく記述すれば,make ファイル名 でそのファイルが生成されるはずだ。

# よい例

all: test_sort

test_sort: test_sort.o sort.o print_eax.o
        $(LD) $(LDFLAGS) $+ -o $@
%.o: %.s
        $(AS) $<
  • 「どのファイルを生成するためにはどのファイルが必要か」が正しく書かれている(ので実行順序をわざわざ書く必要がない)。
  • make ファイル名 でそのファイルが生成される。例えば make test_sort を実行すればファイル test_sort が得られ,make sort.o を実行すれば sort.o が得られる。

練習問題

以下の問題文に従って,リポジトリのサブディレクトリ chap6 の中に Makefile を作成しなさい。

(注意) 複数人が同じ名前のファイルを作成すると,pullしたときに衝突が発生する。 演習1.7-1にてMakefileをコミットする担当者を決めたら,それ以外のメンバーは,自分のMakefileを削除するか別の名前に変更しなさい(保存しておきたければ,名前を変えた後,共有リポジトリにpushしてもよい)。 Makefile以外の名前の設定ファイルを使いたいときは,make -f 設定ファイル名 ターゲット名 を実行すればよい。

(注意) Makefile と makefile(先頭小文字)の両方が存在する場合,makefile が優先される。混乱の素なので,Makefileとmakefileの両方が存在するような状況は避けるべき。

演習1.7-1 リポジトリのサブディレクトリ chap6 の中に,以下の仕様を満たす Makefile を作り,共有リポジトリにpushしなさい。

必須機能:

  • 引数なしでmakeを実行すると,演習1.6-1〜1.6-3で作成したプログラムの動作テスト用実行可能ファイルがすべて生成される(実行可能ファイルの名前は自由に決めてよい)(「演習1.6-2または演習1.6-3の動作テストが演習1.6-1の動作テストを兼ねる」と考えて,演習1.6-1単独の動作テストを省略してもよい)。
  • make XXX(XXX は,上記の実行可能ファイル名のいずれか,または print_eax.o, 10primes.o, sort.o のいずれか)を実行すると,ファイル XXX が生成される。
  • make clean を実行すると,不要なファイル( .o ファイルやa.outや実行可能ファイルやEmacsが作るバックアップファイルなど)を削除する。

オプション機能(実装していればなおよい):

  • make test を実行すると,演習1.6-1〜1.6-3で作成したプログラムの動作テストを行う。
    • 動作テスト用実行可能ファイルを実行し,出力が「正解」と一致するか調べればよい。 diffコマンドやcmpコマンドを使えば,2つのファイルの内容が一致するかどうか調べられる (diffやcmpについては自分で調べなさい)。
    • 「正解」を格納したファイルは,人手で作って共有リポジトリにpushしておけばよい。

(補足)上記の各機能で使うプログラムソースファイルや正解ファイルも,共有リポジトリにpushされている必要がある (そうでないと,他のメンバーはその機能を使おうとしてもソースファイル不足で実行できない)。

results matching ""

    No results matching ""