Visual Studio でプラットフォームにx64 を選ぶとインラインアセンブラを使用できない。理由はスタックやレジスターの管理など、いろいろ考えられる。インラインアセンブラを使用できないということは、アセンブリコードは別のファイルに単独で記述しなければならない。以降に、64 ビット環境で直接アセンブリコードを記述する手順を示す。本プログラムの機能は、インラインアセンブラで書いたものと同様である。
- ファイル構成
- アセンブリ言語で関数を作る
- 呼び出し規約とレジスター
- レジスター一覧と、保存しなくてもよいレジスター
- x64プラットフォームへ
- プロジェクトへasmファイルを含める
- ビルドへアセンブリファイルを含める
アセンブリ言語で関数を作る
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 Studio のC++が呼び出すアセンブリ関数が破壊してよいレジスターと、破壊してはならないレジスターを示す。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のプロジェクトなので、以降の作業はスキップしてよい。- 64 ビット化していないプロジェクトを開く。
- プロジェクトのプロパティページを開き、プロジェクトエクスプローラーのプロジェクト名の上で、マウスの右ボタンを押しプロパティを選択する。ほかにも、[プロジェクト]メニューの[プロパティ]を選択するなど、多くの方法がある。
- プロパティページの[構成マネージャー]をクリックする。
- 「構成マネージャー」ダイアログボックスが現れるので、[アクティブソリューションプラットフォーム]のドロップダウンリストから[< 新規作成...>]を選択する。
- 「新しいソリューションプラットフォーム」ダイアログボックスが現れるので、その[新しいプラットフォームを入力または選択してください]のドロップダウン矢印をクリックし、64 ビットプラットフォームを選択する。
- [OK]をクリックすると、「構成マネージャー」ダイアログボックスの[アクティブソリューションプラットフォーム]に、新しいプラットフォームが表示される。
- 「構成マネージャー」ダイアログボックスの[閉じる]をクリックし、次に「< プロジェクト名> プロパティページ」ダイアログボックスの[OK]をクリックする。