2024/08/22
从今天开始,正式准备启动这个“项目”啦!(其实已经看过一段时间前面的基础介绍了
希望能够坚持做完!!!(ง •_•)ง
这里主要记录一下做PA过程中遇到的问题和思考(以及内心戏
不过后面一个月要去外地培训了。。。还不知道弄
1. PA1
1.3 RTFSC
getopt_long()
函数的作用?
1 | SYNOPSIS |
got,用来解析命令行参数的。
1.3.1 准备第一个客户程序
为什么全部都是函数?
阅读
init_monitor()
函数的代码, 你会发现里面全部都是函数调用. 按道理, 把相应的函数体在init_monitor()
中展开也不影响代码的正确性. 相比之下, 在这里使用函数有什么好处呢?这样有利于抽象内部设计,使得设计更加层次分明
2024/08/23
NEMU是一个用来执行客户程序的程序(模拟计算机),但是NEMU一开始并没有客户程序(OS或其他客户程序),需要有程序将客户程序读入计算机中,NEMU项目src中的monitor就是用来干这个事的(也包含调试的功能sdb
)。
monitor中调用init_isa()
来进行ISA的一些初始化:
- 将一个内置客户程序读入内存中
- 初始化虚拟计算机系统(初始化寄存器,
restart()
函数)
读入客户程序到内存中,读到什么位置?NEMU采用最简单的方式——约定一个位置,可由我们配置,定义在nemu/include/memory/paddr.h
中,定义为RESET_VECTOR。
NEMU默认提供128MB的内存,模拟内存定义在src/memory/paddr.c
中,定义为pmem
RISC-V32(以及MIPS32)的默认物理内存地址是从
0x80000000
开始编址的, 将来CPU访问内存时, 会将CPU将要访问的内存地址映射到pmem
中的相应偏移位置,这是通过nemu/src/memory/paddr.c
中的guest_to_host()
函数实现的。
可能这个RESET_VECTOR就相当于真实计算机启动的第一个地址?存放BIOS的位置?
成功运行!!!
2024.10.26记
果然。。。中途荒废了一个月再捡起来就需要勇气了hhh,
荒废了两个月后,我终于又准备回来了!
2024.12.15记
真的回来了(
2024/12/15
NEMU最开始默认的客户程序定义在init.c函数中,如下:
其中的img就是4条指令+一个数据。也可以在运行NEMU的时候添加一个参数,指定外部的镜像文件加载。
1.3.2 运行第一个客户程序
究竟要执行多久?
在
cmd_c()
函数中, 调用cpu_exec()
的时候传入了参数-1
, 你知道这是什么意思吗?A:将-1转换成uint64_t的最大值,即执行0xFFFFFFFFFFFFFFFF这么多条指令(如不是指令自行退出的话)
潜在的威胁 (建议二周目思考)
“调用
cpu_exec()
的时候传入了参数-1
“, 这一做法属于未定义行为吗? 请查阅C99手册确认你的想法。A:好的,那就二周目再思考(
进入sdb_mainloop()
函数后,会通过cmd_table
结构体中的函数指针(handler
)来调用不同的函数,输入c
就可以运行内置的img。但是再次输入c
的话会提示需要重新运行NEMU才能。。。
sdb_mainloop()
函数中的strtok()
函数是用来分割字符串的,类似python的split函数,可以指定分割标志。
有意思的是其中的nemu_trap
指令,它是为了在NEMU中让客户程序指示执行的结束而加入的, NEMU在ISA手册中选择了一些用于调试的指令, 并将nemu_trap
的特殊含义赋予它们。例如在riscv32的手册中, NEMU选择了ebreak
指令来充当nemu_trap
. 为了表示客户程序是否成功结束, nemu_trap
指令还会接收一个表示结束状态的参数。
RTFM:
RISC-V的基础指令格式有如下4种:
RISC-V种,EBREAK
指令可以用SYSTEM
指令实现,如下:
ECALL
和EBREAK
的功能都是在SYSTEM
这个opcode下实现的,ECALL
的funct12
是0,EBREAK
的funct12
是1。
其中,EBREAK
是用于将控制权返回给调试环境。
NEMU中,EBREAK
指令的实现是将nemu_state.state
置为NEMU_END,将nemu_state.halt_pc
置为当前PC,将nemu_state.halt_ret
置为10。
Note:
什么是trap?
问了一下GPT:
Trap 是指在程序执行过程中,处理器遇到某些事件时,将控制权从当前程序转移到操作系统或异常处理程序的一种机制。
它可以由程序主动触发(如系统调用),也可以由硬件自动触发(如异常或中断)。so,基本上就是用来切换执行的代码流的(个人理解)。
谁来指示程序的结束?
在程序设计课上老师告诉你, 当程序执行到
main()
函数返回处的时候, 程序就退出了, 你对此深信不疑. 但你是否怀疑过, 凭什么程序执行到main()
函数的返回处就结束了? 如果有人告诉你, 程序设计课上老师的说法是错的, 你有办法来证明/反驳吗? 如果你对此感兴趣, 请在互联网上搜索相关内容。STFW:在程序终止前,可能会存在一些清理操作,例如关闭打开的文件、释放分配的内存等。这些清理操作可以在主函数的最后部分执行,调用
atexit()
函数即可。有始有终 (建议二周目思考)
对于GNU/Linux上的一个程序, 怎么样才算开始? 怎么样才算是结束? 对于在NEMU中运行的程序, 问题的答案又是什么呢?
与此相关的问题还有: NEMU中为什么要有
nemu_trap
? 为什么要有monitor?对于下面的问题,一周目的回答:为了暂停/终止当前程序的执行,将CPU的控制权交给其他程序(OS/monitor),monitor是用来调试的(吧。。
代码中有一些值得注意(学习),直接摘抄过来:
三个对调试有用的宏(在
nemu/include/debug.h
中定义)
Log()
是printf()
的升级版, 专门用来输出调试信息, 同时还会输出使用Log()
所在的源文件, 行号和函数. 当输出的调试信息过多的时候, 可以很方便地定位到代码中的相关位置Assert()
是assert()
的升级版, 当测试条件为假时, 在assertion fail之前可以输出一些信息panic()
用于输出信息并结束程序, 相当于无条件的assertion fail内存通过在
nemu/src/memory/paddr.c
中定义的大数组pmem
来模拟. 在客户程序运行的过程中, 总是使用vaddr_read()
和vaddr_write()
(在nemu/src/memory/vaddr.c
中定义)来访问模拟的内存. vaddr, paddr分别代表虚拟地址和物理地址.优美地退出
为了测试大家是否已经理解框架代码, 我们给大家设置一个练习: 如果在运行NEMU之后直接键入
q
退出, 你会发现终端输出了一些错误信息. 请分析这个错误信息是什么原因造成的, 然后尝试在NEMU中修复它.
终于有一个练习了!
在NEMU中直接输入q
和输入c
之后再输入q
的结果确实不一样!如下:
输入c
之后再输入q
能够优雅地退出,并没有报错,而直接输入则会报错。通过echo $?
查看程序的返回值,发现直接退出的返回值是2,而正常的是0,所以问题出在返回值上。
PS:在menu里开启debug配置后,输入make gdb
,可以调试NEMU
一个疑惑是,进入gdb调试后,发现直接退出返回的值是1,并不是2!这是为什么呢?值得思考,怀疑是make程序进行了再次返回,于是直接在命令行运行NEMU而不是通过make命令,得到以下结果:
猜想正确。(还可以深究一下为什么make会在返回值1的基础上返回2?
不过,程序会报错的原因找到了,就是因为返回值不为0,而控制返回值的函数在/src/utils/state.c
中的函数is_exit_status_bad()
:
说明退出时不满足这个条件,返回去看sdb_mainloop()
函数中的调用逻辑,运行c
命令的话,会执行img程序,会使得nemu_state
满足good表达式中的前一个条件,而直接输入q
则直接返回-1,不会修改nemu_state
的状态,于是不满足good的条件,所以为0,返回1,于是修改调用的cmd_q()
函数,设置nemu_state
的状态为NEMU_QUIT
,即可实现“优雅地退出”。
事实上, TRM的实现已经都蕴含在上述的介绍中了.
- 存储器是个在
nemu/src/memory/paddr.c
中定义的大数组- PC和通用寄存器都在
nemu/src/isa/$ISA/include/isa-def.h
中的结构体中定义- 加法器在… 嗯, 这部分框架代码有点复杂, 不过它并不影响我们对TRM的理解, 我们还是在PA2里面再介绍它吧
- TRM的工作方式通过
cpu_exec()
和exec_once()
体现