PA1

南大pa是一个设计非常巧妙而且指引非常足的一个实验,由于其指引已经很足了,我在这里不会写实验具体是怎么做的,具体写一下我的思考。

开天辟地的篇章

程序的执行可以看成是一个有限状态机,每一个时刻可能对应一个状态。状态可以用程序计数器的值来进行表述,在不同的状态下,执行的指令和存储器的值也是不一样的,每一个状态可以指令一个访存或者计算指令。不同的程序是不同的状态机。

RTFSC

pa的实验分成三个部分,一个是基础的nemu,nemu本质上是一个硬件模拟,它并不承担软件的部分,它为其他的部份提供硬件层面上的支持。对于其他的部份来说,他们会天真的认为自己是在一个真实的计算机进行执行,但是这一切都是nemu假扮的。nemu并不完善,它只能假扮CPU的执行和部分IO。

还有一个是am,am是抽象计算机,它可以称为静态链接库,这个本质上属于软件,它为其他运行程序提供基础的IO服务,在编译的时候它会和其他的用户程序一起编译在一起。所以说软件部份=am+用户程序,在实验的不同部分中,用户程序是不同的。

这个实验的运行逻辑其实可以知道了。nemu充当硬件的成份,am+用户程序充当软件,在编译后写入进nemu的假想的存储内,由nemu模拟运行。

Makefile

一般来说一个成熟的系统会使用make来自动化构建。pa使用了下面这条make语句进行构建

1
2
3
4
5
$(OBJ_DIR)/%.o: %.c
@echo + CC $<
@mkdir -p $(dir $@)
@$(CC) $(CFLAGS) -c -o $@ $<
$(call call_fixdep, $(@:.o=.d), $@)

其中%.表示对于任何一个.o文件,这个是什么意思呢,我们看下面的这个make文件:

1
2
3
4
5
6
CC=gcc

%.o: %.cpp
$(CC) -c -o $@ $<
main: main.o fun.o
$(CC) -o main main.o fun.o

其中系统的输出是这样子的:

1
2
3
gcc -c -o main.o main.cpp
gcc -c -o fun.o fun.cpp
gcc -o main main.o fun.o

%.保证在依赖链中所有依赖相似的依赖都会被构建,我们可以将%.的替换称为一次“展开”,在一次展开中,$<表示第一个依赖文件,$^表示所有依赖文件,$@指代所有目标文件。当然这个指代仅指代一次展开。这是一个省力的好办法。

客户程序载入

众所周知,nemu是一个硬件模拟器,硬件需要运行软件,软件是以操作码储存在内存中的,nemu提供了一个默认的客户程序,这个客户程序很小,只有4B,在启动的时候首先把这个客户程序放在指定的位置中,然后让pc寄存器指向这个客户程序的第一个字节码。当然,这个客户程序执行完之后是nemu_trap命令。执行中断操作,这个后面会讲为什么。

当然我们可以定义一个自选的默认程序来执行相关操作。这个程序的读取在parse_arg已经做好了。如果有自选的程序,会覆盖掉之前的默认的客户程序。

客户程序运行

当代码初始化了之后就会进入sdb_mainloop中,sdb_mainloop是nemu自带的一个控制台小程序,它可以接收控制台的命令然后控制客户程序的执行。

执行的核心逻辑是cpu_exec(),括号内可以传一个整数,表示执行多少条指令,如果传的数是-1就一直循环执行下去。

什么东西可以打断cpu的执行呢,那就是中断,当遇到中断的时候,执行结束,nemu会根据中断的类型判断:

  • HIT GOOD TRAP - 客户程序正确地结束执行
  • HIT BAD TRAP - 客户程序错误地结束执行
  • ABORT - 客户程序意外终止, 并未结束执行

对于nemu来说,我们可以简单地认为,遇到BAD TRAP/GOOD TRAP就算是一次程序运行的结束。复杂点说就是对于nemu这一个硬件平台,可以运行很多次软件,一次TRAP就是一次软件的运行。实际上对于C语言或者别的,程序运行的结束有另外的表示,nemu本身也是一个大型的C语言项目,nemu运行的结束也需要有表示。

优雅地退出

作为一个C语言代码,我们可以认为main是C语言的核心框架,当main执行完了之后,整个C语言代码就执行完了,一般OS需要在这个时候回收一个信号,这个信号提示刚刚的C语言代码运行情况。这就是return 0的作用,return 0就是告诉上层(或者叫调用该C语言代码的模块),我运行地挺好。

我们注意到nemu退出的时候输出了一堆奇怪的东西,其实是return的结果不是0而是-1,如果return的结果是-1,一般就是运行出现了问题。RTFSC之后可以很快更改。

PA2

暂停一下!

在做pa2的时候,你可以简单的认为,nemu是模拟硬件的执行,而am-kernel(abstract-machine)模拟最基础的软件的执行。abstract-machine里面模拟了最简单的运行时环境(你可以理解为是“头文件”)

那首先,我们先尝试理解一下nemu的运行机理。nemu尝试去实现了一个简单的硬件,这个简单的硬件完成了一个简单的图灵机,就是设置一个PC,取指令,执行指令,设置下一次执行的PC。对于所有的软件,包括am和用户程序,它是一个二进制字节码。nemu阅读这个二进制字节码,然后执行字节码对应的指令。

现在RTFSC。了解一下程序的运行。首先程序执行am_init_monitor(),然后执行若干个初始化的操作:

  • rand
  • mem 初始化内存,将0-0x8000000的位置随机给一个数据,就算初始化好了。也就是说存储器其实是一个比较大的数组。
  • isa 初始化cpu的执行,将cpu的pc寄存器指向前面提到的开始执行的一段代码的第一条指令的位置。这一段开始执行的代码根据指令集的不同进行执行(这一段特殊的内置的代码很像操作系统的bios,实际上的功能其实也差不多)
  • load_img 将一个镜像文件覆盖上述的那段特殊的内置代码,这个镜像文件是运行NEMU的一个可选参数, 在运行NEMU的命令中指定。如果运行NEMU的时候没有给出这个参数,NEMU将会运行内置客户程序。
  • device 初始化各种设备

有疑问!这个用户的二进制文件大小是怎么确定的?我们可以看到am-bin.S这个文件中,这个文件巧妙地确定了大小

这就是完成了各种初始化的操作,然后跳转到engine_start()。engine_start根据不同的情况进行执行。

  • 如果指定了客户程序,就一直执行下去。
  • 没有指定客户程序,就执行sdb,也就是内置的一个调试器。调试器的实现不去讨论。

到这里其实我们可以知道一点,其实nemu也是一个程序!这个程序在我们的cpu上执行,它的职责,其实是模拟一个cpu!这个程序分为两个部分,一个是代码部分,用于模拟cpu上的执行,一个是数据部分,用于模拟内存,这个数据部分,存储了要被模拟器模拟的程序,和要被模拟器模拟的程序所用的内存!

一次模拟执行

上文中说到,nemu执行完初始化的操作之后就开始执行用户程序的代码,那究竟是怎样执行的呢?具体的执行可以推导execute()->execute_once()->isa_exectue(),这是isa_execute()的执行,和五段流水线的操作一模一样。

IOE的实现

上述实现了一个非常简单的图灵机(TRM),当时只有图灵机的实现是不足以解决问题的。只会计算的计算机,是没有人机交互的能力的,于是实现IO是迫在眉睫的问题。

IO设备有两种模式,一种是中断的模式,一种是内存映射。这里使用内存映射的模式,举个例子吧!比如说你想在命令行窗口里面输出一个字母A,那么你只需要往一个特殊的地方输入”A”这个字母,那么设备从这个特殊的地方获取A这个数据,然后输出字母A。那么特殊的地方在哪里呢?在init_device()

设备是沟通外界物理世界(模拟信号)和逻辑世界(数字信号)的桥梁。

1
2
3
4
5
状态机模型            |           状态机模型之外
S = <R, M> | D
计算机/程序 <----I/O指令----> 设备 <----模拟电路----> 物理世界
|
|

“内存”读写

为什么要给内存打上引号?是因为内存的读写不再是内存本身了,为什么这样说呢?RTFSC(当执行L类指令和S类指令的时候)

内存的读写要分为两部分,第一部分是设备,第二部分是内存本体。对于内存本体的读写很简单,由于这部分的实验没有页表,那么就很简单的移动处理,虚拟地址+offset=物理地址。如果是设备读写,就要调用专门的设备读写函数。

在AM中加上设备读写的支持

这下知道驱动是怎么写的了吧?(应该叫驱动)比如说你要输出一个图片,经过驱动程序的转化,会变成一组S指令和L指令。本次实验的所有IOE完成全部都是这么来的,具体不表。

PA3

中断

中断是每个系统都需要思考的一个问题。在nemu系统中,你可以从软件和硬件的两方面完成中断操作。

  • 硬件处理:中断相关寄存器的实现和中断相关指令的实现
  • 软件处理:中断向量和中断处理程序

再梳理一遍中断相关的操作。

  • nemu程序执行ecall指令。
  • 跳转到mtvec所保存的中断向量处。s->npc = mtvec,保存epc(硬件处理) nemu还在执行指令,但是和普通的TRM没啥区别了
  • 执行中断向量,经典保存栈帧
  • 跳转到am提供的中断处理程序,判断是中断类型,这里默认由用户主动引发的中断是11。(这里是am模块提供的运行时环境支持,和操作系统集成)
  • 跳转到nano的代码,执行系统调用处理。
  • 返回,返回的时候执行特定的返回操作。

这里mcause保存中断处理的原因,c->GPR1保存系统调用号。

带操作系统大应用

上面我们提到,nemu是硬件,am集成了软件的运行时环境为应用程序提供了执行环境。这里的应用程序换成了一个操作系统,这个操作系统包含了代码区域和数据区域,代码区域存储了一系列操作系统执行的代码,数据区域作为ramdisk存放了要被操作系统规划执行的代码。所以本质上是三部分代码。这就是大应用。也就是AM+OS规划application执行。

操作系统下的IOE

所有的IO操作,全部都使用am提供的底层借口。下面举个例子展示一下IO操作是如何逐层抽象的。

  • navy(应用程序) 访问抽象文件\dev\fb
  • 访问文件的操作被转化为了系统调用(详见syscall.c
  • 中断,和上面一样
  • 进入了系统调用处理,处理发现是read系统调用,进入文件系统处理
  • 文件系统通过文件标识符号获取read的处理函数,从read的这个抽象接口中获取了内容。返回。

这一部分是操作系统为应用程序提供的逻辑读写服务。操作系统的read函数调用了AM提供了抽象读写函数io_read(),这就是AM为操作系统提供的抽象读写服务。AM再具体的实现物理设备的读写,和上面一样。

驱动?

这大概就是驱动的概念,驱动是可以随意安装和卸载的。在这里我们也可以安装和卸载驱动。

AM为上文提供了一个抽象接口io_read() or io_write(),这个抽象接口传递一个变长参数。比如io_read(A,B,C,D),这个时候编译器会根据klib库的一个宏,生成一个新的数据类型A_T包含三个成员分别叫B,C,D三个,和一个新的宏A。并且调用ioe_read(int A,A_T buf)然后执行你已经注册好的底层驱动函数。

注册的逻辑实在函数指针数组lut里面添加你这个宏的支持,具体看代码也可以懂。

更新于

请我喝[茶]~( ̄▽ ̄)~*

Sukuna 微信支付

微信支付

Sukuna 支付宝

支付宝