MIPS 程序集:为什么 main 可以在不释放其堆栈 space 的情况下退出?

MIPS assembly: Why can main exit without deallocating its stack space?

我有一道大学习题,我不懂。我们必须将 C 语言转换为汇编 MIPS。主要是我必须为 a[100] 向量分配 400 个字节,但在解决方案中我的教授没有在函数末尾释放它,为什么会这样?是否存在不需要释放内存移动堆栈指针的情况?

这是 C 中的代码:

 int idamax(int n, float * dx, int incx) {
     float dmax;
     int i, ix, itemp;
     if (n < 1) return (-1);
     if (n == 1) return (0);
     if (incx != 1) {
         ix = 1;
         dmax = fabs(dx[0]);
         ix = ix + incx;
         for (i = 1; i < n; i++) {
             if (dmax < fabs(dx[ix])) {
                 itemp = i;
                 dmax = fabs(dx[ix]);
             }
             ix = ix + incx;
         }
     } else {
         itemp = 0;
         dmax = fabs(dx[0]);
         for (i = 1; i < n; i++) {
             if (dmax < fabs(dx[i])) {
                 itemp = i;
                 dmax = fabs(dx[i]);
             }
         }
     }
     return (itemp);
 }
 int main() {
     float a[100];
     int l, k, n = 100, lda = 10;
     for (k = 0; k < n; ++k) a[k] = (float)((k * k * k) % 100);
     k = 4;
     l = idamax(n - lda * k - k, &a[lda * k + k], 1) + k;
     print_int(l);
     exit;
 }

主要汇编代码:

main:
#______CALL_FRAME______
# 100 float: 400B
#______Totale 400B
 addi $sp,$sp,-400
 add $t9,$sp,[=12=] #&a
 addi $t0, [=12=], 100 #n=100
 addi $t1, [=12=], 10 #lda=10
#l in t2, k in t3

 add $t3, [=12=], [=12=] #k=0
main_forini:
 slt $t5,$t3,$t0 #k<?n
 beq $t5,[=12=],main_forend

 mult $t3, $t3 #k*k
 mflo $t5
 mult $t3, $t5
 mflo $t5 #k*k*k
 div $t5,$t0 #()%n
 mfhi $t5

 mtc1 $t5,$f0
 cvt.s.w $f1,$f0 #(float)()

 sll $t5,$t3,2 #k*4
 add $t5,$t5,$t9 #&a[k]
 swc1 $f1,0($t5) #a[k]=()

 addi $t3, $t3, 1 #++k
 j main_forini
main_forend:
 addi $t3,[=12=],4 #k=4
 mult $t1,$t3 #lda*k
 mflo $t5
 add $t5,$t5,$t3 #lda*k+k
 sub $a0,$t0,$t5 #a0=n-lda*k-k
 sll $t5,$t5,2
 add $a1,$t5,$t9 #a1=&a[lda*k+k]
 addi $a2,[=12=],1 #a2=1
 jal idamax
 addi $a0,$v0,4 #a0=l=retval+k
 addi $v0,[=12=],1 #print_int
 syscall
 addi $v0,[=12=],10 #exit
 syscall

这里有两个可能的答案。

第一个答案是main是你程序的第一个也是最后一个函数。 OS 之后会进行清理。

第二个答案是针对使用堆栈内存的其他函数。堆栈内存通常通过恢复调用函数的堆栈帧来释放(main 没有,因此例外)。

main 的执行永远不会到达函数的底部,因此永远不需要清理堆栈; exit() 是一个 "noreturn" 函数。

如果 main did 想要 return 和 jr $ra 而不是进行 exit 系统调用,您需要恢复堆栈指针以及其他调用保留寄存器。否则,您将违反 main 的调用者希望 main 遵循的调用约定。

(已更新,因为您将 asm 添加到使用 MARS 系统调用的问题中:如果 main 位于代码顶部,则它可能不是函数:$ra 不是有效的return 入口地址,所以它不能 return。如果它不是函数,IMO 不要称它为 main。)

当进程退出系统调用时,OS 不关心 user-space 堆栈指针指向何处,因此不需要 main 退出前进行清理。

(在 "normal" C 实现中,exit() 函数将编译为 jal exit 或简单的尾调用 j exit。但是您正在手动编译没有 C 库的 MARS 模拟器,所以你内联系统调用而不是调用包装函数。)

还要注意 ISO C exit(int) 需要一个参数,例如 MARS exit2 (syscall/$v0=17)。事实上,您甚至没有将 exit() 作为函数调用,您只是在 C 中编写了 exit;,它将 exit 计算为函数指针,而无需调用它或对该值执行任何操作。


通常 C main 由 CRT 启动代码调用,例如 运行 C 库初始化函数并将 argc 和 argv[] 指针放入正确的寄存器中。所以 main 通常不是来自 OS 的实际进程入口点,尤其是在托管实现中。 (即编译的 C 程序 运行 在 OS 下,而不是像独立程序一样是它们自己的内核。)

如果您只是为 MARS 或 SPIM 模拟器或其他东西翻译它,那么没有 C 库或任何超出您编写的代码,所以您正在编写的是通常情况下被称为 _start,而不是 main.

在 C 中 main 是函数,但在 MARS 中 you can't jr $ra from the top-level entry point 所以 入口点不是函数。因此不要称它为 main.

在 ISO C 中,main 递归调用自身或其他函数调用 main 甚至是合法的。这只有在 main 确实是一个可以正确清理堆栈和 returns 的函数时才有效。但这意味着它不能同时是需要进行 exit 系统调用的进程入口点。对于 运行 一个具有疯狂递归 main 的程序,最终执行 C return 语句(或脱离 main 的末尾),main 几乎必须编译为一个真正的函数,可以 return 和 jr $ra。所以它必须是一个函数,你 jal main 从你的 _start 入口点。