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を入れてしまった方すみませんでした。  |