x64アセンブラ関数の書き方の注意【すごく要注意】スタックが自由に使えない32ビットまでのインラインアセンブラでは好き勝手にpush/popしたりして使えていたスタックがx64では厳しい使用制限を受けることになりました。 具体的には以下のような制限です。
ただし、push/popやその他の方法でRSPを動かさない、何も呼び出さない、壊してはいけないレジスタをセーブ(push/popに限らずいかなる方法でも)しない、例外処理をしない、のすべての条件を満たす関数は「leaf」(関数呼び出しツリーの枝の末端の葉っぱ、くらいの意味でしょうか)と呼ばれ、この制限を受けません。前ページのコーディング例にprolog/epilogがないのはそのためです。 prologとepilogインラインアセンブラの移植で必要になりそうな、「壊してはいけないレジスタ」のpushと、ローカル変数的なものを入れるのに使うスタックスペースの確保に的を絞って書きます。 関数の入り口ではスタックポインタは必ず16の倍数+8になっています(呼び出し元が16の倍数に調整したところへ戻り番地が積まれたため)。他の関数を呼ぶ場合やXMMレジスタをセーブする場合の境界調整には16の倍数+8バイトのデータを上乗せすれば16バイト境界になります。 prologが終わったあとはpush/popはできなくなりますので、中でpush/popを使っているプログラムの移植の場合は、prologの中でレジスタ退避用のワークエリアを確保しておくといいでしょう。でも使えるレジスタが増えてるからあんまり必要ないかもしれません。 面倒なのは、prologで何をしているのかを記述したunwind codeと呼ばれるデータを実行命令とは別に作って専用のセグメントに置いておかなければならないことです。ただし、prolog専用のマクロ命令でソースを書けば、アセンブラが実行命令を作るついでにunwind codeも作って専用セグメントに置いてくれますので、マクロを使って書くのがいいと思います。 実際の書き方を例をあげて説明します。説明は積み上げ式になってるのでケース1から順番に見てください。 ケース1:レジスタ1個を退避するだけのprologとepilog; マクロ定義の取り込み include ksamd64.inc _TEXT segment align 16 sample_1 proc frame ; prologを使う関数ではproc frameと書く ; prolog rex_push_reg rsi ; マクロ命令でrsiをスタックにpush .endprolog ; prologの終わり ; 関数の処理 ; epilog pop rsi ret sample_1 endp ポイント:
ケース2:レジスタ2個を退避して16バイトの作業エリアを確保するprologとepilog; マクロ定義の取り込み include ksamd64.inc _TEXT segment align 16 sample_2 proc frame ; prologを使う関数ではproc frameと書く ; prolog rex_push_reg rsi ; 最初はrex_push_reg push_reg rdi ; 2番目以降はrexなしのpush_reg alloc_stack(16) ; sub rsp,16 .endprolog ; prologの終わり ; 関数の処理 ; epilog add rsp, 16 pop rdi pop rsi ret sample_2 endp ポイント:
ケース3:欠番ケース4:XMMレジスタをセーブするprologとepilogXMMレジスタはpush/popできないので先にalloc_stackでスタックにエリアを確保してからそこにセーブします。汎用レジスタもpushの代わりにこの方法でセーブすることもできます。 ; マクロ定義の取り込み include ksamd64.inc _TEXT segment ; スタックに積むデータの形の構造体を作っておくとアドレス指定しやすい S4_STKFRM struct xmm6_save xmmword ? xmm7_save xmmword ? rsi_save dq ? rdi_save dq ? dq ? ; 埋め S4_STKFRM ends ; サイズが16の倍数+8でないときアセンブルエラーになるようにしておく .erre (size S4_STKFRM mod 16) eq 8, <S4_STKFRM size must be 16n+8> align 16 sample_4 proc frame ; prolog alloc_stack(size S4_STKFRM) save_xmm128 xmm6, S4_STKFRM.xmm6_save ; セーブエリアのアドレスをRSPの相対番地で指定 save_xmm128 xmm7, S4_STKFRM.xmm7_save save_reg rsi, S4_STKFRM.rsi_save save_reg rdi, S4_STKFRM.rdi_save .endprolog ; 関数の処理 ; epilog movaps xmm6, [rsp + S4_STKFRM.xmm6_save] movaps xmm7, [rsp + S4_STKFRM.xmm7_save] mov rsi, [rsp + S4_STKFRM.rsi_save] mov rdi, [rsp + S4_STKFRM.rdi_save] add rsp, size S4_STKFRM ret sample_4 endp ポイント:
ケース5:4096バイト以上のエリアをスタックに確保するprologとepilog4096以上の値をrspから引く場合は、その前に__chkstkというサブルーチンを呼ぶ必要があります。このサブルーチン自体はC/C++形式の関数ではないので呼び出し規約が違います。
; マクロ定義の取り込み include ksamd64.inc _TEXT segment ; スタックに積むデータの形の構造体を作っておくとアドレス指定しやすい S5_STKFRM struct buf1 db 2048 dup (?) ; ローカルで使うなにかのバッファ 2KB buf2 db 2048 dup (?) ; ローカルで使うなにかのバッファ 2KB rsi_save dq ? rdi_save dq ? dq ? ; 埋め S5_STKFRM ends extern __chkstk:proc ; 外部のサブルーチン名を宣言 align 16 sample_5 proc frame ; prolog mov eax, size S5_STKFRM ; rspから引くサイズをeaxに入れてゼロ拡張。mov raxより省スペース call __chkstk sub rsp, rax ; ※ .allocstack size S5_STKFRM ; ※ save_reg rsi, S5_STKFRM.rsi_save save_reg rdi, S5_STKFRM.rdi_save .endprolog ; 関数の処理 ; epilog mov rsi, [rsp + S5_STKFRM.rsi_save] mov rdi, [rsp + S5_STKFRM.rdi_save] add rsp, size S5_STKFRM ret sample_5 endp ※alloc_stackマクロは定数しか引けないので、sub命令でrspからraxの値を引きます。引いていることを記述したunwind codeを作成するために、.allocstackディレクティブを直後に書きます(alloc_stackマクロとは違い、.allocstackディレクティブは実際の引き算はしません。引いたことを記述するunwind codeを作るだけです。alloc_stackマクロは実はsub命令と.allocstackディレクティブの組み合わせです)。 ※の2行の代わりに「alloc_stack(size S5_STKFRM)」でもかまわないと思いますが上のほうが効率的です。 ケース6:中から他の関数を呼ぶ関数のprologとepilog関数を呼ぶ時点でスタックの一番上にパラメータ用のエリアが置かれている必要がありますが、呼ぶ時になってからスタックにパラメータエリアを確保することはできませんので、prologの時点でパラメータ用のエリアを確保しておく必要があります。 homeのエリアは16バイト境界に置く必要がありますので、スタックに上乗せするサイズは16の倍数+8バイトにする必要があります。太線が16バイト境界です。 このエリアは中から呼ぶすべての関数で共通に利用することになりますので、中から呼んでいる関数のうち最大の引数の数の分のエリアが必要です。引数4個以下の関数しか呼ばない(パラメータはすべてレジスタ渡しになる)場合であってもhomeのエリアは最低4個分(32バイト)必要です。5個以上の引数を取る関数を呼ぶ場合はその数だけ必要です。 ケース7 動的にスタックにエリアを確保する関数フレームポインタというメカニズムを使えばprologより後にRSPを動かして実行時に決まるサイズのメモリをスタックに確保できるようになります。 壊してはいけないレジスタのうちひとつをフレームポインタレジスタに決めて、alloc_stack(n)のあとにRSP+適当な定数のアドレスを設定します。unwindに使うレジスタがRSPからフレームポインタレジスタに移ったのを教えるためには当然unwind codeが必要です。アドレス設定はset_frameマクロで記述するのがよいでしょう。もちろんLEA命令+.setframeディレクティブでもかまいません。これにより関数本体ではRSPは動かせるようになりますが代わりにフレームポインタレジスタは動かせなくなります。epilogでは「lea rsp, (n-適当な定数)[フレームポインタレジスタ]」でRSPを復元します。 詳しくはMSDNの「x64 Software Conventions」と「ksamd64.inc」の項目をご覧ください。 おわりにここに書いてある以上のことはMSDNの「x64 Software Conventions」に詳しく書いてありますので検索してご覧ください。 x64命令セットの詳細については「Intel 64 instruction set」でぐぐると出てくるIntelのマニュアル(PDFファイル・英文)がよろしいかと思います。AMDとの違いはWikipediaの「x64」の項目が参考になると思います。
お詫び:他の関数を呼ばない関数はleafでなくてもスタックの16バイト境界調整は必要ありません。旧版を見て余計なalloc_stackを入れてしまった方すみませんでした。 |