如何解决是否需要“jr $ra”来结束 MIPS 汇编语言程序? MARS 和 QtSpim 的行为不同!
如果在 MARS 中的 MIPS 汇编语言程序末尾放置 jr $ra
,您将收到一条错误消息:
无效的程序计数器值: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,theMsg
syscall
MARS 说“程序已完成运行(从底部掉下来)”,但没有错误消息。
现在,如果您在 QtSpim 中运行示例 2,您将收到一条错误消息:
尝试在 0x00400030 处执行非指令
如果您在 QtSpim 中运行示例 1,它就可以正常工作。
有人能解释一下吗?
哪个 MIPS 模拟器是正确的?
解决方法
标准的工作方式是进行 exit(0) 系统调用:
li $v0,10 # call number
syscall # exit()
- http://courses.missouristate.edu/kenvollmar/mars/help/syscallhelp.html
- https://www.doc.ic.ac.uk/lab/secondyear/spim/node8.html(SPIM 是 MARS 的子集)
如果您想在程序中使用 $ra
,这也避免了必须将传入的 main
保存在 jal
中的任何位置,因此很方便。
对于在 Linux 等主流操作系统中运行的真实手写 asm 程序来说,这也更加“现实”,而不仅仅是 MARS/SPIM 模拟的“玩具”系统。
(与 Linux exit(int)
不同,MARS/SPIM 玩具系统调用不会在 $a0
或其他任何地方采取退出状态。它只是退出。)
在 MARS 中,显然从底部掉下来是它模拟的“玩具”系统的有效选项。但是,这在任何真正的硬件 CPU 中都不起作用;内存中总会有下一个东西,CPU 会尝试获取并执行它1。
MARS 和 SPIM 都没有试图模拟像 Linux 这样的真实操作系统,只是提供自己的特定环境2。 MARS 与 SPIM 模拟的系统彼此之间存在一些细微差别,包括您发现的那个。
没有对与错,只是不同:没有他们试图匹配/模仿的真实世界环境。
SPIM 甚至可以选择在模拟系统的内存 IIRC 中包含一些内核代码或类似内容。我可能记错了,但如果不是,那么一些系统调用处理实际上可能由更多 MIPS 代码完成,更接近于运行操作系统的真正 MIPS CPU。 (与 MARS 不同,在 MARS 中,系统调用实现纯粹是在您通过 syscall
调用的模拟器内的 Java 中,而不是在模拟硬件的 MIPS 指令和设备驱动程序方面。)
在真正的操作系统下(例如带有 gcc 和 glibc 的 GNU/Linux),main
将是一个正确的函数,通常从 _start
进程入口点调用(间接通过 __libc_start_main 来做一些更多的初始化工作在实际调用 main 之前)。 _start
是真正的进程入口点,在用户空间运行的第一条指令(模动态链接),而不是一个函数(任何地方都没有返回地址);您唯一的选择是进行退出系统调用(或崩溃或永远运行)。当 main
返回时,_start
将其返回值(因此在 int
中的 $v0
)作为参数传递给 exit
library function which does cleanup stuff,就像刷新 stdio 缓冲区一样,然后生成一个_exit
系统调用。
显然,SPIM 希望它们的 main
标签类似于 C main
函数,或者至少它获得一个有效的返回地址。 IDK,如果它在 int argc
和 char *argv[]
中得到 $a0
和 $a1
。
要使 jr $ra
工作,SPIM 必须将初始 $ra
设置为某个地址,就像您的 main 是从某个地方调用的一样。您可能会发现将 $v0 复制到 $a0 的代码,然后进行退出系统调用。
有些人确实混淆地使用 main
作为无法返回的入口点的名称,不幸的是,我认为即使在现实世界的嵌入式开发中也是如此。在 GNU/Linux 系统的标准工具链 (gcc / clang) 中,进程入口点默认称为 _start
。
main
令人困惑,因为它是允许返回的 C 函数(由 asm 启动程序调用)的标准名称。你不能返回的东西不是函数,但在C中,main绝对是一个函数。 C 是 Unix/Linux 系统编程的低级语言,许多其他语言都建立在 libc 和 CRT 启动代码的标准工具链之上。
脚注 1: 大多数 ISA 都有关于 PC 如何从 0xffffffc
包装到 0
或其他任何内容的规则,因此即使将您的代码放在地址空间的最末端到达终点时不能让它自己停下来。或者,如果确实如此,这将是某种故障,而不是退出到操作系统。 (在这种情况下,MARS 或 SPIM 充当操作系统,处理您运行的 syscall
指令等)。请注意,裸机上的实际操作系统无法“退出”,只能重置或关闭机器电源。它没有在它可以退出的任何“下方”运行。
脚注 2:系统调用非常有限,例如没有光标移动,以及一些系统调用会做库函数(不是系统调用)在真实系统中会做的事情,例如int 字符串转换。但 MARS/SPIM 仅将其作为 I/O 的一部分提供,没有 atoi
或 sprintf(buf,"%d",num)
。这就是“玩具”标签适用的原因,尤其是它们提供的系统调用集,这与 Linux 的系统调用集非常。
还有像 MARS 具有的简单位图图形之类的东西,以及 MARS 和 SPIM 默认使用的 no-branch-delay 默认选项。真正的 MIPS CPU 有一个分支延迟槽,直到 MIPS32r6 重新排列操作码并提供新的无延迟槽分支指令。
MARS 至少(可能还有 SPIM)在其内置汇编器中对汇编时间常量的支持也非常有限,例如您不能像在 MIPS 的 GNU 汇编器中那样在汇编时执行 .equ
或 msglen = . - msg
来计算 msg: .ascii "hello"
的长度。
添加到@Peter 的非常好的答案中:
SPIM 可以选择包含内核代码,通过 Simulator->Settings->Load Exception Handler(你可以选择一个文件或使用默认值),处理程序是汇编源代码。默认设置是使用默认处理程序(而不是不使用处理程序)。
编写此类处理程序时,您可以在 .ktext
& .kdata
中包含代码,但也可以包含用户 .text
& .data
。在用户代码之前,首先组装和加载任何异常处理程序。
标准异常处理程序文件包括 - 用于放置在用户 .text
中 - argc/argv 的加载,然后执行 jal main
后跟系统调用 #10(所以它有点像 _start
在 crt0
) 中,这意味着我们可以返回 (jr $ra
) 到该启动代码。这就是为什么用户代码在 SPIM 中出现在 [00400020]
而在 MARS 中您的用户代码以 00400000
开头。
SPIM 也不会在运行时报告丢失的符号!!!因此,如果未找到 main
,则在 main
执行时会报告丢失的符号 jal main
。
但是,当您确实有一个有效的 main
符号时,它不必是文件中的第一个——它可以在任何地方。
虽然 MARS 也在 .text
的开头开始执行,但相比之下,MARS 中的默认异常处理程序没有提供 _start
等价物,因此我们必须使用主代码启动汇编程序(但我们不'真的不需要 main
符号)或者在那里放一个 j somewhere
。如果您放弃使用默认处理程序,SPIM 的行为将更像 MARS。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。