ビルド処理

実行ファイル、ライブラリとソースコードファイルとの関係をソースコードの位置付けにて説明しました。ただし説明は概念的なものでした。ここではビルド処理の中心を担うgccのコマンドラインを具体的に示します。またビルドツールの一つであるmakeについて説明します。

gccのコマンドラインを示すのは、2つ目的があります。

一つはビルド処理を解析して、読むべきソースコードを割り出せるようになるための予備知識となるためです。

規模の大きいソフトウェアを調査する場合、ソースコードファイルの数は増え、実行ファイル、ライブラリとソースコードの関係が複雑化します。開発者にとっても複雑なビルド処理を手動で実施するのは困難です。そこでビルドツールを用いて、一連のビルド処理を自動化しています。開発者はビルドスクリプトと呼ばれるファイルにビルド手順を記述します。ビルドツールはビルドスクリプトを入力として実行ファイルやライブラリをビルドします。

そのため、読むべきソースコードを割り出すのにビルドスクリプトを読む必要が出てきます。具体的なコマンドラインを知っていればビルドスクリプトを読むときの礎となります。

もう一つは、実験用のプログラム(以下ミニプログラム、独自用語)を書いて動かすためです。ソースコードを読んでいると、読み辛い箇所が出てきます。そのとき、その部分だけ切り出した、動作確認だけを目的とするプログラムを作成し、実行したくなることがります。その結果を見て読解の補助とします。このときミニプログラムに(読解対象のソフトウェアと同様に)ライブラリをリンクしたり、特別なビルド条件(後述)を与える具体的な方法を知っておく必要があります。

gccのコマンドライン

gccのコマンドラインオプションには大変な種類があります。ここでは主要なものを紹介します。詳細についてはinfoドキュメントを読んで下さい。

プリプロセス

プリプロセスだけの実行を参照して下さい。

コンパイル処理

foo.cというソースコードをコンパイルしてfoo.oを得るための典型的なコマンドラインの例を示します。厳密には-Iと-Dはプリプロセッサへの指示になります。

$ gcc -g -O2 -c -I. -I../include -DDEBUG=1 foo.c

オプションの意味を説明します。

-cは、リンク処理は実施せずコンパイル処理までを進めよ、という意味になります。gccはソースコードファイルの拡張子.c.oで置き換えた名前のオブジェクトファイルにコンパイル結果が出力します。

-IPATHは 指定したパスをインクルードパスへ追加します。

gccはソースコード中にインクルードディレクティブ(#include)があると、ディレクティブで指定されたファイル(インクルードファイル)をインクルードパスから探します。gccにはデフォルトのインクルードパスが内蔵されています。-Iで指定したパスは、デフォルトのインクルードパスに優先してインクルードファイルの検索の対象となります。

例にある-I. -I../includeというのは、カレントディレクトリ(.)と親ディレクトリ(..)の直下にある include という名前のディレクトリから優先して、foo.c中でインクルードしているファイルを探せという意味になります。

gccに内蔵されたデフォルトのインクルードパスを確認するには空の.cファイルを用意して、そのファイル名とともに-vオプションをつけてgccを呼び出します:

$ touch baz.c
$ gcc -v baz.c
#include <...> search starts here:
 /usr/lib/gcc/x86_64-redhat-linux/4.7.2/include
 /usr/local/include
 /usr/include
End of search list.
#include <bar.h>

とある場合、gccはインクルードパスからbar.hを探します。

#include "bar.h"

とある場合、gccはインクルードパスに優先してカレントディレクトリからbar.hを探します。みつからなければインクルードパスから探します。

-Dはマクロを定義します。#defineと同じ意味です。-DDEBUG=1というのは以下の記述と同じ意味になります。

#define DEBUG 1

という意味になります。このようなマクロ定義は、次に示すような条件付きコンパイルの条件として参照されます。

#ifdef DEBUG
#include <stdio.h>
#define LOG(X) fprintf(stderr, "LOG: %s\n", X)
#else
#define LOG(X)
#endif

...

LOG("something debug log");

.cファイルをいくら調べても、コンパイル時にどのような条件が与えられたのかがわからないと、

LOG("something debug log");

の処理を説明できません。

他に良くみかけるオプションに-g-Onがあります。-gはデバッガの使うデバッグ情報をコンパイル結果に埋め込め、という指示となります。一方-Onはレベルnの最適化を実施せよ、という指示となります。

リンク処理

(この説明では共有ライブラリを想定しています。)

コンパイル処理が完了して複数のオブジェクトファイルが用意できているとします。複数のオブジェクトファイルをリンクして実行ファイルを得るための典型的なコマンドラインの例を示します。

$ gcc -o my_app -L . -L ../libs -lmy_cypto -lmy_algorithm foo.o bar.o baz.o

-o FILEで生成する実行ファイルの名前を指定します。

-lNAMEでリンクするライブラリの名前を指定します。-lNAME``と指定す るとgccはlibNAME.so という名前のライブラリファイルを後述するライブラリ サーチパスから探し実行ファイルにリンクします。例では ``-lmy_cypto -lmy_algorithmとしているので gccはlibmy_cryto.so とlibmy_algorithm.so を my_app にリンクします。

ライブラリパスは gcc がリンク処理時にライブラリを指すパスのことです。-print-search-dirsオプションをつけてgccを呼び出すと、gccに内蔵されたデフォルトのライブラリパスを確認できます。

$ gcc -print-search-dirs
...
libraries: =/usr/lib/gcc/x86_64-redhat-linux/4.7.2/:...

-LPATHは 指定したパスをライブラリパスに追加します。

なお実行ファイルの生成にあたり、実行ファイル中から参照されている全ての変数、関数について定義を発見できない場合、リンク処理は失敗します。

ライブラリ

ここで、ビルドの観点からライブラリを使おうとすると.hファイルと.soファイルの2つがかかわってきます。

.hファイル

ライブラリに関連する変数や関数の宣言および型の定義が記載されています。これはコンパイル処理、すなわち.cから.oへ変換するのに必要となります。プログラムの実行時には必要ありません。

.soファイル

(この説明では共有ライブラリを想定しています。)

ライブラリに関連する変数や関数の定義が記載されています。これはリンク処理及び実行時に必要となります。

ライブラリのソースコード中で「プログラム全域」のスコープを持つ変数と関数だけが「エクスポート」されます。そのライブラリにリンクするアプリケーションやライブラリは「エクスポート」された変数と関数だけを利用することができます。

エクスポートされた変数や関数は通常ライブラリのヘッダファイルに宣言が記載されています。しかし稀にライブラリの開発者がエクスポートしていても利用してもらいたくない場合と考える変数や関数について宣言を記載しない場合があります。

nmコマンドを使うとライブラリからエクスポートされている変数や関数の名前を知ることができます。

$ nm -D /usr/lib/libz.so.1
...
         U close
42681e30 T compress
42681d40 T compress2
         U free
...

Tはエクスポートされている関数を意味します。Uは未定義の名前を意味します。例にはありませんが、エクスポートされた変数を意味するB, Dの記号があります。

::
$ nm -D /usr/lib/libc.so.6 | grep ‘ free$‘421ee090 T free

とあるのでzlibで未定義だったfreeはlibcで関数として定義されていることがわかります。

パッケージング

Fedoraでは目的によって構成を柔軟に変更できるよう、ライブラリについて.soファイルを含むバイナリパッケージから独立した.hだけを含むバイナリパッケージを用意しています。

.hだけを含むバイナリパッケージはライブラリを自身のアプリケーションに組込もうとしている開発者にだけ必要であろう、という理由からパッケージ名に -devel というサフィックスが付けられています。

なおソースパッケージは一つで共通です。

例:

$ rpm -qi zlib
Name        : zlib
Version     : 1.2.5
Release     : 7.fc17
...
Summary     : The zlib compression and decompression library

$ rpm -ql zlib
/lib64/libz.so.1
/lib64/libz.so.1.2.5
...

$ rpm -qi zlib-devel
Name        : zlib-devel
Version     : 1.2.5
Release     : 7.fc17
...
Summary     : Header files and libraries for Zlib development

$ rpm -ql zlib-devel
/usr/include/zconf.h
/usr/include/zlib.h
...

ビルドツールとビルドスクリプト

ビルドツールはビルド手順を記述したファイル(ビルドスクリプト)を入力としてビルド処理を実行します。すなわちリンカやコンパイラを呼び出して実行ファイルやライブラリを生成します。またなんらかのツールを呼び出してソースコードを生成する場合もあります。

ビルドツールの引数やビルドスクリプトを変更することでビルド結果を調整できるようになっているものもあります。例えばビルド全体で使うコンパイラの最適化のオプションを与えたり、実行時に開発者向けの情報がより多く出力されるようデバッグモードを有効にしたり、選択的な機能を組込むかどうかを指定できるようになっています。このようなビルド時の調整内容をビルドコンフィギュレーションと呼びます。

ソースコードの記述に使われている言語がC言語で同じであったとしても、ビルドツールの選択とビルドスクリプト、すなわちビルドツールで何をやらせるかというのはソフトウェア毎に大きく異なります。ここではFedora上で動作するソフトウェアのビルドに広く使われているmakeコマンドの概要を説明します。

FedoraなどGNU/Linuxディストリビューションは、様々なソフトウェアをビルドしてバイナリパッケージの形で提供します。ビルドツールやその呼び出し手順が異なる可能性のある様々なソフトウェアをビルドするのは簡単ではありません。

Fedoraの開発ではソフトウェア毎にspecと呼ばれるファイルを用意して、個別のソフトウェアのビルド手順からFedoraのバイナリパッケージを作成する仕掛け(rpmbuild)を分離しています。ソフトウェア毎のパッケージ保守担当者がspecファイルを作成します。specファイルに、そのソフトウェアで採用しているビルドツール(を含むパッケージの名前)とビルドツールの呼び出し手順を記述します。

ソースコードを読む立場からすると、関心のあるソフトウェアについてパッケージ化されているのであれば、specファイルを読むことでどのようなビルドツールを使っていて、それをどのような手順で呼び出すのかがわかります。あるかどうかわからないビルド手順を説明した文章をソースコードツリー中から探すよりも効率が良いでしょう。ただしspecファイルもまた独自の文法と意味を持つので、それを知っておく必要があります。

makeとMakefile

makeコマンドは特に指定しないと実行したディレクトリにあるMakefileという名前のファイルを探しそれを入力、すなわちビルドスクリプトとしてビルド処理を実行します。

Makefileには、ビルドの各ステップ(ルール)を以下の書式で記述します。

成果物: 入力 ...
        入力から成果物を得るコマンドライン
        ...

成果物(ターゲット)は一つとは限りません。あるターゲットが他のターゲットの入力となるように記述することで巨大なビルド処理をルールの集合として記述できます。

あるターゲットをビルドするにはmakeの引数にターゲットを指定して呼び出します:

make [ターゲット]

特にターゲットを指定しない場合、Makefile中にならぶルールのうち、先頭のルールに記載されたターゲットがビルドの対象となります。

makeコマンドは読み込んだMakefileを調べてターゲットをビルドするのに必要な入力が何か特定します。すでに入力(として記載されたファイル)が存在すれば、ルールの記述通り入力からターゲットを得るコマンドラインを実行します。もし入力が存在しなければ、まずその入力をターゲットとするルールを探し、入力にあたるファイルをビルドしようとします。これを再帰的に繰り返し、引数で指定されたターゲットに必要な入力をビルドした上でターゲットをビルドします。

ターゲットがすでに存在する場合でも、ターゲットをビルドし直すことがあります。ファイルシステムから得られる入力の最終更新日時と、ターゲットの最終更新日時を比較して、入力の方が新しい場合にはターゲットをビルドし直します。

Makefileの例

今foo.c、bar.cをコンパイルしてfoo.o、bar.oを生成し、その二つをリンクして、myappという実行ファイルを作成したいとします。foo.c、bar.cがMakefileと同じディレクトリにあるとするとMakefileは次のように書けます。

myapp: foo.o bar.o
        gcc -o myapp foo.o bar.o
foo.o: foo.c
        gcc -c foo.c
bar.o: bar.c
        gcc -c bar.c

変数

Makefile中で変数を定義、参照することができます。繰り返し使う文字列や、ビルド時に変更可能なオプションを明示するのに使うことができます。変数を定義するには、

VAR = VALUE

とします。参照するときは、

$(VAR)

とします。初期化されていない変数を参照すると空文字列となります。

次に例を示します.

DEBUG_FLAGS = -g -DDEBUG=1
OFLAGS      = -O2
CFLAGS      = -I. -I../include $(OFLAGS) $(DEBUG_FLAGS)

myapp: foo.o bar.o
      gcc -o myapp foo.o bar.o
foo.o: foo.c
      gcc -c $(CFLAGS) foo.c
bar.o: bar.c
      gcc -c $(CFLAGS) bar.c

変数の値はmakeコマンドを呼び出すコマンドラインから指定することもできます:

$ make VAR="VALUE" ターゲット

コマンドラインで指定した変数がMakefile中にも定義されている場合、その値はコマンドラインで指定したものが優先します。上の紹介したMakefileの例であれば、次のようにしてDEBUG_FLAGSの値をコマンドラインからクリアできます:

$ make DEBUG_FLAGS= myapp

foo.cやbar.cで条件付きコンパイルの条件にDEBUGを参照している場合、コンパイル結果が変ってきます。それはmakeファイルの呼び出し引数によって、読むソースコード箇所も変ってくる、ということを意味します。

内蔵ルール

makeコマンド自体がcコンパイラを呼び出して.cから.oを得るといった、典型的な利用ケースに対するルールを内蔵しているので、実際には内蔵ルールを利用することでMakefileの内容を短くすることができます。

内蔵ルールを利用した例

DEBUG_FLAGS = -g -DDEBUG=1
OFLAGS      = -O2
CFLAGS      = -I. -I../include $(OFLAGS) $(DEBUG_FLAGS)

myapp: foo.o bar.o
      gcc -o myapp foo.o bar.o

内蔵ルールの閲覧

make -p -f /dev/null

とすると全ての内蔵ルールが出力されます。以下は.cファイルから.oファイルの生成に関連する部分だを抜粋したものです。

CC = cc
OUTPUT_OPTION = -o $@
COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
.c.o:
    $(COMPILE.c) $(OUTPUT_OPTION) $<

rpmbuildとspecファイル

Fedoraの開発プロジェクトが維持するリポジトリからの入手にて延べた手順に従い、ソースコードツリーを合成する方法を延べました。specファイルはsrc.rpmをインストール(rpm -ivh *.src.rpm)すると ~/rpmbuild/SPECS以下に配置されます。

rpmbuild のオプションとして -bp を指定してソースコードツリーを合成しました。バイナリパッケージを作成するには、-bp のかわりに -bb を指定します:

$ rpmbuild -bb ~/rpmbuild/SPECS/coreutils.spec
...

バイナリパッケージは、~/rpmbuild/RPMS/x86_64/coreutils-8.15-7.fc17.x86_64.rpmといった名前で生成されているはずです。出力を見てバイナリパッケージ作成の過程でmakeやgccが呼び出されていることを確認して下さい。

specファイルにはソースコードを合成する手順やビルドするのに必要なビルドツールを含むパッケージ(ビルド時依存パッケージ)とビルド手順が記載されています。

coreutilsパッケージのspecファイル~/rpmbuild/SPECS/coreutils.specを見てみましょう。

ビルド時依存パッケージは:

BuildRequires: パッケージ...

という文法で記載されています。

BuildRequires: libselinux-devel
BuildRequires: libacl-devel
BuildRequires: gettext bison
BuildRequires: texinfo
BuildRequires: autoconf
BuildRequires: automake
%{?!nopam:BuildRequires: pam-devel}
BuildRequires: libcap-devel
BuildRequires: libattr-devel
BuildRequires: gmp-devel
BuildRequires: attr
BuildRequires: strace

coreutils.specからBuildRequiresを指定した行を抜粋しました。Fedoraの開発プロジェクトが維持するリポジトリからの入手にて延べた通り、yum-builddepを使うと、引数で指定したパッケージを作成するのに必要となるパッケージ、すなわち、そのパッケージのspecファイル中でBuildRequiresとして指定されているパッケージをインストールします。

行頭%ではじまる行で、specファイルは理論的に分割されています。分割された各範囲をセクションと呼びます。ソースコードツリーを合成するのに使った-bpオプションを指定してrpmbuildを実行すると %prepセクション に記載された内容までを実行します。バイナリパッケージの作成に使った -bb オプションを指定してrpmbuildを実行すると%prepセクションに加えて%buildセクションに記載された内容までが実行されます。以下にcoreutils.specの%prepセクションと%buildセクションを抜粋します。

%prep
%setup -q

# From upstream
%patch1 -p1 -b .trunc
%patch2 -p1 -b .xnondir

# Our patches
%patch100 -p1 -b .configure
%patch101 -p1 -b .manpages
%patch102 -p1 -b .tcsadrain
%patch103 -p1 -b .sysinfo
%patch104 -p1 -b .dfdirect
%patch107 -p1 -b .mkdirmode

...

chmod a+x tests/misc/sort-mb-tests tests/df/direct tests/cp/attr-existing || :

#fix typos/mistakes in localized documentation(#439410, #440056)
find ./po/ -name "*.p*" | xargs \
 sed -i \
 -e 's/-dpR/-cdpR/'

%build
export CFLAGS="$RPM_OPT_FLAGS -fno-strict-aliasing -fpic"
%{expand:%%global optflags %{optflags} -D_GNU_SOURCE=1}
#autoreconf -i -v
touch aclocal.m4 configure config.hin Makefile.in */Makefile.in
aclocal -I m4
autoconf --force
automake --copy --add-missing
%configure --enable-largefile %{?!nopam:--enable-pam} \
           %{?!noselinux:--enable-selinux} \
           --enable-install-program=su,hostname,arch \
           --with-tty-group \
           DEFAULT_POSIX2_VERSION=200112 alternative=199209 || :

# Regenerate manpages
touch man/*.x

make all %{?_smp_mflags} \
         %{?!nopam:CPPFLAGS="-DUSE_PAM"}

# XXX docs should say /var/run/[uw]tmp not /etc/[uw]tmp
sed -i -e 's,/etc/utmp,/var/run/utmp,g;s,/etc/wtmp,/var/run/wtmp,g' doc/coreutils.texi

%check
...

%prepセクションではアップストリームに由来するソースコードを変更します。%prepセクションの大部分が%patchで始まる行です。%patchで始まる行は、パッチファイルを適用してソースコードを変更することを指示しています。Fedoraの開発プロジェクトが維持するリポジトリからの入手の箇所で ソースコードを「合成する」と書きました。合成とは具体的には%prepに記載されたソースコード変更処理、特にパッチファイルを適用することだったのです。

%buildセクションを見てみると最終的にmake allを実行することがわかります。

変更履歴とパッチファイル

ソースコードを読んでいるうちに、あるソースコード行について変更の経緯を調べたくなることがあります。ソースコードがバージョン管理システムに格納されていれば、変更履歴を調べることになります。対象のソフトウェアがフリー/オープンソースソフトウェアであれば、開発者がメーリングリストに投稿するパッチファイルを調べることになります。

変更履歴もパッチファイルも同じ形式(diff形式)で具体的な変更が説明されています。ここではdiff形式の読み方を説明します。

diffコマンドを使うと2つのファイルの差異をdiff形式で表示できます。

$ cat x.c
#include <stdio.h>
int
main(void)
{
  printf("hello, world\n");
  return 0;
}

$ cat y.c
#include <stdio.h>
int
main(void)
{
  const char* msg = "hello, world";
  printf("%s\n", msg);
  return 0;
}

$ diff -u x.c y.c
--- x.c     2013-01-28 06:20:41.271385668 +0900
+++ y.c     2013-01-28 06:21:11.575162951 +0900
@@ -2,6 +2,7 @@
 int
 main(void)
 {
-  printf("hello, world\n");
+  const char* msg = "hello, world";
+  printf("%s\n", msg);
   return 0;
 }

x.c(オリジナルファイル)を編集更新してy.c(更新ファイル)を得たとして、その差分をdiffコマンドで出力しています。

で始まる行はオリジナルファイルを+++で始まる行で更新ファイルを表します。

--- x.c     2013-01-28 06:20:41.271385668 +0900
+++ y.c     2013-01-28 06:21:11.575162951 +0900

続く@@で始まる行は、オリジナルファイルと変更ファイル中で差異のあった行の番号と範囲を表します。

@@ -2,6 +2,7 @@

オリジナルファイルx.cの2行目から6行、更新ファイルの2行目から7行に差異があったことがわかります。

行頭の-と+が具体的は差異を表します。-は削除された行、+は追加された行です。

 int
 main(void)
 {
-  printf("hello, world\n");
+  const char* msg = "hello, world";
+  printf("%s\n", msg);
   return 0;
 }