何もしない長い命令、マルチバイト NOP (x86/x64)
x86/x64 CPU には、何もしないマシン語「90」(16進数)が存在します。アセンブリ言語でいうと「NOP」です。
では 2 バイト以上を何もしない命令でふさぎたい場合は「90」を繰り返せばいいのかというと、それでも構いませんが、
- 2 バイトの何もしないマシン語「66 90」
- 3 バイトの何もしないマシン語「0F 1F 00」
- 4 バイトの何もしないマシン語「0F 1F 40 00」
- ……
といった専用の命令が存在します。
こうした何もしない長い命令「マルチバイト NOP」について調べてみます。
動作確認環境
- Windows 11 Home 21H2
- Visual Studio Community 2019
マルチバイト NOP
2 バイト以上の何もしない命令「マルチバイト NOP」は、隠しコマンドというわけではなく、インテルの開発者マニュアル [1] の「NOP」の項にも掲載されている公式の命令です。同書から抜粋して意訳します。
NOP - No Operation
これは何もしない命令です。
1 バイト NOP もマルチバイト NOP も、CPU の命令ストリームを流れますが、
EIP レジスタ(インストラクションポインタ)を進める以外何もしません。
マルチバイト NOP の推奨例を以下に示します。これらは 1 命令として処理されます。
長さ: マシン語 : アセンブリ言語
2 : 66 90 : 66 NOP
3 : 0F 1F 00 : NOP DWORD ptr [EAX]
4 : 0F 1F 40 00 : NOP DWORD ptr [EAX + 00H]
5 : 0F 1F 44 00 00 : NOP DWORD ptr [EAX + EAX*1 + 00H]
6 : 66 0F 1F 44 00 00 : 66 NOP DWORD ptr [EAX + EAX+1 + 00H]
7 : 0F 1F 80 00 00 00 00 : NOP DWORD ptr [EAX + 00000000H]
8 : 0F 1F 84 00 00 00 00 00 : NOP DWORD ptr [EAX + EAX*1 + 00000000H]
9 : 66 0F 1F 84 00 00 00 00 00 : 66 NOP DWORD ptr [EAX + EAX*1 + 00000000H]
9 バイトまでのマルチバイト NOP が掲載されています。
NOP を繰り返すとそれだけ命令数が増えるのに対し、マルチバイト NOP はたとえ長くても 1 命令として処理されるとのことで、処理時間が短いことが期待できます。これがマルチバイト NOP の存在意義と考えられます。
マイクロソフトの定義ファイル
Visual Studio に含まれている「listing.inc」ファイルにも、マルチバイト NOP が定義されています。
......
npad macro size
if size eq 1
DB 90H
else
if size eq 2
DB 66H, 90H
else
if size eq 3
DB 0FH, 1FH, 00H
else
if size eq 4
DB 0FH, 1FH, 40H, 00H
else
if size eq 5
DB 0FH, 1FH, 44H, 00H, 00H
else
if size eq 6
DB 66H, 0FH, 1FH, 44H, 00H, 00H
else
if size eq 7
DB 0FH, 1FH, 80H, 00H, 00H, 00H, 00H
else
if size eq 8
DB 0FH, 1FH, 84H, 00H, 00H, 00H, 00H, 00H
else
if size eq 9
DB 66H, 0FH, 1FH, 84H, 00H, 00H, 00H, 00H, 00H
else
if size eq 10
DB 66H, 66H, 0FH, 1FH, 84H, 00H, 00H, 00H, 00H, 00H
else
if size eq 11
DB 66H, 66H, 66H, 0FH, 1FH, 84H, 00H, 00H, 00H, 00H, 00H
else
if size eq 12
DB 0FH, 1FH, 40H, 00H, 0FH, 1FH, 84H, 00H, 00H, 00H, 00H, 00H
else
if size eq 13
DB 0FH, 1FH, 40H, 00H, 66H, 0FH, 1FH, 84H, 00H, 00H, 00H, 00H, 00H
else
if size eq 14
DB 0FH, 1FH, 40H, 00H, 66H, 66H, 0FH, 1FH, 84H, 00H, 00H, 00H, 00H, 00H
else
if size eq 15
DB 0FH, 1FH, 40H, 00H, 66H, 66H, 66H, 0FH, 1FH, 84H, 00H, 00H, 00H, 00H, 00H
......
15 バイトまで定義されていますが、よく見ると 12 バイト以上は短いマルチバイト NOP の組み合わせになっており、1 命令のマルチバイト NOP としては 11 バイトが最大長です。
本当にこんな「何もしない長い命令」が使えるのか試してみましょう。
x86 アセンブリ言語で次のプログラムを書きます。
.model flat, C
.code
_main PROC
db 66H, 66H, 66H, 0FH, 1FH, 84H, 00H, 00H, 00H, 00H, 00H ; ★ マシン語直接埋め込み
ret
_main ENDP
end
「listing.inc」で定義されている npad マクロを使って書くこともできます。
include listing.inc
.model flat, C
.code
_main PROC
npad 11 ; ★ npad マクロを利用
ret
_main ENDP
end
MASM でアセンブルします。
C:\tmp> ml 11nop.asm /link /subsystem:console /entry:_main
Microsoft (R) Macro Assembler Version 14.28.29913.0
......
/OUT:11nop.exe
11nop.obj
/subsystem:console
/entry:_main
逆アセンブルしてみます。
C:\tmp> dumpbin /disasm 11nop.exe
......
00401000: 66 66 66 0F 1F 84 nop word ptr [eax+eax+00000000h]
00 00 00 00 00
0040100B: C3 ret
......
確かに 11 バイトの命令として解釈されています。
実行します。
C:\tmp> 11nop.exe
C:\tmp>
エラーは出ませんでした。「何もしない長い命令」が実行されたようです。
マルチバイト NOP は本当に速いのか
マルチバイト NOP は本当に速いのか、計測してみましょう。
次のひな型を用意します。二重ループで 1000 億回ループする x86 アセンブリ言語のプログラムです。
.model flat, C
.code
_main PROC
mov edi, 1000000
OuterLoop:
mov esi, 100000
InnerLoop:
; ★ ここに測定対象の命令を記述
sub esi, 1
jne SHORT InnerLoop
sub edi, 1
jne SHORT OuterLoop
ret
_main ENDP
end
そして、「★ ここに測定対象の命令を記述」の箇所に「NOP の繰り返し」や「マルチバイト NOP」を記述し、実行時間を測定します。
CPU のモデルやクロック数によって異なるでしょうが、手元の PC での測定結果は次のようになりました。
【 NOP の繰り返し, 1000 億回 】
長さ: マシン語 : 所要時間(秒)
1 : 90 : 27
2 : 90 90 : 27
3 : 90 90 90 : 26
4 : 90 90 90 90 : 35
5 : 90 90 90 90 90 : 39
6 : 90 90 90 90 90 90 : 53
7 : 90 90 90 90 90 90 90 : 53
8 : 90 90 90 90 90 90 90 90 : 61
9 : 90 90 90 90 90 90 90 90 90 : 69
10 : 90 90 90 90 90 90 90 90 90 90 : 78
11 : 90 90 90 90 90 90 90 90 90 90 90 : 78
【 マルチバイト NOP, 1000 億回 】
長さ: マシン語 : 所要時間(秒)
1 : - : -
2 : 66 90 : 28
3 : 0F 1F 00 : 28
4 : 0F 1F 40 00 : 28
5 : 0F 1F 44 00 00 : 28
6 : 66 0F 1F 44 00 00 : 28
7 : 0F 1F 80 00 00 00 00 : 28
8 : 0F 1F 84 00 00 00 00 00 : 28
9 : 66 0F 1F 84 00 00 00 00 00 : 28
10 : 66 66 0F 1F 84 00 00 00 00 00 : 28
11 : 66 66 66 0F 1F 84 00 00 00 00 00 : 28
グラフにします。
「NOP の繰り返し」は、バイト数が増えるにしたがって所要時間が増える傾向にありました。
一方、「マルチバイト NOP」は、バイト数が増えても所要時間が一定でした。
バイト数が長ければ長いほど「マルチバイト NOP」のほうが速くなると言えます。
ただし、3 バイト以下の場合に「NOP の繰り返し」のほうが微妙に速かったのは、誤差なのか CPU の設計によるものなのか確認できませんでした。
マルチバイト NOP はどこで使われているのか
マルチバイト NOP が役に立つケースは少なそうに思えますが、身近なところでは Visual C++ が生成するコードに使われています。
たとえば、二重ループで 1000 億回ループする C 言語プログラムを書きます(計算内容に意味はありません)。
#include <stdio.h>
int main()
{
int v = 0;
for (int i = 0; i < 1000000; i++)
{
v = v & i;
for (int j = 0; j < 100000; j++)
{
v = v | j;
}
}
printf("%d\n", v);
return 0;
}
x64 の C コンパイラで、/O2 オプション(速度優先最適化)を付けてコンパイルします。
C:\tmp> cl /O2 nop_c.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x64
......
/out:nop_c.exe
nop_c.obj
生成されたコードを見てみます。
C:\tmp> dumpbin /disasm nop_c.obj
......
main:
0000000000000000: 48 83 EC 28 sub rsp,28h
0000000000000004: 33 D2 xor edx,edx
0000000000000006: 45 33 C0 xor r8d,r8d
0000000000000009: 0F 1F 80 00 00 00 nop dword ptr [rax] ; ★ 7 バイト NOP
00
; ★ 外部ループのジャンプ先。直前の 7 バイト NOP により 16 バイト整列済み。
0000000000000010: 41 23 D0 and edx,r8d
0000000000000013: 33 C0 xor eax,eax
0000000000000015: 66 66 66 0F 1F 84 nop word ptr [rax+rax] ; ★ 11 バイト NOP
00 00 00 00 00
; ★ 内部ループのジャンプ先。直前の 11 バイト NOP により 16 バイト整列済み。
0000000000000020: 8D 48 01 lea ecx,[rax+1]
0000000000000023: 0B C8 or ecx,eax
0000000000000025: 83 C0 02 add eax,2
0000000000000028: 0B D1 or edx,ecx
000000000000002A: 3D A0 86 01 00 cmp eax,186A0h
000000000000002F: 7C EF jl 0000000000000020 ; ★ 0x20 に条件分岐
0000000000000031: 41 FF C0 inc r8d
0000000000000034: 41 81 F8 40 42 0F cmp r8d,0F4240h
00
000000000000003B: 7C D3 jl 0000000000000010 ; ★ 0x10 に条件分岐
......
7 バイトと 11 バイトのマルチバイト NOP が使われています。これは、ジャンプ先アドレスを 16 バイト整列するための埋め草です。インテルの以前の最適化マニュアル [2] に「すべての分岐先は 16 バイト整列すべき(All branch targets should be 16-byte aligned.)」とあり、ループの高速化を期待して生成されたのでしょう。
しかし、実際に速くなるかは、さまざまな条件が絡んでくるので何とも言えません。上記プログラムの x64 アセンブラ版を作り、16 バイト整列ありとなしとで比較したところ、整列ありの 10 回平均が 26.24 秒、整列なしの 10 回平均が 26.18 秒となり、誤差の範囲内、あるいは微妙に遅くなっているように見えました。
参考文献
[1] Intel(R) 64 and IA-32 Architectures Software Developer’s Manual, December 2021.
https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
[2] Intel(R) 64 and IA-32 Architectures Optimization Reference Manual, June 2016.
https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf