0%

NJU ICS2023 PA开发日记

2024/08/22

从今天开始,正式准备启动这个“项目”啦!(其实已经看过一段时间前面的基础介绍了

希望能够坚持做完!!!(ง •_•)ง

这里主要记录一下做PA过程中遇到的问题和思考(以及内心戏

不过后面一个月要去外地培训了。。。还不知道弄

1. PA1

1.3 RTFSC

getopt_long()函数的作用?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
SYNOPSIS
#include <unistd.h>

int getopt(int argc, char * const argv[],
const char *optstring);

extern char *optarg;
extern int optind, opterr, optopt;

#include <getopt.h>

int getopt_long(int argc, char * const argv[],
const char *optstring,
const struct option *longopts, int *longindex);

int getopt_long_only(int argc, char * const argv[],
const char *optstring,
const struct option *longopts, int *longindex);

DESCRIPTION
The getopt() function parses the command-line arguments. Its arguments argc and argv are the argument count and array as passed to the main() function on program invocation. An element of argv that starts
with '-' (and is not exactly "-" or "--") is an option element. The characters of this element (aside from the initial '-') are option characters. If getopt() is called repeatedly, it returns successively
each of the option characters from each of the option elements.

The variable optind is the index of the next element to be processed in argv. The system initializes this value to 1. The caller can reset it to 1 to restart scanning of the same argv, or when scanning a new argument vector.

If getopt() finds another option character, it returns that character, updating the external variable optind and a static variable nextchar so that the next call to getopt() can resume the scan with the following option character or argv-element.

If there are no more option characters, getopt() returns -1. Then optind is the index in argv of the first argv-element that is not an option.
optstring is a string containing the legitimate option characters. If such a character is followed by a colon, the option requires an argument, so getopt() places a pointer to the following text in the same argv-element, or the text of the following argy-element, in optarg. Two colons mean an option takes an optional arg; if there is text in the current argv-element (i.e., in the same word as the option name itself, for example, "-oarg"), then it is returned in optarg, othenwise optarg is set to zero. This is a GNU extension. If optstring contains W followed by a senicolon, then -W foo is treated as the long option --foo. (The -W option is reserved by POSIX.2 for implementation extensions.) This behavior is a GNU extension, not available with libraries before glibc 2.
By default, getopt() permutes the contents of argy as it scans, so that eventually all the nonoptions are at the end. Two other modes are also implemented. If the first character of optstring is '+' or the environment variable POSIXLY_CORRECT is set, then option processing stops as soon as a nonoption argument is encountered. If the first character of optstring is '-', then each nonoption argv-element is handled as if it were the argument of an option with character code 1. (This is used by prograns that were written to expect options and other argy-elenents in any order and that care about the ordering of the two.) The special argument "--" forces an end of option-scanning regardless of the scanning mode.l

got,用来解析命令行参数的。

1.3.1 准备第一个客户程序

为什么全部都是函数?

阅读init_monitor()函数的代码, 你会发现里面全部都是函数调用. 按道理, 把相应的函数体在init_monitor()中展开也不影响代码的正确性. 相比之下, 在这里使用函数有什么好处呢?

这样有利于抽象内部设计,使得设计更加层次分明

2024/08/23

NEMU是一个用来执行客户程序的程序(模拟计算机),但是NEMU一开始并没有客户程序(OS或其他客户程序),需要有程序将客户程序读入计算机中,NEMU项目src中的monitor就是用来干这个事的(也包含调试的功能sdb)。

monitor中调用init_isa()来进行ISA的一些初始化:

  1. 将一个内置客户程序读入内存中
  2. 初始化虚拟计算机系统(初始化寄存器,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的位置?

Welcome to riscv32-NEMU!

成功运行!!!

2024.10.26记

果然。。。中途荒废了一个月再捡起来就需要勇气了hhh,

荒废了两个月后,我终于又准备回来了!

2024.12.15记

真的回来了(

2024/12/15

NEMU最开始默认的客户程序定义在init.c函数中,如下:

image-20241215223855685

其中的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种:

image-20241217232430933

RISC-V种,EBREAK指令可以用SYSTEM指令实现,如下:

image-20241218224222172

ECALLEBREAK的功能都是在SYSTEM这个opcode下实现的,ECALLfunct12是0,EBREAKfunct12是1。

其中,EBREAK是用于将控制权返回给调试环境。

NEMU中,EBREAK指令的实现是将nemu_state.state置为NEMU_END,将nemu_state.halt_pc置为当前PC,将nemu_state.halt_ret置为10。

Note:

什么是trap?

问了一下GPT:

Trap 是指在程序执行过程中,处理器遇到某些事件时,将控制权从当前程序转移到操作系统或异常处理程序的一种机制。
它可以由程序主动触发(如系统调用),也可以由硬件自动触发(如异常或中断)。

STFW:操作系统小结(三)- 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的结果确实不一样!如下:

image-20241224234747535

输入c之后再输入q能够优雅地退出,并没有报错,而直接输入则会报错。通过echo $?查看程序的返回值,发现直接退出的返回值是2,而正常的是0,所以问题出在返回值上。

PS:在menu里开启debug配置后,输入make gdb,可以调试NEMU

一个疑惑是,进入gdb调试后,发现直接退出返回的值是1,并不是2!这是为什么呢?值得思考,怀疑是make程序进行了再次返回,于是直接在命令行运行NEMU而不是通过make命令,得到以下结果:

image-20241225002014939

猜想正确。(还可以深究一下为什么make会在返回值1的基础上返回2?

不过,程序会报错的原因找到了,就是因为返回值不为0,而控制返回值的函数在/src/utils/state.c中的函数is_exit_status_bad()

image-20241225002317465

说明退出时不满足这个条件,返回去看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()体现

1.4 基础设施