手書きのアセンブリは繰り返しだらけです。 同じ短い命令の並びが、ほとんど変えることなく何度も何度も現れます。定型コードを減らしてくれる抽象化もほとんどありません。 それだけでなく、アセンブリでコードを書くときは、意味が自明でない値を管理する必要があります。 名前の付いた引数はなく、土台となる仕組みの細かい部分のほとんどは、プログラマーが手作業で扱わなければなりません。
このトラックで使われているアセンブラーであるNASMには、どちらの問題にも役立ついくつかのツールがあります。 これらのツールは、命令がプロセッサーに届く前の_アセンブル時_に動作します。 そして、ソーステキストが機械語に翻訳される前に、アセンブラーがどのようにソースを書き換えるべきかを指示します。
こうしたツールの中で最も重要なのがマクロです。マクロは、アセンブラーにとってのレシピのようなものだと考えられます。 マクロは使われた場所で展開され、あたかもそこに手でコードを書いたかのようになります。
マクロはいくつかのリテラルトークンを引数として受け取ることができ、さまざまな場面で再利用できます。
ただし、マクロは関数ではありません。callもretもなく、コードの別の地点へ制御を移すこともありません。
最も単純な定義は、%defineで値や短い式に名前を付けるものです。
%define WIDTH 8
%define BUFFER_SIZE (WIDTH * 16)
以降に現れるWIDTHはすべて、アセンブル前に8へ置き換えられます。
置き換えはテキストとして行われるため、名前はオペランドの中でも、別の定義の中でも、現れる場所すべてで置き換えられます。
数値の_定数_には、%defineよりもequを優先しましょう。
%defineで宣言した1行マクロは、使われたときにだけ展開され、再定義したり未定義にしたりできます。
一方、equの定数は定義した時点で評価され、あとから変更されることはありません。
%defineは_名前付き_の引数を取ることもでき、小さなインライン関数のようにふるまいます。
%define double(x) ((x) + (x))
mov eax, double(WIDTH) ; assembled as mov eax, ((8) + (8))
本体全体と各引数の両方を、括弧(())で囲みます。
%define square(x) x*xだと、square(1+1)という呼び出しは1+1*1+1、つまり3に展開されます。
%define square(x) ((x)*(x))と書けば、同じ呼び出しは((1+1)*(1+1))、つまり4に展開されます。
%defineで定義した名前は大文字と小文字を区別します。
大文字と小文字を区別しない名前にしたい場合は、%idefineという書き方もあります。
1行マクロは、コードのさまざまな部分に意味を与えるのに役立ちます。 注意して使えば、読みやすさが上がり、タイプミスの可能性も減ります。 たとえば、関数の中で専用の役割を持つレジスターは、その役割で定義できます。
%define COUNTER rcx
...
.loop:
...
dec COUNTER
jnz .loop
...
何行にもわたるコードの並びをまるごと取り込むには%macroを使い、%endmacroで閉じます。
名前のあとの数値は、そのマクロが受け取る引数の数を表します。0個でもかまいません。
引数を渡すときは、カンマ(,)で区切ります。
%macro example_macro 0
...
%endmacro
複数行マクロの引数には名前がありません。
渡された順に%1、%2、…として参照します。
; Multiplies two values, returning the result in the first argument
%macro mult_two 2 ; %1 = accumulator, %2 = value
imul %1, %2
%endmacro
mult_two eax, ecx ; multiplies eax and ecx and returns the result into eax
mult_two r8, r9 ; multiplies r8 and r9 and returns the result into r8
引数は、マクロ本体で行う操作にとって有効な構文でなければならない点に注意してください。
ここではmult_two eax, ecxもmult_two r8, r9もアセンブルできますが、mult_two eax, clはできません。imul eax, clが定義されていないからです。
普通のラベルを定義するマクロは、2回使った時点で壊れてしまいます。同じラベルが2回定義されることになるからです。
何度も呼ばれる可能性のあるマクロの中でラベルを使うには、そのラベルに%%を付けます。
こうすると、アセンブラーは展開のたびにそのラベルへ異なる一意な名前を付けます。
; clobbers rcx
%macro clear_n 2 ; %1 = base address, %2 = count of dwords to clear
xor ecx, ecx
%%loop:
mov dword [%1 + rcx*4], 0
inc ecx
cmp ecx, %2 ; %2 is compared with ecx: it is an immediate or a 32-bit operand
jb %%loop
%endmacro
clear_n rdi, 4 ; gets its own %%loop
clear_n rsi, 8 ; a different %%loop, no clash
timesを使うと、1行のコードを繰り返せます。
メモリーに同じ値を繰り返し宣言するときに特に役立ちます。
section .data
negative_block: times 16 db -1
; this assembles to 16 db -1 in sequence, i.e., an array of 16 bytes, all equal to -1
複数行のコードを繰り返すときは、代わりに%repを使います。
%rep 2 ; number of repetitions: 2
add rcx, 32
mov dword [rsi + rcx], 0
%endrep
繰り返しの回数は%repのすぐあとに指定し、繰り返すブロックは%endrepで閉じます。
実行時のループがカウンターを使うのと同じように、%repブロックの中ではアセンブル時のカウンターを使うのが一般的です。
アセンブル時の整数定数はequで宣言できますが、アセンブル時の整数_変数_には代わりに%assignを使います。
%defineと違って、%assignはすぐに数値として評価され、再代入もできます。
xor eax, eax
%assign i 0
%rep 4
add eax, i ; add 0, then 1, then 2, then 3
%assign i i + 1
%endrep
プリプロセッサーが動いたあと、ループは消え、4つのadd命令だけが残ります。即値0、1、2、3が埋め込まれています。
これはアセンブラーによるループ展開です。カウンターレジスターも分岐もなく、ただ一直線のコードが残ります。
プリプロセッサーでは、いくつかの条件付き構文が使えます。 これらは複数の異なる経路から1つを選び、条件が真のときにだけ本体をその場で展開します。
%if ... %elif ... %else ... %endifは、数値の判定によって分岐を選びます。
%if WIDTH > 4
mov eax, 2
%elif WIDTH > 2
mov eax, 1
%else
mov eax, 0
%endif
マクロはよく専用のファイルにまとめておき、必要なソースそれぞれに%includeで取り込みます。
%include "patterns.mac"
指定したファイルは、アセンブルの前にその場所へそのまま挿入されます。中身をそこに直接書いたかのように展開されます。 こうすると定義を1か所にまとめられ、複数のソースファイルで共有できます。
マクロファイルを単体でアセンブルする可能性があるなら、拡張子は.asmにします。
マクロにはリンケージがない点に注意してください。
マクロファイルをアセンブルしても、そのマクロは%includeした別のファイルからしか見えません。
そうでなければ、%include専用のファイルはどんな拡張子でもかまいません。
.asmと.macのどちらを使うのもよくある慣習です。
雑誌の組版エンジンの土台となっているマクロライブラリを保守するのが、あなたの仕事です。 エンジンそのもの、つまり各ページをレイアウトする関数は固定されていて、自分で書く必要はありません。 書くのは、エンジンが組み立てられるときの土台となるアセンブル時の仕組みです。
この演習では、関数を書く必要はなく、マクロだけを書きます。 これらのマクロは、別のファイルですでに定義済みの関数にインクルードされ、呼び出されます。
課題は4つです。
各マクロは、その課題で示されたとおりの名前と引数の順序で呼び出されます。ですから、そのとおりに正確に書いてください。 そうしないと、ファイルはアセンブルできません。
雑誌のフォーマットは、いくつかの決まった数値でできており、レイアウトの残りの部分はそれらを名前でしか参照しません。 それぞれに、アセンブル時に読める名前を付けます。
COLUMN_COUNTは4
COLUMN_STRIDEは18
SPACEは0x20、つまり空白を表すバイトRULEは0x3D、つまり=のバイトRULE_WIDTHは4
PLAINは0x2E、つまり.のバイトRULEDは0x2D、つまり-のバイトBOXEDは0x23、つまり#のバイトエンジンは、これらの値を上の順序で報告します。
out = {4, 18, 32, 61, 4, 46, 45, 35} // the eight named values, in order
fill_runマクロを書くエンジンは、同じバイトを連続して打ち込むことで、ゲラを空白で埋め、マストヘッドの罫線を引きます。 この処理を、エンジンが連続した埋め込みの必要なあらゆる場所で呼び出せる複数行のマクロとしてまとめます。
まず、レジスタrcxにCOUNTERという読みやすい名前を定義します。
次に、配列を同じバイトのN個のコピーで埋めるfill_runマクロを定義します。
引数は次の順序で3つです。
fill_run rdi, rax, SPACE
fill_run rdx, RULE_WIDTH, RULE
fill_runマクロはCOUNTERを自由に壊してかまいませんが、それ以外のレジスタは壊してはいけません。
バイト数は、raxやrsiのような64ビットの汎用レジスタでも、10や4のような即値でも渡せる点に注意してください。
このバイト数が負になることはないと考えてかまいません。
このマクロは、2つの異なる関数の中で3回呼び出されます。
length = 8
page = {32, 32, 32, 32, 32, 32, 32, 32}
width = 8
page = {61, 61, 61, 61, 32, 32, 32, 32} // '====' then spaces
lay_columnsマクロを書くグリッドはフォーマットごとに固定されているので、各カラムの開始位置はアセンブル時に計算できます。
lay_columnsマクロを定義します。このマクロはCOLUMN_COUNT個のオフセットを書き出し、各オフセットは64ビットの整数です。
引数は次の順序で2つです。
lay_columns rsi, rax
書き込み先のアドレスに書き出されるオフセットの個数は固定で、COLUMN_COUNTと等しくなります。
各オフセットはoffset[i] = margin + i * COLUMN_STRIDEで計算される64ビットの値です。ここでiは0から始まるオフセットのインデックスで、marginは2番目の引数です。
このマクロは、引数として受け取った2つのレジスタを壊してかまいませんが、それ以外は壊してはいけません。 呼び出されるのは1回です。
margin = 3
out = {3, 21, 39, 57}
set_headerマクロを書く3つある固定のセクションにはそれぞれ、フォーマットによって決まったヘッダースタイルがあり、そのバイトはアセンブル時にわかります。
set_headerを定義します。引数は次の順序で2つです。
0はプレーン、1は罫線付き、2は枠付きです。書き込み先のアドレスは、実効メモリアドレスの式です。
ベースレジスタに、1、2、4、8のいずれかでスケーリングされたインデックスレジスタを加算でき、さらに即値を取ります。
set_header rdx+8*rcx+2, 1
実行時の分岐ではなく、条件付きアセンブリ(%if / %elif / %else)でヘッダーバイト(PLAIN、RULED、BOXED)を選びます。
このマクロはどのレジスタも壊してはいけません。 同じ関数の中で3回呼び出されます。
pitch = 4
out = {46, 0, 0, 0, 45, 0, 0, 0, 35} // '.', '-', '#'
Exercismに登録すれば、22個のコンセプト130個の演習、そして本物の人間によるメンタリングとともに、x86-64 Assemblyを学んでマスターできます。すべて無料です。