C++とアセンブラー②

Visual Studio でプラットフォームにx64 を選ぶとインラインアセンブラを使用できない。理由はスタックやレジスターの管理など、いろいろ考えられる。インラインアセンブラを使用できないということは、アセンブリコードは別のファイルに単独で記述しなければならない。以降に、64 ビット環境で直接アセンブリコードを記述する手順を示す。本プログラムの機能は、インラインアセンブラで書いたものと同様である。

ファイル構成

以降に、ファイルとコンパイラアセンブラーの概念図を示す。

ファイル構成とプロジェクト

アセンブリ言語で関数を作る

x64 ではアセンブリ言語を記述するには関数として記述し、C++ 言語から呼び出す。まず、呼び出し側のC++ 言語で記述したソースファイルを示す。これは、アセンブリ言語で記述した関数を呼び出す。

#include <iostream>

using namespace std;

extern "C" void myCpuid(int*, int); // assembler function

int main()
{
    int regs[4];
    char CPUString[(sizeof(int)) * 3 + 1]{};

    myCpuid(regs, 0);
    *((int*)(CPUString + 0)) = regs[1];
    *((int*)(CPUString + 4)) = regs[3];
    *((int*)(CPUString + 8)) = regs[2];
    CPUString[sizeof CPUString - 1] = '\0';

    cout << "Vender Id: " << CPUString << endl;
}

アセンブリ言語で記述した関数を呼び出すからといって、通常のC/C++ 言語を使用した例と変わらない。C++は、ファイルの拡張子に".cpp" を使用する。C++ 言語では、オーバーロードを使えるため、関数名が一意に関数を特定していない。つまり、同じ名前であっても引数によって別の関数が呼び出される。このため、C++ コンパイラは、ソースコードで使用されている関数名を引数によって修飾した名前に変換する。このため、関数名の変換を避けるため、C 形式の関数であることを示す「extern "C"」を付与して関数名を宣言する。
以降に、C 言語形式(.c ファイル)で記述した際のプロトタイプ宣言を示す。
extern "C" void myCpuid(int*, int);
いずれにせよ関数のプロトタイプ宣言は必要なので、C++ 言語を使用したからといって、特別手間が増えるわけではない。以降に、アセンブリ言語で記述したソースファイルを示す。

include ksamd64.inc

_TEXT       segment

;
; rcx = stringの先頭アドレス。
; rdx = function #
;
; 結果は入力のrcxが指す領域に格納するが rcx は途中で壊れるので r9 を利用する。
;
; 主要な汎用 非 volatileレジスターは保存・復旧する。
;
        public  myCpuid
        align   16

myCpuid proc    frame           ; prologを使う時はproc frame

                                ; prolog
        rex_push_reg    rbx     ; マクロでrbxをpush, 最初はrex_push_regマクロ
        push_reg        rsi     ; 2番目以降はpush_regマクロ
        push_reg        rdi
        push_reg        rbp
        push_reg        r12
        push_reg        r13
        push_reg        r14
        push_reg        r15
        .endprolog              ; end of prolog, rspは保存せず


        mov     r9, rcx         ; addr

        mov     eax, edx
        cpuid
        mov     [r9+0],  eax
        mov     [r9+4],  ebx
        mov     [r9+8],  ecx
        mov     [r9+12], edx


        pop     r15             ; epilog, restore
        pop     r14
        pop     r13
        pop     r12
        pop     rbp
        pop     rdi
        pop     rsi
        pop     rbx             ; end of epilog
        ret

myCpuid endp

_TEXT   ends
        end

本例のアセンブリ言語で開発した関数で、非 volatileなのはrbxレジスターだけである。このため、スタックへ退避するのはrbxレジスターだけで良いが、本例では、ほとんどの汎用レジスター保存・復旧した。もし、破壊してはならないレジスターで、破壊してしまうものが予め判明している場合、当該レジスターのみを保存・復旧するだけでよい。

呼び出し規約とレジスタ

これまで呼び出し規約、特にレジスターの説明を省いている。ここで、アセンブリ言語で作成した関数の呼び出しや、戻り値の扱いについて説明する。

引数と戻り値

アセンブリ命令で記述した関数が呼び出されたときの、引数とスタックを示す。第1 ~第4 引数は直接レジスターに格納される。使用されるレジスターは、引数のデータ型によって決定さる。

表 引数とレジスタ
第5 引数以降はスタックに格納される。
引数とスタック
すべての引数のサイズは8 バイト。RSP はスタックの位置を示すレジスター。スタックに格納されている引数はRSP レジスター相対でアクセスする。引数を格納するためのスタック領域は、呼び出し側で確保と解放が行われる。スタックの第1 ~第4 引数の位置には何も格納されない。 関数に戻り値があるとき、整数やアドレスなどはRAX レジスターに格納する。実数型を返す場合、XMM0 レジスターに格納する。C++では、通常の関数戻り値と同じように扱う。以降に、引数と戻り値をまとめる。
引数
戻り値
整数型・ポインタ
実数型

レジスター一覧と、保存しなくてもよいレジスタ

以降に、Visual StudioC++が呼び出すアセンブリ関数が破壊してよいレジスターと、破壊してはならないレジスターを示す。SSE ではYMM レジスターをXMM レジスターと読み替えること。AVX-512 では、ZMM レジスターが追加されておりZMMレジスターの数は32 まで拡張されている。それぞれのレジスターは、あるレジスターの部分を参照するエイリアスである。 RAX、RCX、RDX、R8 ~ R11、YMM0 ~ YMM5 の各レジスターは破壊して構わない。それ以外のレジスターは保護しなければならない。次図に示す白地のレジスターは、壊して構わない、影付きのレジスターは、保護する必要がある。 XMM レジスターはYMM レジスターの下位ビット、YMM レジスターはZMM レジスターの下位ビット。ZMM0 ~ ZMM5 は、YMM0 ~ YMM5、あるいはXMM0 ~ XMM5 と読み替えることもできる。
破壊してよいレジスターと保護しなければならないレジスター(網掛け部分)
RAX などは64 ビットのレジスターである。RAX レジスターの下位32 ビットを参照するには、EAXレジスターでアクセスする。同様にEAX レジスターの下位16 ビットは、AX レジスターで、AX レジスターの上位8 ビットと下位8 ビットは、それぞれAH レジスターとAL レジスターでアクセスする。それぞれ独立したレジスターではなく、レジスターの一部を参照する別名(alias)である。たとえば、AX レジスターに値を設定すると、EAX レジスターの下位16 ビットが変更されることを意味します。下位ビットを変更すると、上位も影響を受ける場合がある。 YMM レジスターは、ZMM レジスターの下位256 ビット、YMM レジスターも上位128 ビットと下位128 ビットで規定されている。XMM、YMM、そしてZMM レジスターの関係は、全図を参照すること。

x64プラットフォームへ

Visual Studio で作成したプロジェクトが64 ビット環境でない場合があるので、そのようなときは、以下の手順でx64(64 ビット)を追加する。一般的には、現在のVisual Studio を利用していると、最初からx64のプロジェクトなので、以降の作業はスキップしてよい。
  1. 64 ビット化していないプロジェクトを開く。
  2. プロジェクトのプロパティページを開き、プロジェクトエクスプローラーのプロジェクト名の上で、マウスの右ボタンを押しプロパティを選択する。ほかにも、[プロジェクト]メニューの[プロパティ]を選択するなど、多くの方法がある。
  3. プロパティページの[構成マネージャー]をクリックする。
  4. 「構成マネージャー」ダイアログボックスが現れるので、[アクティブソリューションプラットフォーム]のドロップダウンリストから[< 新規作成...>]を選択する。
  5. 「新しいソリューションプラットフォーム」ダイアログボックスが現れるので、その[新しいプラットフォームを入力または選択してください]のドロップダウン矢印をクリックし、64 ビットプラットフォームを選択する。
  6. [OK]をクリックすると、「構成マネージャー」ダイアログボックスの[アクティブソリューションプラットフォーム]に、新しいプラットフォームが表示される。
  7. 「構成マネージャー」ダイアログボックスの[閉じる]をクリックし、次に「< プロジェクト名> プロパティページ」ダイアログボックスの[OK]をクリックする。
これで、64 ビットプラットフォームのプロジェクトが作成できた。

プロジェクトへasmファイルを含める

普通にプロジェクトを作成した場合、asmファイルはプロジェクトに含まれていない。
asmファイルはプロジェクトに含まれていない
まず、このファイルをプロジェクトに追加する。普通にC++ソースファイルを追加し、エクステンションをasmにしても良いが、あらかじめテキストエディタなどで作成しておき、プロジェクトへ既存ファイルとして追加しても良いだろう。以降にasmファイル追加前と、追加、そして追加後を示す。
asmファイル追加前と、追加、そして追加後

ビルドへアセンブリファイルを含める

プロジェクトへasmファイルが追加されたが、このままではasmファイルはビルドに含まれない。このため、ビルドへ追加する必要がある。ソリューションエクスプローラーのプロジェクトを右クリックし「ビルド依存関係」-「ビルドのカスタマイズ」を選択する。
[ビルドのカスタマイズ...]を選択
「Visual C++ ビルドカスタマイズファイル」ダイアログボックスが現れるので、[masm]にチェックを付ける。
「Visual C++ ビルドカスタマイズファイル」ダイアログボックス
ダイアログボックスを閉じ、ソリューションエクスプローラーの*.asm ファイルを選択した状態で、マウスの右ボタンをクリックし、現れたメニューの[プロパティ]を選択する。
[プロパティ]を選択
「.asm のプロパティページ」ダイアログボックスが現れる。
「.asm のプロパティページ」ダイアログボックス
[ビルドからの除外]を[いいえ]に設定し、[項目の種類]に[Microsoft Macro Assembler]を選ぶ。
「.asm のプロパティページ」ダイアログボックス設定
この状態でビルドを行うと、アセンブリ言語で記述した関数も正常にリンクされる。

実行例:Vender Id: GenuineIntel




コマンドライン[x64 Native Tools Command Prompt for VS 20xx]を使って、ビルド・実行した例も示す。
コマンドラインで実行