В настоящее время я изучаю свою среднюю компьютерную организацию, и я стараюсь полностью понять указатель стека и стек. Я знаю следующие факты, которые окружают концепцию:
То, что я думаю, мешает мне полностью понять, что я не могу придумать адекватную, явную ситуацию, в которой мне понадобится, и/или хочу отслеживать данные с помощью указателя стека.
Может ли кто-нибудь разработать концепцию в целом и дать мне несколько полезных примеров кода?
Ответ 1
Важным использованием стека является вызов подпрограмм вложенности.
Каждая подпрограмма может иметь набор переменных, локальных для этой подпрограммы. Эти переменные удобно хранить в стеке в стеке. Некоторые вызывающие соглашения также передают аргументы в стеке.
Использование подпрограмм также означает, что вам нужно отслеживать вызывающего абонента, то есть обратный адрес.
Для этой цели некоторые архитектуры имеют выделенный стек, а другие неявно используют "нормальный" стек. 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
Ответ 2
Соглашение о вызове 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 эффективно выполняют сразу следующую команду перед фактической передачей управления новому местоположению. Это может сбить с толку.