C++とアセンブラー④

Visual StudioC++が呼び出すアセンブリ関数でレジスターを保護する方法を紹介したが、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レジスターのみを保護しても不十分であることが分かる。

まとめ

このことは、xmmレジスターのみを保護すればよいのではなく、ymmやzmmレジスターを保護する必要があると言うことだろう。ただ、CPU種別でxmmレジスターまで、またはymmレジスターまで、あるいはzmmレジスターまですべてのSIMDレジスターを実装したCPUが存在する。
どこまでポートしているかはcpuid命令を使って知ることができるため、動的に呼び出す関数を切り替えると良いだろう。切り替えについては、簡単なので解説しない。
つづく解説で、ymmレジスターを実装したCPUやzmmレジスターまで実装したCPUのプロローグとエピローグを解説する。