スタックはアセンブリ言語でどのように機能しますか?


83

私は現在、スタックがどのように機能するかを理解しようとしているので、自分自身にいくつかのアセンブリ言語を教えることにしました。私はこの本を使用しています。

http://savannah.nongnu.org/projects/pgubook/

私が使用しているガスをし、上の私の開発を行っているLinuxのミント

私は何かに少し混乱しています:

私の知る限り、スタックは単なるデータ構造です。したがって、アセンブリでコーディングする場合は、スタックを自分で実装する必要があると思いました。ただし、次のようなコマンドがあるため、これは当てはまらないようです。

pushl
popl

したがって、x86アーキテクチャのアセンブリでコーディングし、Gas構文を使用する場合、スタックはすでに実装されている単なるデータ構造ですか?それとも実際にハードウェアレベルで実装されていますか?それとも何か他のものですか?また、他のチップセットのほとんどのアセンブリ言語には、スタックがすでに実装されていますか?

これは少しばかげた質問だと思いますが、実際にはかなり混乱しています。


2
これらの回答のほとんどは、言語で使用されるスタックについて説明しています。特に、スタックで引数を渡すことについて説明しています。多くのCPUでは、これは言語の実装を容易にするためだけです。アセンブリを手動でコーディングする場合は、通常、レジスタ内の関数にパラメータを渡します(少なくとも、言語が非常に頻繁に使用するため、CPUがスタック操作用に最適化される前)。スタックは主に呼び出し/戻りを順番に保持するためのものであり、割り込み(CPUの状態を保存する必要がある)も、使用するレジスタに既存の値をプッシュし、返す前にそれらをポップします。
ビル

回答:


82

私は主にあなたが間に混乱していると思うprogram's stackany old stack

スタック

後入れ先出しシステムの情報で構成される抽象的なデータ構造です。任意のオブジェクトをスタックに置いてから、それらを再び取り外します。これは、イン/アウトトレイのように、常に一番上のアイテムが取り外され、常に一番上に置かれます。

プログラムスタック

スタックであり、実行中に使用されるメモリのセクションです。通常、プログラムごとに静的なサイズがあり、関数パラメータを格納するために頻繁に使用されます。関数を呼び出すときにパラメーターをスタックにプッシュすると、関数はスタックを直接アドレス指定するか、スタックから変数をポップオフします。

プログラムスタックは通常ハードウェアではありませんが(メモリに保持されているため、そのように主張できます)、スタックの現在の領域を指すスタックポインタは通常CPUレジスタです。これにより、スタックがアドレス指定するポイントを変更できるため、LIFOスタックよりも少し柔軟性があります。

ウィキペディアの記事を読んで理解していることを確認する必要があります。これは、扱っているハードウェアスタックの適切な説明を提供するためです。

あり、このチュートリアル古い16ビット・レジスタの面でスタックを説明したが役立つことができ、別の1のスタックについて、具体的には。

Nils Pipenbrinckから:

一部のプロセッサは、スタックにアクセスして操作するためのすべての命令(プッシュ、ポップ、スタックポインタなど)を実装していませんが、x86は使用頻度が高いため、実装していることに注意してください。このような状況でスタックが必要な場合は、自分で実装する必要があります(一部のMIPSおよび一部のARMプロセッサはスタックなしで作成されます)。

たとえば、MIPでは、プッシュ命令は次のように実装されます。

addi $sp, $sp, -4  # Decrement stack pointer by 4  
sw   $t0, ($sp)   # Save $t0 to stack  

ポップ命令は次のようになります。

lw   $t0, ($sp)   # Copy from stack to $t0  
addi $sp, $sp, 4   # Increment stack pointer by 4  

2
ところで、x86にはこれらの特別なスタック命令があります。スタックからのプッシュとポップが頻繁に発生するため、短いオペコードを使用することをお勧めします(コードスペースが少ない)。MIPSやARMなどのアーキテクチャにはこれらがないため、スタックを自分で実装する必要があります。
ニルスピペンブリンク

4
ホットな新しいプロセッサは、ある程度8086とバイナリ互換であり、最初のマイクロプロセッサである8008の開発である8080とソース互換であることに注意してください。これらの決定のいくつかは、長い道のりを遡ります。
デビッドソーンリー

4
ARMには、スタックを操作するための単一の命令がありますが、それらはSTMDB SPと呼ばれるため、それほど明白ではありません。(PUSH用)およびLDMIA SP!(POP用)。
アダムグッド

1
私の神、この答えは+500を必要としています...私は永遠にこれをうまく説明するものを何も見つけていません。...今のよう+1これに新しいアカウントを作成するために考える
ガブリエル


34

(あなたがそれで遊びたい場合に備えて、私はこの答えのすべてのコードの要点を作りました)

2003年のCS101コースでは、asmで最も基本的なことを行ったことがあります。そして、基本的にすべてCまたはC ++でのプログラミングに似ていることに気付くまで、asmとスタックがどのように機能するかを実際に「理解」したことはありませんでした...ただし、ローカル変数、パラメーター、および関数はありません。おそらくまだ簡単に聞こえないでしょう:)お見せしましょう(Intel構文のx86 asmの場合)。


1.スタックとは

スタックは通常、スレッドが開始する前にすべてのスレッドに割り当てられる連続したメモリのチャンクです。何でも保存できます。C ++用語(コードスニペット#1):

const int STACK_CAPACITY = 1000;
thread_local int stack[STACK_CAPACITY];

2.スタックの上部と下部

原則として、stack配列のランダムセルに値を格納できます(スニペット#2.1)。

stack[333] = 123;
stack[517] = 456;
stack[555] = stack[333] + stack[517];

しかし、どのセルを覚えるのがどれほど難しいか想像してみてください stackがすでに使用されており、が「無料」。そのため、新しい値を隣り合わせのスタックに格納します。

(x86)asmのスタックの奇妙な点の1つは、最後のインデックスから始めて下位のインデックスに移動することです。stack[999]、stack [998]など(スニペット#2.2):

stack[999] = 123;
stack[998] = 456;
stack[997] = stack[999] + stack[998];

そして、まだのための「公式」名(注意、あなたは今混同しているつもり)stack[999]であるスタックの底に
最後に使用されたセル(stack[997]上記の例)は、スタックの最上位と呼ばれますスタックの最上位がx86上にある場所を参照)。)。


3.スタックポインタ(SP)

この説明の目的のために、CPUレジスタがグローバル変数として表されていると仮定しましょう(汎用レジスタを参照)。

int AX, BX, SP, BP, ...;
int main(){...}

スタックの最上位を追跡する特別なCPUレジスタ(SP)があります。SPはポインタです(0xAAAABBCCのようなメモリアドレスを保持します)。ただし、この投稿では、配列インデックス(0、1、2、...)として使用します。

スレッドが開始するSP == STACK_CAPACITYと、プログラムとOSが必要に応じてスレッドを変更します。ルールは、スタックの最上位を超えてスタックセルに書き込むことはできず、SP未満のインデックスは無効で安全ではないため(システム割り込みのため)、 最初にSPをデクリメントしてその後、新たに割り当てられたセルに値を書き込みます。

スタック内の複数の値を連続してプッシュする場合は、それらすべてのスペースを事前に予約できます(スニペット#3)。

SP -= 3;
stack[999] = 12;
stack[998] = 34;
stack[997] = stack[999] + stack[998];

注意。これで、スタックへの割り当てが非常に高速である理由がわかります。これは、レジスタの1つのデクリメントだけです。


4.ローカル変数

この単純な関数(スニペット#4.1)を見てみましょう。

int triple(int a) {
    int result = a * 3;
    return result;
}

ローカル変数を使用せずに書き直します(スニペット#4.2):

int triple_noLocals(int a) {
    SP -= 1; // move pointer to unused cell, where we can store what we need
    stack[SP] = a * 3;
    return stack[SP];
}

そしてそれがどのように呼ばれているかを見てください(スニペット#4.3):

// SP == 1000
someVar = triple_noLocals(11);
// now SP == 999, but we don't need the value at stack[999] anymore
// and we will move the stack index back, so we can reuse this cell later
SP += 1; // SP == 1000 again

5.プッシュ/ポップ

スタックの最上位に新しい要素を追加することは非常に頻繁な操作であるため、CPUにはそのための特別な命令がありpushます。このように強制します(スニペット5.1):

void push(int value) {
    --SP;
    stack[SP] = value;
}

同様に、スタックの最上位要素を取得します(スニペット5.2)。

void pop(int& result) {
    result = stack[SP];
    ++SP; // note that `pop` decreases stack's size
}

プッシュ/ポップの一般的な使用パターンは、一時的に値を節約することです。たとえば、変数に役立つものがmyVarあり、何らかの理由でそれを上書きする計算を行う必要があります(スニペット5.3)。

int myVar = ...;
push(myVar); // SP == 999
myVar += 10;
... // do something with new value in myVar
pop(myVar); // restore original value, SP == 1000

6.関数パラメーター

次に、スタック(スニペット#6)を使用してパラメーターを渡します。

int triple_noL_noParams() { // `a` is at index 999, SP == 999
    SP -= 1; // SP == 998, stack[SP + 1] == a
    stack[SP] = stack[SP + 1] * 3;
    return stack[SP];
}

int main(){
    push(11); // SP == 999
    assert(triple(11) == triple_noL_noParams());
    SP += 2; // cleanup 1 local and 1 parameter
}

7。 returnステートメント

AXレジスタに値を返しましょう(スニペット#7):

void triple_noL_noP_noReturn() { // `a` at 998, SP == 998
    SP -= 1; // SP == 997

    stack[SP] = stack[SP + 1] * 3;
    AX = stack[SP];

    SP += 1; // finally we can cleanup locals right in the function body, SP == 998
}

void main(){
    ... // some code
    push(AX); // save AX in case there is something useful there, SP == 999
    push(11); // SP == 998
    triple_noL_noP_noReturn();
    assert(triple(11) == AX);
    SP += 1; // cleanup param
             // locals were cleaned up in the function body, so we don't need to do it here
    pop(AX); // restore AX
    ...
}

8.スタックベースポインタ(BP)フレームポインタとも呼ばれます)とスタックフレーム

より「高度な」関数を使用して、asmのようなC ++(スニペット#8.1)で書き直してみましょう。

int myAlgo(int a, int b) {
    int t1 = a * 3;
    int t2 = b * 3;
    return t1 - t2;
}

void myAlgo_noLPR() { // `a` at 997, `b` at 998, old AX at 999, SP == 997
    SP -= 2; // SP == 995

    stack[SP + 1] = stack[SP + 2] * 3; 
    stack[SP]     = stack[SP + 3] * 3;
    AX = stack[SP + 1] - stack[SP];

    SP += 2; // cleanup locals, SP == 997
}

int main(){
    push(AX); // SP == 999
    push(22); // SP == 998
    push(11); // SP == 997
    myAlgo_noLPR();
    assert(myAlgo(11, 22) == AX);
    SP += 2;
    pop(AX);
}

ここで、tripple(スニペット#4.1)のように、戻る前に結果を格納するために新しいローカル変数を導入することにしたと想像してください。関数の本体は次のようになります(スニペット#8.2):

SP -= 3; // SP == 994
stack[SP + 2] = stack[SP + 3] * 3; 
stack[SP + 1] = stack[SP + 4] * 3;
stack[SP]     = stack[SP + 2] - stack[SP + 1];
AX = stack[SP];
SP += 3;

ご覧のとおり、関数パラメーターとローカル変数へのすべての参照を更新する必要がありました。これを回避するには、スタックが大きくなっても変化しないアンカーインデックスが必要です。

現在のトップ(SPの値)をBPレジスタに保存することにより、関数の入力直後(ローカルにスペースを割り当てる前)にアンカーを作成します。スニペット#8.3

void myAlgo_noLPR_withAnchor() { // `a` at 997, `b` at 998, SP == 997
    push(BP);   // save old BP, SP == 996
    BP = SP;    // create anchor, stack[BP] == old value of BP, now BP == 996
    SP -= 2;    // SP == 994

    stack[BP - 1] = stack[BP + 1] * 3;
    stack[BP - 2] = stack[BP + 2] * 3;
    AX = stack[BP - 1] - stack[BP - 2];

    SP = BP;    // cleanup locals, SP == 996
    pop(BP);    // SP == 997
}

関数に属し、関数を完全に制御するスタックのスライスは、関数のスタックフレームと呼ばれます。たとえば、myAlgo_noLPR_withAnchorのスタックフレームはstack[996 .. 994](両方のidexeを含む)です。
フレームは関数のBPで始まり(関数内で更新した後)、次のスタックフレームまで続きます。したがって、スタック上のパラメーターは、呼び出し元のスタックフレームの一部です(注8aを参照)。

注:
8a。 ウィキペディアはパラメータについて別の言い方をしていますが、ここではインテルのソフトウェア開発者向けマニュアルを順守しています。1、セクション6.2.4.1スタックフレームベースポインタおよびセクション6.3.2ファーコールおよびRET操作の図6-2 。関数のパラメーターとスタックフレームは、関数のアクティブ化レコードの一部です(関数ペリログのgenを参照)。
8b。BPポイントから関数パラメーターへの正のオフセットと負のオフセットはローカル変数を指します。これは、8cのデバッグに非常に便利です
stack[BP]前のスタックフレームのアドレスを格納し、stack[stack[BP]]前のスタックフレームなどを格納します。このチェーンに続いて、まだ戻っていないプログラム内のすべての関数のフレームを見つけることができます。これは、デバッガーがスタック
8dを呼び出すことを示す方法ですmyAlgo_noLPR_withAnchorフレームをセットアップする(古いBPの保存、BPの更新、ローカル用のスペースの予約)の最初の3つの命令は、関数プロローグと呼ばれます。


9.呼び出し規約

スニペット8.1では、パラメータをmyAlgo右から左にプッシュし、結果をに返しましたAX。パラメータを左から右に渡して、に戻ることもできBXます。または、BXとCXでパラメータを渡し、AXで返します。明らかに、呼び出し元(main())と呼び出された関数は、これらすべてのものが格納される場所と順序に同意する必要があります。

呼び出し規約は、パラメーターが渡され、結果が返される方法に関する一連のルールです。

上記のコードでは、cdecl呼び出し規約を使用しています。

  • パラメータはスタックに渡され、最初の引数は呼び出し時にスタックの最下位アドレスに渡されます(最後にプッシュされた<...>)。呼び出し元は、呼び出し後にパラメーターをスタックからポップバックする責任があります。
  • 戻り値はAXに配置されます
  • EBPとESPはmyAlgo_noLPR_withAnchor、呼び出し元(この場合はmain関数)が保持する必要があります。これにより、呼び出し元(関数)は、呼び出しによって変更されていないレジスターに依存できます。
  • 他のすべてのレジスタ(EAX、<...>)は、呼び出し先が自由に変更できます。呼び出し元が関数呼び出しの前後に値を保持したい場合は、値を他の場所に保存する必要があります(これはAXで行います)

(出典:例えば、スタックオーバーフローのドキュメントから「32ビットCDECL」;著作権2016年までにicktoofayピーター・コルド。; CC BY-SA 3.0の下でライセンスアンのフルスタックオーバーフローのドキュメントのコンテンツのアーカイブはここで、archive.orgで見つけることができますこの例は、トピックID3261と例ID11196によって索引付けされています。)


10.関数呼び出し

今最も興味深い部分。データと同様に、実行可能コードもメモリに格納され(スタックのメモリとはまったく関係ありません)、すべての命令にアドレスがあります。
特に命令がない場合、CPUはメモリに格納されている順序で命令を次々に実行します。ただし、CPUにメモリ内の別の場所に「ジャンプ」して、そこから命令を実行するように命令することはできます。asmでは任意のアドレスにすることができ、C ++などの高級言語では、ラベルでマークされたアドレスにのみジャンプできます(回避策はありますが、控えめに言ってもきれいではありません)。

この関数を見てみましょう(スニペット#10.1):

int myAlgo_withCalls(int a, int b) {
    int t1 = triple(a);
    int t2 = triple(b);
    return t1 - t2;
}

そして、trippleC ++の方法を呼び出す代わりに、次のようにします。

  1. trippleのコードをmyAlgo本文の先頭にコピーします
  2. myAlgoエントリー飛び越えますtrippleとのコードをgoto
  3. trippleのコードを実行する必要がある場合は、tripple呼び出し直後にコード行のスタックアドレスを保存して、後でここに戻って実行を続行できるようにします(PUSH_ADDRESS以下のマクロ)
  4. 1行目(tripple関数)のアドレスにジャンプして最後まで実行します(3.と4.一緒にCALLマクロです)
  5. tripple(ローカルをクリーンアップした後)の最後に、スタックの一番上からリターンアドレスを取得し、そこにジャンプします(RETマクロ)

C ++では特定のコードアドレスにジャンプする簡単な方法がないため、ジャンプの場所をマークするためにラベルを使用します。以下のマクロがどのように機能するかについては詳しく説明しません。マクロが私が言うことを実行すると信じてください(スニペット#10.2):

// pushes the address of the code at label's location on the stack
// NOTE1: this gonna work only with 32-bit compiler (so that pointer is 32-bit and fits in int)
// NOTE2: __asm block is specific for Visual C++. In GCC use https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html
#define PUSH_ADDRESS(labelName) {               \
    void* tmpPointer;                           \
    __asm{ mov [tmpPointer], offset labelName } \
    push(reinterpret_cast<int>(tmpPointer));    \
}

// why we need indirection, read https://stackoverflow.com/a/13301627/264047
#define TOKENPASTE(x, y) x ## y
#define TOKENPASTE2(x, y) TOKENPASTE(x, y)

// generates token (not a string) we will use as label name. 
// Example: LABEL_NAME(155) will generate token `lbl_155`
#define LABEL_NAME(num) TOKENPASTE2(lbl_, num)

#define CALL_IMPL(funcLabelName, callId)    \
    PUSH_ADDRESS(LABEL_NAME(callId));       \
    goto funcLabelName;                     \
    LABEL_NAME(callId) :

// saves return address on the stack and jumps to label `funcLabelName`
#define CALL(funcLabelName) CALL_IMPL(funcLabelName, __LINE__)

// takes address at the top of stack and jump there
#define RET() {                                         \
    int tmpInt;                                         \
    pop(tmpInt);                                        \
    void* tmpPointer = reinterpret_cast<void*>(tmpInt); \
    __asm{ jmp tmpPointer }                             \
}

void myAlgo_asm() {
    goto my_algo_start;

triple_label:
    push(BP);
    BP = SP;
    SP -= 1;

    // stack[BP] == old BP, stack[BP + 1] == return address
    stack[BP - 1] = stack[BP + 2] * 3;
    AX = stack[BP - 1];

    SP = BP;     
    pop(BP);
    RET();

my_algo_start:
    push(BP);   // SP == 995
    BP = SP;    // BP == 995; stack[BP] == old BP, 
                // stack[BP + 1] == dummy return address, 
                // `a` at [BP + 2], `b` at [BP + 3]
    SP -= 2;    // SP == 993

    push(AX);
    push(stack[BP + 2]);
    CALL(triple_label);
    stack[BP - 1] = AX;
    SP -= 1;
    pop(AX);

    push(AX);
    push(stack[BP + 3]);
    CALL(triple_label);
    stack[BP - 2] = AX;
    SP -= 1;
    pop(AX);

    AX = stack[BP - 1] - stack[BP - 2];

    SP = BP; // cleanup locals, SP == 997
    pop(BP);
}

int main() {
    push(AX);
    push(22);
    push(11);
    push(7777); // dummy value, so that offsets inside function are like we've pushed return address
    myAlgo_asm();
    assert(myAlgo_withCalls(11, 22) == AX);
    SP += 1; // pop dummy "return address"
    SP += 2;
    pop(AX);
}

注:
10a。リターンアドレスはスタックに格納されているため、原則として変更できます。これがスタックスマッシング攻撃の仕組みです
10b。triple_label(ローカルのクリーンアップ、古いBPの復元、戻り)の「最後」にある最後の3つの命令は、関数のエピローグと呼ばれます。


11.組み立て

それでは、の実際のasmを見てみましょうmyAlgo_withCalls。Visual Studioでこれを行うには:

  • ビルドプラットフォームをx86に設定します(x86_64ではありません
  • ビルドタイプ:デバッグ
  • myAlgo_withCalls内のどこかにブレークポイントを設定します
  • 実行し、ブレークポイントで実行が停止したら、Ctrl + Alt + Dを押します。

asmのようなC ++との違いの1つは、asmのスタックがintではなくバイトで動作することです。したがって、1つのスペースを予約するためにint、SPは4バイトずつデクリメントされます。
ここに行きます(スニペット#11.1、コメントの行番号は要点からのものです):

;   114: int myAlgo_withCalls(int a, int b) {
 push        ebp        ; create stack frame 
 mov         ebp,esp  
; return address at (ebp + 4), `a` at (ebp + 8), `b` at (ebp + 12)
 
 sub         esp,0D8h   ; reserve space for locals. Compiler can reserve more bytes then needed. 0D8h is hexadecimal == 216 decimal 
 
 push        ebx        ; cdecl requires to save all these registers
 push        esi  
 push        edi  
 
 ; fill all the space for local variables (from (ebp-0D8h) to (ebp)) with value 0CCCCCCCCh repeated 36h times (36h * 4 == 0D8h)
 ; see https://stackoverflow.com/q/3818856/264047
 ; I guess that's for ease of debugging, so that stack is filled with recognizable values
 ; 0CCCCCCCCh in binary is 110011001100...
 lea         edi,[ebp-0D8h]     
 mov         ecx,36h    
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
 
;   115:    int t1 = triple(a);
 mov         eax,dword ptr [ebp+8]   ; push parameter `a` on the stack
 push        eax  
 
 call        triple (01A13E8h)  
 add         esp,4                   ; clean up param 
 mov         dword ptr [ebp-8],eax   ; copy result from eax to `t1`
 
;   116:    int t2 = triple(b);
 mov         eax,dword ptr [ebp+0Ch] ; push `b` (0Ch == 12)
 push        eax  
 
 call        triple (01A13E8h)  
 add         esp,4  
 mov         dword ptr [ebp-14h],eax ; t2 = eax
 
 mov         eax,dword ptr [ebp-8]   ; calculate and store result in eax
 sub         eax,dword ptr [ebp-14h]  

 pop         edi  ; restore registers
 pop         esi  
 pop         ebx  
 
 add         esp,0D8h  ; check we didn't mess up esp or ebp. this is only for debug builds
 cmp         ebp,esp  
 call        __RTC_CheckEsp (01A116Dh)  
 
 mov         esp,ebp  ; destroy frame
 pop         ebp  
 ret  

そしてasmfor trippleスニペット#11.2):

 push        ebp  
 mov         ebp,esp  
 sub         esp,0CCh  
 push        ebx  
 push        esi  
 push        edi  
 lea         edi,[ebp-0CCh]  
 mov         ecx,33h  
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
 imul        eax,dword ptr [ebp+8],3  
 mov         dword ptr [ebp-8],eax  
 mov         eax,dword ptr [ebp-8]  
 pop         edi  
 pop         esi  
 pop         ebx  
 mov         esp,ebp  
 pop         ebp  
 ret  

この投稿を読んだ後、アセンブリが以前ほど不可解に見えないことを願っています:)


投稿の本文からのリンクといくつかのさらなる読み物は次のとおりです。


私がこれを尋ねたのはずっと前のことでした、それは本当に素晴らしい深い答えです。ありがとう。
bplus 2017

回答の最初の部分でレジスタに16ビット名を使用しているのはなぜですか?あなたが実際の16ビットコードについて話して[SP]いたなら、それは有効なアドレス指定16ビットモードではありません。おそらく使用するのが最善ESPです。また、SPとして宣言する場合はint、要素ごとに1ではなく4ずつ変更する必要があります(宣言した場合はlong *SPSP += 2増分する2 * sizeof(int)ため、2つの要素が削除されます。ただし、intSPの場合は、32の。のSP += 8ようになりadd esp, 8ます。
Peter Cordes 2018

魅力的!Cを使って組み立てを説明しようとするのは面白いと思います。これまで見たことがありません。きちんとした。「ローカル変数なし」の名前を「ローカル変数のしくみ」または単に「ローカル変数」に変更することをお勧めします。
デイブドプソン2018

@PeterCordes 16ビット名(SP、BP)の理由は明快さです-SPは簡単に「スタックポインタ」に変換されます。適切な32ビット名を使用する場合は、16/32/64ビットモードの違いを説明するか、説明しないでおく必要があります。私の意図は、JavaまたはPythonしか知らない人でも、頭をあまり傷つけずに投稿をフォローできるようにすることでした。そして、メモリアドレス指定は読者の気を散らすだけだと思います。さらに、好奇心旺盛なトピックにウィキブックスのリンクを貼って、投稿の最後にESPについていくつかの言葉を述べました。
アレクサンダーマラホフ2018

1
これを回避するには、スタックが大きくなっても変化しないアンカーインデックスが必要です。 必要は間違った言葉です。-fomit-frame-pointer何年もの間、gccとclangのデフォルトでした。実際のasmを見ている人は、EBP / RBPが通常フレームポインタとして使用されないことを知っておく必要があります。「従来、人間はプッシュ/ポップで変化しないアンカーを望んでいましたが、コンパイラーはオフセットの変化を追跡できます」と私は言います。次に、バックトレースに関するセクションを更新して、これがレガシーメソッドであり、DWARF.eh_frameメタデータまたはWindowsx86-64メタデータが使用可能な場合にデフォルトで使用されないようにすることができます。
Peter Cordes 2018

7

スタックがハードウェアに実装されているかどうかについては、このWikipediaの記事が役立つ場合があります。

x86などの一部のプロセッサフ​​ァミリには、現在実行中のスレッドのスタックを操作するための特別な命令があります。PowerPCやMIPSを含む他のプロセッサフ​​ァミリは、明示的なスタックサポートを持っていませんが、代わりに規則に依存し、スタック管理をオペレーティングシステムのアプリケーションバイナリインターフェイス(ABI)に委任します。

その記事とそれがリンクしている他の記事は、プロセッサでのスタックの使用法を理解するのに役立つかもしれません。


4

コンセプト

まず、あなたがそれを発明した人であるかのように全体を考えてください。このような:

まず、配列とそれが低レベルでどのように実装されているかを考えてください->基本的には、連続したメモリ位置(互いに隣接するメモリ位置)のセットにすぎません。頭の中にその精神的なイメージができたので、配列内のデータを削除または追加するときに、これらのメモリ位置のいずれかにアクセスして自由に削除できるという事実を考えてみてください。ここで、同じ配列について考えますが、場所を削除する可能性の代わりに、配列内のデータを削除または追加するときに最後の場所のみを削除することにします。その配列のデータをそのように操作する新しいアイデアはLIFOと呼ばれ、後入れ先出しを意味します。配列から何かを削除するたびに並べ替えアルゴリズムを使用しなくても、配列の内容を追跡しやすくなるため、非常に優れたアイデアです。また、配列の最後のオブジェクトのアドレスが何であるかを常に知るために、CPU内の1つのレジスタを専用にして追跡します。現在、レジスタがそれを追跡する方法は、配列に何かを削除または追加するたびに、配列から削除または追加したオブジェクトの量だけレジスタ内のアドレスの値をデクリメントまたはインクリメントすることです(彼らが占有したアドレス空間の量)。また、レジスタをデクリメントまたはインクリメントする量が、オブジェクトごとに1つの量(4つのメモリ位置、つまり4バイトなど)に固定されていることを確認する必要があります。これも、追跡を容易にし、それを可能にするためです。ループは反復ごとに固定のインクリメントを使用するため、一部のループ構造でそのレジスタを使用します(例:配列をループでループするには、反復ごとにレジスタを4ずつインクリメントするループを作成します。これは、配列に異なるサイズのオブジェクトが含まれている場合は不可能です)。最後に、この新しいデータ構造を「スタック」と呼ぶことにします。これは、レストランのプレートのスタックを思い出させるため、常にそのスタックの一番上にあるプレートを削除または追加します。

実装

ご覧のとおり、スタックは、それを操作する方法を決定した連続したメモリ位置の配列にすぎません。そのため、スタックを制御するために特別な命令やレジスタを使用する必要さえないことがわかります。基本的なmov、add、sub命令を使用し、次のようにESPとEBPの代わりに汎用レジスタを使用して自分で実装できます。

mov edx、0FFFFFFFFh

; ->これは、コードとデータから最も離れたスタックの開始アドレスになります。これは、前に説明したスタック内の最後のオブジェクトを追跡するレジスタとしても機能します。これを「スタックポインタ」と呼ぶので、ESPが通常使用されるレジスタEDXを選択します。

sub edx、4

mov [edx]、dword ptr [someVar]

; ->これらの2つの命令は、スタックポインタを4つのメモリ位置でデクリメントし、[someVar]メモリ位置で始まる4バイトをEDXが指すメモリ位置にコピーします。PUSH命令がESPをデクリメントするのと同じように、ここでのみ実行しました。手動でEDXを使用しました。したがって、PUSH命令は基本的に、ESPで実際にこれを行う短いオペコードです。

mov eax、dword ptr [edx]

edxを追加、4

; ->ここでは逆のことを行います。まず、EDXがポイントするメモリ位置から始まる4バイトをレジスタEAXにコピーします(ここで任意に選択したので、必要な場所にコピーできます)。次に、スタックポインタEDXを4メモリ位置ずつインクリメントします。これは、POP命令が行うことです。

これで、PUSH命令とPOP命令、およびレジスタESPとEBPが、上記の「スタック」データ構造の概念の書き込みと読み取りを容易にするためにIntelによって追加されたことがわかります。スタック操作用のPUSHおよびPOP命令と専用レジスタを持たないRISC(縮小命令セット)CPUがまだいくつかあります。これらのCPUのアセンブリプログラムを作成するときは、次のように自分でスタックを実装する必要があります。見せました。


3

抽象スタックとハードウェア実装スタックを混同します。後者はすでに実装されています。


3

あなたが探している主な答えはすでにほのめかされていると思います。

x86コンピューターが起動すると、スタックはセットアップされません。プログラマーは、起動時に明示的に設定する必要があります。ただし、すでにオペレーティングシステムを使用している場合は、これは処理されています。以下は、単純なブートストラッププログラムのコードサンプルです。

最初にデータとスタックセグメントレジスタが設定され、次にスタックポインタがそれを超えて0x4000に設定されます。


    movw    $BOOT_SEGMENT, %ax
    movw    %ax, %ds
    movw    %ax, %ss
    movw    $0x4000, %ax
    movw    %ax, %sp

このコードの後、スタックを使用できます。今ではさまざまな方法でそれを行うことができると確信していますが、これはその考えを説明するものになると思います。



1

スタックはすでに存在しているので、コードを書くときにそれを想定できます。スタックには、関数の戻りアドレス、ローカル変数、および関数間で渡される変数が含まれています。使用できるBP、SP(Stack Pointer)などのスタックレジスタも組み込まれているため、前述の組み込みコマンドがあります。スタックがまだ実装されていない場合、関数を実行できず、コードフローが機能しませんでした。


1

スタックは、スタックポインタを使用して「実装」されます。スタックポインタは、(ここではx86アーキテクチャを想定して)スタックセグメントを指します。何かがスタックにプッシュされるたびに(pushl、call、または同様のスタックオペコードによって)、スタックポインターが指すアドレスに書き込まれ、スタックポインターがデクリメントされます(スタックは下に向かって成長します。つまり、アドレスが小さくなります)。 。スタックから何かをポップすると(popl、ret)、スタックポインターがインクリメントされ、値がスタックから読み取られます。

ユーザースペースアプリケーションでは、アプリケーションの起動時にスタックがすでに設定されています。カーネル空間環境では、最初にスタックセグメントとスタックポインタを設定する必要があります...


1

ガスアセンブラは特に見たことがありませんが、一般に、スタックの最上位が存在するメモリ内の場所への参照を維持することにより、スタックが「実装」されます。メモリの場所は、アーキテクチャごとに異なる名前を持つレジスタに格納されますが、スタックポインタレジスタと考えることができます。

popコマンドとpushコマンドは、マイクロ命令に基づいて構築することにより、ほとんどのアーキテクチャに実装されています。ただし、一部の「教育アーキテクチャ」では、自分で実装する必要があります。機能的には、プッシュは次のように実装されます。

   load the address in the stack pointer register to a gen. purpose register x
   store data y at the location x
   increment stack pointer register by size of y

また、一部のアーキテクチャでは、最後に使用されたメモリアドレスがスタックポインタとして保存されます。次に利用可能なアドレスを保存するものもあります。


1

スタックとは何ですか?スタックはデータ構造の一種であり、コンピューターに情報を格納する手段です。新しいオブジェクトがスタックに入力されると、以前に入力されたすべてのオブジェクトの上に配置されます。言い換えると、スタックデータ構造は、カード、書類、クレジットカードの郵送物、またはその他の考えられる現実世界のオブジェクトのスタックとまったく同じです。スタックからオブジェクトを削除すると、一番上のオブジェクトが最初に削除されます。この方法はLIFO(後入れ先出し)と呼ばれます。

「スタック」という用語は、ネットワークプロトコルスタックの略語でもあります。ネットワークでは、コンピューター間の接続は一連の小さな接続を介して行われます。これらの接続またはレイヤーは、同じ方法で構築および破棄されるという点で、スタックデータ構造のように機能します。


0

スタックがデータ構造であることは正しいです。多くの場合、使用するデータ構造(スタックを含む)は抽象的であり、メモリ内の表現として存在します。

この場合に使用しているスタックには、より重要な存在があります。これは、プロセッサの実際の物理レジスタに直接マップされます。データ構造として、スタックはFILO(ファーストイン、ラストアウト)構造であり、入力された順序とは逆の順序でデータが削除されるようにします。ビジュアルについては、StackOverflowのロゴをご覧ください。;)

命令スタックを使用しています。これは、プロセッサに供給する実際の命令のスタックです。


違う。これは「命令スタック」ではありません(そのようなものはありますか?)これは単にスタックレジスタを介してアクセスされるメモリです。一時ストレージ、プロシージャパラメータ、および(最も重要な)関数呼び出しのリターンアドレスに使用
Javier

0

コールスタックは、x86命令セットとオペレーティングシステムによって実装されます。

プッシュやポップなどの命令は、スタックがスレッドごとに大きくなるにつれてオペレーティングシステムがメモリの割り当てを処理する間、スタックポインタを調整します。

x86スタックが上位アドレスから下位アドレスに「成長」するという事実により、このアーキテクチャはバッファオーバーフロー攻撃の影響を受けやすくなります。


1
x86スタックが大きくなると、バッファオーバーフローの影響を受けやすくなるのはなぜですか?エキスパンドアップセグメントでも同じオーバーフローが発生しませんか?
ネイサンフェルマン

@nathan:アプリケーションに負の量のメモリをスタックに割り当てることができる場合のみ。
ハビエル

1
バッファオーバーフロー攻撃は、スタックベースの配列の終わりを超えて書き込みます-char userName [256]、これはメモリを低い方から高い方に書き込み、リターンアドレスなどを上書きできるようにします。スタックが同じ方向に成長した場合、未割り当てのスタックのみを上書きできます。
モーリスフラナガン

0

スタックは「単なる」データ構造であるというのは正しいことです。ただし、ここでは、特別な目的で使用されるハードウェアで実装されたスタック、つまり「スタック」を指します。

多くの人が、ハードウェアで実装されたスタックと(ソフトウェア)スタックのデータ構造についてコメントしています。3つの主要なスタック構造タイプがあることを付け加えたいと思います-

  1. コールスタック-これはあなたが求めているものです!関数パラメータやリターンアドレスなどを格納します。その本の第4章(4ページ目、つまり53ページ)の関数を読んでください。良い説明があります。
  2. 特別なことをするためにプログラムで使用する可能性のある汎用スタック...
  3. 汎用ハードウェアスタック
    これについてはよくわかりませんが、一部のアーキテクチャで利用可能な汎用ハードウェア実装スタックがあることをどこかで読んだことを覚えています。これが正しいかどうか誰かが知っているなら、コメントしてください。

最初に知っておくべきことは、あなたがプログラミングしているアーキテクチャであり、それは本が説明しています(私はちょうどそれを調べました--link)。物事を本当に理解するために、x86のメモリ、アドレス指定、レジスタ、およびアーキテクチャについて学ぶことをお勧めします(本から学んでいることだと思います)。


0

LIFO方式でローカル状態を保存および復元する必要がある関数の呼び出し(一般化されたコルーチンアプローチとは対照的に)は、アセンブリ言語とCPUアーキテクチャが基本的にこの機能を組み込むという非常に一般的なニーズであることが判明しました。同じおそらく、スレッド化、メモリ保護、セキュリティレベルなどの概念について言えます。理論的には、独自のスタック、呼び出し規則などを実装できますが、一部のopcodeとほとんどの既存のランタイムは、このネイティブの「スタック」の概念に依存していると思います。 。


0

stackメモリの一部です。それがために使用inputしてoutputfunctions。また、関数の戻り値を記憶するためにも使用されます。

esp レジスタはスタックアドレスを記憶しています。

stack そして espハードウェアによって実装されます。また、自分で実装することもできます。それはあなたのプログラムを非常に遅くします。

例:

nop // esp= 0012ffc4

push 0 // esp= 0012ffc0、Dword [0012ffc0] = 00000000

proc01を呼び出す// esp= 0012ffbc、Dword [0012ffbc] = eipeip= adrr [proc01]

pop eax// eax= Dword [ esp]、esp= esp+ 4


0

私はスタックが機能の観点からどのように機能するかを探していましたが、このブログは素晴らしいものであり、スタックの概念を最初から説明し、スタックがスタックに値を格納する方法を説明しています。

今あなたの答えに。Pythonで説明しますが、どの言語でもスタックがどのように機能するかがわかります。

ここに画像の説明を入力してください

そのプログラム:

def hello(x):
    if x==1:
        return "op"
    else:
        u=1
        e=12
        s=hello(x-1)
        e+=1
        print(s)
        print(x)
        u+=1
    return e

hello(3)

ここに画像の説明を入力してください

ここに画像の説明を入力してください

出典:Cryptroix

それがブログでカバーするそのトピックのいくつか:

How Function work ?
Calling a Function
 Functions In a Stack
 What is Return Address
 Stack
Stack Frame
Call Stack
Frame Pointer (FP) or Base Pointer (BP)
Stack Pointer (SP)
Allocation stack and deallocation of stack
StackoverFlow
What is Heap?

しかし、Python言語で説明しているので、必要に応じて確認できます。


Criptoixサイトは廃止され、web.archive.orgにコピーはありません
Alexander Malakhov

1
@AlexanderMalakhov Cryptroixは、ホスティングの問題のために機能していませんでした。Cryptroixは現在稼働中です。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.