Visual Studio のC++が呼び出すアセンブリ関数でレジスターを保護する方法を紹介したが、xmmレジスターまでしか考慮していない。ymmレジスターやzmmレジスターを搭載したCPUで、本当にxmmレジスターをほごするだけで良いのか考察してみる。
保存するSIMDレジスター
512ビットのSIMDをサポートしたCPUはzmmレジスターを搭載しており、256ビットのSIMDをサポートしたCPUはymmレジスターを搭載している。このような環境でxmmレジスターのみを保護するだけで良いのかプログラムを作って試してみる。もちろん、仕様書から判断するのが良いが、ここでは安直にプログラムで試す。
レジスター一覧
SIMDレジスターとCPUがサポートするビット数で、どのようなレジスターが実装されるか図で示す。zmmレジスターは512ビットである。ymmレジスターは256ビットでありzmmレジスターの下位ビットである。xmmレジスターは128ビットでありymmレジスターの下位ビットである。このため、xmm0~xmm15レジスターは、zmmもしくはymmレジスターのaliasと考えてよい。
いくつかプログラムを作成し、汎用レジスターの下位aliasを操作した時に上位ビットへ影響を与えないか、またSIMDレジスターへも同様の実験を行ってみる。図の汎用レジスターは小さく描いているが、実際のサイズは64ビットであり、SIMDレジスター同様aliasが設定されている。
汎用レジスター
汎用レジスターの下位をアクセスしたときに上位ビットへ影響があるか調べてみる。まず呼び出し側のC++ソースコードを示す。
#include <iostream> #include <ios> #include <iomanip> using namespace std; extern "C" void tGenReg(long long*, long long*); // assembler function int main() { long long iRegs[4], oRegs[4]; for (int i = 0; i < sizeof iRegs / sizeof iRegs[1]; i++) { iRegs[i] = 0x5A5A5A5A5A5A5A5A; oRegs[i] = 0x3939393939393939; } tGenReg(iRegs, oRegs); for (int i = 0; i < sizeof iRegs / sizeof iRegs[1]; i++) { cout << " in=" << hex << setfill('0') << right << setw(16) << iRegs[i] << " "; cout << "out=" << hex << setfill('0') << right << setw(16) << oRegs[i] << endl; } }
次に、アセンブリ言語で記述したソースファイルを示す。
_TEXT segment ; rcx = 入力 long long[4]の先頭アドレス。 ; rdx = 出力 long long[4]の先頭アドレス。 public tGenReg align 16 tGenReg proc mov rax, qword ptr[rcx] sub al, 59h ;期待通り mov qword ptr[rdx], rax mov rax, qword ptr[rcx+8] sub ax, 5a59h ;期待通り mov qword ptr[rdx+8], rax mov rax, qword ptr[rcx+16] sub eax, 3a3a3a3ah ;上位がクリアされる mov qword ptr[rdx+16], rax mov rax, qword ptr[rcx+24] mov qword ptr[rdx+24], rax ;just copy ret tGenReg endp _TEXT ends end
al、axレジスターを操作した時は、上位ビットは影響を受けない:
rax = 5a5a5a5a5a5a5a5a al -= 0x59 rax=5a5a5a5a5a5a5a01 rax = 5a5a5a5a5a5a5a5a ax -= 0x5a59 rax=5a5a5a5a5a5a0001
eaxレジスターを操作すると、raxの上位ビットがクリア:
rax = 5a5a5a5a5a5a5a5a eax -= 0x3a3a3a3a rax=0000000020202020
eaxレジスターを操作すると、raxの上位ビットがクリアされる。汎用レジスターに関しては、全体を保護しているので、下位ビットを操作した時に上位ビットが影響を受けても、レジスター保護の意味では問題は起きない。
ymmレジスター
256ビットのSIMDをサポートしたCPUはymmレジスターを搭載している。このような環境でxmmレジスターのみを保護してymmレジスターまで保護されるか調べてみる。まず、呼び出し側のC++ソースコードを示す。
include <iostream> #include <ios> #include <iomanip> using namespace std; extern "C" void ymmRegs(unsigned char*, unsigned char*); // assembler function int main() { unsigned char iRegs[32], oRegs[32]; for (int i = 0; i < sizeof iRegs / sizeof iRegs[1]; i++) { iRegs[i] = 0x30 + i; oRegs[i] = 0x39; } ymmRegs(iRegs, oRegs); int length = sizeof iRegs / sizeof iRegs[1]; cout << " in="; for (int i = 0; i < length; i++) { cout << hex << setfill('0') << right << setw(2) << (unsigned int)iRegs[i]; } cout << endl << "out="; for (int i = 0; i < length; i++) { cout << hex << setfill('0') << right << setw(2) << (unsigned int)oRegs[i]; } }
次に、アセンブリ言語で記述したソースファイルを示す。
_TEXT segment ; rcx = 入力の先頭アドレス。 ; rdx = 出力の先頭アドレス。 public ymmRegs align 16 ymmRegs proc vmovups ymm1, ymmword ptr [rcx] ; ymm1 = input vmovups xmm1, xmmword ptr [rcx+16] ; xmm1 = inputの後半 vmovups ymmword ptr [rdx], ymm1 ; output = ymm1 ret ymmRegs endp _TEXT ends end
実行結果から分かること:
ymm1=303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f xmm1=ymm1の上位128ビット ymm1=404142434445464748494a4b4c4d4e4f00000000000000000000000000000000
xmm1レジスターを操作すると、ymm1の上位ビットがクリアされる。つまりxmmレジスターのみを保護しても不十分であることが分かる。
zmmレジスター
512ビットのSIMDをサポートしたCPUはzmmレジスターを搭載している。このような環境でxmmレジスターのみを保護してzmmレジスターまで保護されるか調べてみる。まず、呼び出し側のC++ソースコードを示す。
#include <iostream> #include <ios> #include <iomanip> using namespace std; extern "C" void zmmReg(unsigned char*, unsigned char*); // assembler function extern "C" void zmmReg2(unsigned char*, unsigned char*); // assembler function // init void init(unsigned char* in, unsigned char* out, int length) { for (int i = 0; i < length; i++) { in[i] = 0x30 + i; out[i] = 0x39; } } // print void printRegs(unsigned char* in, unsigned char* out, int length) { cout << endl << " in="; for (int i = 0; i < length; i++) cout << hex << setfill('0') << right << setw(2) << (unsigned int)in[i]; cout << endl << "out="; for (int i = 0; i < length; i++) cout << hex << setfill('0') << right << setw(2) << (unsigned int)out[i]; } int main() { unsigned char iRegs[64], oRegs[64]; int length = sizeof iRegs / sizeof iRegs[1]; init(iRegs, oRegs, length); zmmReg(iRegs, oRegs); printRegs(iRegs, oRegs, length); init(iRegs, oRegs, length); zmmReg2(iRegs, oRegs); printRegs(iRegs, oRegs, length); }
次に、アセンブリ言語で記述したソースファイルを示す。
_TEXT segment ; rcx = 入力の先頭アドレス。 ; rdx = 出力の先頭アドレス。 public zmmReg align 16 zmmReg proc vmovups zmm1, zmmword ptr [rcx] ; ymm1 = input vmovups xmm1, xmmword ptr [rcx+32] ; xmm1 = inputの後半 vmovups zmmword ptr [rdx], zmm1 ; output = ymm1 ret zmmReg endp ; rcx = 入力の先頭アドレス。 ; rdx = 出力の先頭アドレス。 public zmmReg2 align 16 zmmReg2 proc vmovups zmm1, zmmword ptr [rcx] ; ymm1 = input vmovups ymm1, ymmword ptr [rcx+32] ; xmm1 = inputの後半 vmovups zmmword ptr [rdx], zmm1 ; output = ymm1 ret zmmReg2 endp _TEXT ends end
実行結果から分かること:
zmm1=303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f xmm1=zmm1の32バイト目から16バイトをコピー zmm1=505152535455565758595a5b5c5d5e5f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
xmm1レジスターを操作すると、zmmの上位3/4がクリアされる。
zmm1=303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f ymm1=zmm1の32バイト目から32バイトをコピー zmm1=505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f0000000000000000000000000000000000000000000000000000000000000000
ymm1を操作すると、zmm1の上位ビットがクリアされる。つまりxmmレジスターのみを保護しても不十分であることが分かる。