分割コンパイルとリンケージ

| 分割コンパイルと翻訳単位 | extern記憶クラス指定子 | リンケージ | オブジェクトモジュールとライブラリ | 演習問題 | [付録]nmコマンド |

分割コンパイルと翻訳単位

だんだん複雑なプログラムを開発するようになると ソーステキストが長くなり,一つのソースファイルで作成していると 全体を見通すのが難しくなってきます. またほんの一部だけを修正をしたときにも 全体をコンパイルし直さなければならないので, コンパイルに時間がかかるようになります. よって,プログラムをいくつかの部分に分けて 別々に開発したいという要求が自然に起こってきます.

このような考えからプログラミング言語には 一つのプログラムを複数に分割できる機能が提供されることが多く, そのような各部分のことを一般にはモジュールと呼びます. C言語ではモジュールは翻訳単位と呼ばれ, おおまかには一つのソースファイルを一つの翻訳単位に対応 させることで実現しています. ただしヘッダファイル(拡張子が.hのファイル)のインクルードの処理があるので, 厳密には一つのソースファイルに 前処理を行った後のプログラムテキストが翻訳単位になります.

複数のソースファイルが同じヘッダファイルを インクルードしている場合は,それぞれの翻訳単位に 同じ内容が現れることに注意してください. これは次節のリンケージやextern記憶クラス指定子に関わってきます.

従って,C言語のプログラムをコンパイルするときには,詳しく言えば, ソースファイルに前処理を施してから, 翻訳単位ごとにコンパイルを行い, 最後にそれらを結びつけるリンクと呼ばれる処理を行って, 一つの実行可能なプログラムを作成することになります. このような流れを図で示すと次のようになります.

これまで皆さんが使ってきたccコマンドは, このような一連の処理を自動的にやってくれていたわけです. プログラムを分割して開発する場合には (一度にコンパイルすることも出来ますが) 各翻訳単位を別々にコンパイルするには ccコマンドに-cオプションを用いることで 各モジュールごとにオブジェクトモジュール と呼ばれる.oという拡張子の付いたファイルを作成した後, やはりccコマンドを用いてこれらをリンクし, 最終的に実行可能形式と呼ばれる 実行可能なプログラムファイルを作成します. このようにccコマンドは,オプションなどを適切に用いることによって 上の処理の流れを細かくコントロールすることが出来ます. このような手順を分割コンパイルと呼びます.

大抵のccコマンドは実際にはcpp, as,ldなど各処理に応じた 別のコマンドを起動しています. (ただし皆さんの使っているコンパイラでは 前処理を行うcppはccに内蔵されてしまっています.)

それでは,実際に分割コンパイルを行ってみましょう. まず次のような三つのファイルを用意します. これらは前処理により二つの翻訳単位になることに注意してください.

ex1.h

#define NUMBER 5
int f(int);

ex1.c

#include <stdio.h>
#include "ex1.h"

int x=NUMBER;

int main(void)
{
  printf("x = %d\n", x);
  printf("f(x) = %d\n", f(x));
  return 0;
}

f.c

#include "ex1.h"

int f(int x)
{
  x=x*NUMBER;
  return x;
}

まずex1.cとf.cを別々にコンパイルしてみましょう. はじめにex1.cを-cオプションを付けてコンパイルしてみます.

% cc -c ex1.c

するとex1.oというファイルが生成されます. 次に同様にf.cを-cオプションを付けてコンパイルします.

% cc -c f.c

するとf.oというファイルが生成されます.最後にex1.oとf.oをリンクして ex1という実行可能形式ファイルを生成してみましょう.

% cc -o ex1 ex1.o f.o

これを実行すると次のように表示されるはずです.

% ./ex1
x =5
f(x) = 25

このように,ccを使いこなすことによって 各翻訳単位ごとに分割コンパイルしてプログラムを開発することが出来ます.

ちなみにこれらのソースファイルを次のように 一度にコンパイルしてリンクすることもできます.

% cc -o ex1 ex1.c f.c 
% ./ex1
x =5
f(x) = 25

さらに,例えばファイルf.cだけ (例えば*を+にするなどして)修正して再コンパイルし, 残りは以前コンパイルしたオブジェクトモジュールをリンクすることも出来ます.

% cc -o ex1 ex1.o f.c 
% ./ex1
x =5
f(x) = 10

どのようにコンパイルするかは開発に都合の良いように決めればよいでしょう. 実際に大きなプログラムを開発するときには,makeコマンドなど 分割コンパイルをサポートするための様々なツールを活用します.

ページ先頭へ戻る

extern記憶クラス指定子

前々章で大域変数について説明しましたが, そのときはプログラムを分割していなかったので 一つの翻訳単位しか考えていませんでした. しかしプログラムを分割する場合には 一つの大域変数を複数の翻訳単位で共通に使いたい場合も考えられます. このような場合にはextern記憶クラス指定子を用います.例を挙げてみましょう.

ex2.c

#include <stdio.h>

int x=5;  // 大域変数xの定義

void modify(void);

int main(void)
{
  printf("original: x=%d\n", x);
  modify();
  printf("modified: x=%d\n", x);
  return 0;
}

m.c

extern int x; // 大域変数xの宣言

void modify(void)
{
  x=x*2;
}

これをコンパイルして実行するには次のようなコマンドを実行します. すると次のように表示されます.

% cc -o ex2 ex2.c m.c
% ./ex2
original: x=5
modified: x=10

このように書かれたex2.cとm.cをそれぞれコンパイルしてからリンクすると 両方の大域変数xは同じメモリ領域を参照します.

この動作を理解するためにはオブジェクト(変数や関数)の 定義 (definition) と宣言 (declaration)の違いを きちんと区別しておく必要があります.

上の例ではex2.cで行われているのは大域変数xの定義であり, この翻訳単位でこの変数に静的領域のメモリを割り当てるための処理が行われます. それに対してm.cではexternが付いているために これは大域変数xの宣言であるとみなされ, この翻訳単位の別の場所か他の翻訳単位に定義があるはずなので ここではメモリを割り当てずにそちらを参照します.

ここで一つ大事な原則を挙げておきます.それは, プログラム全体で定義はちょうど一つでなければならない ということです. しかしそれを参照する宣言はいくつあっても構いません. 例えば,同じ変数の定義が複数あるのもおかしいし, 一つもないのもおかしいのでどちらもエラーになります. これはしばしば混乱の原因になる ポイントですので,この原則はしっかり押さえておきましょう.

次にさらに微妙な点を確認するために上のプログラムをわざと次のように変更して 試してみましょう.

  1. m.cにおいてextern int x; の代わりにint x;とする.
  2. m.cにおいてextern int x; の代わりにint x=5;とする.

ちょっと不思議なことに1だとコンパイルも実行も正しくできてしまいますが, 2だとちゃんとコンパイル時にエラーが出ます. この挙動をきちんと理解するには次のように大域変数の 外部定義,extern宣言,仮定義というものを区別して考える必要があります

外部定義 (external definition)
int x=5;のように 初期設定付きで記述されたもの.(紛らわしいですがこの 「外部(external)」はexternとは異なり 「すべての関数の外部で定義されたもの」という意味です). メモリが割り当てられて初期値がセットされます.
extern宣言 (extern declaration)
extern int x;のように extern付きで記述されたもの. メモリは割り当てられません.関数の外部にも内部にも出現できます.
仮定義 (tentative definition)
int x;のように初期設定も externもなしで記述されたもの. これはプログラム全体のどこかに外部定義があればextern宣言として振る舞い, なければ外部定義として振る舞います.また,同じ大域変数の仮定義が 複数あっても構いませんが,それらは同じメモリ領域を参照します

つまり上の1の場合は仮定義がextern宣言として振る舞っていたのでエラーにならず, 2の場合は外部定義なので大域変数xの定義が重複して エラーになっていたというわけです. このような仮定義の挙動は紛らわしいので, 特に必要でない限り仮定義を用いるのは避けた方が無難でしょう

大域変数のextern宣言はブロックの内部でも用いることが出来ます. その場合,その翻訳単位におけるそのextern宣言の有効範囲は 宣言からブロックの終わりまでになります. 例えば,その大域変数の定義が別の翻訳単位にある場合には, これは意味のある挙動になります (次節の例を参照).

プログラミング言語における 定義と宣言は厳密には上に述べたように異なる動作であり, この資料ではできるだけ区別して用いていますが,C言語では これらの用語をきちんと区別せずに使っている場合もかなりあります. 特に変数の場合は慣習的に定義も宣言も変数宣言 と呼ばれていたために, 市販の書籍などではそのように書いてあることが多いので, 実際にどちらの動作を意味しているのかは 前後の文脈から読み取るようにしましょう. 上の大域変数の外部定義も動作としては定義なのですが,多くの書籍では 外部宣言(external declaration)と呼ばれることが多く, extern宣言(extern declaration)と非常に紛らわしくなっています.

ページ先頭へ戻る

リンケージ

前節で述べてきたような複数の翻訳単位にまたがる オブジェクト(変数や関数)の共有について整理するために, C言語ではリンケージという概念が導入されています.

ここでいう共有とは,例えば前節の例で 二つの翻訳単位の大域変数xが同じメモリ領域を 参照しているというようなことを意味します.

変数名や関数名のようなオブジェクトの識別子は それぞれ次のいずれかのリンケージを持っています.

外部リンケージ
外部リンケージを持つ識別子は翻訳単位を超えて プログラム全体にわたって他の識別子宣言と結びついて 同じオブジェクトを参照することが出来る.
内部リンケージ
内部リンケージを持つ識別子は翻訳単位内で 他の識別子宣言と結びついて 同じオブジェクトを参照することが出来る. 翻訳単位内ならば有効範囲を超えて共有することが出来る.
リンケージなし
リンケージを持たない識別子は他のいかなる識別子宣言とも結びつかず 一意的にそのメモリ領域を参照する.

外部リンケージと内部リンケージの違いのポイントは 翻訳単位を超えて共有出来るかどうかです. 前節で挙げた例における 大域変数xは翻訳単位を超えて同じメモリ領域を参照していますから 外部リンケージということになります. 内部リンケージを持つオブジェクトの具体例は 記憶クラスと変数の初期設定の章で 説明しなかったstatic大域変数です. そこで以前にとりあげた記憶クラス指定子 とリンケージとの関係についてここで説明しましょう.

まず大域オブジェクトと局所オブジェクトについて, 記憶クラス指定子がauto, static, なし,の場合 のリンケージを表にしてみましょう.

記憶クラス大域局所
指定子なし外部リンケージリンケージなし
static内部リンケージリンケージなし
auto×リンケージなし

つまり,局所変数はすべてリンケージなしなので 共有のことは考えなくて良い,すなわち 大域変数や関数についてのみ共有に気をつければよいということです. (なおCには局所関数がないので 関数はすべて大域変数と同様に大域オブジェクトです).

ここで改めてstatic大域変数について説明しておきましょう. static大域変数,すなわち static記憶クラス指定子付きで定義された大域変数の 名前は内部リンケージを持つので, その変数が定義された翻訳単位内の変数とのみ メモリ領域を共有することが可能です. 従って例えば,複数の翻訳単位で定義された 同じ名前のstatic大域変数はそれぞれ異なる変数として扱われます. なお,そのように翻訳単位内で共有する変数も外部リンケージの場合と同様に extern記憶クラス指定子付きの宣言により宣言することが出来ます.

そこで次に,そのような場合も含めて, extern記憶クラス指定子について考えてみましょう. extern記憶クラス指定子は前節で説明した大域変数のように 大域オブジェクトにだけつきます.これについては 以下のようにまとめることができます.

その翻訳単位内の定義の有無リンケージ
同じ識別子の大域オブジェクトが定義されている その識別子と同じリンケージ
同じ識別子の大域オブジェクトが定義されていない 外部リンケージ

すなわち前の表と合わせると, extern記憶クラス指定子付きで宣言されたオブジェクトは 同じ翻訳単位内に同じ名前のstatic大域オブジェクトが定義されている場合 は内部リンケージであり,それ以外はすべて外部リンケージである ということになります. 例えば下のように始まるプログラムファイルの場合, コメントにあるように x は外部リンケージ,y は内部リンケージ,z は この翻訳単位内に他に定義がないとすれば外部リンケージとなります.

int x;           // 大域変数,外部リンケージ
static int y;    // static大域変数,内部リンケージ

int main(void)
{
  extern int x;  // 外部リンケージ
  extern int y;  // 内部リンケージ
  extern int z;  // 外部リンケージ

システムによってはリンケージによる共有を区別できる 識別子の長さに制限があることがあります. 内部リンケージについては31文字までは規格で保証されています. 外部リンケージについてはシステムに依存しますが 短いものでは6文字程度までしか区別しない場合もあります. このようなシステムの場合は, 変数名の最初の6文字が一致している大域変数は 同じ変数と見なされてしまうおそれがありますので, システムの制限に注意しましょう.

ページ先頭へ戻る

オブジェクトモジュールとライブラリ

各翻訳単位をコンパイルすることにより作成されたオブジェクトモジュールは, その翻訳単位に含まれる各関数をコンパイルしたものと 各大域変数の領域を一つのファイルにまとめたものです.

このように分割コンパイルにより オブジェクトモジュールに関数と大域変数をまとめておけるのであれば, さらに, コンパイルした関数をあらかじめパーツとして作って保存しておいて 必要になったときに利用するためのメカニズムがあれば 便利ではないかと思い浮かびます.現在ほとんどすべてのC言語の 開発環境では,ライブラリと呼ばれる このような機能を提供するメカニズムが用意されています.

ここではライブラリの作成の仕方を詳しく説明することはしませんが. 大まかに言えば複数のコンパイルしたオブジェクトモジュール(.oファイル) を一つのライブラリファイル(.aファイルまたは.soファイル)にまとめる方法 (arコマンドなど)が用意されており, ユーザはそれをccコマンドのオプションを通じて利用することが出来ます.

例えばsin, cosといった数学関数のライブラリは /usr/lib/libm.aおよび/usr/lib/libm.soにまとめられています. このような数学関数のライブラリを利用するには,次のような手順で行います. ここではsin関数を使ってみることにしましょう. まず使いたい関数sinをmanコマンドで調べます. すると次のように表示されます.

% man sin
SIN(3)              Linux Programmer's Manual              SIN(3)



NAME
       sin, sinf, sinl - sine function

SYNOPSIS
       #include <math.h>

       double sin(double x);

       float sinf(float x);

       long double sinl(long double x);

DESCRIPTION
       The sin() function returns the sine of x, where x is given
       in radians.

RETURN VALUE
       The sin() function returns a value between -1 and 1.
.....

引数や戻り値などの説明も重要ですが,ここではSYNOPSISに #include <math.h>とあることから, このようにヘッダファイルを使う必要があることがわかります. そこでこの用法に従って次のような簡単なプログラムを作成してみます.

ex3.c

#include <stdio.h>
#include <math.h>

int main(void)
{
  printf("sin of 1 is %f.\n", sin(1));
  return 0;
}

このプログラムをコンパイルするためにはsin関数をライブラリから 持ってこなければなりません.そのためにはccコマンドの最後に-lm オプションを付けます.

% cc -o ex3 ex3.c -lm
% ./ex3
sin of 1 is 0.841471.

この-lオプションの意味は次のようなものです.すなわち 一般にlibxxx.aもしくはlibxxx.soというライブラリを使用したいときには ccコマンドの最後に-lxxxをつけます. 複数のライブラリを使用したい場合は-lオプションを続けて並べます.

ここで,今までのプログラムでprintfやscanfなどは あらかじめ用意されているライブラリ関数であるはずなのに -lオプションをつけなくてもリンクされていることに 気が付いた人がいるかもしれません. これらはC言語の規格で決められている 標準ライブラリ関数と呼ばれる関数で libc.aもしくはlibc.soというライブラリに含まれており, このライブラリに含まれる関数については 特に-lオプションを付けなくても 自動的にリンクされることになっています.

-lオプションを複数用いる時には 並べる順序に意味があるということに注意しましょう. 後のライブラリに含まれる関数は 前のライブラリに含まれる関数を使わないように 並べなければなりません. これは標準ではないライブラリを複数使用する場合によく問題になります.
 また-lオプションで使用するライブラリファイルは,/lib/や/usr/lib/など コンパイラが自動的に検索するいくつかのディレクトリの いずれかにある必要がありますが, 検索する先(例えばカレントディレクトリなど)を 自分で追加したい場合には-Lオプションを用います.
 さらに,自分でライブラリを作成する際にはその関数の関数プロトタイプを含む ヘッダファイルをたいてい同時に作成することになりますので, ヘッダファイルの検索先を追加する-Iオプションも重要になります. ここではこれ以上説明しませんが, 興味のある人はman ccなどで 各オプションの意味を調べてみるとよいでしょう.

.aライブラリファイルと.soライブラリファイルの違いは 大まかに言えばライブラリ関数をコンパイル時にまとめてひとつの 実行可能形式ファイルにしてしまうか(スタティックリンク), プログラムの起動時にライブラリから引き出すか(ダイナミックリンク)の違いです. 前者はオブジェクトモジュールのリンクと同じなので安全ですが, ファイルが大きくなるために,ディスク容量が必要になったり, 起動に時間がかかったり,複数のプログラムが同じライブラリ関数を使うときに メモリに無駄が生じたりするので,最近のコンパイラでは 両方のライブラリが用意されている場合は特に指定しない限り ダイナミックリンクライブラリを優先的に使用するようになっています.

ページ先頭へ戻る

演習問題

A. 次のプログラムの関数addと残りの部分を別のファイルに分割して, それぞれを別々にコンパイルしてリンクし実行できるように修正しなさい.

#include <stdio.h>

int x=0;

void add(int);

int main(void)
{
  int i;

  for (i=1; i<=10; i++) {
    add(i);
    printf("total=%d\n", x);
  }
  return 0;
}

void add(int n)
{
 x=x+n;
}

B. 次の三つの翻訳単位からなるプログラムに現れる変数xのうち, 同じメモリ領域を参照しているのはどれですか.

ex-a.c

int x;

int main(void)
{
  x=100;
  f1();
  f2();
  return 0;
}

f1.c

extern int x;
int x;

void f1()
{
  extern int x;
  printf("x in f1 is %d.\n", x);
}

f2.c

static int x=200;

void f2()
{
  extern int x;
  printf("x in f2 is %d.\n", x);
}

ページ先頭へ戻る

[付録]nmコマンド

オブジェクトモジュールに含まれるコンパイルされた関数と大域変数は nmコマンドにより見ることが出来ます. 例えば第2節の例に挙がっているm.cをコンパイルして作成した m.oファイルをnmコマンドで表示すると次のように表示されます.

$ nm m.o
00000000 b .bss
00000000 d .data
00000000 t .text
00000000 T _modify
         U _x

細かいことはおいておくとして, とりあえず関数modifyと大域変数xを表す_modifyと_xが 表示されていることがわかると思います.

実際にはnmコマンドはコンパイル時に作成されるシンボルテーブルと 呼ばれるデータを読み出しているだけなので, stripコマンドでm.oファイルからシンボルテーブルを取り去ってしまうと nmでは見ることが出来なくなります.

またライブラリの内容もオブジェクトモジュールと同様に nmコマンドで調べることが出来ます. 例えば数学ライブラリファイル /usr/lib/libm.aの中身をnmコマンドで見るには次のようにします (長いのでパイプでlessを通して少しずつ見ています). 表示される内容はシステムによって異なりますが, 複数のオブジェクトモジュール(.oファイル) から作られていることがわかるでしょう.

$ nm /usr/lib/libm.a | less

k_standard.o:
         U _LIB_VERSION
         U __copysign
         U __errno_location
00000000 T __kernel_standard
         U __rint
         U fputs
         U fwrite
         U matherr
         U stderr
00000000 b zero

s_lib_version.o:
00000000 D _LIB_VERSION

s_matherr.o:
00000000 W __matherr
00000000 W matherr

s_signgam.o:
00000004 C signgam
....

ページ先頭へ戻る