目前我正在为我的计算机组织中期学习,我正在努力完全理解堆栈指针和堆栈。我知道围绕这个概念的以下事实:
向堆栈添加内容需要两个步骤:
addi $sp, $sp, -4
sw $s0, 0($sp)
我认为阻止我完全理解的是,我无法想出一个相关的,自我明显的情况,我需要和/或想要用堆栈指针跟踪数据。
有人可以详细说明这个概念并给我一些有用的代码示例吗?
答案 0 :(得分:27)
堆栈的一个重要用途是嵌套子程序调用。
每个子程序可能有一组本子程序的局部变量。这些变量可以方便地存储在堆栈框架中的堆栈上。一些调用约定也在栈上传递参数。
使用子程序还意味着您必须跟踪调用者,即返回地址。 一些架构为此目的具有专用堆栈,而其他架构隐含地使用“正常”堆栈。默认情况下,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
答案 1 :(得分:7)
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上的所有跳转/跳转指令都会有效地执行紧随其后的指令。这可能令人困惑。