web-dev-qa-db-ja.com

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

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

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

Gas を使用しており、 Linux Mint で開発を行っています。

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

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

pushl
popl

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

これは少し馬鹿げた質問であることは知っていますが、実際これはかなり混乱しています。

70
bplus

主に、program's stackおよびany old stack

Aスタック

Last In First Outシステムの情報で構成される抽象データ構造です。任意のオブジェクトをスタックに配置し、イン/アウトトレイのように、それらを再び取り外します。一番上のアイテムは常に取り外したもので、常に一番上に置きます。

Aプログラムスタック

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

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

wikipedia の記事を読んで理解していることを確認する必要があります。この記事は、対象のハードウェアスタックの適切な説明を提供します。

このチュートリアル もあります。これは、古い16ビットレジスタに関してスタックを説明しますが、役に立つかもしれません 別の1つ 特にスタックについて。

Nils Pipenbrinckから:

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

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

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

pop命令は次のようになります。

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

(プレイしたい場合に備えて、この回答のすべてのコードの Gist を作成しました)

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


1。スタックとは何ですか

スタックは、起動時にすべてのスレッドに割り当てられる連続したメモリチャンクです。そこに好きなものを保存できます。 C++の用語(コードスニペット#1):

_const int STACK_CAPACITY = 1000;
thread_local int stack[STACK_CAPACITY];
_

2。スタックの上部/下部

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

_cin >> stack[333];
cin >> stack[517];
stack[555] = stack[333] + stack[517];
_

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

(x86)asmのスタックに関する奇妙な点の1つは、最後のインデックスから開始して、より低いインデックスに移動することです:stack [999]、次にstack [998]など(snippet#2.2) :

_cin >> stack[999];
cin >> stack[998];
stack[997] = stack[999] + stack[998];
_

そして(まだ混乱しているので)_stack[999]_の「公式」名はbottom of the stackです。
最後に使用したセル(上記の例では_stack[997]_)はtop of the stackと呼ばれます( を参照してください。 x86 )で。


3。スタックポインター(SP)

Asmコードのどこにでも見えるのはスタックだけではありません。 CPUレジスタを操作することもできます( 汎用レジスタ を参照)。それらは本当にグローバル変数のようなものです:

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

スタックに追加された最後の要素を追跡する専用のCPUレジスタ(SP)があります。名前が示すように、まあ、ポインター(0xAAAABBCCのようなメモリアドレスを保持しています)です。しかし、この投稿の目的のために、私はそれをインデックスとして使用します。

スレッドの開始_SP == STACK_CAPACITY_で、必要に応じてデクリメントします。ルールは、スタックの最上部を超えてスタックセルに書き込むことはできず、SPより小さいインデックスは無効なので、最初にSPおよびをデクリメントします次に新しく割り当てられたセルに値を書き込みます。

スタックに複数の値を連続して追加することがわかっている場合、それらすべての値のために事前にスペースを予約できます(スニペット#3):

_SP -= 3;
cin >> stack[999];
cin >> stack[998];
stack[997] = stack[999] + stack[998];
_

注。 これで、スタックへの「割り当て」が非常に高速である理由がわかります。実際には何も割り当てません(newキーワードやmallocのように)、それは単なる整数のデクリメントです。


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。プッシュ/ pop

スタックの一番上に新しい要素を追加することは非常に頻繁に行われるため、CPUには特別な命令Pushがあります。このように宣言します(snippet 5.1):

_void Push(int value) {
    --SP;
    stack[SP] = value;
}
_

同様に、スタックの一番上の要素(snippet 5.2)を取得します:

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

プッシュ/ポップの一般的な使用パターンは、一時的に値を保存することです。たとえば、変数myVarに何か有用なものがあり、何らかの理由でそれを上書きする計算を行う必要があります(snippet 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)フレームポインターとも呼ばれる)およびstack frame

さらに「高度な」機能を使用して、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)で行うように、結果を返す前にそこに結果を格納する新しいローカル変数を導入することにしたと想像してください。関数の本体は(snippet#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
}
_

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

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


9。呼び出し規約

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

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

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

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

(出典:Stack Overflow Documentationの「32ビットcdecl」の例、 icktoofay および Peter Cordes による著作権2016、CC BY- SA 3.0。Stack Overflowドキュメントの完全なコンテンツの アーカイブ はarchive.orgにあり、この例ではトピックID 3261および例ID 11196でインデックスが付けられています。)


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;
}
_

そして、tripple C++の方法を呼び出す代わりに、次のことを行います。

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

C++では特定のコードアドレスにジャンプする簡単な方法がないため、ラベルを使用してジャンプの場所をマークします。以下のマクロがどのように機能するかについては詳しく説明しませんが、私が言っていることを実行すると信じてください(snippet#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。 リターンアドレスはスタックに格納されているため、原則として変更できます。これは stack smashing attack の仕組みです
10b。 _triple_label_の「終わり」にある最後の3つの命令(ローカルのクリーンアップ、古いBPの復元、戻る)は、関数のエピローグと呼ばれます


11。アセンブリ

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

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

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

_;   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  
_

trippleのasm(スニペット#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  
_

この記事を読んだ後、アセンブリは以前ほど謎めいていません:)


投稿の本文からのリンクと、さらに詳しい説明を次に示します。

  • Eli Bendersky、 スタックのトップがx86 にある場所-トップ/ボトム、プッシュ/ポップ、SP、スタックフレーム、呼び出し規約
  • Eli Bendersky、 x86-64 のスタックフレームレイアウト-x64を渡す引数、スタックフレーム、レッドゾーン
  • マリランド大学、 Understanding the Stack -スタックの概念についての非常によく書かれた入門書。 (これはMIPS(x86ではない)およびGAS構文用ですが、これはトピックにとって重要ではありません)。 MIPS ISA Programming に関する他のメモを参照してください。
  • x86 Asm wikibook、 汎用レジスタ
  • x86逆アセンブリwikibook、 The Stack
  • x86逆アセンブリwikibook、 関数とスタックフレーム
  • Intelソフトウェア開発者向けマニュアル -本当にハードコアだと思っていましたが、驚くほど読みやすいです(情報量は圧倒的ですが)
  • ジョナサン・ド・ボイン・ポラード、 関数ペリログ のジェネレーション-プロローグ/エピローグ、スタックフレーム/アクティベーションレコード、レッドゾーン
20

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

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

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

7
Leaf Garland

コンセプト

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

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

実装

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

mov edx、0FFFFFFFFh

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

sub edx、4

mov [edx]、dword ptr [someVar]

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

mov eax、dword ptr [edx]

edx、4を追加

;->そして、ここで反対のことを行います。まず、EDXがポイントするメモリ位置から始まる4バイトをレジスタEAXにコピーします(ここで任意に選択し、どこにでもコピーできます)欲しかった)。そして、スタックポインターEDXを4つのメモリ位置だけインクリメントします。これは、POP命令が行うことです。

上記の「スタック」データ構造の概念をより簡単に読み書きできるようにするために、PushおよびPOP命令とレジスタESP ans EBPが追加されたことがわかります。プッシュ操作とスタック操作用の専用レジスタを持たないRISC(Reduced Instruction Set)CPUがまだあり、それらのCPU用のアセンブリプログラムを作成している間、私が示したように自分でスタックを実装する必要があります君は。

4
Zod

あなたが探している主な答えはすでに示唆されていると思います。

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

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


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

このコードの後、スタックを使用できます。今では、さまざまな方法で実現できると確信していますが、これはアイデアを説明するものだと思います。

3
Mr. Shickadance

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

3
sharptooth

スタックは、プログラムや関数がメモリを使用する方法です。

スタックは常に私を混乱させたので、イラストを作りました:

The stack is like stalactites

svgバージョンはこちら

2
Alexander

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

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

1
rahul soni

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

1
Gal Goldman

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

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

1
DevSolar

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

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

   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
Charlie White

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

0
aaron

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

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

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

最初に知っておくべきことは、プログラミングの対象となるアーキテクチャであり、これについては本で説明しています(リンクを調べたところです)。物事を本当に理解するために、x86のメモリ、アドレス指定、レジスター、アーキテクチャーについて学ぶことをお勧めします(この本から学んでいることを前提としています)。

0
batbrat

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

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

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

0

stackはメモリの一部です。 inputおよびoutputfunctionsに使用します。また、関数の戻り値を記憶するために使用します。

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

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

例:

nop // esp = 0012ffc4

プッシュ0 // esp = 0012ffc0、Dword [0012ffc0] = 00000000

call proc01 // esp = 0012ffbc、Dword [0012ffbc] = eipeip = adrr [proc01]

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

0
Amir

私はスタックが機能の観点からどのように機能するかを探していましたが、私は見つけました このブログ その素晴らしいとそのスタックの概念をゼロから説明し、スタックがスタックに値を格納する方法。

さあ、あなたの答えに。 pythonで説明しますが、どの言語でもスタックがどのように機能するかをよく理解できます。

enter image description here

そのプログラム:

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)

enter image description here

enter image description here

ソース: 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言語で説明していますので、必要に応じてご覧ください。

0
user6932350

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

この場合、使用しているスタックにはより多くのマテリアルが存在します。これは、プロセッサの実際の物理レジスタに直接マップされます。データ構造として、スタックはデータが入力された逆の順序で削除されることを保証するFILO(先入れ先出し)構造です。 StackOverflowのロゴをご覧ください! ;)

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

0
Dave Swersky