MIPS: соответствующее использование для указателя стека ($sp) и стека

В настоящее время я учусь на среднесрочном этапе организации своего компьютера и пытаюсь полностью понять указатель стека и стек. Я знаю следующие факты, которые окружают концепцию:

  • Это следует из принципа первый в последнем
  • А добавление чего-либо в стек занимает два этапа:

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

То, что я думаю, мешает мне полностью понять, что я не могу придумать соответствующую, самоочевидную ситуацию, в которой мне понадобится и / или я хочу отслеживать данные с помощью указателя стека.

Может ли кто-нибудь подробно остановиться на концепции в целом и дать мне несколько полезных примеров кода?

2 ответа

Решение

Важным использованием стека является вложенность вызовов подпрограмм.

Каждая подпрограмма может иметь набор переменных, локальных для этой подпрограммы. Эти переменные могут быть удобно сохранены в стеке в кадре стека. Некоторые соглашения о вызовах также передают аргументы в стек.

Использование подпрограмм также означает, что вы должны отслеживать вызывающего абонента, то есть адрес возврата. Некоторые архитектуры имеют выделенный стек для этой цели, в то время как другие неявно используют "нормальный" стек. 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 требует, чтобы первые четыре параметра функции были в регистрах a0 через a3 а остальные, если их больше, в стеке. Более того, он также требует, чтобы вызывающая функция выделяла четыре слота в стеке для первых четырех параметров, несмотря на то, что они передаются в регистрах.

Итак, если вы хотите получить доступ к параметру пять (и другие параметры), вам нужно использовать sp, Если функция, в свою очередь, вызывает другие функции и использует ее параметры после вызовов, ее необходимо сохранить 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

Я надеюсь, что я аннотировал код, не делая ошибок.

Итак, обратите внимание, что компилятор выбирает использовать r16 а также r17 в функции, но сохраняет их в стеке. Поскольку функция вызывает другую, ей также необходимо сохранить свой адрес возврата в стеке, а не просто сохранять его в r31,

PS Помните, что все инструкции перехода / перехода на MIPS эффективно выполняют сразу следующую инструкцию, прежде чем фактически передать управление в новое местоположение. Это может сбить с толку.

Другие вопросы по тегам