現在、コンピューターの組織のために中期的に勉強しており、スタックポインターとスタックを完全に理解しようとしています。コンセプトを取り巻く以下の事実を知っています。
スタックに何かを追加するには、2つのステップのプロセスが必要です。
addi $sp, $sp, -4
sw $s0, 0($sp)
私が完全に理解できなくなっているのは、スタックポインターを使用してデータを追跡する必要がある、または追跡したい、関連性のある、自明の状況を思いつくことができないことです。
誰かがコンセプト全体について詳しく説明して、有用なコード例を教えてもらえますか?
スタックの重要な用途は、サブルーチン呼び出しのネストです。
各サブルーチンは、そのサブルーチンにローカルな変数のセットを持つことができます。これらの変数はスタックフレームのスタックに便利に格納できます。一部の呼び出し規約は、スタック上の引数も渡します。
サブルーチンを使用すると、呼び出し元、つまり戻りアドレスを追跡する必要があります。一部のアーキテクチャにはこの目的のための専用スタックがありますが、他のアーキテクチャは暗黙的に「通常の」スタックを使用します。 MIPSはデフォルトでレジスターのみを使用しますが、アドレスを返す非リーフ関数(つまり、他の関数を呼び出す関数)では上書きされます。したがって、通常はローカル変数間のスタックに元の値を保存する必要があります。呼び出し規約では、一部のレジスタ値を関数呼び出し間で保存する必要があることも宣言している場合があります。スタックを使用してそれらを同様に保存および復元できます。
次のCフラグメントがあるとします。
extern void foo();
extern int bar();
int baz()
{
int x = bar();
foo();
return x;
}
MIPSアセンブリは次のようになります。
addiu $sp, $sp, -8 # allocate 2 words on the stack
sw $ra, 4($sp) # save $ra in the upper one
jal bar # this overwrites $ra
sw $v0, ($sp) # save returned value (x)
jal foo # this overwrites $ra and possibly $v0
lw $v0, ($sp) # reload x so we can return it
lw $ra, 4($sp) # reload $ra so we can return to caller
addiu $sp, $sp, 8 # restore $sp, freeing the allocated space
jr $ra # return
MIPS呼び出し規約では、最初の4つの関数パラメーターがレジスターa0
からa3
までであり、残りがスタックにある場合はそれが残ります。さらに、レジスタで渡されているにもかかわらず、関数の呼び出し元は、最初の4つのパラメータ用にスタック上の4つのスロットを割り当てる必要があります。
したがって、パラメーター5(およびその他のパラメーター)にアクセスする場合は、sp
を使用する必要があります。関数が他の関数を呼び出し、呼び出し後にそのパラメーターを使用する場合、a0
からa3
をスタックの4つのスロットに格納して、それらが失われたり上書きされたりしないようにする必要があります。ここでも、sp
を使用して、これらのレジスタをスタックに書き込みます。
関数にローカル変数があり、それらをすべてレジスターに保持できない場合(他の関数を呼び出すときにa0
からa3
を保持できない場合など)、on-を使用する必要がありますsp
を使用する必要があるこれらのローカル変数のスタックスペース。
たとえば、次の場合:
int tst5(int x1, int x2, int x3, int x4, int x5)
{
return x1 + x2 + x3 + x4 + x5;
}
その分解は次のようになります:
tst5:
lw $2,16($sp) # r2 = x5; 4 slots are skipped
addu $4,$4,$5 # x1 += x2
addu $4,$4,$6 # x1 += x3
addu $4,$4,$7 # x1 += x4
j $31 # return
addu $2,$4,$2 # r2 += x1
sp
はx5
へのアクセスに使用されます。
そして、次のようなコードがある場合:
int binary(int a, int b)
{
return a + b;
}
void stk(void)
{
binary(binary(binary(1, 2), binary(3, 4)), binary(binary(5, 6), binary(7, 8)));
}
これは、コンパイル後に逆アセンブリで表示されるものです。
binary:
j $31 # return
addu $2,$4,$5 # r2 = a + b
stk:
subu $sp,$sp,32 # allocate space for local vars & 4 slots
li $4,0x00000001 # 1
li $5,0x00000002 # 2
sw $31,24($sp) # store return address on stack
sw $17,20($sp) # preserve r17 on stack
jal binary # call binary(1,2)
sw $16,16($sp) # preserve r16 on stack
li $4,0x00000003 # 3
li $5,0x00000004 # 4
jal binary # call binary(3,4)
move $16,$2 # r16 = binary(1,2)
move $4,$16 # r4 = binary(1,2)
jal binary # call binary(binary(1,2), binary(3,4))
move $5,$2 # r5 = binary(3,4)
li $4,0x00000005 # 5
li $5,0x00000006 # 6
jal binary # call binary(5,6)
move $17,$2 # r17 = binary(binary(1,2), binary(3,4))
li $4,0x00000007 # 7
li $5,0x00000008 # 8
jal binary # call binary(7,8)
move $16,$2 # r16 = binary(5,6)
move $4,$16 # r4 = binary(5,6)
jal binary # call binary(binary(5,6), binary(7,8))
move $5,$2 # r5 = binary(7,8)
move $4,$17 # r4 = binary(binary(1,2), binary(3,4))
jal binary # call binary(binary(binary(1,2), binary(3,4)), binary(binary(5,6), binary(7,8)))
move $5,$2 # r5 = binary(binary(5,6), binary(7,8))
lw $31,24($sp) # restore return address from stack
lw $17,20($sp) # restore r17 from stack
lw $16,16($sp) # restore r16 from stack
addu $sp,$sp,32 # remove local vars and 4 slots
j $31 # return
nop
間違いなくコードに注釈を付けていただければ幸いです。
したがって、コンパイラは関数でr16
およびr17
を使用することを選択しますが、それらをスタックに保存することに注意してください。この関数は別の関数を呼び出すため、単にr31
に保持するのではなく、スタック上の戻りアドレスを保持する必要もあります。
[〜#〜] ps [〜#〜] MIPSのすべての分岐/ジャンプ命令は、実際に制御を新しい場所に転送する前に、直後の命令を効果的に実行することに注意してください。これは混乱するかもしれません。