19

現在、私はコンピューター組織の中間試験の勉強をしており、スタック ポインターとスタックを完全に理解しようとしています。私は、この概念を取り巻く次の事実を知っています。

  • 先入れ後出しの原則に従います
  • スタックに何かを追加するには、次の 2 段階のプロセスが必要です。

    addi $sp, $sp, -4
    sw $s0, 0($sp)
    

私が完全に理解するのを妨げていると思うのは、スタックポインターを使用してデータを追跡する必要がある、および/または追跡したい、関連する自明の状況を思い付くことができないということです。

誰かが全体として概念を詳しく説明し、役立つコード例をいくつか教えてもらえますか?

4

2 に答える 2

37

スタックの重要な用途は、サブルーチン呼び出しのネストです。

各サブルーチンは、そのサブルーチンにローカルな変数のセットを持つことができます。これらの変数は、スタック フレーム内のスタックに簡単に格納できます。一部の呼び出し規約では、引数もスタックに渡します。

サブルーチンを使用すると、呼び出し元、つまり戻りアドレスを追跡する必要があることも意味します。この目的のために専用のスタックを持つアーキテクチャもあれば、「通常の」スタックを暗黙的に使用するアーキテクチャもあります。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
于 2013-02-26T23:11:03.870 に答える
8

MIPS 呼び出し規則では、最初の 4 つの関数パラメーターをレジスターに配置a0a3、それ以上ある場合は残りをスタックに配置する必要があります。さらに、最初の 4 つのパラメーターがレジスターで渡されているにもかかわらず、関数の呼び出し元がスタックに 4 つのスロットを割り当てる必要があります。

したがって、パラメーター 5 (およびその他のパラメーター) にアクセスする場合は、 を使用する必要がありますsp。関数が他の関数を順番に呼び出し、呼び出しの後にそのパラメーターを使用する場合、それらが失われたり上書きされたりしないように、スタック上のこれら 4 つのスロットに格納a0する必要があります。a3繰り返しますが、spこれらのレジスタをスタックに書き込むために使用します。

関数にローカル変数があり、それらすべてをレジスターに保持できない場合 (他の関数を呼び出すときに保持できない場合a0などa3)、それらのローカル変数用にスタック上のスペースを使用する必要があります。の使用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

間違いなくコードに注釈を付けられたことを願っています。

そのため、コンパイラは関数内でr16andを使用することを選択しますr17が、それらをスタックに保持することに注意してください。関数は別の関数を呼び出すため、戻りアドレスを単純に に保持するのではなく、スタックに保持する必要もありますr31

PS MIPS のすべての分岐/ジャンプ命令は、実際に制御を新しい場所に移す前に、直後の命令を効果的に実行することに注意してください。これは紛らわしいかもしれません。

于 2013-02-26T23:21:47.980 に答える