Pylone Blog

再帰実行しないmakefile

この記事ではmakeの再帰呼びだしに頼らないmakefileを書くためのtipsを紹介します。 社内勉強会向けに作成した資料をベースとして、公開用に再構成しました。

ある程度の規模のプロジェクトのビルドを効率化したり、 LinuxカーネルやAndroid、OpenJDKなどで使われている(kbuild)ビルド機構を理解するのに役立つでしょう。 記述はMakeの拡張機能が使えることを前提としていますが、BSD系のmakeでも類似の技法は機能するはずです。

Recursive Make Considered Harmful

過去にはよく使われたmakefileの書きかたとして、makefileのルールから別のmakeを (recursiveに) 呼びだす、というものがありました。 そのようなmakeの使い方は

  • なぜかビルドにとても時間がかかる
  • ときどきファイルが正しく再生成されない

といった問題を起こすのでやめるべきといわれて10年以上たちますが、いまだにそのような構成のプロジェクトはよく見られます。 makefileを適当に動くものを雛形にして作るなら、できるだけ内容を理解して適切なコピー元を選びましょう。

サンプルプロジェクト

以降では例としてソースファイルの構成が

- (toplevel)
  - src/
      main.c
  - parser/
      parser.grammar

ビルド手順は

  1. parser.grammarからパーサジェネレータ(lemon)でparser.cとparser.hを生成
  2. parser.cをコンパイルしてparser.oを生成
  3. parser.hとmain.cからmain.oを生成
  4. main.oとparser.oをリンクして実行ファイルappを生成

という場合を想定したmakefile群の書き方を考えます。

分割しない場合

今回の例のサイズであれば、単一のmakefileに全ての依存関係を書く:

all: app

src/main.o: src/main.c parser/parser.h

parser/parser.o: parser/parser.c
parser/parser.c parser/parser.h: parser/parser.grammar
        lemon $<

app: src/main.o parser/parser.o
        $(CC) $(LDFLAGS) -o $@ $+

ことも十分可能です。

しかし、現実的なサイズのプロジェクトを単一のmakefileでビルドするのは難しいでしょう。

再帰バージョン

旧い方法に従うと、

  1. サブディレクトリのためのmakefile群 (parser/Makefile, src/Makefile) を用意する
  2. トップレベルのMakefileのルールとして、サブディレクトリでmakeを実行する

ということになります。

トップレベルのMakefile

parser/、src/それぞれのディレクトリでmakeコマンドを起動することになります。

#Makefile
SUBDIRS := parser src
all: $(SUBDIRS) app

$(SUBDIRS):
        $(MAKE) -C $@

app: src/main.o parser/parser.o
        $(CC) $(LDFLAGS) -o $@ $+

.PHONY: all $(SUBDIRS)
サブディレクトリのmakefile

関連するファイルの生成規則だけを含むように分割すると

#parser/Makefile
parser.o: parser.c
parser.h parser.c: parser.grammar
        lemon $<
#src/Makefile
main.o: main.c ../parser/parser.h

となるでしょう。

再帰的makeの問題点

とりあえず使える程度の動作はしますが、

  • 必要がなくても下位makeプロセスが起動されるので遅い
  • makeに平行処理を許した (たとえば make -j3) 場合はビルドに失敗することがある

といった問題を解決するのは困難です。

非再帰バージョン (include directive)

GNU Makeの、makefile内に別のファイルを読み込む拡張機能(include directive)を使えばmakefileの一部を別のファイルへと分割可能です。

この例の場合、src/、parser/以下の依存関係を

#src/build.mk
src/main.o: src/main.o parser/parser.h

および

#parser/build.mk
parser/parser.o: parser/parser.c
parser/parser.h parser/parser.c: parser/parser.grammar
        lemon $<

として抜きだして、トップレベルのMakefileからそれぞれを読みこむ

#Makefile
all: main

main: src/main.o parser/parser.o
        $(CC) $(LDFLAGS) -o $@ $+

include src/build.mk
include parser/build.mk

ようにできます。

非再帰バージョン (下位へのパラメータ渡し)

前の例ではincludeされるファイル内で明示的にディレクトリ名 (src/、parser/) を都度書いていますが、もしこれを修正することになるとかなりの手間がかかります。 makeの変数を使ってincludeする側から渡すようにして、ディレクトリ名に依存してしまう要素を減らしましょう。

makeの変数RELに対象ディレクトリ名が格納されるようにすれば、(ディレクトリ名を$(REL)に置きかえることで) src/build.mkを

$(REL)main.o: $(REL)main.c parser/parser.h

parser/build.mk を

$(REL)parser.o: $(REL)parser.c
$(REL)parser.c $(REL)parser.h: $(REL)parser.grammar
        lemon $<

のように書きなおすことができます。

この場合、トップレベルのmakefile側でincludeする前にRELを設定しておく

all: app

REL := src/
include $(REL)build.mk

REL := parser/
include $(REL)build.mk

app: src/main.o parser/parser.o
        $(CC) $(LDFLAGS) -o $@ $+

ことになります。 なお、includeされるファイル毎に値を変えたいRELはsimply-expanded な (:=を用いた) 変数にしないと、最後の定義によって全ての$(REL)の値が上書きされてしまうことに注意してください。

非再帰バージョン (上位へのパラメータ渡し)

関連するオブジェクトファイルが多くなる場合、すべてを手作業で列挙するのは危険なので、includeされる側でファイルのリストを構築した方がよいでしょう。

トップレベルのmakefileを(simply-expandedな)変数OBJSを利用するように:

all: app
OBJS :=

REL := src/
include $(REL)build.mk

REL := parser/
include $(REL)build.mk

app: $(OBJS)
        $(CC) $(LDFLAGS) -o $@ $+

書いておくと、OBJSはmakefile全体で共有されるため、 src/build.mk、parser/build.mk内でOBJSへ加えた変更がトップレベルの "app" のルール処理に反映されます。

src/build.mk、parser/build.mk 内では、

# src/build.mk
OBJS += $(REL)main.o
# parser/build.mk
OBJS += $(REL)parser.o

のようにして、必要なオブジェクトファイルを順に追加していけばよいでしょう。

ただし、includeされる側で行なう操作はリストへの追加だけにするべきです。リスト自体を上書きしてしまう (「+=」ではなく「:=」を使う)

OBJS := $(REL)foo.o

と、グローバルなOBJSの内容は破壊されます。 下位のmakefileの作成者が信用できないなら、OBJSへの操作を (includeされる側には委ねないで) 上位側に移した方がよいかもしれません。

非再帰バージョン (自動include)

下位ディレクトリが多数存在し、それぞれに対する個々のincludeをトップレベルに書きたくない場合、GNU Make自身に等価な記述を生成させることができます。

下位のmakefileの名前をbuild.mkとしたとき、build.mkが存在する下位ディレクトリのリストは

$(dir $(wildcard */build.mk))

で得られます。これと

  • 引数を与えると
          REL := [引数のディレクトリ名]
          include $(REL)build.mk
    を生成するmakeのユーザー定義関数
  • 組み込み関数foreachによるリスト要素への関数適用

を組み合わせれば、下位ディレクトリのbuild.mkを自動的にincludeするmakefileを

all: app

SUBDIRS := $(dir $(wildcard */build.mk))
OBJS :=

define include_fragment
 REL:= $(1)
 include $(1)build.mk
endef

$(foreach i, $(SUBDIRS), \
        $(eval $(call include_fragment, $(i))))

app: $(OBJS)
        $(CC) $(LDFLAGS) -o $@ $+

のように書くことができます。

こうしておけば、下位ディレクトリを追加した際にはbuild.mkを置くだけでトップレベルのmakefileから自動的に読みこまれます。 あとからmakefileを解読するのは難しくなってしまいますが、目的によっては便利に使えるのではないでしょうか。

ターゲットごとに変数値を設定

includeを使ってmakefileを合成する場合、makeの変数は全体で共有されます。 しかし、一部のターゲットについてだけCFLAGS、LDFLAGSといった変数の値を変更したい場合もあるでしょう。

幸いGNU Makeには

ターゲット : 変数定義

とすることで、ターゲットごとの変数値を設定する機能 (target-specific variable value) が存在します。

たとえば main.o の生成規則を

$(REL)main.o: $(REL)main.c parser/parser.h

として定義していたとき、

$(REL)main.o: CFLAGS += -I parser/
$(REL)main.o: $(REL)main.c parser/parser.h

のように定義を追加することで、このルールを実行する時だけ CFLAGS に「-I parser/」が追加されます。

この機能はincludeディレクティブによって展開された時点の変数の値を保存しておくのにも使えるので、makefile自体のデバッグにも役立ちます。たとえば

$(REL)parser.o: LOCAL_REL := $(REL)
$(REL)parser.o: $(REL)parser.c
        @echo "LOCAL_REL=" $(LOCAL_REL)
        $(CC) $(CFLAGS) -c -o $@ $<

とすれば、上書きされてしまう前にRELを保存しておいて確認することもできるでしょう。

まとめ

新しく作るmakefileでは「$(MAKE) -C」を止めて「include」を使いましょう。

makefileを書くとき、makeの出力に大量の「Entering directory」「Leaving directory」(残念な環境では「入りますディレクトリ」) といったメッセージが含まれるようなプロジェクトを参考にするのは避けるべきです。

年末年始休業のお知らせ

誠に勝手ではございますが、株式会社パイロンは2011年12月27日から2012年1月5日の間を休業とさせていただきます。ご迷惑をおかけいたしますが、よろしくお願いいたします。

ロジック・アナライザ Logic Cube

photo1

Logic Cube LAP-C(16032)

ZEROPLUS 社製 ロジックアナライザ Logic Cube を紹介します。 Logic Cube にはいくつかのラインナップがありますが、本記事で紹介するのはローエンドモデルの LAP-C(16032) です。

パイロンでは、カスタムボードにブートローダや OS を移植する際やデバイスドライバ開発のデバッグに大変重宝しています。また、コンパクトでUSBバスパワーで動作するので客先出張デバッグでも役立っています。

リンク先のサイトを見て頂ければわかる通り、実に様々なプロトコルに対応しています。 標準で添付される、7-SEGMENT LED, I2C, SPI, UART(RS-232C/422/485) に加えて、オプションとしてライセンス購入によりプロトコルが追加できます。

パイロンでは ストロベリー・リナックス さんから購入しました。

本記事執筆時点ではプロトコル解析が30本無料となっているようです。

ロジック・アナライザ

バスタイミングの設定やドライバ開発のデバッグにおいて

  • バスタイミングに従った波形が出ているか
  • (その波形から)プロトコルが成立しているか

この2点が確認できればざっくりとした問題の切り分けが可能になります。

ロジアナでは波形の確認もさることながら、その波形からプロトコルを解析しグラフィカルに表示してくれるので容易にプロトコルの確認が行えます。

RTC(via I2C)のデバッグを例にとると、RTCが動作しない場合はI2CドライバとRTCドライバの両方を疑う必要があります。

  1. そこでまず、I2Cコントローラを制御しきちんとした波形が出ていることが確認できればI2Cドライバは問題なさそうと判断できます。
  2. 次に、その波形からプロトコルを読み取ることができれば、RTCドライバのどの箇所が問題なのかが判明します。

といった具合にロジアナがあるとデバッグがグッと楽になります。

SBC6000X上での使用

では実際に SBC6000X で使用してみます。

RTCをプローブしI2Cバス上の通信を見てみます。I2CではSDAとSDLとGNDの3本をプローブします。 RTCチップは基板上のU32のRX8025SAです。

各ピン配置は以下のようになっています。

Pin FunctionPin No.アプリの識別子
SDAPin 13A0(茶色線)
SDLPin 4A1(赤色線)
GNDPin 11GND(黒色線)

よって、写真のように付属のクリップ(白と黒)を使って茶色線(A0)をPin13に、赤色線(A0)をPin4に、黒色線(GND)をPin11に接続します。

photo2

アプリの設定方法は省略しますがアプリ設定後、本体のボタンを押すかアプリのRun[F5]を押したらhwclockコマンドを実行しRTCとの通信を発生させます。

すると以下のようなスクリーンショットの結果が得られます。

screenshot

RTCのI2Cアドレスである0x32に対して

  1. (時刻)データを要求(write)し
  2. (時刻)データの応答(read)

があるのが見て取れると思います。

最後に

組込み系のデバッグではロジアナによる可視化が問題の切り分けの手助けになることがあります。

通常のロジアナはオシロのようにモニタを有し単体で動作するものが一般的ですが、その中で Logic Cube は画面出力をPCのモニタに任せることによりコンパクトでかつ低価格な製品になっています。

昨今のAndroidブームで評価ボードを個人で所有されている方もいるかと思います。デバッグのお供にこの Logic Cube も所有してみても面白いのではないでしょうか。 個人的にはLinuxから使えれば満点をあげたい製品です。

SBC6000X エミュレータ

先日発売いたしました、組込みLinux開発用CPUボード SBC6000XQEMU でサポートしましたので公開します。

ベースにした QEMU のバージョンは 0.15.1 です。

SBC6000X エミュレータ
qemu-sbc6000x-0.15.1-pylone1.tar.bz2
ソースコード
0.15.1-pylone1ダウンロード4.6MB

概要

ARM926 は本家 QEMU でサポートされているため、同コア の SoC を搭載したプラットフォームがいくつかサポートされています。 しかし、同コアを使用した SBC6000X のマイクロプロセッサである AT91SAM9261 は今のところサポートされていません。

そこで、Linux の起動に必要となる周辺デバイスのエミュレーションを追加しました。

現状

SBC6000X 用 Linux が起動できる必要最小限のエミュレーションを目標にしたため、実機を完全にエミュレート出来るまでに至っていません。

クイックスタート

ホスト OS として Debian GNU/Linux Squeeze を例に説明します。

SBC6000X 用の Buildroot を用いたビルド にて環境一式が整っていることを前提とします (以降、SBC6000X 用 Buildroot のディレクトリを ~/buildroot-sbc6000x とします)。

現時点では、NAND デバイスをまだエミュレートできないため、ここでは、rootfs に NFS を使用します。 以下の設定を追加し、反映します (以降、NFS として export するディレクトリを /opt/sbc6000x とします)。

# vi /etc/exports
/opt/sbc6000x    127.0.0.1(rw,sync,subtree_check,no_root_squash,insecure)
# exportfs -a

次に、QEMU をビルドするためのライブラリ (開発版パッケージ) をインストールします。 既にインストール済みの場合は不要です。

# apt-get install libglib2.0-dev
# apt-get install zlib1g-dev
# apt-get install libsdl1.2-dev

最後に、ビルドして実行するまでの手順です。

$ mkdir ~/qemu-sbc6000x
$ cd ~/qemu-sbc6000x
$ wget http://downloads.pylone.jp/sbc6000x/src/qemu-sbc6000x-0.15.1-pylone1.tar.bz2
$ tar xjf qemu-sbc6000x-0.15.1-pylone1.tar.bz2
$ cd qemu-sbc6000x-0.15.1-pylone1
$ ./configure --target-list=arm-softmmu
$ make
$ ./arm-softmmu/qemu-system-arm \
      -M sbc6000x \
      -m 256 \
      -serial stdio \
      -kernel ~/buildroot-sbc6000x/output/images/uImage \
      -append "console=ttyS0 root=/dev/nfs rw nfsroot=10.0.2.2:/opt/sbc6000x ip=dhcp"

起動後にログイン可能なユーザーは "root"、または "default" (一般ユーザ) です (何れもパスワードはありません)。

終了するには、QEMU ウィンドウを閉じるか、起動した端末上で Ctrl-C を発行して QEMU を終了してください。

その他の詳しい使い方については SBC6000X エミュレータマニュアル を参照してください。

既知の問題点

起動中、まれに、

mmc0: host doesn't support card's voltages
mmc0: error -22 whilst initialising SDIO card
mmc0: host doesn't support card's voltages
mmc0: error -22 whilst initialising MMC card
mmc0: host doesn't support card's voltages
mmc0: error -22 whilst initialising SDIO card
mmc0: host doesn't support card's voltages
mmc0: error -22 whilst initialising MMC card

というタイミングで起動が停止してしまいますが、終了して再起動してみてください。

おわりに

QEMU に対して追加実装した部分の完成度はまだ低いですが、Linux の基本的な動作は確認できると思います。 SBC6000X のソフトウェアの検討や、組み込み Linux 開発の入門を目的とした使い方をしていただければ幸いです。

ドキュメント

SBC6000X の発売について

写真

組込み Linux 開発用 CPU ボード SBC6000X の発売についてご案内いたします。

本製品について

SBC6000XEmbest 社が製造販売している CPU ボードです。輸入元の 株式会社エヌ・エム・アール 様のご協力のもと、付属するソフトウェアを弊社で開発したソフトウェアに置き換えて提供させていただきます。

価格

型番 価格 (税込・送料別)
SBC6000X 28,350

発売日

2011年10月24日

購入方法

弊社の通信販売でお求め頂けます。

製品概要

ハードウェア
  • Atmel AT91SAM9261 SoC (16KB I-Cache, 16KB D-Cache, MMU)
  • 64MB SDRAM
  • 128MB NAND Flash
  • SD (SD Host interace version 1.0)
  • USB 2.0(Full Speed) Host x2
  • USB 2.0(Full Speed) Device x1
  • 100Base-T Ethernet x1
  • UART x2
  • Audio (CODEC: TSC2301)
  • RTC
  • 20ビットバス x1
  • SPI x1
  • USB x1
  • UART x1
ソフトウェア
  • u-boot-2010.09
  • linux-3.0.4
  • buildroot-2011.08
  • uClibc-0.9.32
  • gcc-4.5.3