"jr $ra" 是否需要结束一个 MIPS 汇编语言程序? (MARS 和 QtSpim 表现不同!)

Is "jr $ra" required to end a MIPS assembly language program? (MARS & QtSpim behave differently!)

如果您在 MARS 中的 MIPS 汇编语言程序末尾放置一个 jr $ra,您将收到一条错误消息:

invalid program counter value: 0x00000000

下面的示例 1 失败了:

.data

theMsg: .asciiz "Hello World\n"

.text
.globl main

main:   li $v0, 4       
        la $a0, theMsg  
        syscall         
        
        jr $ra

      

下面的示例 2 有效:

.data

theMsg: .asciiz "Hello World\n"

.text
.globl main

main:   li $v0, 4       
        la $a0, theMsg  
        syscall

     
    

MARS 说 “程序已完成 运行ning(掉到底部)” 但没有错误消息。

现在,如果您在 QtSpim 中 运行 示例 2,您将收到一条错误消息:

Attempt to execute non-instruction at 0x00400030

如果您 运行 QtSpim 中的示例 1,它工作得很好。

任何人都可以阐明这一点吗?
哪个 MIPS 模拟器是正确的?

标准的无处不在的方法是进行 exit(0) 系统调用:

   li $v0, 10         # call number
   syscall            # exit()

如果您想在程序中使用 jal,这也避免了必须将传入的 $ra 保存在 main 的任何位置,因此很方便。

这对于真实世界的手写 asm 程序来说也更“现实”运行宁在主流 OS 像 Linux,而不仅仅是 Linux 的“玩具”系统 MARS/SPIM模拟.

(与 Linux exit(int) 不同,MARS/SPIM 玩具系统调用不会在 $a0 或其他任何地方获取退出状态。它只是退出。)


在 MARS 中,显然从底部掉落是它模拟的“玩具”系统的有效选项。不过,这在任何真实硬件中都行不通 CPU;内存中总会有下一个内容,CPU 将尝试获取并执行它1.

MARS 和 SPIM 都没有试图模拟像 Linux 这样的真实 OS,只是提供它们自己的特定环境2MARS 与 SPIM 模拟的系统彼此之间存在一些细微差异,包括您找到的系统。

没有对错,只是不同:没有他们试图匹配/模拟的真实世界环境。

SPIM 甚至可以选择在模拟系统的内存 IIRC 中包含一些内核代码或类似的东西。我可能记错了,但如果没有,那么一些系统调用处理实际上可能是由更多的 MIPS 代码完成的,更接近真正的 MIPS CPU 运行ning 和 OS。 (与 MARS 相反,系统调用实现完全在您通过 syscall 调用的模拟器内部的 Java 中,而不是模拟硬件的 MIPS 指令和设备驱动程序。)

在真正的 OS 下(例如 GNU/Linux 与 gcc 和 glibc),main 将是一个正常从 _start 进程入口点调用的正确函数(间接通过 __libc_start_main 在实际调用 main 之前做一些更多的初始化工作)。 _start 是真正的进程入口点,第一个指令是 运行 在 user-space(模动态链接)中,并且不是一个函数(没有任何地方的 return 地址) ;您唯一的选择是进行退出系统调用(或崩溃或永远保持 运行ning)。当 main returns 时,_start 将其 return 值(int 因此在 $v0 中)作为参数传递给 就像刷新 stdio 缓冲区,然后进行 _exit 系统调用。

显然 SPIM 希望他们的 main 标签像一个 C main 函数,或者至少它得到一个有效的 return 地址。 IDK 如果它在 $a0$a1.

中得到 int argcchar *argv[]

要使 jr $ra 正常工作,SPIM 必须将初始 $ra 设置为某个地址,就像您的 main 是从某个地方调用的一样。您可能会找到将 $v0 复制到 $a0,然后进行退出系统调用的代码。

有些人混淆地使用 main 作为不能 return 的入口点的名称,不幸的是,我认为即使在现实世界的嵌入式开发中也是如此。在 GNU/Linux 系统 (gcc / clang) 的标准工具链中,进程入口点默认称为 _start.

main 令人困惑,因为它是 C 函数的标准名称(由 asm 启动程序调用),允许 return。您不能 return 的东西不是 函数 ,但在 C 中, main 绝对 函数。 C 是 Unix/Linux 系统编程的低级语言,许多其他语言构建在 libc 和 CRT 启动代码的标准工具链之上。


脚注 1: 大多数 ISA 都有关于 PC 如何从 0xffffffc 换行到 0 或其他任何内容的规则,因此即使将代码放在最前面end of address space 不能让它在到达末尾时自行停止。或者如果是,那将是某种故障,没有退出到 OS。 (在这种情况下,MARS 或 SPIM 充当 OS,处理 syscall 指令,您 运行 等)。请注意,裸机上的实际 OS 无法“退出”,只能重置或关闭机器电源。它不会运行宁“在”任何它可以退回的地方。

脚注 2:系统调用非常有限,例如没有光标移动,一些系统调用做库函数(不是系统调用)在真实系统中会做的事情,例如int<->字符串转换。但是 MARS/SPIM 仅作为 I/O 的一部分提供,没有 atoisprintf(buf, "%d", num)。这就是“玩具”标签适用的原因,尤其是它们提供的系统调用集,这与 Linux 的系统调用集非常

而且还有 MARS 具有的简单位图图形之类的东西,以及 MARS 和 SPIM 默认的无分支延迟默认选项。真正的 MIPS CPUs 有一个分支延迟槽,直到 MIPS32r6 重新安排操作码并提供新的无延迟槽分支指令。

MARS 至少(也许还有 SPIM)在其内置 assembler 中对 assemble-时间常数的支持也非常有限,例如您不能像在 MIPS 的 GNU assembler 中那样,在 assemble 时执行 .equmsglen = . - msg 来计算 msg: .ascii "hello" 的长度。

添加到@Peter 的非常好的答案:

SPIM有包含内核代码的选项,通过Simulator->Settings->Load Exception Handler(你可以选择一个文件或使用默认的),处理程序是汇编源。默认设置是使用默认处理程序(与不使用处理程序相反)。

当您编写这样的处理程序时,您可以在 .ktext & .kdata 中包含代码,但也可以在用户 .text & .data 中包含代码。在用户代码之前首先组装和加载任何异常处理程序。

标准异常处理程序文件包括——用于放置在用户 .text 中——加载 argc/argv 然后执行 jal main 然后是系统调用 #10(所以它有点像 _start in crt0),这意味着我们可以 return (jr $ra) 到该启动代码。这就是为什么用户代码在 SPIM 中出现在 [00400020] 而在 MARS 中您的用户代码从 00400000.

开始

SPIM 直到运行时才报告丢失的符号!!!因此,如果未找到 main,则在执行 jal main 时会报告缺少符号 main

但是,当您确实有一个有效的 main 符号时,它不必在文件中位于第一个 — 它可以在任何地方。

虽然 MARS 也在 .text 开始执行,相比之下,MARS 中的默认异常处理程序没有提供 _start 等效项,所以我们必须从主代码开始汇编程序(但是我们真的不需要 main 符号)或者在那里放一个 j somewhere。如果您放弃使用默认处理程序,SPIM 的行为将更像 MARS。