2014-04-18

nm(1) の出力の読みかた、あるいはFortranやCの分割コンパイルとリンクとは

なんか最近こういうのを書いておかないといけないと思うんです。どっかに良いテキストがあるんでしょうが、探すより書いちゃったほうが速いかなと。

まずもって実用的なところから。nm コマンドはオブジェクトファイルまたはライブラリを引数にとってオブジェクト内のシンボルを表示するものです。出力の各行はアドレス(あれば)、シンボルの種類、名前です。

まあちょっと例を見せたほうがいいですね:

$ cat a.c
#include <stdio.h>
int x;
int main()
{
  static int s;
  int r;
  r = printf("%d\n", x);
  return r;
}
$ cc -c a.c
$ nm a.o
00000000 T main
         U printf
00000000 b s.1780
00000004 C x

プログラム中の名前がいくつか nm の出力に現れています。種別はこんなところですね:

b: BSS領域(小文字はリンク対象ではないことを示す)
C: コモン領域
D: データ領域(上にはないけどついでに)
T: テキスト領域
U: 未定義シンボル

で、なんたら領域と言われてもわからんという人が多いと思うわけです。

ノイマン型コンピュータではメモリは命令列とデータ(演算対象)のふたとおりに使われると教えると思います。ちょっと古いです。CやFortran(90以降)の実用上は、命令列・データ・スタックの3通りに使うと理解したほうがいいと思います。

(スタックというのは、手続(Cの関数、Fortranのサブルーチンや関数)に入る時に戻り先アドレスを保存したり、手続の中だけで使う変数を確保しておく場所で、手続の呼び出し階層が深くなると一方向に伸びていくものです。これによって、手続が同じ手続を呼び出すこと(再帰呼び出し)が可能になります。いいかえると、FORTRAN77が再帰呼び出しを禁止していたのは、かつてメインフレームにスタックがなかったからです。)

データのメモリを動的に確保して開放のタイミングを自分で制御(手続が終わった後も残したり、手続の途中で開放したり)したいことがあります(Cのmalloc(3)やFortranのポインタに対するALLOCATE)が、これはヒープと呼ばれます。都合4種類になりますね:

メモリの用途
└┬命令列(静的=位置固定)
 ├スタック(動的、手続終了時に開放)
 └データ
     ├ 固定アドレスのデータ(静的)
     └ヒープ(動的、明示的に開放)

さて、大抵のOSではプログラムはファイル(executable file = 実行ファイル。メインフレーム方言ではロードモジュールという)に格納されます。そのファイルには何が書かれるかというと、だいたいは静的な(プログラム開始時にアドレスが確定する)ものです。どこに何という名前で何バイトとっておいて何を書いておけ、というようなことが並んでいるわけです。動的なものは大きさくらいしか書いてないです。

CやFortranでは分割コンパイルができます。ソースコードひとつひとつをコンパイルしたもの(オブジェクトファイル)をリンクして実行ファイルができるわけです。実のところ実行ファイルとオブジェクトファイルは共通のフォーマットになっています。リンクという操作は、オブジェクトファイル内の名前を全部調べて、仮のアドレスをつけかえてアドレスを種類別に連続して並ぶようにしたり、未定義シンボルとなっていたところを他のオブジェクトファイルの同名のシンボルのアドレスで埋めてやるというような操作です。

で、ようやく最初の nm に戻るわけですが、命令列をテキストというのはいいとして(何故ですかね。私も知りません)、固定アドレスのデータにはいくつか種類があります。おもに初期値の与え方によって分かれるわけです:

シンボルの主な種類
└┬ U 未定義シンボル
 ├ T テキスト(命令列)
 └固定アドレスのデータ
     ├ D 非零の初期値があるもの(データ領域)
     ├ B 初期値がゼロなもの(BSS領域)
     └ C 初期値が他のファイルにあるかもしれないもの(コモン領域)

まずいちばん基礎的なのがデータ領域ですわね。初期値があってゼロとか決め打ちできないわけですから、それをファイルに書いておかねばなりません。同じ名前が複数あったらリンカが duplicated symbol というエラーを吐いて教えてくれます。

次はBSS領域。これは初期値がゼロなものです。初期値がゼロな変数はしばしばあるものだし、初期値をファイルに書いておかなくてもいいですわね(あれ、昔はBSS領域を使いすぎるとファイルがでかくなるからだめだと言われたような気がするんだけどなあ…今実験してみてもそんなことはありません)。こいつも外部リンケージを持つなら重複してはいけません。

さっきの関数外 static 変数の例では nm が小文字 "b" を表示しているので、別のソースコードに同じ変数 s があったとしてもかまいません(別物としてアドレスが確保されます)。なんか gcc の場合は単なる s じゃなくて s.1780 とかいう名前になっていますが、デバッガの都合かなんかでしょうかね。よくわかりません。

最後のコモン領域というのは、特色として複数のオブジェクトファイルで同名のシンボルがあってもエラーにならず、一本化されます。初期値は指定できません(どれを選んだらいいかわかりませんね)が、同名のデータ領域のシンボルが1つだけならまとめて一本化されます。

これはC言語としては初期値のない関数外の「int x;」みたいなのでできるものですね。宣言(名前に型を指定するだけでメモリは割り付けない)なのか定義(メモリを割り付ける=大抵は同名が重複してはいけない)なのかよくわからない文で、複数のソースコードで同じ 「int x;」とやってもエラーにならない(宣言みたいだ)けれど、最終的にメモリなしになってしまうのでなく1箇所のメモリが割り当てられるわけですが、そういう性質はコモン領域のものです。

関数内の static じゃない変数 int r; は nm の出力にあらわれていないことにも注意しておきましょう。こういった変数はスタック上にとられます。関数冒頭にスタックを伸ばして r のぶんのメモリを書く命令と、関数の脱出時にスタックを同量縮める命令が入るわけですが、r そのものは固定したアドレスをもたず、関数外から参照することもできない=リンカがリンクすることもないわけです。

ところで、コモンというと Fortran の悪名高きコモンブロックが思い出されますが、実際そうです。モジュールなんかを使わない古典的な(77的な)例を下に示しますが、コモンブロックがコモン領域に、サブルーチン内のSAVEがついた変数がBSS領域に割り当てられます。

$ cat b.f
      SUBROUTINE MAIN(L)
      COMMON /BLK/ I
      INTEGER J, K
      SAVE K
      J = K
      K = L
      L = J
      PRINT *, I
      END SUBROUTINE
$ gfortran -c b.f
$ nm b.o
         U _gfortran_st_write
         U _gfortran_st_write_done
         U _gfortran_transfer_integer
00000004 C blk_
00000000 b k.695
00000000 T main_

【補遺2014-04-20】
フォロワーさんから次の本を勧められました。未見ですが挙げておきます。
CQ出版 リンカ・ローダ実践開発テクニック
    JANコード:JAN9784789838078
2010年9月1日発行
    坂井 弘亮 / 著
http://shop.cqpub.co.jp/hanbai/books/38/38071.html

0 件のコメント:

コメントを投稿