不停计算的机器
最简单的计算机TRM的运行方式:
1 | while (1) { |
具体地,cpu执行指令的方式是:
- 取指:从内存空间中读取出一条指令,具体位置由pc寄存器给出。
- 译码:读取出来的是一个字长的01串,cpu会查表翻译指令。
- 执行:根据译码的结果来执行具体的行为。
- 更新pc,指向下一条指令
回顾一下,用户程序是运行在模拟的硬件之上的,所以这里的任务是模拟一个cpu的执行,来支撑用户程序的执行。
:question: cpu一次只能执行一条指令吗
在这个模型里,如果细化到下去,在一个时刻cpu应该只能同时执行一条指令,这是一个重要的假设,当然这个假设不是谁规定的,他实际上从一开始编程就成了一种习惯:代码是一行一行执行,指令也应该是这样的。
- 但是现代的cpu似乎对这个事情有所打破,可能是一个叫微指令的概念
理解YEMU如何执行程序
加法状态的状态机:把内存和寄存器的值作为状态。
两者的联系?cpu执行一条指令就是一次状态转移。
这个例子可以说明,只要有加法器和跳转功能,理论上可以做出任何计算机程序(@brainfuck)。只是这样效率太低了。
RTFSC(2)
RTFM
在PA中,riscv32的客户程序只有RV32I和RV32M组成。
- RV32I: Base Integer Instruction Set
- RV32M: Extension for Integer Multiplication and Division
通过目录就可以轻松定位到位置,但是找opcode table的时候我发现我现在看的手册版本里位置有变化,确实有点难发现。还有一点是The RISC-V Reader里的opcode有错误,建议还是看官方手册,虽然reader有中文可以看但是确实有问题。
RTFSC(2)
立即数背后的故事
这个事情应该是和isa相关的,如果要移植大端序的isa,具体的读取应该要转换字节序列?
立即数背后的故事(2)
目前我的认知是他可以分开读取+某些指令不关心末尾的0(比如地址的末尾位)。
之前的代码中出现过两种pc:
- snpc: 静态pc,总是=pc+4,即顺序执行的pc位置
- dnpc:动态pc,对于跳转指令下一个pc的位置就不一定是+4
所以实际是用s->dnpc来更新cpu的pc位置。
结构化程序设计
来体会一下
写一个正确的函数然后两个函数对拍?既然都有了’正确的’程序还要他干什么,直接去掉。
RTFSC理解指令执行的过程
运行第一个C程序
为什么执行了未实现指令会出现上述报错信息
兜底命令
:question: li命令是什么
首先li是一个伪指令,上面的一个蓝框题里有提问到这个问题:因为riscv32的指令一共就32位长,而I型指令又只有12位的立即数,你是不可能通过一条指令就加载32位的立即数的。
但是我发现一个问题,现在我下载到的spec里没有关于伪指令的说明,google一下发现似乎是拆到了别的manual里,简直坑死人。列表见asm manual里这个位置。
一个备用选择是看riscv reader。
先解决一个问题:当调用make的时候发生了什么?执行一下make -nB可以拿到
1 | /bin/echo -e "NAME = dummy\nSRCS = tests/dummy.c\ninclude ${AM_HOME}/Makefile" > Makefile.dummy |
所以他应该是先生成了一个简单的makefile,然后立即执行他,执行完了之后就移除了。
:question: 为什么他要创建一个临时的结果文件result?
将结果导出到.result然后cat .result,之后就立即删除了。为什么要绕一道?
可能是为了先打印程序名称然后再打印结果。
然后是这个makefile里的内容,比较简单就是
1 | NAME=dummy |
就是定义了一下变量然后引用了AM的makefile,看来剩下的还要去AM里面找结果。在现阶段看来,因为AM是一个抽象层,dummy程序是运行在AM这个抽象层之上的,所以在AM的makefile里可能需要将这个dummy.c
编译成一个可执行文件,然后将指令数据带给NEMU。后者比较好做,可能就是之前提过的NEMU加载指定用户程序的方式,前者目前还有些迷雾,一个比较明显的问题是不可能直接链接GNU/LINUX的库,这里需要交叉编译。
运行更多的程序
:exclamation: 未测试的代码永远是错的!
只有两个需要klib的测试需要提供更多内容才可以做。
这里有几个高位相关的指令需要补齐。
程序, 运行时环境与AM
运行时环境
将运行时环境封装成库函数
这又能怎么样呢
在am上运行的程序不需要关心具体平台的具体细节,可移植。
AM - 裸机(bare-metal)运行时环境
1 | AM = TRM + IOE + CTE + VME + MPE |
为什么要有AM? (建议二周目思考)
OS提供的运行时环境和AM提供的有什么区别?
RTFSC(3)
这里提到了上面的问题,需要在GNU/Linux的环境里编译出在riscv32-nemu里运行的可执行文件。
- gcc将AM实现源文件编译到目标文件,然后通过ar打包
- 将要运行的用户程序编译到目标文件
- gcc和ar把klib也编译打包起来
- 根据makefile里定义的规则让ld将上述文件链接起来
以及makefile里定义的规则还会规定entry point到0x80000000
,经过一些设置之后起始点从start.S开始,设置好栈顶之后跳到_trm_init
去执行。
:question:Hey,程序的起点不是main吗
是的,开始的那个问题又出现了,但是目前还没有足够的能力解决它。至少现在可以说的是,在AM上启动的起点是需要各种设置之后的start.S这个汇编文件里的指令。
注意到AM的makefile第一个目标是make html
,可以输出更好看的形式,似乎具体只是因为文本文件就是一个markdown格式写出的,然后生成到html。
交叉编译的工具链是通过变量控制的,例如gcc是$(CROSS_COMPILE)gcc
,具体地这个变量是在inlclude makefile的时候带出来的,比如riscv32就会写出CROSS_COMPILE := riscv64-linux-gnu-
,显然这个事情是和isa相关的。
这里可以注意到isa相关的makefile和平台相关的makefile也是拆开的。
后面AM的makefile就是一些编译规则了,用于指向最后生成的文件位置,具体的目标定义在平台的makefile里,在NEMU的makefile找到运行的目标之后就可以知道是在什么位置启动NEMU以及给NEMU传递参数的了。目前他只设置了-l $(shell dirname $(IMAGE).elf)/nemu-log.txt
用于控制log文件生成的位置。
通过批处理模式运行NEMU
通过AM的Makefile默认启动批处理模式的NEMU,知道NEMU传入参数的地方之后就可以写了。这个东西一个好处是可以直接运行所有测试然后打印对应的结果。
因为这里并不是直接在AM启动的make,所以bear得到的结果实际上是对am-kernel这边才能用。因此要想一个办法在AM那里启动bear然后生成结果让我的clangd正常工作。但是并不是很成功,如果直接将内容放到AM下面会找不到elf文件。
实现常用的库函数
实现字符串处理函数
重新认识计算机:计算机是个抽象层
基础设施(2)
跳过了,没什么用
输入输出
设备与cpu
访问设备 = 读出数据 + 写入数据 + 控制状态
端口IO:使用专门的IO指令对设备进行访问,端口即设备的地址。
内存映射IO:端口IO的问题是如果已经确定了端口号,就不能再做修改,即只能加不能减或者改。而且这件事是直接写到指令集里的。
内存映射的想法是将一段内存空间拿出来专门给设备用,当cpu访问这段内存空间的时候,通过一个中间层映射到设备的位置上。这样的想法提供的编程模型和普通的内存访问并没有什么不同,唯一的缺点只是这段内存空间不再可用,不过它比起总量来说差距太大了,可以忽略不计。
理解volatile关键字
我的机器上全部是死循环
端口IO和内存映射IO都是映射,这里设计成了共用一个IOMap结构体来管理。具体这个IOMap保存了名称,映射的起始地址和结束地址,映射的目标空间以及一个回调函数。这里保存的映射的起始地址和结束地址
是当cpu访问的到的地址,映射的目标空间是实际的地址(设备在的地址?)。
访问设备的读写api如下,用于将地址addr映射到map所指示的目标空间。对于端口IO,上游接口是pio_read/pio_write
,最终调用map_read/map_write
来实现访问;内存映射IO类似,在paddr_read/padd_r_write
时会判断addr落在实际的物理内存空间还是落在了设备空间,如果是后者就调用api访问设备。
1 | word_t map_read(paddr_t addr, int len, IOMap *map); |
NEMU中的输入输出
在menuconfig里打开devices,然后运行NEMU就可以看到一个空窗口(但是实际上并不是空窗口,他会把当前屏幕上的内容带上去,为什么?)。
NEMU是一个单线程程序,因此他是通过串行的方式模拟设备的执行的:在每次cpu_exec
执行指令之后会查看距离上次设备执行的时间,如果已经超过了一定时间(tick)就尝试刷新屏幕,然后检查是否有按键按下释放以及窗口的x事件。
:question:为什么我的x按钮不起作用?
是单纯的tick太高了还是有什么bug存在?
将输入输出抽象成IOE
首先设备访问的具体实现肯定是和具体架构相关的,其次设备访问是给计算机提供输入输出的能力的,应该归类为IOE。具体地抽象了下面三个api:
1 | bool ioe_init(); |
这里的reg并不是具体的设备寄存器而是一个抽象的寄存器概念,显然不同架构的设备寄存器不可能一样。read是从寄存器中读写到缓冲区buf,write是把缓冲区buf的内容写到寄存器里。约定在不同的系统架构里也是用同样的抽象寄存器编号。
amdev.h里定义了常见设备的抽象寄存器编号和相应的结构,具体地,他里面是一个宏:
1 |
对于下面这个调用,他会展开成类似这样的:
1 | AM_DEVREG( 1, UART_CONFIG, RD, bool present); |
其中的Perm参数作为描述权限的字段似乎没有使用?
将输入输出抽象成IOE
首先是串口,串口初始化时会分别注册0x3F8
处长度为8个字节的端口(对于使用端口IO使用的ISA来说)以及0xa00003F8
处8字节的MMIO空间,具体地,他的调用是:
1 | add_mmio_map("serial", CONFIG_SERIAL_MMIO, serial_base, 8, serial_io_handler); |
当cpu访问MMIO这个物理内存地址时就会被映射到串口?具体的逻辑是怎样的,目标空间space是什么意思?为什么这里的space是一个申请内存调用?
trm.c会调用putch将数据发送到串口,具体的他只是调用了一个outb(SERIAL_PORT, ch)
,这里前者就是之前提到的0xa00003F8
,所以确实应该是cpu访问这个地址的时候被映射。这里outb的实现就是直接向对应的地址写数据,会被翻译成一条write指令?这里添加的volatile可能是为了防止被优化,保证是一条对应长度的write指令。
:question: 怎么证明这个volatile是必须的
运行Hello World
riscv32可以直接运行,因为MMIO已经被实现了。
运行后可以看到一条Hello World1和mainargs=’’
但是到这里还没有解决serial_base是干什么用的。map_write的具体实现是这样的:
1 | void map_write(paddr_t addr, int len, word_t data, IOMap *map) { |
其中调用了host_write的时候用到了map->space,而host_write就是直接的内存写了。然后找一下谁调用过他,发现:
1 | static void pmem_write(paddr_t addr, int len, word_t data) { |
而 pmem_write的调用只有paddr_write,具体地,paddr_write会先检查目标位置是否落在物理空间里,如果不是则调用mmio_write,否则调用pmem_write。在pmem_write里他会先将目标地址通过guest_to_host转换,其最终的目标就是NEMU中模拟的内存pmem数组。
所以一开始new_space实际上是找NEMU模拟的内存空间里分配了8字节的空间,然后具体写的时候是通过host_write直接向NEMU模拟的硬件上写数据的。所以这里理一下:0xa00003F8
这个地址确实是约定的,他是在一开始就在内存中划分好的一个特定区域,new_space
是运行时动态分配的。当putch准备向0xa00003F8
这个物理地址写数据的时候,他会被转换到写一个运行时动态分配的内存地址。
:question: 哦等等,谁来规定
0xa00003F8
这个地址是串口的?事实上我们并没有规定这个值,回想一下串口的定义,其中有一个宏参数
#define CONFIG_SERIAL_MMIO 0xa00003f8
,这个定义是在执行了make menuconfig打开了设备的时候才出现的,事实上当我们知道了系统中有哪些设备的时候,每个设备需要映射的空间大小和映射的起始地址的时候就可以算出每个设备的地址了。
这里有提到,现在这样在AM上的输出是裸机编程:直接操作设备。这是一个很危险的设计,当引入操作系统的时候,设备就不是一个直接能访问的东西了,需要经过操作系统调用。
理解mainargs
- isa-nemu和native存在不同的传递方式
实现printf
这个任务不是之前就完成了吗
第二个设备是时钟,这里有一个让我一开始有一些误解的地方:文档中提到定义了两个抽象寄存器,但是实际的设备寄存器有两个,都是32位的寄存器。这里的实际情况是RTC抽象寄存器没有实现,然后两个实际存在的寄存器实际上一起组成了一个抽象寄存器UPTIME。
实现IOE
在timer.c中实现
AM_TIMER_UPDATE
的功能,框架中提供了将当前时间写入设备寄存器的部分。然后在ioe/timer.c
里补齐读取的部分就可以了。在rtc.c中的io_read是一个宏,对其展开可以获得:
1
2
3
4
5 ({
AM_TIMER_UPTIME_T __io_param;
ioe_read(AM_TIMER_UPTIME, &__io_param);
__io_param;
})调用
ioe_read
,将结果写到一段连续数据里,然后把这段连续数据当作一个struct解读再直接移出去,非常有意思的写法。这个必做题把如何运行测试的方法作为了题目的一部分,只要去看对应目录的文件内容就可以了。
看看NEMU跑多快
我怎么跑挂了,返回的结果是。Dhrystone FAIL
。而且microbench的速度异常的慢,换了之后也还是慢的异常发现是klib里的实现有问题,修正之后就PASS了。
为什么等到我做性能测试才爆出来。慢的离谱的原因是因为没有关掉trace。我的跑分结果是ref的microbench 1000分。
设备访问的踪迹-dtrace
感觉没啥用,跳过了
实现IOE(2)
和前面两个一样。
如何检测多个键同时被按下?
有可能是按二进制写状态,具体还不清楚。
最后一个是实现VGA图像,初始化时在0xa1000000
开始注册了一段内存映射,具体的大小是屏幕的大小乘每个像素的信息大小:400x300x32
,不带透明度信息。
GPU设备这边的文案感觉有点割裂,不知道是因为什么原因。
一共定义了五个设备寄存器:GPU_CONFIG
, GPU_STATUS
, GPU_FBREDRAW
, GPU_MEMCPY
, GPU_RENDER
。然后说NEMU中只会用到GPU_CONFIG
:读出屏幕大小和GPU_FBDRAW
:帧缓冲,向(x,y)坐标绘制w*h的矩形,然后有一个像素数据数组和一个是否同步。
实现IOE(3)
文案开头的话可能是没删干净,实际上已经介绍了。这部分实现之后就可以看到全屏的颜色信息。
实现IOE(4)
fbdraw寄存器还有一个绘制图形的功能没有实现,现在只是实现了sync效果。实现完之后可以看到动画效果。
但是为什么会有黑边?是我实现的不对吗。应该是N=32不是整数倍的原因。
声卡部分暂时跳过了。后面可能会补齐。
冯诺依曼计算机系统
展示你的计算机系统
为什么这个bad-apple是纯字符版本的
游戏是怎样运行的
暂时搁置
在NEMU上运行NEMU
听起来很疯狂,但是之后再补齐。
必答题
之后补齐
:sunny: 温馨提示: pa2到此结束