makeコマンドによる省力化

準備

以下,このページでは,sqsum.s, print_eax.s, isprime.s の3つのソースファイルからなるプログラム(例えば,sqsum.s の中身が素数の2乗和を計算するプログラムで,サブルーチンとして print_eaxisprime を呼び出す,など)を例として考える。

このページの記述例の実行は,前章の演習課題で作成した print_eax.s 及び sqsum.s があるサブディレクトリで行いなさい。 また,isprime.s は以下のようにして作成しなさい(空ファイルを作るだけだが,makeコマンドの使い方を試すだけなら十分)。

$ touch isprime.s                      -- 空ファイルを作成

なお,本章の演習課題の成果物の置き場は,sqsum.s 等のあるサブディレクトリではないので注意。

(複数のメンバーが同じ場所に同じ名前のファイルを作成すると衝突するので,以下で練習のために作るMakefileはコミットしないこと(または,メンバー間で異なるファイル名にしてコミットすること)。)

makeコマンドによる省力化

例として,sqsum.s, print_eax.s, isprime.s の3つのソースファイルからなるプログラムを考える。 これらをアセンブルして結合するには以下の手順を実行する必要がある。

$ nasm sqsum.s                          -- sqsum.oを生成
$ nasm print_eax.s                      -- print_eax.oを生成
$ nasm isprime.s                        -- isprime.oを生成
$ ld sqsum.o print_eax.o isprime.o      -- 結合してa.outを生成
$ ./a.out                               --  実行
...

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

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

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

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

参考資料:

Makefileの書き方

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

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

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

sqsum.o: sqsum.s
        nasm -felf sqsum.s

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

isprime.o: isprime.s
        nasm -felf isprime.s

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

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

$ make
nasm -felf sqsum.s
nasm -felf print_eax.s
nasm -felf isprime.s
ld -m elf_i386 sqsum.o print_eax.o isprime.o -o sqsum

マクロ

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

# マクロ定義
AS = nasm -felf
LD = ld
LDFLAGS = -m elf_i386
OBJS_SQSUM = sqsum.o print_eax.o isprime.o

# 生成規則
sqsum: $(OBJS_SQSUM)
        $(LD) $(LDFLAGS) $+ -o $@

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

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

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

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

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

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

型規則(パタン規則)

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

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

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

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

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

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

疑似ターゲット

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

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

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

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

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

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

# 自動テスト (sqsumの出力がanswer.txtと一致するか)
# (正解ファイル answer.txt は別途作成済みと仮定)
test: sqsum answer.txt
        ./sqsum | 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_printsqsumtribo を生成するようにしたい場合:

.PHONY: all

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

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

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: sqsum.s
        $(AS) $<
step2: print_eax.s
        $(AS) $<
step3: isprime.s
        $(AS) $<
step4: sqsum.o print_eax.o isprime.o
        $(LD) $(LDFLAGS) $+ -o sqsum

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

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

# よい例

all: sqsum

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

その他,知っておくとよい事柄

上記はmakeコマンドの使い方の基本中の基本しか述べていない。 より詳しいことは,Web上のマニュアル書籍(PDFが無料で公開されている)などを参照。

上では述べていないが演習に関係しそうな事柄をいくつか列挙する。

-nオプション: make -n ターゲット名 を実行すると,そのターゲットに対して実行する予定のコマンドの列が表示される(表示するだけで実行しない)。実際に行う前に,何が行われるか確認したい場合に使う。

-fオプション: make -f ファイル名 ターゲット名 を実行すると,Makefileの代わりに,指定したファイルをビルド設定ファイルとして用いる。 提出用(チームで一つ)のMakefile以外に自分用のビルド設定ファイルを作りたい場合などに役立つ。

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

定義済み規則 (built-in rules) : Makefileに書かなくても予め組み込まれている生成規則がある(Webマニュアルの 10.2 Catalogue of Built-In Rules書籍の2.5節「暗黙ルールのデータベース」を参照)。

例えば tribo.s が存在するディレクトリで make tribo を実行すると,tribo というターゲットを定義していなくても,下記のコマンドが実行される。 これは「%.sから%を作る」規則が予め組み込まれているからだ。

$ make tribo
cc  -m elf_i386  tribo.s   -o tribo
tribo.s: Assembler messages:
tribo.s:1: Error: junk at end of line, first unrecognized character is `,'
tribo.s:2: Error: no such instruction: `equ 10'
tribo.s:4: Error: no such instruction: `section .text'
-- 中略 --
tribo.s:26: Error: suffix or operands invalid for `int'
make: *** [tribo] エラー 1

しかし,上の実行例の通り,本科目で作成するアセンブリ言語プログラムは,この組み込み生成規則を適用するとアセンブルできずにエラーになる。 これは,makeコマンドが想定しているアセンブラ (GNU as) ではないアセンブラ (NASM) を本科目で使っているからだ。

必要な規則をMakefileに明示的に記述すればほとんど問題は起こらないはずだが,もし組み込み生成規則を削除したい場合は,以下のように,コマンド列が空の規則を記述すればよい。

% : %.s      # %.sから%を作る規則をキャンセル

results matching ""

    No results matching ""