はじめに
昨今の x86 プロセッサ用 C コンパイラはフェッチ効率を上げるためジャンプ先のアドレスが 16 バイトの整数倍になるようにアラインメント調整を行うが,この際に 1~15 バイト長の NOP 命令(通称,マルチバイト NOP 命令)を挿入してアドレスを調整している。
ところが,アセンブラでも同様にジャンプ先アドレスを 16 バイトの整数倍に並べようとして組み込みの ALIGN
ディレクティブを使用すると jmp
命令などを生成してしまう。現代のプロセッサでは迂闊に jmp
命令を用いるとパイプラインが乱れてパフォーマンスが著しく低下してしまうのに。
そもそもパフォーマンス向上のためにアセンブラを使っているにも関わらず,アラインメント調整用にマルチバイト NOP 命令を使用できないのはおかしい。もちろん命令長とアドレスを調べて,マルチバイト NOP 命令を直接コード内に埋めることはできるが,あくまで筆者はアラインメント調整およびマルチバイト NOP 命令の生成を自動化したいのだ。
なぜ Netwide Assembler(NASM)か?
-
筆者の PC には既に Netwide Assembler(NASM)がインストールされていたから。mozjpeg をビルドするときに必要だったのだ1。
-
Microsoft Macro Assember(MASM)は1パスだが,Netwide Assembler(NASM)は2パスなので複雑な構文を扱うことができるかもしれない2。
-
Microsoft Macro Assembler(MASM)とオブジェクトコード互換のため3。
おしながき(仕様案)
- アラインメントするサイズは 16 バイト固定とする。これはマルチバイト NOP 命令が最大 15 バイト長だからだ。
- 16 バイト整列用コードはマクロ
align16
として定義し,ヘッダファイルalign16.inc
として分離する。使用する際にはヘッダファイルを%include
して呼び出す。
実装コード
この結果を得られるまで試行錯誤を繰り返したが,いきなり結果を見せることにしよう。アラインメント調整用マクロのヘッダファイルを示す。
%macro align16 0
%%skip equ ($ - $$) % 16
%if %%skip = 15
db 0x90
%elif %%skip = 14
db 0x66, 0x90
%elif %%skip = 13
db 0x0F, 0x1F, 0x00
%elif %%skip = 12
db 0x0F, 0x1F, 0x40, 0x00
%elif %%skip = 11
db 0x0F, 0x1F, 0x44, 0x00, 0x00
%elif %%skip = 10
db 0x66, 0x0F, 0x1F, 0x44, 0x00, 0x00
%elif %%skip = 9
db 0x0F, 0x1F, 0x80, 0x00, 0x00, 0x00, 0x00
%elif %%skip = 8
db 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00
%elif %%skip = 7
db 0x66
db 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00
%elif %%skip = 6
db 0x66, 0x2E
db 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00
%elif %%skip = 5
db 0x66, 0x66, 0x2E
db 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00
%elif %%skip = 4
db 0x66, 0x66, 0x66, 0x2E
db 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00
%elif %%skip = 3
db 0x66, 0x66, 0x66, 0x66, 0x2E
db 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00
%elif %%skip = 2
db 0x66, 0x66, 0x66, 0x66, 0x66, 0x2E
db 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00
%elif %%skip = 1
db 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x2E
db 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00
%endif
%endmacro
上記のマクロを用いたテスト用コードを示す。なお,Netwide Assembler (NASM) の times 10 db 0x90
は Microsoft Macro Assember(MASM)の db 10 dup(90h)
と同じである。
%include "align16.inc"
section .text align=16
global _main
_main:
db 0x90
align16
times 2 db 0x90
align16
times 3 db 0x90
align16
times 4 db 0x90
align16
times 5 db 0x90
align16
times 6 db 0x90
align16
times 7 db 0x90
align16
times 8 db 0x90
align16
times 9 db 0x90
align16
times 10 db 0x90
align16
times 11 db 0x90
align16
times 12 db 0x90
align16
times 13 db 0x90
align16
times 14 db 0x90
align16
times 15 db 0x90
align16
ret
Makefile を示す。メイク nmake.exe およびリンカー link.exe は Visual Studio のものを用いた。
target: TEST.EXE
TEST.EXE: TEST.OBJ
link $** /entry:main
.ASM.OBJ:
nasm $< -f win32
clean:
if exist *.OBJ del *.OBJ
if exist *.EXE del *.EXE
テスト用コードの逆アセンブル結果
Netwide Assembler (NASM) にも逆アセンブラ ndisasm.exe は付属しているが,今回は Visual Studio の dumpbin.exe を使用した。
マルチバイト NOP 命令の箇所を黄色 #FFFF00
で示すが,無事 1~15 バイト長のマルチバイト NOP 命令を出力できている。
解説
Netwide Assembler (NASM) も Microsoft Macro Assember(MASM)と同じく記号 $
を用いて現在のアドレスを得ることができるが,このアドレスはリンク時に決定されるのでアセンブル時には分からない。よって下記のコードは Netwide Assembler (NASM) でもエラーになってしまう。
%%skip equ $ % 16
ところが Netwide Assembler (NASM) ではセクションの先頭アドレスを記号 $$
を用いて得ることができ,現在のアドレスとの差分 $ - $$
であればアセンブル時に確定するためエラーにならない。
%%skip equ ($ - $$) % 16
このマクロを用いる場合は下記のようにセクション自体を 16 バイト境界に整列させておく必要がある。
section .text align=16
基礎的な実験
Netwide Assembler (NASM) において,現在のアドレス $
およびセクション先頭からの相対アドレス $ - $$
がどのように取り扱われているのか確認してみた。具体的には下記の3種類のアドレスをコードセクション内に組み込んだ。
-
@abs
現在のアドレス(絶対アドレス)#00FFFF
-
@rel
セクション先頭からの相対アドレス#00FF00
-
@diff
プロシジャー先頭からのアドレス差分#FFFF00
アセンブルしたオブジェクトファイルをダンプしてみると,このうち相対アドレス @rel
とアドレス差分 @diff
の値は確定しており,絶対アドレス @abs
のみ 0x00000001
という暫定値が割り当てられた上で RELOCATION
リストに登録されている。なお,リストの DIR32
の意味は 32bit のVA(仮想アドレス)という意味である4。
次にリンクして出来た実行イメージファイルをダンプすると,絶対アドレス @abs
の値が書き換えられていることが分かる。すなわち絶対アドレスが確定するのはリンク時である。
dumpbin コマンドでオブジェクトファイルのダンプが取れることを今回初めて知った。