随想録(関数スタック)


【声明:著作権所有、転載歓迎、商業用途に使用しないでください.連絡ポスト:[email protected]
 
コンパイラがファイルをコンパイルするとき、ソフトウェアはプログラム自体の要求に応じて関数を異なるスタック処理します.あるスタックは左から右へスタックを押して、あるスタックは右から左へスタックを押して、あるスタックはスタックを押さないで、直接レジスタで取って代わって、あるものは呼び出される関数自身でスタックをバランスさせる必要があります.次は、一人一人自分で見ることができます.まず、勝手に関数を書いて、
int add(int a, int b)
{
	return a + b;
}

 
(1)右から左へスタックを押す
    _cdelcはコンパイラのデフォルトのスタック方式であり、関数で定義しないのは意味がない.しかし、コードを貼って説明したいのですが、
int __cdecl add(int a, int b)
{
	return a + b;
}

次にmain関数でこのadd関数を呼び出し、対応するアセンブリがどのように処理されているかを見てみましょう.
14:       int p = add(2, 3);
00401068   push        3
0040106A   push        2
0040106C   call        @ILT+0(_add) (00401005)
00401071   add         esp,8
00401074   mov         dword ptr [ebp-4],eax
15:       return 1;
00401077   mov         eax,1

上のコードから見ると,3は先にスタックを押さえ,次いでデータ2であり,明らかに右から左にスタックを押さえる.
 
(2)左から右へスタックを押す
実はwindows以前のコンパイラは左から右にスタックを押すのをサポートしていましたが、今はサポートされていません.例えば次のコードを入力すると
int __pascal add(int a, int b)
{
	return a + b;
}

このとき、コンパイラはエラーメッセージを貼り付けます.error C 4226:nonstandard extension used:'_pascal' is an obsolete keyword.ヒントはよくわかりました.pascalは古いキーワードで、今はサポートされていません.スタックスタックスタックは左から右へ、右から左へどちらでも構いません.しかし,出会った関数が変参であれば,この時点で問題がある.なぜならpascalでは,最後のパラメータがebpのどのオフセット位置にあるのか分からない.
 
(3)スタックの代わりにレジスタを用いる
データスタックの代わりにレジスタを用いることはarm,powerpcなどのcpuで用いられる比較的多い方法である.スタックの代わりにレジスタを用いるため,主に速度の面を考慮した.結局、データを取得したり、データを保存したりしてレジスタの操作を比較するのは非常に時間がかかります.これは、この2つのcpuのレジスタリソースが特に豊富です.同じように、まず__を使います.fastcallは関数を飾り、
int __fastcall add(int a, int b)
{
	return a + b;
}

次に呼び出し時に何が変わったか見てみましょう
14:       int p = add(2, 3);
00401068   mov         edx,3
0040106D   mov         ecx,2
00401072   call        @ILT+10(_add) (0040100f)
00401077   mov         dword ptr [ebp-4],eax
15:       return 1;
0040107A   mov         eax,1

上のスタックとは異なり,ここではedxでデータ3を保存し,eaxでデータeaxを保存した.結局レジスタ演算はメモリ演算よりずっと速い.
(4)呼び出しと自己によるスタック回復__stdcallは、ここで述べる最後のスタックモードである.関数スタックの場合、彼は__cdelcと同じであるが、肝心なのはadd関数の終了位置で変化する.まず、__stdcallで関数を飾る必要がある.
int __stdcall add(int a, int b)
{
	return a + b;
}


では、このとき関数がアセンブリされると、コードが変わりますか?
7:    int __stdcall add(int a, int b)
8:    {
00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,40h
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-40h]
0040102C   mov         ecx,10h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]
9:        return a + b;
00401038   mov         eax,dword ptr [ebp+8]
0040103B   add         eax,dword ptr [ebp+0Ch]
10:   }
0040103E   pop         edi
0040103F   pop         esi
00401040   pop         ebx
00401041   mov         esp,ebp
00401043   pop         ebp
00401044   ret         8


ここのアセンブリコードには特別なところはありません.でも最後のret 8はどういう意味ですか?実は前に2つのパラメータaとbがあったので、8はこの2つのパラメータが占有する空間です.このときret bは実際にebpを元の空間に復元する.それだけです.しかし、このステップが実行された後、ebpが8ではなく12を追加した可能性があることに気づきました.これはなぜですか.あと4バイトの戻りアドレスが付いていないのでね.