正確にベースポインターとスタックポインターは何ですか?彼らは何を指していますか?


225

WikiSquare()がDrawLine()を呼び出すウィキペディアからのこの例を使用すると、

代替テキスト

(この図の下部には高いアドレスがあり、上部には低いアドレスがあることに注意してください。)

誰かが私に何ebpを説明してくれespますか?

私が見るところから、スタックポインターは常にスタックの先頭を指し、ベースポインターは現在の関数の先頭を指していると思いますか?または何?


編集:これはWindowsプログラムのコンテキストでこれを意味します

edit2:また、どのように機能しeipますか?

edit3: MSVC ++からの次のコードがあります。

var_C= dword ptr -0Ch
var_8= dword ptr -8
var_4= dword ptr -4
hInstance= dword ptr  8
hPrevInstance= dword ptr  0Ch
lpCmdLine= dword ptr  10h
nShowCmd= dword ptr  14h

それらはすべてdwordのようで、それぞれ4バイトを使用します。したがって、hInstanceから4バイトのvar_4へのギャップがあることがわかります。彼らは何ですか?ウィキペディアの写真に見られるように、私はそれが戻りアドレスだと思いますか?


(編集者のメモ:質問に属さないマイケルの回答から長い引用を削除しましたが、フォローアップ質問が編集されました):

これは、関数呼び出しのフローが次のとおりであるためです。

* Push parameters (hInstance, etc.)
* Call function, which pushes return address
* Push ebp
* Allocate space for locals

私の質問(最後に、私は願っています!)は、私が呼び出したい関数の引数をポップした瞬間からプロローグの最後まで正確に何が起こるのでしょうか?ebp、espがそれらの瞬間にどのように進化するかを知りたいです(プロローグがどのように機能するかはすでに理解しています、スタックに引数をプッシュした後、プロローグの前に何が起こっているのか知りたいだけです)。


23
注意すべき重要な点の1つは、メモリ内でスタックが "下方"に成長することです。つまり、スタックポインタを上に移動するには、その値を減らします。
BS

4
EBP / ESPとEIPが行っていることを区別するための1つのヒント:EBPとESPはデータを扱い、EIPはコードを扱います。
mmmmmmmm

2
グラフでは、ebp(通常)は「フレームポインター」、特に「スタックポインター」です。これにより、スタックポインター(関数内で頻繁に変更される)とは関係なく、[ebp-x]を介してローカルに、[ebp + x]を介してスタックパラメーターに一貫してアクセスできます。ESPを介してアドレス指定を行うことで、EBPを他の操作に解放できますが、デバッガーはコールスタックやローカルの値を通知できません。
peterchen 2009

4
@ベン。必要ありません。一部のコンパイラは、スタックフレームをヒープに入れます。スタックが成長していくという概念は、それだけであり、理解しやすい概念です。スタックの実装は何でもかまいません(ヒープのランダムなチャンクを使用すると、確定的ではないため、スタックの一部を上書きするハックがはるかに困難になります)。
マーティンヨーク

1
言い換えると、スタックポインタにより、プッシュ/ポップ操作が機能します(したがって、プッシュとポップは、データの配置/取得場所を認識します)。ベースポインタを使用すると、スタックに以前にプッシュされたデータをコードで個別に参照できます。
tigrou

回答:


229

esp あなたが言う通り、スタックのトップです。

ebp通常esp、関数の開始時に設定されます。関数のパラメーターとローカル変数は、からの定数オフセットをそれぞれ加算および減算することによってアクセスされebpます。すべてのx86呼び出し規約はebp、関数呼び出し全体で保持されるものとして定義されています。 ebpそれ自体は実際には前のフレームのベースポインターを指します。これにより、デバッガーでスタックウォークを実行し、他のフレームのローカル変数を表示して機能させることができます。

ほとんどの関数プロローグは次のようになります。

push ebp      ; Preserve current frame pointer
mov ebp, esp  ; Create new frame pointer pointing to current stack top
sub esp, 20   ; allocate 20 bytes worth of locals on stack.

次に、関数の後半に次のようなコードを含めることができます(両方のローカル変数が4バイトであると想定)

mov [ebp-4], eax    ; Store eax in first local
mov ebx, [ebp - 8]  ; Load ebx from second local

有効にできるFPOまたはフレームポインターの省略の最適化により、実際にこれが排除ebpされ、別のレジスターとして使用され、から直接ローカルにアクセスespしますが、デバッガーは以前の関数呼び出しのスタックフレームに直接アクセスできなくなるため、デバッグが少し難しくなります。

編集:

更新された質問の場合、スタックに不足している2つのエントリは次のとおりです。

var_C = dword ptr -0Ch
var_8 = dword ptr -8
var_4 = dword ptr -4
*savedFramePointer = dword ptr 0*
*return address = dword ptr 4*
hInstance = dword ptr  8h
PrevInstance = dword ptr  0C
hlpCmdLine = dword ptr  10h
nShowCmd = dword ptr  14h

これは、関数呼び出しのフローが次のとおりであるためです。

  • プッシュパラメータ(hInstanceなど)
  • リターンアドレスをプッシュする呼び出し関数
  • 押す ebp
  • 地元の人のためのスペースを割り当てる

1
説明ありがとう!しかし、私は今少し混乱しています。私が関数を呼び出し、そのプロローグの最初の行にいて、それから1行も実行していないと仮定します。その時点で、ebpの値は何ですか?スタックには、プッシュされた引数以外にその時点で何かありますか?ありがとう!
貪欲なエリジウム

3
EBPは魔法のように変更されていないため、関数の新しいEBPを確立するまで、呼び出し元の値が残ります。引数に加えて、スタックは古いEIP(戻りアドレス)も保持します
MSalters 09/09/09

3
素敵な答え。エピローグの内容に言及せずに完了することはできませんが、「リーブ」と「レット」の指示があります。
Calmarius 2013

2
この画像は、フローがどのようなものかを明確にするのに役立つと思います。また、スタックが下向きに増えることにも注意してください。 ocw.cs.pub.ro/courses/_media/so/laboratoare/call_stack.png
Andrei-Niculae Petre

それは私ですか、それとも上記のコードスニペットからすべてのマイナス記号が欠落していますか?
BarbaraKwarc 2017年

96

ESPは現在のスタックポインターであり、ワードまたはアドレスがスタックにプッシュまたはポップオフされるたびに変化します。EBPは、ESPを直接使用するよりも、コンパイラーが関数のパラメーターとローカル変数を追跡するためのより便利な方法です。

一般に(これはコンパイラごとに異なる可能性があります)、呼び出される関数のすべての引数は、呼び出し元の関数によってスタックにプッシュされます(通常、関数プロトタイプで宣言されているのとは逆の順序ですが、これは異なります)。 。次に、関数が呼び出され、戻りアドレス(EIP)がスタックにプッシュされます。

関数に入ると、古いEBP値がスタックにプッシュされ、EBPはESPの値に設定されます。次に、ESPがデクリメントされ(スタックがメモリ内で下向きに成長するため)、関数のローカル変数と一時変数にスペースを割り当てます。その時点から、関数の実行中、関数の引数はEBPからの正のオフセットでスタックに配置され(関数呼び出しの前にプッシュされたため)、ローカル変数はEBPからの負のオフセットに配置されます。 (それらは関数エントリの後にスタックに割り当てられたため)。これが、関数呼び出しフレームの中心を指すため、EBPがフレームポインターと呼ばれる理由です。

終了時に、関数がしなければならないことはすべて、ESPをEBPの値に設定し(スタックからローカル変数の割り当てを解除し、スタックの一番上にエントリEBPを公開する)、スタックから古いEBP値をポップします。その後、関数は戻ります(戻りアドレスをEIPにポップします)。

呼び出し元の関数に戻ると、他の関数を呼び出す直前にスタックにプッシュした関数引数を削除するために、ESPをインクリメントできます。この時点で、スタックは、呼び出された関数を呼び出す前の状態に戻ります。


15

あなたはそれを正しく持っています。スタックポインターは、スタックの一番上の項目を指し、ベースポインターは、関数が呼び出される前のスタックの「前の」一番上を指します

関数を呼び出すと、ローカル変数がスタックに格納され、スタックポインターがインクリメントされます。関数から戻ると、スタック上のすべてのローカル変数がスコープ外になります。これを行うには、スタックポインターをベースポインター(関数呼び出しの前の「以前の」トップ)に戻します。

メモリ割り当てを行うこの方法は、非常に非常に高速かつ効率的。


14
@Robert:関数が呼び出される前にスタックの「前の」トップと言うと、関数と呼び出し元のEIPを呼び出す直前にスタックにプッシュされる両方のパラメーターを無視しています。これは読者を混乱させるかもしれません。標準のスタックフレームでは、EBP は関数入った直後 ESPがポイントしたのと同じ場所をポイントするとだけ言いましょう。
09:09

7

編集:より適切な説明については、x86アセンブリに関するWikiBookのx86逆アセンブリ/関数およびスタックフレームを参照してください。Visual Studioの使用に興味があるかもしれない情報をいくつか追加してみます。

呼び出し元のEBPを最初のローカル変数として格納することは標準スタックフレームと呼ばれ、これはWindowsのほとんどすべての呼び出し規約に使用できます。呼び出し元と呼び出し先のどちらが、渡されたパラメータの割り当てを解除するか、どのパラメータがレジスタに渡されるかは異なりますが、これらは標準のスタックフレームの問題とは関係ありません。

Windowsプログラムについて言えば、おそらくVisual Studioを使用してC ++コードをコンパイルできます。Microsoftはフレームポインターの省略と呼ばれる最適化を使用しているため、dbghlpライブラリーと実行可能ファイル用のPDBファイルを使用せずにスタックをウォークすることはほぼ不可能です。

このフレームポインターの省略は、コンパイラーが古いEBPを標準の場所に格納せず、EBPレジスターを別の場所に使用することを意味します。そのため、特定の関数に必要なローカル変数の容量を知らずに、呼び出し元のEIPを見つけるのは困難です。もちろん、Microsoftはこの場合でもスタックウォークを実行できるAPIを提供していますが、PDBファイルでシンボルテーブルデータベースを検索すると、一部のユースケースでは時間がかかりすぎます。

コンパイルユニットでFPOを回避するには、/ O2の使用を回避するか、プロジェクトのC ++コンパイルフラグに/ Oy-を明示的に追加する必要があります。おそらく、リリース構成でFPOを使用するCまたはC ++ランタイムに対してリンクするため、dbghlp.dllなしでスタックウォークを実行するのは困難です。


EIPがスタックに格納される方法がわかりません。レジスターじゃないの?レジスタをスタックに配置するにはどうすればよいですか?ありがとう!
食欲をそそるエリジウム

呼び出し元のEIPは、CALL命令自体によってスタックにプッシュされます。RET命令はスタックのトップをフェッチして、それをEIPに入れるだけです。バッファオーバーランがある場合、この事実は、特権スレッドからユーザーコードにジャンプするために使用される可能性があります。
wigy

@devouredelysium EIPレジスタの内容(または)は、レジスタ自体ではなく、スタックに配置(またはコピー)されます。
BarbaraKwarc 2017年

@BarbaraKwarc 価値のある入力をありがとう。OPが何を欠いているのかわからなかった。実際、レジスタはそのままの場所にあり、その値のみがCPUからRAMに送信されます。amd64モードでは、これは少し複雑になりますが、別の質問に任せてください。
2017年

そのamd64はどうですか?私は興味がある。
BarbaraKwarc 2017年

6

まず第一に、x86スタックは高いアドレス値から低いアドレス値に構築されるため、スタックポインターはスタックの一番下を指します。スタックポインタは、プッシュ(または呼び出し)の次の呼び出しで次の値が配置されるポイントです。その操作は、C / C ++ステートメントと同等です。

 // push eax
 --*esp = eax
 // pop eax
 eax = *esp++;

 // a function call, in this case, the caller must clean up the function parameters
 move eax,some value
 push eax
 call some address  // this pushes the next value of the instruction pointer onto the
                    // stack and changes the instruction pointer to "some address"
 add esp,4 // remove eax from the stack

 // a function
 push ebp // save the old stack frame
 move ebp, esp
 ... // do stuff
 pop ebp  // restore the old stack frame
 ret

ベースポインタは、現在のフレームの上にあります。ebpは通常、返信先アドレスを指します。ebp + 4は、関数の最初のパラメーター(またはクラスメソッドのthis値)を指します。ebp-4は、関数の最初のローカル変数、通常は以前のebpの値を指すため、前のフレームポインターを復元できます。


2
いいえ、ESPはスタックの最下部を指していません。メモリアドレス指定スキームは、それとは関係ありません。スタックが下位アドレスまたは上位アドレスに成長するかどうかは関係ありません。スタックの「トップ」は常に次の値がプッシュされる場所(スタックのトップに置かれる)です。または、他のアーキテクチャーでは、最後にプッシュされた値が置かれ、現在配置されている場所です。したがって、ESPは常にスタックの先頭を指します。
BarbaraKwarc 2017年

1
一方、スタックの一番下またはベースは、最初の(または最も古い)値が配置され、その後、より新しい値で覆われている場所です。これが、EBPの「ベースポインター」という名前の由来です。これは、サブルーチンの現在のローカルスタックのベース(または底)を指すはずでした。
BarbaraKwarc 2017年

Barbara、Intel x86では、スタックはUPSIDE DOWNです。スタックの一番上には、スタックにプッシュされた最初のアイテムが含まれ、その後の各アイテムは一番上のアイテムの下にプッシュされます。スタックの一番下は、新しいアイテムが配置される場所です。プログラムは1kからメモリに配置され、無限に成長します。スタックは無限から始まり、現実的には最大メモリからROMを引いた値で、0に向かって増加します。ESPは、最初にプッシュされたアドレスよりも値が小さいアドレスを指します。
jmucchiello 2018年

1

アセンブリプログラミングを行って久しぶりですが、このリンクは役に立つかもしれません...

プロセッサには、データを格納するために使用されるレジスタのコレクションがあります。これらの一部は直接値であり、他はRAM内の領域を指しています。レジスタは特定の特定のアクションに使用される傾向があり、アセンブリのすべてのオペランドは特定のレジスタに特定の量のデータを必要とします。

スタックポインターは、他のプロシージャを呼び出すときに主に使用されます。最新のコンパイラーでは、大量のデータが最初にスタックにダンプされ、次に戻りアドレスが続くので、システムは戻るように指示されたらどこに戻るかを認識します。スタックポインターは、新しいデータをスタックにプッシュできる次の場所を指します。スタックは、再びポップされるまでそこに留まります。

ベースレジスタまたはセグメントレジスタは、大量のデータのアドレス空間をポイントするだけです。2番目のレジスターがこのブロック内のアイテムを指す間、2番目のレジスターと組み合わせて、ベースポインターは巨大なブロックにメモリを分割します。そのためのベースポインタは、データのブロックのベースを指します。

アセンブリは非常にCPU固有であることに注意してください。リンクしたページには、さまざまなタイプのCPUに関する情報が表示されます。


セグメントレジスタはx86では別個です。それらはgs、cs、ssであり、メモリ管理ソフトウェアを作成しているのでない限り、それらに触れることはありません。
マイケル

dsもセグメントレジスタであり、MS-DOSおよび16ビットコードの時代には、64 KBを超えるRAMを指すことができないため、これらのセグメントレジスタを時々変更する必要がありました。しかし、DOSは20ビットのアドレスポインタを使用したため、最大1 MBのメモリにアクセスできました。その後、32ビットシステムが導入され、一部には36ビットアドレスレジスタがあり、現在は64ビットレジスタがあります。したがって、今日では、これらのセグメントレジスタを変更する必要はありません。
Wim ten Brink

最近のOSは386セグメントを使用していません
Ana Betts

@Paul:間違っています!違う!違う!16ビットセグメントは32ビットセグメントに置き換えられます。プロテクトモードでは、これによりメモリの仮想化が可能になり、基本的にプロセッサが物理アドレスを論理アドレスにマップできるようになります。ただし、OSがメモリを仮想化しているので、アプリケーション内は依然としてフラットな状態です。カーネルはプロテクトモードで動作し、アプリケーションをフラットメモリモデルで実行できます。en.wikipedia.org/wiki/Protected_mode
Wim ten Brink

@Workshop ALex:それは専門性です。すべての最近のOSは、すべてのセグメントを[0、FFFFFFFF]に設定します。それは本当に重要ではありません。リンクされたページを読むと、すべての凝ったものがページで行われていることがわかります。
MSalters 2009

-4

編集ええ、これはほとんど間違っています。それは誰かが興味を持っている場合に備えて、まったく異なる何かを説明します:)

はい、スタックポインタはスタックの一番上を指します(それが最初の空のスタックの場所か、私が確信が持てない最後の完全な場所か)。ベースポインタは、実行中の命令のメモリ位置を指します。これはオペコードのレベルにあります-コンピュータで取得できる最も基本的な命令です。各オペコードとそのパラメータは、メモリロケーションに保存されます。1つのC、C ++、またはC#の行は、複雑度に応じて、1つのオペコードまたは2つ以上のシーケンスに変換できます。これらは順次プログラムメモリに書き込まれて実行されます。通常の状況では、ベースポインタは1つの命令でインクリメントされます。プログラム制御(GOTO、IFなど)の場合、複数回インクリメントするか、次のメモリアドレスで置き換えることができます。

このコンテキストでは、関数はプログラムメモリの特定のアドレスに格納されます。関数が呼び出されると、特定の情報がスタックにプッシュされ、プログラムは関数が呼び出された場所への戻りとパラメーターを関数に戻すことができます。その後、プログラムメモリ内の関数のアドレスがプッシュされます。ベースポインタ。次のクロックサイクルで、コンピューターはそのメモリアドレスから命令の実行を開始します。その後、ある時点で、関数を呼び出した命令の後のメモリ位置に戻り、そこから続行します。


ebpが何であるかを理解するのに少し問題があります。MASMコードが10行ある場合、つまり、これらの行を実行していくと、ebpは常に増加します。
貪欲なエリジウム

1
@Devoured-いいえ。それは真実ではありません。eipは増加します。
マイケル

私が言ったことは正しいですが、EBPではなく、IEPにとって、それはそれですか
食欲をそそるエリジウム

2
はい。EIPは命令ポインタであり、各命令が実行された後に暗黙的に変更されます。
マイケル

2
ああ、私の悪い。別のポインターを考えています。私は脳を洗い流すつもりだと思います。
Stephen Friederichs

-8

espは "Extended Stack Pointer"を表します。 。ベースポインターは、追加セグメントのオフセットアドレスを指します。命令ポインタは、コードセグメントのオフセットアドレスを指します。さて、セグメントについて...それらはプロセッサのメモリ領域の小さな64KBの分割です.....このプロセスはメモリセグメンテーションと呼ばれます。この投稿がお役に立てば幸いです。


3
これは古い質問ですが、spはスタックポインタを表し、bpはベースポインタを表し、ipは命令ポインタを表します。みんなの最初のeは、32ビットポインターだと言っているだけです。
ハイデン、2015

1
セグメンテーションはここでは無関係です。
BarbaraKwarc 2017年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.