目录
- 硬编码与软编码
- YEMU
- NEMU 执行一条指令的过程
- ELF文件的组成
- ELF文件解析
- BIOS程序
- 输入输出
- 键盘的数据传输过程
- 键盘的 枚举 宏定义 展开过程
- VGA的数据传输过程(没有回调函数)
- PA2.3必答题
硬编码与软编码
- 硬编码: 数据与程序都通过代码来编程
- 软编码:数据单独利用txt等文件来编写,程序单独通过代码实现,将数据与代码分开
YEMU
#include <stdint.h>
#include <stdio.h>
#define NREG 4 //reg的大小
#define NMEM 16 //mem的大小
// 定义指令格式
//union中每次只能存在三个数据中的一个数据
//三种数据长度都为8位,rtype表示reg的状态,mtype表示mem的状态,inst表示指令,即下面的0bxxxxxxxx
typedef union {
//struct的数据排布为反过来的:先op,再rt,最后rs
struct { uint8_t rs : 2, rt : 2, op : 4; } rtype; //:2表示位宽为2
struct { uint8_t addr : 4 , op : 4; } mtype;
uint8_t inst;
} inst_t;
//定义宏,用来提取union中的数据
#define DECODE_R(inst) uint8_t rt = (inst).rtype.rt, rs = (inst).rtype.rs
#define DECODE_M(inst) uint8_t addr = (inst).mtype.addr
uint8_t pc = 0; // PC, C语言中没有4位的数据类型, 我们采用8位类型来表示
uint8_t R[NREG] = {}; // 寄存器
uint8_t M[NMEM] = { // 内存, 其中包含一个计算z = x + y的程序
//0b表示二进制,后面的八位二进制则是指令,编写方法与pa2中提供的指令手册对应
0b11100110, // load 6# | R[0] <- M[y] 第六个字节处是y的值
0b00000100, // mov r1, r0 | R[1] <- R[0]
0b11100101, // load 5# | R[0] <- M[x] 第五个字节处是x的值
0b00010001, // add r0, r1 | R[0] <- R[0] + R[1]
0b11110111, // store 7# | M[z] <- R[0] 将z值存在第七个字节处
0b00010000, // x = 16
0b00100001, // y = 33
0b00000000, // z = 0
};
int halt = 0; // 结束标志
// 执行一条指令
void exec_once() {
inst_t this;
this.inst = M[pc]; // 将mem中的指令读取到union中的inst中
//将inst转化成rtype来解读,并译码
switch (this.rtype.op) {
// 操作码译码 操作数译码 执行
case 0b0000: { DECODE_R(this); R[rt] = R[rs]; break; }
case 0b0001: { DECODE_R(this); R[rt] += R[rs]; break; }
case 0b1110: { DECODE_M(this); R[0] = M[addr]; break; }
case 0b1111: { DECODE_M(this); M[addr] = R[0]; break; }
default:
printf("Invalid instruction with opcode = %x, halting...\n", this.rtype.op);
halt = 1;
break;
}
pc ++; // 更新PC
}
int main() {
while (1) {
exec_once();
if (halt) break;
}
printf("The result of 16 + 33 is %d\n", M[7]);
return 0;
}
NEMU 执行一条指令的过程
(以执行auipc指令为例子)
-
取指令
- isa_exec_once(s)
- 取指令并更新pc指向的地址
- 利用所取指令译码(decode_exec)
- 返回译码后的值
- isa_exec_once(s)
-
译码decode_exec(s)
-
制定匹配规则:INSTPAT
-
提取指令中用于匹配的信息:pattern_decode
1. 提取key:提取模式字符串中的0、1,变成 key,不同的key代表不同的模式
2. 产生mask:产生key的掩码mask
3. 产生shift:表示opcode距离最低位的比特数量, 用于帮助编译器进行优化.
4. 结果为:(以auipc指令为例) -
开始匹配:将(( 所取指令>>shift ) & mask ) == key 来匹配模式
-
提取指令中用于操作的信息:decode_operand:
1. 提取寄存器号码
2. 提取两个源操作数
3. 提取立即数(直接在指令中可以使用的常数)
-
-
执行指令:按指令中的信息来执行(在INSTPAT_MATCH中)
-
更新pc(exec_once)
-
下一条静态指令snpc:在指令集中按顺序存放的下一条指令
-
下一条动态指令dnpc:程序运行将运行的下一条指令
-
要用dnpc来更新pc
-
ELF文件的组成
-
需求:要做到代码和数据分离,并分别记录他们的信息
-
解决:使用结构体(数据结构)
-
常见的节
-
结构
ELF文件解析
用fopen打开文件
读取elf header的信息
elf header:显示elf文件总体的信息
Elf32_Ehdr 结构体 (在elf.h中定义)
和上面的header结构相对应,只需将数据读取进这个结构体中即可
利用fread,将文件中 elf header的数据,读入Elf32_Ehdr 结构体中(内部各个类型的数据会自动匹配)
解析elf header
现在结构体中存储了 elf header 的全部信息
- 检测魔数是否符合elf文件的规定 -> 检查读入的数据是不是elf文件
- 获取section headers 的起始位置
- 获取section headers 的数量
- 获取section headers 的大小
解析section headers
section headers: 列举了几个section header的信息
Elf32_Shdr 结构体,存放每一个section header的信息(每一行)
寻找字符串表:
- 用for循环遍历每一个section,并读入数据
- 从读入数据中查看type,看看是不是strtab
- 如果是,则获取 strtab 的 off 和 size
- 读取strtab的数据:将光标移动到 off 处 ,读取 size 大小的数据
寻找符号表:
- 用for循环遍历每一个section,并读入数据
- 从读入数据中查看type,看看是不是symtab
- 如果是,则获取 strtab 的 off ,并计算符号表中有多少个符号
解析符号表
symtab:记录了每个符号的信息
Elf32_Sym 结构体: 用于存储每个符号的信息
- 将光标移到符号表
- 遍历所有符号,读入每个符号的数据并存入结构体中
- 查看符号的类型是不是 func 函数
- 如果是:
- 存储函数名称
- 存储函数大小
- 存储函数地址
BIOS程序
全称:Basic Input/Output System
输入输出
cpu与设备的交互方式 ( 内存映射 )
- cpu与设备约定好某一块内存作为两者交互的通道,两方通过往该内存地址写入或读出数据来进行交互
- 在nemu中am中的test程序扮演运行在计算机上的程序(不是设备)
- 设备(回调函数)
- nemu主文件夹中程序扮演cpu的角色
(serial为例)
- cpu与设备约定
- 在nemu/src/device/serial.c中的init_serial()中
- 程序的操作(实际是对内存操作,会被转化成对内存操作的指令)
- 程序:am-kernels/tests/am-tests/src/tests/hello.c
- 接口:abstract-machine/am/src/riscv/riscv.h
- out(b/w/l):将数据输出给串口(将数据写入内存,让串口读取)
- 程序的操作不会直接对cpu操作,而是转换成指令,再由cpu执行
- cpu对执行程序的操作
- 程序被编译成对内存操作的指令,执行Mw的宏(其实是paddr_write)
- paddr_write发现内存地址不是物理地址(那就是与设备约定好的内存地址,则调用map_write
- 在nemu/src/device/io/map.c中的map_write
- map_write
- 将数据写入内存后(cpu)
- 触发读出内存数据的回调函数(设备)
- map_write
- 设备(回调函数)
- nemu/src/device/serial.c
- serial_io_handler将内存中的数据读出并通过串口输出
(RTC为例)
- cpu与设备约定
- 在nemu/src/device/timer.c中的 init_timer() 中
- 程序的操作(实际是对内存操作,会被转化成对内存操作的指令)
- 程序:am-kernels/tests/am-tests/src/tests/rtc.c
- 接口:abstract-machine/am/src/platform/nemu/ioe/timer.c
- in(b/w/l):读取时钟(设备)的数据(读取时钟写入内存的数据)
- 程序的操作不会直接对cpu操作,而是转换成指令,再由cpu执行
- cpu对执行程序的操作
- 程序被编译成对内存操作的指令,执行Mr的宏(其实是paddr_read
- paddr_read发现内存地址不是物理地址(那就是与设备约定好的内存地址,则调用map_read
- 在nemu/src/device/io/map.c中的map_read
- map_read
- 先触发写入数据的回调函数(设备)
- 再读出内存的数据(cpu)
- 设备(回调函数)
- nemu/src/device/timer.c
- rtc_io_handler将时钟(设备)的数据写入内存中
键盘的数据传输过程
- cpu与设备约定
- 在nemu/src/device/keyboard.c中的 init_i8042() 中
- 程序的操作(实际是对内存操作,会被转化成对内存操作的指令)
- 程序:am-kernels/tests/am-tests/src/tests/keyboard.c
- 接口:abstract-machine/am/src/platform/nemu/ioe/input.c
- in(b/w/l):读取键盘(设备)的数据(读取键盘写入内存的数据)
- 程序的操作不会直接对cpu操作,而是转换成指令,再由cpu执行
- cpu对执行程序的操作
- 程序被编译成对内存操作的指令,执行Mr的宏(其实是paddr_read
- paddr_read发现内存地址不是物理地址(那就是与设备约定好的内存地址,则调用map_read
- 在nemu/src/device/io/map.c中的map_read
- map_read
- 先触发写入数据的回调函数(设备)
- 再读出内存的数据(cpu)
- 设备(回调函数)
- nemu/src/device/keyboard.c
- i8042_data_io_handler 将键盘(设备)的数据写入内存中
键盘的 枚举 宏定义 展开过程
如何检测多个键同时被按下?
- 当按键被按下或松开,都有专门的指示信号keydown
- 因此游戏让按键生效的判断条件可以是:
- 当keydown=1时,按键生效
- 当keydown=0时,按键失效
- 这样可以让多个按键的keydown同时为1
VGA的数据传输过程(没有回调函数)
- 设备:初始化void init_vga()
- 存入屏幕大小的信息
- 将显存内容清零
- 程序
- abstract-machine/am/src/platform/nemu/ioe/gpu.c 和 am-kernels/tests/am-tests/src/tests/video.c
- 取出屏幕大小信息
- 存入图像信息
- 将同步信号存1
- cpu执行程序
- map read读出屏幕大小信息
- map write写入图像数据
- map write写入同步信号为1
- 设备:自己每隔一段时间更新状态 device_update()
- 调用 vga_update_screen 检查同步信号
- 若同步信号为1,则读取图像信息并显示(刷新屏幕)
- 刷新屏幕后会将同步信号置0
- 重复2.到5.
PA2.3必答题
编译与链接
-
在
nemu/include/cpu/ifetch.h
中, 你会看到由static inline
开头定义的inst_fetch()
函数. 分别尝试去掉static
, 去掉inline
或去掉两者, 然后重新进行编译, 你可能会看到发生错误. 请分别解释为什么这些错误会发生/不发生? 你有办法证明你的想法吗?- 因为函数定义在.h文件中,如果去掉
static
则可能会出现重定义的情况 - 如果去掉inline,作为函数调用,需要占用更多的资源
- 因为函数定义在.h文件中,如果去掉
-
在
nemu/include/common.h
中添加一行volatile static int dummy;
然后重新编译NEMU. 请问重新编译后的NEMU含有多少个dummy
变量的实体? 你是如何得到这个结果的?- 有一个实体变量,因为static保证变量的作用域只在common.h中
-
添加上题中的代码后, 再在
nemu/include/debug.h
中添加一行volatile static int dummy;
然后重新编译NEMU. 请问此时的NEMU含有多少个dummy
变量的实体? 与上题中dummy
变量实体数目进行比较, 并解释本题的结果.- 含有两个实习,因为都是static,所以都在各自的作用域中
-
修改添加的代码, 为两处
dummy
变量进行初始化:volatile static int dummy = 0;
然后重新编译NEMU. 你发现了什么问题? 为什么之前没有出现这样的问题? (回答完本题后可以删除添加的代码.)- 为同名的变量初始化后会被视为重复定义,