私は現在、スタックがどのように機能するかを理解しようとしているので、自分自身にいくつかのことを教えることにしました アセンブリ言語 、私はこの本を使用しています:
http://savannah.nongnu.org/projects/pgubook/
Gas を使用しており、 Linux Mint で開発を行っています。
私は何かに少し混乱しています:
私が知る限り、スタックは単なるデータ構造です。したがって、アセンブリでコーディングしている場合、自分でスタックを実装する必要があると思いました。ただし、次のようなコマンドがあるため、これは当てはまらないようです。
pushl
popl
x86 アーキテクチャのAssemblyでコーディングし、Gas構文を使用する場合、スタックは既に実装されている単なるデータ構造ですか?それとも、実際にハードウェアレベルで実装されていますか?それとも別のものですか?また、他のチップセットのほとんどのアセンブリ言語には、既にスタックが実装されていますか?
これは少し馬鹿げた質問であることは知っていますが、実際これはかなり混乱しています。
主に、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
(プレイしたい場合に備えて、この回答のすべてのコードの 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呼び出し規約を使用しました:
myAlgo_noLPR_withAnchor
_関数)によって保持される必要があります。そのため、呼び出し元(main
関数)は、電話。(出典: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++の方法を呼び出す代わりに、次のことを行います。
tripple
の本体全体をmyAlgo
内にコピーしますmyAlgo
エントリで、tripple
のコードをgoto
でジャンプしますtripple
のコードを実行する必要がある場合は、tripple
呼び出しの直後にコード行のスタックアドレスを保存して、後でここに戻って実行を継続できるようにします(以下の_Push_ADDRESS
_マクロ)tripple
関数のアドレスにジャンプして最後まで実行します(3と4は一緒にCALL
マクロです)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でこれを行うには:
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
_
この記事を読んだ後、アセンブリは以前ほど謎めいていません:)
投稿の本文からのリンクと、さらに詳しい説明を次に示します。
スタックがハードウェアに実装されているかどうかについては、この Wikipediaの記事 が役立つ場合があります。
X86などの一部のプロセッサファミリには、現在実行中のスレッドのスタックを操作するための特別な命令があります。 PowerPCやMIPSを含む他のプロセッサファミリは、明示的なスタックサポートを持ちませんが、代わりに規約に依存し、スタック管理をオペレーティングシステムのApplication Binary Interface(ABI)に委任します。
その記事とそれがリンクしている他の記事は、プロセッサーでのスタックの使用感をつかむのに役立つかもしれません。
コンセプト
まず、あなたがそれを発明した人であるかのように全体を考えてください。このような:
まず、配列と低レベルでの実装方法を考えてください->基本的には、連続した一連のメモリ位置(互いに隣接するメモリ位置)だけです。頭の中にその精神的なイメージがあるので、配列のデータを削除または追加するときに、これらのメモリロケーションのいずれかにアクセスし、自由に削除できるという事実を考えてください。ここで同じ配列を考えますが、場所を削除する可能性の代わりに、配列のデータを削除または追加するときに最後の場所のみを削除することにします。この配列のデータをそのように操作する新しいアイデアは、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用のアセンブリプログラムを作成している間、私が示したように自分でスタックを実装する必要があります君は。
あなたが探している主な答えはすでに示唆されていると思います。
X86コンピューターが起動すると、スタックはセットアップされません。プログラマは、起動時に明示的に設定する必要があります。ただし、すでにオペレーティングシステムを使用している場合は、これが処理されます。以下は、単純なbootstrapプログラムからのコードサンプルです。
最初にデータおよびスタックセグメントレジスタが設定され、次にスタックポインターが0x4000を超えて設定されます。
movw $BOOT_SEGMENT, %ax
movw %ax, %ds
movw %ax, %ss
movw $0x4000, %ax
movw %ax, %sp
このコードの後、スタックを使用できます。今では、さまざまな方法で実現できると確信していますが、これはアイデアを説明するものだと思います。
抽象スタックとハードウェア実装スタックを混同します。後者はすでに実装されています。
Stackとは何ですか?スタックは、データ構造の一種であり、コンピューターに情報を保存する手段です。新しいオブジェクトがスタックに入力されると、以前に入力されたすべてのオブジェクトの上に配置されます。言い換えると、スタックデータ構造は、カード、書類、クレジットカードの郵送物、または他の現実世界のオブジェクトのスタックのようなものです。スタックからオブジェクトを削除する場合、一番上のオブジェクトが最初に削除されます。このメソッドは、LIFO(後入れ先出し)と呼ばれます。
「スタック」という用語は、ネットワークプロトコルスタックの略語でもあります。ネットワークでは、コンピューター間の接続は一連のより小さな接続を介して行われます。これらの接続またはレイヤーは、同じ方法で構築および破棄されるという点で、スタックデータ構造のように機能します。
スタックはすでに存在しているため、コードを記述するときにそれを想定できます。スタックには、関数の戻りアドレス、ローカル変数、および関数間で渡される変数が含まれます。 BP、SP(Stack Pointer)ビルトイン、使用できるビルトインなどのスタックレジスタもあります。したがって、前述のビルトインコマンドです。スタックがまだ実装されていない場合、関数を実行できず、コードフローが機能しませんでした。
スタックは、スタックポインタを使用して「実装」されます。スタックポインタは、(ここではx86アーキテクチャを想定して)スタックを指しますセグメント。 (pushl、call、または同様のスタックオペコードによって)スタックに何かがプッシュされるたびに、スタックポインターが指すアドレスに書き込まれ、スタックポインターdecremented(stack is成長下向き、つまりより小さいアドレス)。スタックから何かをポップすると(popl、ret)、スタックポインターはincrementedになり、値はスタックから読み取られます。
ユーザースペースアプリケーションでは、アプリケーションの起動時にスタックが既に設定されています。カーネル空間環境では、最初にスタックセグメントとスタックポインタを設定する必要があります...
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
また、一部のアーキテクチャは、最後に使用されたメモリアドレスをスタックポインタとして保存します。いくつかは、次に利用可能なアドレスを保存します。
ローカル状態をLIFO方式(一般的なコルーチンアプローチとは対照的に)で保存および復元する必要がある関数の呼び出しは、アセンブリ言語とCPUアーキテクチャは基本的にこの機能を組み込みます。同じことはおそらくスレッド、メモリ保護、セキュリティレベルなどの概念についても言えます。理論的には、独自のスタック、呼び出し規約などを実装できますが、オペコードと既存のランタイムこの「スタック」のネイティブコンセプトに依存します。
スタックは「単なる」データ構造であることは正しいです。ただし、ここでは、特別な目的に使用されるハードウェア実装スタック「スタック」を指します。
多くの人が、ハードウェア実装のスタックと(ソフトウェア)スタックのデータ構造についてコメントしています。 3つの主要なスタック構造タイプがあることを付け加えます。
最初に知っておくべきことは、プログラミングの対象となるアーキテクチャであり、これについては本で説明しています(リンクを調べたところです)。物事を本当に理解するために、x86のメモリ、アドレス指定、レジスター、アーキテクチャーについて学ぶことをお勧めします(この本から学んでいることを前提としています)。
呼び出しスタックは、x86命令セットとオペレーティングシステムによって実装されます。
プッシュやポップなどの命令はスタックポインタを調整し、オペレーティングシステムは各スレッドのスタックが大きくなるにつれてメモリの割り当てを処理します。
X86スタックが上位アドレスから下位アドレスに「成長」するという事実により、このアーキテクチャはより多くの バッファオーバーフロー攻撃を受けやすくなります。
stack
はメモリの一部です。 input
およびoutput
のfunctions
に使用します。また、関数の戻り値を記憶するために使用します。
esp
レジスタはスタックアドレスを記憶しています。
stack
およびesp
はハードウェアによって実装されます。また、自分で実装することもできます。プログラムが非常に遅くなります。
例:
nop // esp
= 0012ffc4
プッシュ0 // esp
= 0012ffc0、Dword [0012ffc0] = 00000000
call proc01 // esp
= 0012ffbc、Dword [0012ffbc] = eip
、eip
= adrr [proc01]
pop eax
// eax
= Dword [esp
]、esp
= esp
+ 4
私はスタックが機能の観点からどのように機能するかを探していましたが、私は見つけました このブログ その素晴らしいとそのスタックの概念をゼロから説明し、スタックがスタックに値を格納する方法。
さあ、あなたの答えに。 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言語で説明していますので、必要に応じてご覧ください。
スタックがデータ構造であることは正しいです。多くの場合、使用するデータ構造(スタックを含む)は抽象的であり、メモリ内の表現として存在します。
この場合、使用しているスタックにはより多くのマテリアルが存在します。これは、プロセッサの実際の物理レジスタに直接マップされます。データ構造として、スタックはデータが入力された逆の順序で削除されることを保証するFILO(先入れ先出し)構造です。 StackOverflowのロゴをご覧ください! ;)
命令スタックで作業しています。これは、プロセッサに供給する実際の命令のスタックです。