リンカーとライブラリ

シンボル解析

シンボル解析は、簡単で直感的に分かるものから、複雑で当惑するようなものまで、すべての範囲を実行します。解析は、リンカーによって自動的に実行されるか、警告診断プログラムを伴って表示されるか、またはその結果、重大なエラー状態になります。

2 つのシンボルの解析は、シンボルの属性、シンボルを入手したファイルのタイプおよび生成されるファイルのタイプによって異なります。シンボルの属性についての詳細は、「シンボルテーブル」を参照してください。ただし、以下に説明するシンボルタイプは、識別する価値のある 3 つの基本的なシンボルタイプです。

未定義シンボル

このシンボルは、ファイル内で参照されましたが、記憶領域アドレスが割り当てられていません。

一時的シンボル

このシンボルは、ファイル内で作成されましたが、まだサイズが決められていないか、または記憶領域内に割り当てられていません。このようなシンボルは、初期化されていない C シンボル、または FORTRAN COMMON ブロックとしてファイル内に表示されます。

定義シンボル

このシンボルは、作成されてからファイル内の記憶領域アドレスおよびスペースが割り当てられています。

この最も単純な形式においては、シンボル解析には優先度関係の使用が伴います。つまり、定義シンボルは一時的シンボルよりも優先され、次に一時的シンボルは、未定義シンボルよりも優先されます。

次の C コードの例では、これらのシンボルタイプがどのようにして生成されるかを示しています (未定義シンボルには接頭辞 u_ が、一時的シンボルには接頭辞 t_ が、定義シンボルには接頭辞 d_ がそれぞれ付いています)。


$ cat main.c
extern int      u_bar;
extern int      u_foo();

int             t_bar;
int             d_bar = 1;

d_foo()
{
        return (u_foo(u_bar, t_bar, d_bar));
}
$ cc -o main.o -c main.c
$ nm -x main.o

[Index]   Value      Size      Type  Bind  Other Shndx   Name
...............
[8]     |0x00000000|0x00000000|NOTY |GLOB |0x0  |UNDEF  |u_foo
[9]     |0x00000000|0x00000040|FUNC |GLOB |0x0  |2      |d_foo
[10]    |0x00000004|0x00000004|OBJT |GLOB |0x0  |COMMON |t_bar
[11]    |0x00000000|0x00000000|NOTY |GLOB |0x0  |UNDEF  |u_bar
[12]    |0x00000000|0x00000004|OBJT |GLOB |0x0  |3      |d_bar

単純な解析

このシンボル解析は、群をぬいて最もよく使用されるもので、類似した特徴を持つ 2 つのシンボルが削除された場合や、1 つのシンボルが他のシンボルよりも優先される場合に実行されます。このシンボル解析は、リンカーによって自動的に実行されます。たとえば、同じ結びつきを伴う複数のシンボルでは、あるファイルから未定義シンボルへのリファレンスは結合されるか、または他のファイルからの定義シンボルまたは一時的シンボル定義によって満たされます。あるいは、あるファイルからの一時的シンボル定義は、他のファイルからの定義シンボルの定義に結合されます。

解析を受けるシンボルは、大域結合またはウィーク結合されます。ウィーク結合の方が、大域結合よりも優先度が低くなります。そのため、異なる結びつきを伴うシンボルは、基本規則のわずかな変更に従って解析されます。しかし、まず初めに、シンボルがどのように作成されるかを紹介しましょう。

ウィークシンボルは通常、個別にあるいは大域シンボルの別名として、コンパイラによって定義されます。この機構では、pragma 定義を使用します。


$ cat main.c
#pragma weak    bar
#pragma weak    foo = _foo

int             bar = 1;

_foo()
{
        return (bar);
}
$ cc -o main.o -c main.c
$ nm -x main.o
 
[Index]   Value      Size      Type  Bind  Other Shndx   Name
...............
[7]     |0x00000000|0x00000004|OBJT |WEAK |0x0  |3      |bar
[8]     |0x00000000|0x00000028|FUNC |WEAK |0x0  |2      |foo
[9]     |0x00000000|0x00000028|FUNC |GLOB |0x0  |2      |_foo

ウィークの別名 foo に、大域シンボル _foo と同じ属性が割り当てられていることに注意してください。この関係は、リンカーによって保持され、その結果、シンボルには出力イメージ内の同じ値が割り当てられます。シンボル解析においては、ウィーク定義シンボルは、同じ名前の大域定義によって自動的に上書きされます。

単純なシンボル解析のこの他の形式は、再配置可能オブジェクトと共有オブジェクト間、または複数の共有オブジェクト間に発生し、割り込み (interposition) と呼ばれます。このような場合、シンボルが複数回定義されている場合、リンカーにより、再配置可能オブジェクト、または複数の共有オブジェクト間の最初の定義が自動的に採用されます。再配置可能オブジェクトの定義、または最初の共有オブジェクトの定義は、他のすべての定義上に割り込みを行うといわれます。この割り込みを使用すると、1 つの共有オブジェクト、動的実行可能ファイル、または他の共有オブジェクトによって提供された機能を上書きできます。

ウィークシンボルと割り込みを組み合わせることにより、非常に有用なプログラミングテクニックを使用できます。たとえば、標準 C ライブラリは、再定義可能ないくつかのサービスを提供していますが、ANSI C は、システム上になければならない一連の標準サービスを定義し、厳密に適合するプログラム内に置き換えることはできません。

たとえば、関数 fread(3C) は、ANSI C ライブラリの関数ですが、関数 read(2) は、ANSI C ライブラリの関数ではありません。適合する ANSI C プログラムは、read(2) を再定義でき、予測できる方法で fread(3C) を使用できなければなりません。

ここでの問題は、read(2) は、標準 C ライブラリ内に fread(3C) を実装する基盤になるため、read(2) を再定義するプログラムは、fread(3C) の実装を間違える可能性があるのではないかと思われます。このような間違いをなくすためには、ANSI C は、実装には、そこに予約されていない名前は使用できないように指定します。さらに、以下に示す pragma 指示語を使用することにより、この予約名を定義できます。


#pragma weak read = _read

また、この予約名から、関数 read(2) の別名が生成されます。こうすることにより、ユーザーは、fread(3C) の実装に妥協することなく、自分専用の read() 関数を自由に定義できます。

標準 C ライブラリの、共有オブジェクトまたはアーカイブバージョンのどちらにリンクしている場合でも、ユーザーによる read() の再定義に対するリンカーからのクレームはありません。前者の場合には、割り込みによって方法が決められ、後者の場合には、read(2) の C ライブラリの定義をウィークにすることにより、自動的に上書き可能になります。

リンカーの -m オプションを使用すると、割り込みされるすべてのシンボルリファレンスのリストが、セクションの読み込みアドレス情報とともに標準出力に書き込まれます。

複雑な解析

複雑な解析は、同じ名前を持つ 2 つのシンボルが、異なる属性とともに検出された場合に発生します。この場合、リンカーは最も適切なシンボルを選択し、そのシンボル、対立する属性、およびそのシンボル定義を取り出したファイルの ID を示す警告メッセージを生成します。次に例を示します。


$ cat foo.c
int array[1];

$ cat bar.c
int array[2] = { 1, 2 };

$ cc -dn -r -o temp.o foo.c bar.c
ld: warning: symbol `array' has differing sizes:
        (file foo.o value=0x4; file bar.o value=0x8);
        bar.o definition taken

ここで、データ項目の配列の定義が指定された 2 つのファイルでは、サイズの必要条件が異なっています。シンボル配列の必要条件が異なる場合には、同様の診断プログラムが作成されます。この 2 つのケースの場合、リンカーの -t オプションを使用すると、診断プログラムを抑制できます。

この他の属性形式の違いに、シンボルの「タイプ」があります。次に例を示します。


$ cat foo.c
bar()
{
        return (0);
}
$ cc -o libfoo.so -G -K pic foo.c
$ cat main.c
int     bar = 1;

main()
{
        return (bar);
}
$ cc -o main main.c -L. -lfoo
ld: warning: symbol `bar' has differing types:
        (file main.o type=OBJT; file ./libfoo.so type=FUNC);
        main.o definition taken

ここで、シンボル bar() は、データ項目と関数の両方として定義されています。


注 -

このコンテキスト内の型とは、ELF で表されたシンボルタイプのことです。このシンボルタイプは、まったく手を加えられていない場合を除いて、プログラミング言語が使用するデータ型には関連していません。


このような場合には、再配置可能オブジェクトと共有オブジェクト間で解析が発生したときには、再配置可能オブジェクトの定義がとられます。または、2 つの共有オブジェクト間で解析が発生した場合には、最初の共有オブジェクトの定義がとられます。このような解析が異なる結合間 (「ウィーク」または「大域」) で発生すると、警告メッセージも同時に作成されます。

リンカーの -t オプションを使用しても、シンボルタイプ間の不一致は抑制できません。

重大な解析

解析できないシンボルの重複によって、重大なエラー状態が発生します。この場合、シンボルを入手したファイルの名前と同時に、そのシンボル名を指摘した適切なエラーメッセージが表示されて、出力ファイルは生成されません。この重大なエラー状態によってリンカーは停止しますが、すべての入力ファイルの処理が、まず最初に完了します。この要領で、重大な解析エラーをすべて識別できます。

最も一般的な、重大エラー状態は、2 つの再配置可能オブジェクトが両方とも同じ名前のシンボルを定義していて、どちらのシンボルもウィーク定義ではない場合に発生します。


$ cat foo.c
int bar = 1;

$ cat bar.c
bar()
{
        return (0);
}

$ cc -dn -r -o temp.o foo.c bar.c
ld: fatal: symbol `bar' is multiply defined:
        (file foo.o and file bar.o);
ld: fatal: File processing errors. No output written to int.o

ここで、foo.cbar.c には、シンボル bar に対する重複する定義があります。リンカーは、どちらを優先すべきか判別できないため、通常、この処理は放棄されます。ただし、リンカーの -z muldefs オプションを使用すると、このエラー状態を防ぐことができ、リンカーが、最初のシンボル定義をとるように設定できます。