第2章
アセンブラーとリンカーのしくみ
この章では、TATARAとTANRENがそれぞれ何をするのか、そしてなぜプログラムを 1回ではなく2つの段階に分けて作るのかを説明します。あわせて、このマニュアルの以降の章で使う用語を紹介し、小さなプログラムを1つ取り上げて、自分で書いたソース・ファイルから実行するプログラムができあがるまでを追っていきます。
2.1 2つの段階
プログラムは、1つまたは複数のソース・ファイル (source file)から始まります。ソース・ファイルは、エディターを使ってアセンブリ言語で書くテキスト・ファイルです。Tataraのソース・ファイルには、ふつう.ASという拡張子を付けます。
TATARAはソース・ファイルを読み、拡張子.TROのオブジェクト・ファイル(object file)を書き出します。オブジェクト・ファイルにはプログラムのマシン語が入っていますが、まだ実行できるプログラムではありません。
TANRENは1つ以上のオブジェクト・ファイルを読み、完成したプログラム・ファイル(program file)を書き出します。MSX-DOS2の場合、これは拡張子.COMのファイルで、MSX-DOS2のプロンプトでその名前を入力すると実行されます。この2つの段階を示したのが図2.1です。
1章のたとえでいえば、ソース・ファイルは原料、オブジェクト・ファイルは玉鋼、プログラムは完成した刀にあたります。この章の残りでは、それぞれの段階で何が起きているのか、そしてなぜ玉鋼を作っておく価値があるのかを説明します。
2.2 アセンブラーがすること
ソース・ファイルは行の並びです。その多くは、ld c,_STROUTのように、人が読める短い言葉で書いたZ80への命令です。しかし、Z80は言葉を読むことができません。Z80が読むのはバイト、つまり0から255までの数で、それがメモリ上に1つずつ並んだものがプログラムのマシン語(machine code)です。ソース・ファイルの各行をそのバイトに変換することをアセンブル(assembling)といいます。
表2.1は、3つの行と、TATARAがそれをアセンブルした結果のバイトを示したものです。バイトは16進数で書いてあります。
各行の最初のバイトは、それがどの命令なのかを表します。命令にオペランドがあれば、そのあとのバイトがオペランドです。アドレスは2バイトを使い、Z80は下位のバイトを先に置きます。そのため、アドレス0005hは、05、00の順に並びます。
_STROUTとBDOSは命令ではありません。これらは09hと0005hという数に付けた名前で、Tataraに付属するインクルード・ファイルの1つ、MSXDOS.INCで定義されています(14章)。このファイルを取り込んだプログラムではこれらの名前が使え、アセンブラーはそれぞれの名前を対応する数に置き換えます。このように数の代わりに使う名前をシンボル(symbol)といいます。
プログラムには、自分自身の中の場所を指す名前も必要です。たとえば、ループの始まりや、表示するメッセージの位置です。このような名前をラベル(label)といいます。ラベルは、msg1:のように行の先頭に書き、あとにコロンを付けます。ラベルは、その行が作る最初のバイトのアドレス(address)、つまりそのバイトが置かれるメモリの位置の番号を表します。ラベルもシンボルの一種です。
それぞれのラベルが何を表すのかを知るために、アセンブラーはバイトを数えながら進みます。プログラムの最初の行が3バイトを作り、2行目がさらに3バイトを作れば、3行目は最初の行から6バイト先から始まるわけです。
注. このマニュアルでは、バイト、16進数、メモリのアドレスが何であるかはわかっているものとします。これらについては、Z80のプログラミングを学ぶための書籍や講座で説明されています。
2.3 アセンブラーにわからないこと
バイトを数えれば、アセンブラーには、それぞれのラベルがプログラムの先頭からどれだけ離れているかがわかります。しかし、1つのソース・ファイルだけではわからないことが2つあります。
- プログラムがメモリのどこに置かれるか たとえば、あるラベルがコードの先頭から17バイト目にあることはわかっても、そのコードがどのアドレスから始まるのかはわかりません。それは、あとでプログラムのすべての部分をまとめるときに決まるからです。このように、コードを置く場所によって変わるアドレスをリロケータブル(relocatable)といいます。
- ほかのファイルにあるものがどこにあるか プログラムは複数のソース・ファイルに分けて書くことができ、あるファイルの行から、別のファイルに書かれたルーチンを呼び出すこともできます。TATARAは1度に 1つのファイルをアセンブルするので、別のファイルで定義された名前のアドレスを知ることはできません。
そこでTATARAは、できるかぎりの仕事をしたうえで、終えられなかったことを書き留めておきます。まだアドレスがわからない場所には、マシン語の中に穴(hole)を残し、その穴に何が入るのかを記録します。こうしてできるのが、オブジェクト・ファイルです。つまり、穴のあいたマシン語と、それぞれの穴についての記録です。これが玉鋼にあたります。
1つのソース・ファイルのコードを、オブジェクト・ファイルに入った形で呼ぶときはモジュール(module)といいます。モジュールは、自分のシンボルの一部をほかのモジュールに提供したり、ほかのモジュールが提供するシンボルを使ったりすることができます。
- パブリック・シンボル(public symbol)は、モジュールが定義し、ほかのモジュールから使えるようにしたシンボルです。PUBLIC疑似命令で宣言します。
- 外部シンボル(external symbol)は、モジュールが使うものの、定義はほかのモジュールにあるシンボルです。EXTRN疑似命令で宣言します。
疑似命令とは、Z80ではなくアセンブラーに対して指示を与える行のことで、それ自体はマシン語を作りません。
2.4 リンカーがすること
TANRENは1つ以上のオブジェクト・ファイルを読み、次の3つのことを行います。
- モジュールを配置する モジュールをメモリ上に順に並べていきます。始まりのアドレスは0100hで、ここはMSX-DOS2がプログラムを読み込み、実行を始める場所です。1
- すべての穴を埋める リロケータブルなアドレスの場合は、TATARAが穴に残しておいた値に、そのモジュールを置いたアドレスを足します。外部シンボルの場合は、そのシンボルをパブリックとして宣言しているモジュールを探し、そのシンボルのアドレスを穴に書き込みます。穴を埋めるこの作業をリロケーション(relocation、再配置)といいます。
- プログラム・ファイルを書き出す 完成したバイトを、MSX-DOS2が読み込んで実行できるファイルに書き出します。2
この仕事全体をリンク(linking)といいます。玉鋼を鍛えて刀にする鍛錬にあたる作業です。
2.5 実例
例として取り上げるTWOMODは、2つのモジュールからなるプログラムで、Tataraに付属するサンプル・プログラムの1つです。4章で実際に作り、28章でもう一度取り上げます。1つ目のモジュールMAIN.ASは、putstrという名前のルーチンを呼び出して2行を表示します。putstrは外部シンボルとして宣言しています。
; MAIN.AS - one of two modules. ; ; It knows that putstr exists. It does not know where putstr is, and ; it does not need to: EXTRN says the name is somebody else's, and the ; linker fills the address in. extrn putstr ; defined in PUTSTR.AS include msxdos.inc ; BDOS and _TERM0 cseg start: ld hl,msg1 call putstr ld hl,msg2 call putstr ld c,_TERM0 jp BDOS msg1: db "Two modules, one program.",13,10,0 msg2: db "The linker joined them.",13,10,0 end start
2つ目のモジュールPUTSTR.ASは、putstrを定義し、それをパブリックとして宣言しています。
; PUTSTR.AS - the other module. ; ; PUBLIC is what makes the name visible outside this file. Without it ; the name would still assemble here and the link of MAIN.AS would ; stop with an undefined symbol - which is worth trying once. public putstr include msxdos.inc ; BDOS, _CONOUT and "system" cseg ; putstr - print a string. ; ; HL is pushed around the BDOS call because MSX-DOS promises nothing ; about the registers it hands back. ; ; Input: HL -> the string, ending in a zero byte ; Output: it is printed ; Modifies: AF, BC, DE, HL putstr: ld a,(hl) or a ret z push hl ld e,a system _CONOUT pop hl inc hl jr putstr end
どちらのモジュールもMSXDOS.INCを取り込んでいます。INCLUDEの行は、そのファイルの行がその場所に書かれているかのように読み込み、それ自体はバイトを作りません。PUTSTR.ASではsystem _CONOUTを使っています。systemはMSXDOS.INCで定義されたマクロで、いくつかの行のまとまりに付けた1つの名前です。ここでは、ld c,_CONOUTとcall BDOSの2行を表します。マクロについては16章で説明します。
2.5.1 TATARAがMAIN.ASから作るもの
MAIN.ASをアセンブルすると、71バイトのマシン語ができ、その中には 4つの穴があります(表2.2)。オフセットは、モジュールの先頭からのバイト数です。
| オフセット | 行 | バイト | 穴 |
| 0 | ld hl,msg1 | 21 11 00 | リロケータブル:msg1、17バイト目 |
| 3 | call putstr | CD 00 00 | 外部:putstr |
| 6 | ld hl,msg2 | 21 2D 00 | リロケータブル:msg2、45バイト目 |
| 9 | call putstr | CD 00 00 | 外部:putstr |
| 12 | ld c,_TERM0 | 0E 00 | なし |
| 14 | jp BDOS | C3 05 00 | なし:BDOSは常に0005h |
| 17 | msg1: db ... | テキスト28バイト | なし |
| 45 | msg2: db ... | テキスト26バイト | なし |
リロケータブルな穴には、ラベルのオフセットが入っています。11hは17で、msg1のオフセットです。外部シンボルの穴には0が入っています。アセンブラーは、putstrについて名前以外は何も知らないからです。オフセット14のjp BDOSは穴ではありません。BDOSは 0005hという決まったアドレスで、プログラムをどこに置いても変わらないからです。
2.5.2 TATARAがPUTSTR.ASから作るもの
PUTSTR.ASをアセンブルすると14バイトになり、穴は1つもありません。system _CONOUTの中のcall BDOSは、決まったアドレスを呼び出します。最後のjr putstrも、アドレスを持っているわけではありません。持っているのは「14バイト戻る」という距離で、コードをどこに置いても同じだからです。このモジュールには、オフセット0にputstrというパブリック・シンボルが1つあります。
2.5.3 TANRENがすること
TANRENは、先に指定されたMAINを最初に置き、そのすぐあとにPUTSTRを置きます。そして、表2.3のように4つの穴を埋めます。
| MAINの配置 | 0100h(71バイト、0146hまで) |
| PUTSTRの配置 | 0147h(14バイト、0154hまで) |
| したがってputstrは | 0147h |
| 0101hの穴 | 0011h + 0100h = 0111h、msg1のアドレス |
| 0104hの穴 | 0147h、putstrのアドレス |
| 0107hの穴 | 002Dh + 0100h = 012Dh、msg2のアドレス |
| 010Ahの穴 | 0147h、putstrのアドレス |
こうしてできるのが85バイトのTWOMOD.COMで、MSX-DOS2はこれを0100hに読み込みます。2つのオブジェクト・ファイルと、それからできるプログラムを示したのが図2.2です。
2.6 コードとデータ:セグメント
プログラムには、命令と、命令が扱うデータという2種類のものが入っています。Tataraでは、この2つを2つのセグメント(segment)に分けておくことができます。コード・セグメント(code segment)はCSEG疑似命令で、データ・セグメント(data segment)はDSEG疑似命令で始めます。
TANRENはモジュールを配置するとき、すべてのモジュールのコード・セグメントを先に順に並べ、そのあとにデータ・セグメントを並べます。そのため、完成したプログラムでは、それぞれの種類がひとまとまりになります。
TWOMODは2つのメッセージをコード・セグメントに置いています。このくらい小さなプログラムならそれで問題はなく、データ・セグメントは空です。セグメントについては、独自の名前を持つセグメントや、プログラムの別々の部分で共有するデータ領域も含めて、10章で詳しく説明します。
2.7 2つの段階に分ける利点
プログラムを2つの段階で作ると、1つの段階で作るよりもコマンドが1つ増えます。そのかわりに、次の4つの利点が得られます。
- 再利用
-
PUTSTRのようなモジュールは、アセンブルし直すことなく、いくつものプログラムにリンクできます。
- 大きさ
-
大きなプログラムは複数のファイルに分けたほうが扱いやすく、アセンブルし直すのは変更したファイルだけですみます。
- 共同作業
-
ルーチンのライブラリを書いた人は、それをアセンブルしてオブジェクト・ファイルを公開することができます。ほかの人は、そのコードを自分のプログラムにコピーすることなく、自分のプログラムにリンクして使えます。ソース・ファイルがまったくなくてもかまいません。オブジェクト・ファイルがあれば十分です。
- 出力の選択
-
プログラムをどこに置くかは最後に決まるので、同じオブジェクト・ファイルから、MSX-DOS2用のプログラム、MSX-BASICのBLOAD用のバイナリ、ROMカートリッジのどれでも作ることができます(26章)。
2.8 この章で出てきた用語
表2.4は、この章で紹介した用語の一覧です。このマニュアルのすべての用語は、用語集(付録L)にまとめてあります。
| 用語 | 意味 |
| ソース・ファイル | アセンブリ言語で書くテキスト・ファイル |
| マシン語 | Z80が読んで実行するバイト |
| アセンブル | ソースの行をマシン語に変換すること |
| シンボル | 数の代わりに使う名前 |
| ラベル | プログラム中のアドレスを表すシンボル |
| アドレス | メモリの位置の番号 |
| リロケータブル | コードを置く場所によって変わるアドレス |
| 穴 | マシン語の中で、アドレスがまだわからない場所 |
| オブジェクト・ファイル | TATARAが書き出す、穴のあいたマシン語 |
| モジュール | オブジェクト・ファイルに入った、1つのソース・ファイルのコード |
| パブリック・シンボル | モジュールがほかのモジュールに提供するシンボル |
| 外部シンボル | モジュールが使うが、定義はほかのモジュールにあるシンボル |
| リロケーション | 穴を埋めること |
| リンク | モジュールを配置し、穴を埋め、プログラムを書き出すこと |
| プログラム・ファイル | TANRENが書き出す、完成したプログラム |
| セグメント | コードまたはデータという、1種類の内容のためのプログラムの領域 |