C言語の関数呼び出しとスタックフレームを整理する
call、ret、esp、ebp の動きを手がかりに、関数呼び出しの裏側を読む
Copyright © 2026 LWP 山中 一弘
本資料は、出典を明記いただければ、商用・非商用を問わず、ご自由に複製・改変・再配布していただけます。なお、著作権表示は改変せず、そのまま記載してご利用くださいますようお願いいたします。
記事要約
C言語で関数を呼び出すとき、ソースコード上では foo(1) のように一行で書けます。しかしCPUの側では、引数をどこに置くか、戻る場所をどこに保存するか、呼び出された関数のローカル変数をどこに置くか、処理が終わったあとどう戻るか、という作業が必要になります。
この記事では、筆者が2020年3月に「エンジョイC」向けに投稿した関数呼び出しメモをもとに、32bit x86 の典型的な関数呼び出しを整理します。中心になるのは、call 命令、ret 命令、スタックポインタの esp、ベースポインタの ebp、そしてスタックフレームです。
元の投稿は講義後の補足メモであり、図を見ながら理解する前提の短い説明でした。この記事では、画像の内容も本文で読めるように補足し、関数呼び出しを「戻り先」「退避された ebp」「引数」「ローカル変数」の配置として理解できるように再構成します。
本記事の対象とゴール
想定読者
C言語の関数呼び出しを、アセンブリやスタックの視点から理解したい人
call、ret、push ebp、mov ebp, esp が何をしているか整理したい人
スタックフレームという言葉を聞いたことはあるが、具体的な境界が曖昧な人
ローカル変数と引数が、関数呼び出し時にどこへ置かれるか知りたい人
本記事で得られること
call 命令が戻り先アドレスをスタックへ積む理由が分かります。
ebp と esp の役割分担を整理できます。
引数、戻り先、退避された ebp、ローカル変数をスタックフレームとして読めるようになります。
本記事で扱わないこと
すべてのCPUアーキテクチャに共通する関数呼び出し規約
x64、ARM、最適化後コードを含む現代的なABI全般
コンパイラごとの差異を含む厳密な呼び出し規約の網羅
先に結論
C言語の関数呼び出しは、見た目には「関数へ飛ぶ」だけですが、実際には「戻る場所をスタックに積み、呼び出された関数用の作業領域を作り、最後にその作業領域を片付けて戻る」処理です。
32bit x86 の典型的な説明では、call が戻り先アドレスをスタックに積み、呼び出された関数は push ebp と mov ebp, esp で前の関数の基準点を保存し、自分用の基準点を作ります。その後、必要に応じて esp を動かしてローカル変数の領域を確保します。処理が終わると、esp を ebp へ戻し、保存していた ebp を復元し、ret で戻り先アドレスを取り出して呼び出し元へ戻ります。
つまり、esp は現在のスタック先端を指す動くポインタであり、ebp は関数内で引数やローカル変数を安定して参照するための基準点です。この2つの違いが見えると、関数呼び出しの仕組みはかなり読みやすくなります。
1. 関数呼び出しでCPUがやること
この章では、C言語の関数呼び出しをCPU側の作業として整理します。
関数呼び出しは、移動だけでは足りない
C言語では、次のように関数を呼び出します。
foo(1);ソースコード上では、これだけで関数へ処理が移ります。しかしCPUの実行として見ると、単に foo の先頭へジャンプするだけでは足りません。関数が終わったあと、どこへ戻るかを覚えておく必要があります。
その戻り先を記録するために、x86 の call 命令は、呼び出し先へ移動する前に「次に実行すべき命令のアドレス」をスタックに積みます。これが戻り先アドレスです。
戻り先は ret で取り出される
関数が終わると、ret 命令が実行されます。ret は、スタックの先頭にある値を取り出し、その値を命令ポインタに入れることで、呼び出し元の続きへ戻ります。
元素材では「ret 命令で pop するスタックの内容は EIP に POP される、という理解でよい」と説明しています。ここでの EIP は、32bit x86 における命令ポインタです。次に実行する命令の位置を表します。
この理解は、関数呼び出しを追ううえで重要です。call は戻り先を積む。ret は戻り先を取り出して戻る。まずこの対応を押さえます。
2. esp と ebp の役割
この章では、スタックを読むときに重要になる esp と ebp を分けて考えます。
esp は現在のスタック先端を指す
esp はスタックポインタです。現在のスタックの先端を指します。
スタックへ値を積むと、esp は移動します。スタックから値を取り出すと、esp は戻ります。つまり esp は、処理の進行に応じて動くポインタです。
この性質のため、関数内で esp だけを基準に引数やローカル変数を参照すると、途中で push や pop が増えたときに位置関係が追いにくくなります。
ebp は関数内の基準点になる
ebp はベースポインタです。典型的な関数プロローグでは、次のような命令が出てきます。
push ebpmov ebp, esppush ebp は、呼び出し元で使っていた ebp をスタックに保存します。mov ebp, esp は、現在の esp を ebp にコピーし、呼び出された関数の基準点を作ります。
元素材では、画像内の説明について「espの退避と書かれているが、実際にはebpの設定」と補足しています。ここは重要です。push ebp によって保存されるのは古い ebp です。そして mov ebp, esp によって、新しい関数内の基準点として ebp が設定されます。
ebp を基準に引数とローカル変数を読む
ebp が設定されると、関数内では次のように位置関係を読めます。
ebp の近くには、退避された古い ebp がある。
ebp より上側には、戻り先アドレスと引数がある。
ebp より下側には、ローカル変数用の領域が作られる。
ここでいう上側、下側は、図や説明の向きによって表現が変わります。大事なのは、ebp を基準にして、引数側とローカル変数側を分けて読めることです。
3. 図1の読み取り:引数と戻り先
この章では、元記事の1枚目の画像が示していることを文字で補足します。
画像の構成
1枚目の画像は、関数呼び出し直後のスタックと命令列を示すシミュレーション画面です。左側に命令列、右側にスタックの中身が表示されています。
命令列には、おおむね次の流れが見えます。
push 1call plus1add esp, 4...push ebpmov ebp, esp右側のスタックには、引数として積まれた値 1 と、call によって積まれた戻り先アドレスが表示されています。
call の前に引数が積まれる
図では、plus1(1) のような関数呼び出しを行う前に、引数 1 がスタックに積まれています。これは、関数へ渡す値を呼び出し先が読める位置に置くためです。
32bit x86 の __cdecl では、関数パラメーターはスタックで渡され、呼び出し元がスタックを後片付けします。Microsoft Learn の x86 アーキテクチャ説明でも、__cdecl ではパラメーターがスタックで渡され、呼び出し元がスタックをクリーンアップすると説明されています。
つまり、図の add esp, 4 は、関数から戻ったあと、呼び出し元が積んだ引数1個分の領域を片付ける処理です。
call は戻り先も積む
call plus1 が実行されると、CPUは plus1 へ処理を移します。しかしその前に、呼び出し元へ戻るためのアドレスをスタックへ積みます。
図では、スタックに「callの直後の位置」に対応する戻り先が積まれていることが分かります。関数が終わったあと ret がこの値を取り出すことで、呼び出し元の続きへ戻れます。
図1を本文だけで言い換える
図1が言いたいことを本文だけで表すと、次のようになります。
呼び出し元は、まず引数をスタックに積む。
call が戻り先アドレスをスタックに積む。
呼び出された関数は、古い ebp を保存する。
呼び出された関数は、ebp を現在の esp に合わせ、自分用の基準点を作る。
関数から戻ったあと、呼び出し元は引数分だけ esp を戻す。
この流れが見えると、関数呼び出しは「飛ぶ」だけではなく、「戻るための情報を積んでから飛ぶ」処理だと分かります。
4. スタックフレームとは何か
この章では、スタックフレームを「関数1回分の作業領域」として整理します。
スタックフレームは関数ごとに積み重なる
関数が呼び出されるたびに、戻り先、退避された ebp、ローカル変数などがスタック上に配置されます。このひとまとまりを、スタックフレームと呼びます。
元素材では、ebp の退避が重なると、退避された ebp で区切られた領域がいくつもでき、それがスタックフレームのように見える、と説明しています。
これは非常に実感に近い説明です。関数を呼ぶたびに、前の関数の基準点を保存し、新しい関数の基準点を作るため、スタック上に関数ごとの層ができます。
どこからどこまでがスタックフレームか
スタックフレームの厳密な範囲は、文脈や説明の仕方によって多少変わります。
ただし、学習用には「ある関数が実行中に使う、戻り先、退避された ebp、引数参照、ローカル変数領域を含むひとまとまり」と理解すれば十分です。
元素材では、ebp から、次に退避された ebp の直前あたりまでをスタックフレームと呼んでよいのではないか、と述べています。厳密な定義として固定するよりも、ebp がフレームを読むための区切りになっている、という見方を重視すると理解しやすくなります。
5. 関数から戻るときに何が起きるか
この章では、関数の終了処理を esp と ebp の復元として整理します。
まず esp を ebp に戻す
関数内では、ローカル変数の確保や一時的な push によって esp が動きます。関数を終了する前には、この esp をフレームの基準点まで戻す必要があります。
典型的には、次のような命令で戻します。
mov esp, ebpこれにより、ローカル変数用に下げたスタック領域がまとめて解放されたような状態になります。
次に ebp を復元する
esp を ebp に戻すと、スタックの先頭には保存しておいた古い ebp が来ます。そこで次の命令を実行します。
pop ebpこれにより、呼び出し元の ebp が復元されます。同時に esp は次の位置、つまり戻り先アドレスを指す位置へ進みます。
ret が戻り先アドレスを使う
pop ebp が終わると、スタックの先頭には戻り先アドレスがあります。ここで ret を実行すると、そのアドレスが取り出され、命令ポインタに入ります。
元投稿の言い方では、「ebp を pop して回復すると、今度は esp は戻り値を指すようになる。これで ret 命令が使える」という流れです。ここでの「戻り値」は、C言語の戻り値ではなく、戻り先アドレスの意味で読むと分かりやすくなります。
C言語の戻り値、たとえば int の戻り値は、多くの場合 eax に入ります。戻り先アドレスとは別物です。
6. 図2の読み取り:ローカル変数がある場合
この章では、元記事の2枚目の画像が示していることを文字で補足します。
画像の構成
2枚目の画像は、関数内でローカル変数を使う場合のスタック状態を示しています。左側の命令列には、push ebp、mov ebp, esp のあと、sub esp, 4 のように esp を動かす処理が見えます。
右側のスタックには、引数、戻り先、退避された ebp、そしてローカル変数用の領域が並んでいます。画像内のコメントでは、[foo] 変数 a、[foo] EBP退避、[foo] 呼出し元、[foo] 引数1 のような意味づけがされています。
ローカル変数は esp 側に確保される
関数内でローカル変数を使う場合、esp を動かしてスタック上に領域を作ります。
sub esp, 4この命令は、4バイト分の領域をスタック上に確保するイメージです。たとえば int a のようなローカル変数1個分の置き場として使えます。
図2では、esp がローカル変数領域を指し、ebp が引数や保存された ebp を読む基準点として使われていることが分かります。
引数は ebp から正方向、ローカル変数は ebp から負方向
典型的な32bit x86の説明では、ebp を基準にして次のように読みます。
[ebp + 8] 第1引数[ebp + 4] 戻り先アドレス[ebp] 保存された古い ebp[ebp - 4] ローカル変数これが、元投稿の「spはローカル変数にアクセスするために使用して、bpはスタックフレームの構築と引数にアクセスするために使用する」という説明につながります。
厳密には、ローカル変数も ebp からのオフセットで参照することが多くあります。ただし、学習の入口としては、esp がローカル変数領域の確保で動き、ebp が安定した基準点になる、と押さえると読みやすくなります。
7. 呼び出し規約に注意する
この章では、この記事の説明がどこまで一般化できるかを整理します。
この記事は32bit x86の典型例である
この記事で説明しているのは、32bit x86 の典型的な関数呼び出しです。特に、引数をスタックで渡し、呼び出し元が後片付けする __cdecl に近い説明です。
Microsoft Learn の説明では、x86には複数の呼び出し規約があり、__stdcall、thiscall、__fastcall、__cdecl などで、引数をどこへ渡すか、誰がスタックを片付けるかが異なります。
したがって、この記事の図を「C言語のすべての環境で必ずこうなる」と読むのは危険です。あくまで、関数呼び出しとスタックフレームを理解するための基礎モデルとして読みます。
最適化後のコードでは ebp が省略されることがある
実際のコンパイラ出力では、最適化によって ebp をフレームポインタとして使わないことがあります。その場合、この記事で説明した push ebp、mov ebp, esp という形が見えないことがあります。
しかし、学習の最初にこの形を理解しておく価値はあります。戻り先、保存された基準点、引数、ローカル変数という構成を理解できると、最適化後のコードを読むときにも、何が省略されているのかを考えやすくなるからです。
8. まとめ
C言語の関数呼び出しは、ソースコード上では一行でも、CPU側では戻り先、引数、基準点、ローカル変数領域を扱う処理です。
call は戻り先アドレスをスタックに積み、呼び出し先へ移動します。呼び出された関数は、古い ebp を保存し、新しい ebp を設定します。ローカル変数が必要なら、esp を動かして領域を確保します。関数が終わると、esp を戻し、ebp を復元し、ret で戻り先アドレスへ戻ります。
この流れを押さえると、関数呼び出しは「名前の付いた処理へ飛ぶ」だけではなく、「スタック上に関数1回分の作業領域を作って、終わったら元に戻す」仕組みとして見えるようになります。これがスタックフレーム理解の入口です。
元記事・出典メモ
この記事は、筆者自身のPosfieまとめ「エンジョイC001回目関数呼び出しの仕組み(2020-03-15)」をもとに、LWP記事向けに再構成したものです。
元URL: https://posfie.com/@hoehoe1234/p/Go11N9e
元まとめタイトル: エンジョイC001回目関数呼び出しの仕組み(2020-03-15)
元投稿者: ほえほえ@マクロ師のひとりごとDX/AX
取得日: 2026-05-14
元記事内の参照先: https://vanya.jp.net/os/x86call/
補足参照: Microsoft Learn「x86 アーキテクチャ」
画像の扱い: 元記事内の2枚のスタック図を確認し、図の要点を本文中の「図の読み取り」として文字化
再構成方針: 元投稿の講義補足メモを中心に、32bit x86 の典型的な関数呼び出し、スタックフレーム、ローカル変数配置を補足して記事化
出典メモ
正規Posfie URL: https://posfie.com/@hoehoe1234/p/Go11N9e
取得日: 2026-06-13
素材として使った範囲: 投稿本文、添付画像から読み取れる範囲、関連する技術論点
注意: 外部投稿を長文転載せず、記事本文はLWP公開記事として再構成した。
