関数呼び出し時にスタックで何が起こりましたか?

5376 ワード

  • 結論
  • スタック
  • エラーの小例
  • インスタック
  • 出桟
  • 小結
  • 問題分析
  • Sなぜ文字化けしたのか
  • cなぜ常にh
  • なのか


    本論文で解析した問題は,関数のスタック呼び出しメカニズムである.結論を先に述べる
    結論
  • スタックを介してパラメータ
  • を渡す
  • 右から左へパラメータスタック
  • 先圧パラメータ入桟
  • は、その後、アドレスインスタック
  • に戻る.
  • ebp等のレジスタ入スタック
  • 呼び出し中のスタックは、呼び出し元によって
  • を維持する.
    レジスタスタックとは、実際には、レジスタスタックのセットを指す.新しい呼び出しの関数では、これらのレジスタは依然として使用されるため、呼び出し関数を終了して状態を回復するために、変更される可能性のあるレジスタはすべてスタックに入れなければならない.アウトスタック順序とインスタック順序は逆です.このプロセスはコンパイラによって維持されます.
    スタック
    現在一般的に応用されている単一命令ストリーム,単一データストリームコンピュータでは,コンパイル後のプログラムはスタックに基づいてスケジューリングされている.プログラムがメモリにロードされると、コード命令はメモリ空間の命令領域にマッピングされ、操作されたデータは対応するスタック空間とスタック空間にマッピングされます.スタックスペースは、ダイナミックメモリの割り当て、適用に使用されます.本論文では,解析スタックの問題をしばらく考慮しない.
    エラーの小例
    私たちは以下の例を例に挙げます.
    char* get_memory()
    {
        char p[]="hello world";
        return p;       //    
    }
    int main()
    {
        char* str = NULL;
        str = get_memory();
        printf("%s",str);   //           
        printf("%c",*str);  //          h
        return 0;
    }

    このコードはget_にありますmemoryでは、古典的な間違いを犯しました.解放されたローカル変数を返します.詳しく分析してみましょう
    インスタック
    メモリ領域は線形のメモリ領域と見なすことができる.スタックはプログラムのデータ領域にあり、各関数の局所変数の記憶空間に対応している.プログラムがmain関数の最初の命令に実行されると、main関数のスタックが確立され、cpuのespレジスタとebpレジスタであり、前者はmain関数スタックのスタックトップを指し、後者はスタックの底を指す.具体的には、図1に示すように、char*str=NULLが実行されたとき
    このコードの後のスタックステータス.コンピュータはすでにポインタstrに4バイトの空間を割り当てており、もちろん現在の内容はNULLであり、何も指していない.
    X 86プラットフォームでは、スタックの成長空間は高位から低位に増加し、スタックメモリは低位から高位に増加することに注意してください.現在のスタックはmain関数のスタックにすぎず、ローカル変数はcharポインタstringが1つしかなく、4バイトを占めており、espはスタックのトップを指しています.上のプログラムがget_を呼び出すとmemory関数の場合、新しい関数のスタックスペースに入り、新しい関数の実行が終了した後にmain関数を正しく返すために、呼び出し現場を保護する必要があります.私たちのプログラムで保護する必要があるのは、ebpアドレスとmain関数が実行を中断した実行点、すなわち戻りアドレスであり、C関数に従って管理を呼び出し、先にスタックに入ったのは戻りアドレスであり、その後、ebpポインタが指すアドレスである.ebpスタック後のmain関数スタックを図2に示す:この場合、アドレスとebpポインタスタックを返すだけで、関数はまだget_に入っていないmemory()関数.しかし、espスタックトップポインタはすでに新しいアドレスを指しており、main関数のスタック空間も大きくなっている.本当にget_に入りますmemory関数でchar p[]=「hello world」文を実行した後のスタック空間は図3に示すように、このときget_memory関数のスタックなので、ebpレジスタは新しい関数スタックのスタック底を指し、このスタック底は新しい関数に入る前の最後の時刻のespが指すアドレスである.アセンブリコードを表示すると、すべての関数の最初の2つの命令が次のように表示されます.
    pushl   %ebp
    movl    %esp, %ebp

    いずれも、図3のold ebpであるebpをスタックに入れてから、ebpを新しいespに更新します.このとき、espはちょうどebpの次のアドレスを指します.
    新しいebpの前の位置にはold ebpが格納されており、新しいebpからget_memoryのスタックスペースです.新しい関数のローカル変数が表示され、配列pは12バイトのストレージ「hello wolrd」に割り当てられます.コンピュータはespの位置を調整することによって空間を割り当て,これがスタックにメモリを割り当てる原理である.簡単なesp調整です.
    なお、図中のeaxレジスタは、eaxがX 86プラットフォーム上で戻り値を伝達する役割を果たすため、p配列のヘッダアドレスを指す.get_momeory関数はp配列のヘッダアドレスを返し,自然eaxはp配列ヘッダアドレスを格納する.
    宿屋を出る
    関数が終了するプロシージャは,入るプロシージャとは正反対であり,このようにしてこそ割り込み状態を正しく回復することができる.本明細書の例では,まずp配列空間を解放し,解放の過程は分配過程と同様にespレジスタの値を調整するだけでよい.局所変数を解放するとespはold ebpを指し,pop ebp命令を実行すると,main関数におけるebpの値,すなわちmain関数のスタック底アドレスを復元する.その後、戻りアドレスを取得できます.jmpはこのアドレスに、get_からmemory関数でmain関数に戻りました.このときのスタック状態は、図4に示すように、espはstring変数の次のアドレスを指しており、esp以降のすべての空間は解放されているが、このときは再割り当てされていないため、彼らの値は元の値であり、変化していない.string変数はget_ですmemory関数の戻り値は,元のp配列のヘッダアドレス位置:0 xCFFFFF 0を指す.
    小結
    上記の解析手順から,関数は呼び出し中にすべての局所変数がスタックに割り当てられ,関数を終了すると解放されることが分かる.これがC関数における局所変数役割ドメインが関数内でのみ有効である理由である.明らかなのは呼び出し中のスタックであり、出スタック順序:先に入ったのは戻りアドレスであり、それからold ebpである.
    もんだいぶんせき
    %Sはなぜ文字化けしているのか
    まず、上記の小結では、関数を呼び出すときに先にスタックに入るのは戻りアドレスであり、実際には、戻りアドレスよりも先にスタックに入るのは呼び出し関数のパラメータであると述べています.上のget_memory関数にはパラメータがないので、直接スタックに入って戻りアドレスを返します.パラメータのある関数呼び出しでは、実際にはスタックパラメータを先に入力する必要があります.また,C/C++関数では,スタック順序は右から左であり,最も右のパラメータが最初にスタックに入る.
    私たちのプログラムは次にprintf関数を呼び出すので、この関数はパラメータがあり、私たちのプログラムでは2つのパラメータです.まず,printf("%s",string)という最初の呼び出し方法について議論する.この関数に入ったスタック空間は、図5に示すように、関数に入った場合、右端のパラメータarg 2が先にスタックに入り、C関数の値伝達特性に従って、stringのコピー、すなわちarg 2もアドレスであり、0 xCFFFFFFF 0を指す.その後arg 1はスタックに入り、次いでアドレスを返してスタックに入る.arg 2は4バイトであるため、arg 1も文字列定数のアドレスであり、4バイトでもある.このときの0 xCFFFFF 0アドレスは、前回呼び出した配列pの開始位置であり、mainのローカル変数stringとprintfの2番目のパラメータarg 2がこのアドレスを指しているが、このアドレスの値は「h」ではなく、printfがローカル変数にメモリを割り当てるため、hello worldの12バイトはすべて上書きされます.
    以上、printfは入った瞬間、コードを実行しなくても元hello worldの空間が上書きされ、自然と正しい出力が得られない.得られたのはランダムな文字化けしだ.実際にはランダムとは簡単には言えませんが、戻りアドレス、printfのローカル変数はすべて確定しているので、これらのアドレス、ローカル変数をchar出力としている場合、文字化けに違いありませんが、確定した文字化けに違いありません.
    %cはなぜ常にhなのか
    2つ目の問題を見てみましょう.前者とは異なり,今回呼び出された2番目のパラメータはアドレスではなくcharである.値伝達の特性に従って、このときのarg 2はstringのコピー、すなわちarg 2=stringである.さらに,この付与プロセスは関数printfに入る前に発生する.図6に示すように、printfの2番目のパラメータarg 2をスタックに入れた直後の様子を図6に示す.stringは0 xCFFFFF 0であり、この位置はこのときまだ上書きされていないため*string=’h’であり、値伝達後argv 2=’h’である.これがarg 2スタック後のスタック状態である.
    その後、最初のパラメータをスタックに押し込み続け、関数をアドレススタックに戻し、このとき4+1+4=9バイトを押し込み、espは0 xCFFFFEF位置に達した.図7に示すように、printf関数スタックに入ったスタック状態が図7に示されている.このとき、元の0 xCFFFFFFF 0位置から始まった「hello world」が上書きされているが、argv 2値に格納されているのは依然として「h」というコピーである.したがって,2番目のプログラムが最終的に出力するのは‘h’であるのも正常である.