ソースファイルの分割
「EAXレジスタの値を10進数で出力する(前ページのprint_eax)」のような汎用的なサブルーチンは,いろいろなプログラムで使いたくなる。 しかし,print_eaxの中身をいろいろなプログラムにコピー & ペーストするのは,手間も掛かるし,同じもののコピーがたくさんできてディスク領域が無駄だ。さらに,print_eaxに修正の必要が生じると,それらのコピーをすべて修正しなければならない。
もっとよい方法は,print_eaxを独立したソースファイルに記述して,ほかのプログラムからそれを利用することだ。 以下は,プログラム本体を定義するソースファイル test_print.s と,print_eaxを定義するソースファイル print_eax.s の二つに分けてプログラムを記述した例だ。
test_print.s
section .text
global _start
extern print_eax ; 別ファイルのラベル
_start:
mov eax, 0xffff
call print_eax ; eaxの値を10進数で出力
mul eax ; eaxの2乗
call print_eax ; eaxの値を10進数で出力
mov eax, 1
mov ebx, 0
int 0x80 ; exit
print_eax.s
; eaxの値を10進数で標準出力に出力する.
; 汎用レジスタの値は保存される (破壊しない).
section .text
global print_eax ; 別ファイルから参照可能にする
print_eax:
-- print_eaxの本体 --
ret ; print_eaxの終端
-- 以降, print_eaxのためのデータ領域の定義など --
アセンブル手順は以下のようになる。
$ nasm test_print.s -- test_print.oを生成
$ nasm print_eax.s -- print_eax.oを生成
$ ld test_print.o print_eax.o -- 結合してa.outを生成
$ ./a.out
65535
4294836225
アセンブル結果であるオブジェクトファイル(.o ファイル)は,複数を結合 (link) することが可能だ。上記の例の場合,test_print.s にはprint_eaxの定義がなく,print_eax.s には実行開始点 (_start) がなく,どちらも単独では不完全だが,それぞれアセンブルしてオブジェクトファイルを生成することは支障なくできる。 これらのオブジェクトファイルをldコマンドに入力すると,.textセクション同士や.dataセクション同士が一つにまとめられ,test_print.o では未確定だったラベルの値(print_eaxの番地)が確定されて,完成した実行可能ファイルが出来上がる。
複数のソースファイルに分割してプログラムを記述する際のポイントは以下の2点だ。
- 別ファイルで使うラベルは「global宣言」する。
- 別ファイルのラベルを使う場合は「extern宣言」する。
例えば上記の例で,ラベル print_eax は print_eax.s でglobal宣言され,test_print.s でextern宣言されている。つまり,ラベルを提供する側でglobal宣言(「このラベルは皆で使うよ」),ラベルを使う側でextern宣言(「このラベルはこのファイルの外で定義されてるよ」)する。test_print.s 中のextern宣言がないと,アセンブル時に「print_eaxが未定義」というエラーになる。print_eax.s 中のglobal宣言がないと,リンク時に「print_eaxが未定義」というエラーになる。
練習問題
以下の問題で述べるプログラムを作りなさい。
リポジトリのルートディレクトリにサブディレクトリ chap6 を作成し,その中にソースファイルを置きなさい。
演習1.6-1 EAXレジスタの値を10進数で標準出力に出力するサブルーチン print_eax を,print_eax.s というファイルにアセンブリ言語で記述しなさい。 リポジトリ中にサブディレクトリ chap6 を作成し,その中に print_eax.s を置くこと。 作成したプログラムを共有リポジトリにpushしなさい。
下記の仕様を満たすように作成すること。
- print_eax.s には,サブルーチン print_eax の定義(及びこのサブルーチンが必要とするデータ領域の定義)のみ記述すること。特に,print_eax.s の中でラベル _start を定義してはいけない。
- ラベル print_eax をglobal宣言すること。
- print_eaxを1回呼び出したときに出力される文字列は,演習1.4-1における出力文字列と同じ仕様を満たすこと。
- print_eax の呼び出し前と復帰後で汎用レジスタ (eax, ebx, ecx, edx, esi, edi, ebp, esp) の値が変化しないようにすること。
(補足)print_eax.s は,上述の test_print.s のような部分プログラムとリンクして使うことを想定している。test_print.s の作成はこの演習では求めていないが,作ってリポジトリにpushしても支障はない。
演習1.6-2 N という名前の名前付き定数を定義し,N 以下の素数を大きい方から順に10個出力するアセンブリ言語プログラムを,10primes.s という名前のファイルに記述しなさい。 ただし,その出力は,演習1.6-1で作成したファイル print_eax.s 中のサブルーチン print_eax を10個の各素数に対して呼び出すことで行いなさい。 print_eax.s は演習1.6-1の成果物をそのまま使用すること(この演習問題用に変更してはいけない)。 N は 10primes.s 中でEQU命令を使って定義しなさい。 N の値は (10/22訂正) 31 以上 220 未満とする。 ソースファイル 10primes.s をサブディレクトリ chap6 に置き,共有リポジトリにpushしなさい。 ただし,演習1.6-1のプログラムをコミットしたメンバーとは別のメンバーがコミットすること(共有リポジトリ上の10primes.sの最終変更者が,print_eax.sの最終変更者と異なるようにすること)。
(補足)「出力は,print_eax.s 中の print_eax を呼び出すことで行う」よう指定されているので,例えば仮に print_eax.s を「EAXの値を16進数で出力する」ように書き換えれば,10primes.s を変更することなく,16進数で10個の素数を出力するプログラムに変わるはずだ。
演習1.6-3 指定された主記憶内領域中のダブルワードの列を,昇順に整列するサブルーチンを記述しなさい。 ソースファイル名を sort.s とし,サブルーチン名を sort とする。 サブディレクトリ chap6 の中に sort.s を置き,共有リポジトリにpushしなさい。 ただし,3人グループの場合,演習1.6-1及び演習1.6-2のプログラムをコミットしたメンバーとは別のメンバーがコミットすること(共有リポジトリ上のsort.sの最終変更者が,print_eax.sの最終変更者とも10primes.sの最終変更者とも異なるようにすること)。2人グループの場合はどちらがコミットしても構わない。
下記の仕様を満たすように作成すること。
- sort.s には,サブルーチン sort の定義(及びこのサブルーチンが整列対象以外のデータ領域を必要とする場合はそのデータ領域の定義)のみ記述すること。特に,sort.s の中でラベル _start を定義してはいけない。
- ラベル sort をglobal宣言すること。
- sort を呼び出す側は,整列対象の先頭番地を EBX,ダブルワードの個数を ECX で指定する。sort は,EBX が指す番地から始まる ECX 個のダブルワードからなる領域の中を書き換える(昇順に整列する)。
- sort は,標準出力に何も出力しない。
- sort の呼び出し前と復帰後で汎用レジスタ (eax, ebx, ecx, edx, esi, edi, ebp, esp) の値が変化しないようにすること。
整列対象のダブルワードはそれぞれ0以上231未満とする(符号の有無を考慮しなくてよい)。 また,ダブルワードの個数は1個以上30万個以下とする。 整列アルゴリズムは自由に選んでよい。 ただし,平均時間計算量が O(n log n) である整列アルゴリズム(クイックソート,マージソート,ヒープソートなど)を実装した方が評価は高い。
(補足)sort.s は,以下のような部分プログラム test_sort.s とリンクして使うことを想定している(このプログラムはさらに print_eax.s もリンクすることを想定している)。
test_sort.s
section .text
global _start
extern sort, print_eax
_start:
mov ebx, data ; データの先頭番地
mov ecx, ndata ; ダブルワードの個数
call sort ; 昇順に整列
mov eax, [data] ; 先頭=最小値
call print_eax
mov eax, [data + 4 * (ndata - 1)] ; 末尾=最大値
call print_eax
mov eax, 1
mov ebx, 0
int 0x80 ; exit
section .data
data: dd 3, 1, 4, 1, 5, 9
dd 2, 6, 5, 3, 5, 8
ndata: equ ($ - data)/4 ; ダブルワードの個数(=バイト数/4)
アセンブル及びリンク手順は以下の通り。
$ nasm sort.s
$ nasm print_eax.s
$ nasm test_sort.s
$ ld sort.o print_eax.o test_sort.o
$ ./a.out
1
9