NJU PA2思路(riscv32)
记录做PA2的一些思路和笔记
运行NEMU后,当键入c
或si
时的原理是一样的,都是调用cpu_exec(n)
,执行n条指令,n是一个无符号整数,传入-1的话变成无符号整数的最大值,可视为把指令不停地执行下去无停顿,否则执行完n条指令后程序会回到sdb_mainloop
中等待下一条用户的sdb命令。
这段代码是cpu_exec函数的实现,下面是对其功能的详细说明:
首先,根据传入的参数n和预定义的MAX_INST_TO_PRINT比较,确定是否打印每条指令的执行信息。
接着,根据当前的nemu_state.state状态进行判断:
如果状态为NEMU_END或NEMU_ABORT,输出程序执行已结束的提示信息,并返回函数。
否则,将nemu_state.state设置为NEMU_RUNNING表示程序正在运行。
获取当前时间作为计时器的起始时间。
调用execute(n)函数,执行指定数量的指令。
执行完指定数量的指令后,获取当前时间作为计时器的结束时间,并计算指令执行的时间。
根据nemu_state.state的值进行判断:
如果状态为NEMU_RUNNING,将nemu_state.state设置为NEMU_STOP,表示程序执行已暂停。
如果状态为NEMU_END或NEMU_ABORT,根据具体的状态输出相应的日志信息,提示程序是否执行成功。
如果状态为NEMU_QUIT,执行统计操作。
总体来说,该函数的功能是模拟CPU的工作。它根据给定的指令数量执行相应数量的指令,并根据当前的状态进行相应的处理,包括输出提示信息、设置状态、记录执行时间以及执行统计操作。*/
void cpu_exec(uint64_t n) {
g_print_step = (n < MAX_INST_TO_PRINT);
switch (nemu_state.state) {
case NEMU_END: case NEMU_ABORT:
printf("Program execution has ended. To restart the program, exit NEMU and run again.\n");
return;
default: nemu_state.state = NEMU_RUNNING;
}
uint64_t timer_start = get_time();
execute(n);
uint64_t timer_end = get_time();
g_timer += timer_end - timer_start;
switch (nemu_state.state) {
case NEMU_RUNNING: nemu_state.state = NEMU_STOP; break;
case NEMU_END: case NEMU_ABORT:
Log("nemu: %s at pc = " FMT_WORD,
(nemu_state.state == NEMU_ABORT ? ANSI_FMT("ABORT", ANSI_FG_RED) :
(nemu_state.halt_ret == 0 ? ANSI_FMT("HIT GOOD TRAP", ANSI_FG_GREEN) :
ANSI_FMT("HIT BAD TRAP", ANSI_FG_RED))),
nemu_state.halt_pc);
// statistic()这个函数用于记录关于模拟器性能和运行状态的信息,例如主机运行时间、总指令数和模拟频率。这些信息对于性能分析和调试非常有用。
case NEMU_QUIT: statistic();
}
}
流程集中在调用execute(n)
执行n条指令,因此来到execute
函数中
- 调用
exec_once
执行每条指令 - 调用
trace_and_difftest
记录trace、执行difftest和检查watchpoint - 检查是否程序应该退出(运行到了最后一条指令或者别的原因退出)
- 更新设备状态
static void execute(uint64_t n) {
Decode s;
for (;n > 0; n --) {
exec_once(&s, cpu.pc);
g_nr_guest_inst ++;
trace_and_difftest(&s, cpu.pc);
if (nemu_state.state != NEMU_RUNNING) break;
IFDEF(CONFIG_DEVICE, device_update());
}
}
接下来看exec_once
的流程:
exec_once()
函数覆盖了指令周期的所有阶段: 取指, 译码, 执行, 更新PC
- 调用
isa_exec_once
取指并执行,指令与架构相关,从isa_exec_once()
返回后s->snpc
正好为下一条指令的PC - 将
cpu.pc
置为下一条应该执行的指令的地址,这个地址在s->dnpc
中 - 根据是否开启了itrace,将第一步执行的指令记录到s->logbuf中,然后会在
trace_and_difftest
中输出
/*主要作用是执行一次指令,并更新PC的值。它接收一个Decode结构体指针s和一个指令的PC值作为参数,
将PC值保存到s->pc和s->snpc中,然后调用isa_exec_once()执行指令,最后将执行后的动态下一条指令的PC值赋给cpu.pc。
整个过程涵盖了指令周期的各个阶段,实现了指令的执行和PC的更新。*/
static void exec_once(Decode *s, vaddr_t pc) {
s->pc = pc;
s->snpc = pc;
isa_exec_once(s);
cpu.pc = s->dnpc;
#ifdef CONFIG_ITRACE
char *p = s->logbuf;
p += snprintf(p, sizeof(s->logbuf), FMT_WORD ":", s->pc);
int ilen = s->snpc - s->pc;
int i;
uint8_t *inst = (uint8_t *)&s->isa.inst.val;
for (i = ilen - 1; i >= 0; i --) {
p += snprintf(p, 4, " %02x", inst[i]);
}
int ilen_max = MUXDEF(CONFIG_ISA_x86, 8, 4);
int space_len = ilen_max - ilen;
if (space_len < 0) space_len = 0;
space_len = space_len * 3 + 1;
memset(p, ' ', space_len);
p += space_len;
#ifndef CONFIG_ISA_loongarch32r
void disassemble(char *str, int size, uint64_t pc, uint8_t *code, int nbyte);
disassemble(p, s->logbuf + sizeof(s->logbuf) - p,
MUXDEF(CONFIG_ISA_x86, s->snpc, s->pc), (uint8_t *)&s->isa.inst.val, ilen);
#else
p[0] = '\0'; // the upstream llvm does not support loongarch32r
#endif
#endif
}
isa_exec_once
执行流程如下:
inst_fetch
取指
inst_fetch()
最终会根据参数len
来调用vaddr_ifetch()
(在nemu/src/memory/vaddr.c
中定义), 而目前vaddr_ifetch()
又会通过paddr_read()
来访问物理内存中的内容. 因此, 取指操作的本质只不过就是一次内存的访问而已.len为4是因为内存是由uint8_t类型的数组组成的,而指令类型是uint32_t,所以数组4个单元存一条指令。
decode_exec
译码并执行
// 该函数执行取下一条指令(首地址为s -> snpc)并送入decode_exec函数进行译码操作,通过模式匹配解析指令的内容
int isa_exec_once(Decode *s) {
s->isa.inst.val = inst_fetch(&s->snpc, 4);
return decode_exec(s);
//#define src3R() do { *dest = BITS(i, 11, 7); } while (0)
}
// snpc: static next PC 指的是在内存中下一个地址位置上的指令
// dnpc: dynamic next PC 指的是程序运行时执行的下一条指令,由于存在指令的跳转,下一个执行的指令未必是内存中的下一个指令
到decode_exec()
函数, 它首先进行的是译码相关的操作. 译码的目的是得到指令的操作和操作对象
INSTPAT
(意思是instruction pattern)是一个宏(在nemu/include/cpu/decode.h
中定义), 它用于定义一条模式匹配规则. 其格式如下:
INSTPAT(模式字符串, 指令名称, 指令类型, 指令执行操作);
模式字符串
中只允许出现4种字符:
0
表示相应的位只能匹配0
1
表示相应的位只能匹配1
?
表示相应的位可以匹配0
或1
- 空格是分隔符, 只用于提升模式字符串的可读性, 不参与匹配
指令名称
在代码中仅当注释使用, 不参与宏展开; 指令类型
用于后续译码过程; 而指令执行操作
则是通过C代码来模拟指令执行的真正行为.
pattern_decode()
函数在nemu/include/cpu/decode.h
中定义, 它用于将模式字符串转换成3个整型变量:key, mask, shift
NEMU取指令的时候会把指令记录到s->isa.inst.val
中
如果取的客户程序指令与上述被转换后的3个整型变量:key, mask, shift 匹配,说明客户程序的这条指令对应相匹配的模式指令
如果匹配的话,使用INSTPAT_MATCH
宏,其中调用decode_operand
根据指令类型提取操作数、立即数等,并执行指令相应的运算(INSTPAT
宏的最后一个参数),最后跳过下面的pattern,因为已经匹配成功,不用再检查别的pattern0
decode_operand()
函数将会根据传入的指令类型type
来进行操作数的译码, 译码结果将记录到函数参数dest
, src1
, src2
和imm
中, 它们分别代表目的操作数, 两个源操作数和立即数.
译码阶段结束之后, 代码将会执行模式匹配规则中指定的指令执行操作
最后decode_exec
再做些收尾工作,比如将$0寄存器置0
函数开始时s->dnpc = s->snpc,指令周期结束后cpu.pc=s->dnpc,如果在指令执行过程中没有更改dnpc,那么cpu.pc就是s->snpc,如果更改了dnpc(jal指令),就会执行到具体的指令
#include "local-include/reg.h"
#include <cpu/cpu.h>
#include <cpu/ifetch.h>
#include <cpu/decode.h>
#define R(i) gpr(i)
#define Mr vaddr_read
#define Mw vaddr_write
enum {
TYPE_I, TYPE_U, TYPE_S, TYPE_J, TYPE_B, TYPE_R,
TYPE_N, // none
};
#define src1R() do { *src1 = R(rs1); } while (0)
#define src2R() do { *src2 = R(rs2); } while (0)
#define immI() do { *imm = SEXT(BITS(i, 31, 20), 12); } while(0)
#define immU() do { *imm = SEXT(BITS(i, 31, 12), 20) << 12; } while(0)
#define immS() do { *imm = (SEXT(BITS(i, 31, 25), 7) << 5) | BITS(i, 11, 7); } while(0)
#define immJ() do { *imm = (SEXT(BITS(i, 31, 31), 1) << 20) | BITS(i, 30, 21) << 1 \
| BITS(i, 20, 20) << 11 | BITS(i, 19, 12) << 12 ; } while(0)
#define immB() do { *imm = SEXT(BITS(i, 31, 31), 1) << 11 | ((SEXT(BITS(i, 7, 7), 1) << 63) >> 63) << 10 | ((SEXT(BITS(i, 30, 25), 6) << 58) >> 58) << 4 | ((SEXT(BITS(i, 11, 8), 4) << 60) >> 60); *imm = *imm << 1; } while (0)
/*在decode_exec函数中调用
首先从 s->isa.inst.val 中获取当前指令的值,存储在变量 i 中。
使用 BITS 宏从指令中提取出相应的字段值。例如,BITS(i, 19, 15) 表示从指令的第 19 位到第 15 位提取出一个字段值,存储在变量 rs1 中。
将 BITS(i, 11, 7) 的字段值赋给 *rd,即将目标操作数的寄存器编号存储在 rd 指针指向的位置。
根据指令的类型 type 进行不同的操作数解析:
如果 type 是 TYPE_I,则调用 src1R() 宏将源操作数1的值存储在 *src1 中,调用 immI() 宏将立即数的值存储在 *imm 中。
如果 type 是 TYPE_U,则调用 immU() 宏将立即数的值存储在 *imm 中。
如果 type 是 TYPE_S,则调用 src1R() 宏将源操作数1的值存储在 *src1 中,调用 src2R() 宏将源操作数2的值存储在 *src2 中,调用 immS() 宏将立即数的值存储在 *imm 中。
总体来说,这段代码根据指令的类型解析指令的操作数。根据不同的指令类型,从指令中提取出对应的字段值,并将其存储在相应的变量中,以便后续使用。*/
static void decode_operand(Decode *s, int *rd, word_t *src1, word_t *src2, word_t *imm, int type) {
uint32_t i = s->isa.inst.val;
int rs1 = BITS(i, 19, 15);
int rs2 = BITS(i, 24, 20);
*rd = BITS(i, 11, 7);
switch (type) {
case TYPE_I: src1R(); immI(); break; //框架代码定义了src1R()和src2R()两个辅助宏, 用于寄存器的读取结果记录到相应的操作数变量中
case TYPE_U: immU(); break;//immI等辅助宏, 用于从指令中抽取出立即数
case TYPE_S: src1R(); src2R(); immS(); break;
case TYPE_J: immJ(); break;
case TYPE_B: src1R(); src2R(); immB(); break;
case TYPE_R: src1R(); src2R(); break;
}
}
/*在函数isa_exec_once最后调用,函数isa_exec_once的返回值是decode_exec函数的返回值,用于指示指令执行的结果。具体的解码和执行过程在decode_exec函数中实现
在decode_exec()函数中,首先进行一些初始化操作。
然后,使用宏定义的方式定义了一些指令模式匹配规则。
接下来,根据当前指令的二进制编码,与模式字符串进行匹配。
如果匹配成功,根据指令的类型和操作数,执行相应的操作。
如果匹配失败,则继续匹配下一条指令的模式。
最后,将零寄存器的值重置为0,并返回0。*/
static int decode_exec(Decode *s) {
int rd = 0;
word_t src1 = 0, src2 = 0, imm = 0;//首先定义了一些局部变量 rd、src1、src2 和 imm,用于存储解码过程中的操作数值。
s->dnpc = s->snpc;//将 s->snpc 的值赋给 s->dnpc,即将下一条指令的地址保存到 dnpc 中。执行例如jal指令会再次更改dnpc的值。
//INSTPAT_INST(s) 宏定义将 s->isa.inst.val 作为宏展开的结果,用于获取指令的值。
#define INSTPAT_INST(s) ((s)->isa.inst.val)
//INSTPAT_MATCH(s, name, type, ...) 宏定义用于进行指令模式匹配和执行。该宏接受多个参数,
//其中 s 表示指向 Decode 结构体的指针,name 表示指令的名称,type 表示指令的类型,... 表示可变参数,用于指定执行的操作。
#define INSTPAT_MATCH(s, name, type, ... /* execute body */ ) { \
decode_operand(s, &rd, &src1, &src2, &imm, concat(TYPE_, type)); \
__VA_ARGS__ ; \
}
//__VA_ARGS__ 是在宏中表示可变参数的特殊标识符。它允许宏接受可变数量的参数,并在宏的展开中将这些参数传递给其他函数或进行其他操作。
//具体来说,当在宏的定义中使用 __VA_ARGS__ 时,它会在宏展开时替换为传递给宏的实际参数。这样,可以在宏的定义中将参数列表作为可变参数处理。
INSTPAT_START();//指示指令模式匹配的开始。INSTPAT(模式字符串, 指令名称, 指令类型, 指令执行操作);
INSTPAT("??????? ????? ????? 000 ????? 00100 11", li , I, R(rd) = src1 + imm); // 此处的dest是函数 decode_exec中定义的int 类型 dest
INSTPAT("??????? ????? ????? ??? ????? 01101 11", lui , U, R(rd) = imm); // 此处的dest是函数 decode_exec中定义的int 类型 dest
INSTPAT("??????? ????? ????? ??? ????? 00101 11", auipc , U, R(rd) = s -> pc + imm);
INSTPAT("??????? ????? ????? 010 ????? 00000 11", lw , I, R(rd) = Mr(src1 + imm, 4)); // 从内存相应位置读出并写入到寄存器中
INSTPAT("??????? ????? ????? 100 ????? 00000 11", lbu , I, R(rd) = Mr(src1 + imm, 1));
INSTPAT("??????? ????? ????? 000 ????? 00000 11", lb , I, R(rd) = SEXT(Mr(src1 + imm, 1), 16));
INSTPAT("??????? ????? ????? 001 ????? 00000 11", lh , I, R(rd) = SEXT(Mr(src1 + imm, 2), 16));
INSTPAT("??????? ????? ????? 101 ????? 00000 11", lhu , I, R(rd) = Mr(src1 + imm, 2));
INSTPAT("??????? ????? ????? 111 ????? 00100 11", andi , I, R(rd) = imm & src1);
INSTPAT("??????? ????? ????? 000 ????? 01000 11", sb , S, Mw(src1 + imm, 1, src2));
INSTPAT("??????? ????? ????? 010 ????? 01000 11", sw , S, Mw(src1 + imm, 4, src2)); // 向内存中写入
INSTPAT("??????? ????? ????? 001 ????? 01000 11", sh , S, Mw(src1 + imm, 2, src2));
INSTPAT("??????? ????? ????? ??? ????? 11011 11", jal , J, R(rd) = s -> pc + 4; s -> dnpc += imm -4;); // jal指令
INSTPAT("??????? ????? ????? 100 ????? 00100 11", xori , I, R(rd) = src1 ^ imm);
INSTPAT("??????? ????? ????? 110 ????? 00100 11", ori , I, R(rd) = src1 | imm);
INSTPAT("??????? ????? ????? 000 ????? 00100 11", addi , I, R(rd) = src1 + imm); // addi指令
// mv 指令是addi指令的一个语法糖,无需单独实现
// 手册中未发现li指令的描述,但查阅资料时对li指令的描述表示这也是addi指令的语法糖,R[rd] = R[rs1] + imm(符号扩展12位到32位) 其特殊之处是rs1总是0号寄存器,riscv体系中0号寄存器总是0,因此作用是加载立即数。
INSTPAT("0100000 ????? ????? 101 ????? 00100 11", srai , I, imm = BITS(imm, 4, 0); R(rd) = (SEXT(BITS(src1, 31, 31), 1) << (32 - imm)) | (src1 >> imm));
INSTPAT("0000000 ????? ????? 101 ????? 00100 11", srli , I, R(rd)= src1 >> imm);
INSTPAT("0000000 ????? ????? 001 ????? 00100 11", elli , I, R(rd)= src1 << imm);
INSTPAT("??????? ????? ????? 000 ????? 11001 11", ret , I, R(rd) = s -> pc + 4; s -> dnpc = (src1 + imm) & ~1); // jalr(ret)指令
INSTPAT("??????? ????? ????? 100 ????? 11000 11", blt , B, s -> dnpc += (int)src1 < (int)src2 ? imm - 4: 0);
INSTPAT("??????? ????? ????? 110 ????? 11000 11", bltu , B, s -> dnpc += (uint32_t)src1 < (uint32_t)src2 ? imm - 4: 0);
INSTPAT("??????? ????? ????? 101 ????? 11000 11", bge , B, s -> dnpc += (int)src1 >= (int)src2 ? imm - 4: 0);
/*
* 使用bge指令代替blez,ble指令仅仅将bge指令的操作数顺序改变,而blez只是将其中的一个操作数选择为0号寄存器(始终为0)
*/
INSTPAT("??????? ????? ????? 111 ????? 11000 11", bgeu , B, s -> dnpc += src1 >= src2 ? imm - 4: 0;);
INSTPAT("??????? ????? ????? 000 ????? 11000 11", beq , B, s -> dnpc += src1 == src2 ? imm - 4: 0;);
INSTPAT("??????? ????? ????? 001 ????? 11000 11", bne , B, s -> dnpc += src1 != src2 ? imm - 4: 0;);
INSTPAT("0100000 ????? ????? 101 ????? 01100 11", sra , R, R(rd) = (SEXT(BITS(src1, 31, 31), 1) << (32 - src2)) | (src1 >> src2));
INSTPAT("0000000 ????? ????? 101 ????? 01100 11", srl , R, R(rd) = src1 >> src2);
INSTPAT("0000001 ????? ????? 110 ????? 01100 11", rem , R, R(rd) = (int32_t)src1 % (int32_t)src2);
INSTPAT("0000001 ????? ????? 111 ????? 01100 11", remu , R, R(rd) = src1 % src2);
INSTPAT("0000001 ????? ????? 101 ????? 01100 11", divu , R, R(rd) = (uint32_t)src1 / (uint32_t)src2);
INSTPAT("0000001 ????? ????? 100 ????? 01100 11", div , R, R(rd) = (int32_t)src1 / (int32_t)src2);
INSTPAT("0000001 ????? ????? 000 ????? 01100 11", mul , R, R(rd) = src1 * src2);
INSTPAT("0000001 ????? ????? 001 ????? 01100 11", mulh , R, int32_t a = src1; int32_t b = src2; int64_t tmp = (int64_t)a * (int64_t)b; R(rd) = BITS(tmp, 63, 32));
INSTPAT("0000001 ????? ????? 011 ????? 01100 11", mulhu , R, uint64_t tmp = (uint64_t)src1 * (uint64_t)src2; R(rd) = BITS(tmp, 63, 32));
INSTPAT("0000000 ????? ????? 111 ????? 01100 11", and , R, R(rd) = src1 & src2);
INSTPAT("0000000 ????? ????? 001 ????? 01100 11", sll , R, R(rd) = src1 << src2);
INSTPAT("0000000 ????? ????? 000 ????? 01100 11", add , R, R(rd) = src1 + src2);
INSTPAT("0100000 ????? ????? 000 ????? 01100 11", sub , R, R(rd) = src1 - src2);
INSTPAT("0000000 ????? ????? 011 ????? 01100 11", sltu , R, R(rd) = (uint32_t)src1 < (uint32_t)src2 ? 1: 0;);
INSTPAT("??????? ????? ????? 011 ????? 00100 11", sltiu , I, R(rd) = (uint32_t)src1 < (uint32_t)imm ? 1: 0);
INSTPAT("??????? ????? ????? 010 ????? 00100 11", slti , I, R(rd) = (int32_t)src1 < (int32_t)imm ? 1: 0);
INSTPAT("0000000 ????? ????? 010 ????? 01100 11", slt , R, R(rd) = (int)src1 < (int)src2 ? 1: 0);
INSTPAT("0000000 ????? ????? 100 ????? 01100 11", xor , R, R(rd) = src1 ^ src2);
INSTPAT("0000000 ????? ????? 110 ????? 01100 11", or , R, R(rd) = src1 | src2);
INSTPAT("0000000 00001 00000 000 00000 11100 11", ebreak , N, NEMUTRAP(s->pc, R(10))); // R(10) is $a0
INSTPAT("??????? ????? ????? ??? ????? ????? ??", inv , N, INV(s->pc)); // 对所有模式都无法匹配的指令,判定为非法指令
INSTPAT_END();//指示指令模式匹配的结束
/*
在RISC-V架构中,通常将寄存器 $zero(编号为0)称为零寄存器,它的值始终为0。这是因为在RISC-V汇编语言中,$zero 寄存器是一个特殊的寄存器,不能被写入,任何对它的写入操作都会被忽略。
在执行指令序列后,将 $zero 寄存器的值设置为0是一种规范的做法,以确保 $zero 寄存器的值不会因为执行前面的指令而受到影响。这是一种编程约定,以便保持代码的一致性和可移植性。*/
R(0) = 0; // reset $zero to 0
return 0;
}
/*理解上面宏展开后的代码
{ const void ** __instpat_end = &&__instpat_end_;
do {
uint32_t key, mask, shift;
pattern_decode("??????? ????? ????? ??? ????? 01101 11", 38, &key, &mask, &shift);
if (((s->isa.inst.val >> shift) & mask) == key) {
{
decode_operand(s, &dest, &src1, &src2, &imm, TYPE_U);
R(dest) = imm;
}
goto *(__instpat_end);
}
} while (0);
// ...
__instpat_end_: ; }
在这段代码中,首先定义了一个局部变量__instpat_end,用于存储标签地址。
接着使用do { ... } while (0)包裹起来,形成一个循环体。
在循环体中,定义了三个局部变量key、mask和shift,用于存储模式解析的结果。
调用pattern_decode函数将模式字符串解析成key、mask和shift。
key:表示指令编码中关键位的值。在模式匹配中,我们希望通过比较指令的编码和模式字符串的编码来确定是否匹配。key记录了模式字符串中关键位的期望值,用于与指令编码进行比较。
mask:表示指令编码中需要匹配的位的掩码。模式匹配时,我们通常只关注指令编码中特定位置的比特位,其他位置的值可以是任意的。mask用于标识哪些位是需要进行匹配的,1表示需要匹配的位,0表示可以任意值的位。
shift:表示指令编码中关键位距离最低位的偏移量。由于指令编码的不同位可能表示不同的意义,我们需要知道关键位相对于最低位的位置。shift的值表示关键位相对于最低位的偏移量,可以用于从指令编码中提取关键位的值。
通过解析模式字符串并提取出key、mask和shift这些信息,我们就可以将模式匹配和译码的过程更加抽象化和灵活化。通过比较指令编码的关键位与key的值,使用掩码mask确定哪些位需要匹配,然后根据shift的值从指令编码中提取出关键位的值,我们可以进行指令的匹配和译码操作。这种模式匹配的方式可以更加清晰地描述指令的编码格式,并且具有较好的可扩展性和灵活性,适用于不同的指令集架构。
接下来,通过位运算判断当前指令的部分字段是否匹配模式字符串的解析结果。
如果匹配成功,则执行指令的操作,并跳转到标签__instpat_end,结束模式匹配。
最后,通过标签__instpat_end_来定义一个空语句。
总结起来,模式匹配机制通过解析模式字符串,将其转换为关键字、掩码和位移量,然后根据位运算进行指令的匹配。当指令匹配成功时,执行相应的操作,并跳转到标签结束模式匹配。这种机制使得指令的译码和执行操作可以通过模式匹配的方式进行灵活的定义和扩展。
*/
/*在exec_once中调用
这段代码是一个函数isa_exec_once的实现,它接受一个指向Decode结构体的指针s作为参数。函数的目的是执行一条指令,其中包括指令的取指和解码过程。
代码的执行流程如下:
inst_fetch(&s->snpc, 4):调用inst_fetch函数,根据s->snpc(static next pc)获取指令的值。函数的第二个参数4表示要取4个数组单位的指令。
s->isa.inst.val = ...:将获取到的指令值存储到s->isa.inst.val中,即存储到Decode结构体的isa成员的inst联合体中的val成员。
decode_exec(s):调用decode_exec函数,将指令解码并执行。该函数会根据s中存储的指令信息执行相应的操作,并返回执行的结果。
函数isa_exec_once的返回值是decode_exec函数的返回值,用于指示指令执行的结果。具体的解码和执行过程在decode_exec函数中实现,*/
int isa_exec_once(Decode *s) {
s->isa.inst.val = inst_fetch(&s->snpc, 4);
return decode_exec(s);
}
运行第一个客户程序
反汇编结果(am-kernels/tests/cpu-tests/build/dummy-$ISA-nemu.txt
)中: 你只需实现那些目前还没实现的指令就可以了. 框架代码引入的模式匹配规则, 对在NEMU中实现客户指令提供了很大的便利, 为了实现一条新指令, 你只需要在nemu/src/isa/$ISA/inst.c
中添加正确的模式匹配规则即可.
查看反汇编结果,看看需要实现哪些指令,以下链接是riscv中文手册
http://riscvbook.com/chinese/RISC-V-Reader-Chinese-v2p1.pdf
80000000 <_start>:
80000000: 00000413 li s0,0
80000004: 00009117 auipc sp,0x9
80000008: ffc10113 addi sp,sp,-4 # 80009000 <_end>
8000000c: 00c000ef jal ra,80000018 <_trm_init>
80000010 <main>:
80000010: 00000513 li a0,0
80000014: 00008067 ret
80000018 <_trm_init>:
80000018: 80000537 lui a0,0x80000
8000001c: ff010113 addi sp,sp,-16
80000020: 03850513 addi a0,a0,56 # 80000038 <_end+0xffff7038>
80000024: 00112623 sw ra,12(sp)
80000028: fe9ff0ef jal ra,80000010 <main>
8000002c: 00050513 mv a0,a0
80000030: 00100073 ebreak
80000034: 0000006f j 80000034 <_trm_init+0x1c>
查手册把指令添加到模式匹配那里就行了
NEMU选择了ebreak
指令来充当nemu_trap
程序、运行时环境和AM
要运行在NEMU的客户程序,必须通过交叉编译在GNU/Linux下根据AM的运行时环境编译出能够在$ISA-nemu
这个新环境中运行的可执行文件. 为了不让链接器ld使用默认的方式链接, 我们还需要提供描述$ISA-nemu
的运行时环境的链接脚本.
- gcc将
$ISA-nemu
的AM实现源文件编译成目标文件, 然后通过ar将这些目标文件作为一个库, 打包成一个归档文件abstract-machine/am/build/am-$ISA-nemu.a
- gcc把应用程序源文件(如
am-kernels/tests/cpu-tests/tests/dummy.c
)编译成目标文件 - 通过gcc和ar把程序依赖的运行库(如
abstract-machine/klib/
)也编译并打包成归档文件 - 根据Makefile文件
abstract-machine/scripts/$ISA-nemu.mk
中的指示, 让ld根据链接脚本abstract-machine/scripts/linker.ld
, 将上述目标文件和归档文件链接成可执行文件
归档文件(archive file)是包含一个或多个目标文件(object file)的文件,通常用于将多个目标文件打包在一起以便于链接。不同的节来自不同的目标文件,这些目标文件通常是程序的各个部分的编译结果,例如源代码中的不同文件会被编译成不同的目标文件。
在链接的过程中,链接器会根据链接脚本中的定义将不同的目标文件中的节组合到输出文件的相应节中。归档文件中的目标文件的不同节会被链接到不同的输出文件的节中。链接脚本的目的是定义这些映射关系,以确保最终的可执行文件包含了所有需要的代码和数据。
查看linker.ld代码
-
.text
节:这个节通常包含了程序的可执行代码。在编译过程中,编译器会将源代码文件(C、C++ 等)转化为汇编代码,然后将汇编代码汇编成目标文件。在目标文件中,可执行代码部分会被分配到.text
节。这些代码包括程序的函数和程序的主函数。 -
.data
节:这个节通常包含了程序的已初始化数据。在源代码中,程序员会定义全局变量,这些变量在目标文件中会被分配到.data
节。这些变量的初值会被保存在可执行文件中,以便程序在运行时使用。
这些节的定义是编译器在将源代码转化为目标文件时生成的
ENTRY(_start) //这是指定程序入口点的部分。它告诉链接器程序应该从 _start 标签处开始执行。
PHDRS { text PT_LOAD; data PT_LOAD; } //这定义了程序头 (Program Headers) 的信息,指定了不同段的属性。text 和 data 段都被标记为 PT_LOAD,表示它们包含可加载的代码和数据。
//这部分定义了链接器如何组织不同的段。
SECTIONS {
/* _pmem_start and _entry_offset are defined in LDFLAGS */
. = _pmem_start + _entry_offset;//这一行将链接器的当前位置设置为 _pmem_start 和 _entry_offset 的和。可执行程序重定位后的节从0x80000000开始
.text : {
//首先是.text节, 其中又以abstract-machine/am/src/$ISA/nemu/start.S中自定义的entry节开始, 然后接下来是其它目标文件的.text节. 这样, 可执行程序起始处总是放置start.S的代码, 而不是其它代码, 保证客户程序总能从start.S开始正确执行
*(entry) //将 entry 部分的内容(程序的入口代码)复制到 .text 节中。
*(.text*) //将所有以 .text 开头的小节内容(通常是程序的代码)复制到 .text 节中。
} : text
etext = .;
_etext = .; //etext = .; 和 _etext = .;:这两行定义了 etext 和 _etext 标签,它们表示 .text 节的末尾。
.rodata : { //它定义了只读数据段
*(.rodata*)
}
.data : {
*(.data)
} : data
edata = .;
_data = .;
.bss : {
_bss_start = .;
*(.bss*)
*(.sbss*)
*(.scommon)
}
_stack_top = ALIGN(0x1000);
. = _stack_top + 0x8000;
_stack_pointer = .;
end = .;
_end = .;
_heap_start = ALIGN(0x1000);
}
链接脚本最开始的_start位于abstract-machine/am/src/$ISA/nemu/start.S
//这段代码的目的是初始化程序的入口点 _start,将 s0 寄存器设置为零,初始化程序的栈指针 sp,然后跳转到初始化函数 _trm_init 来执行 Abstract Machine 的初始化操作
.section entry, "ax"
.globl _start
.type _start, @function
_start:
mv s0, zero
la sp, _stack_pointer
jal _trm_init
jal跳转到_trm_init函数,在_trm_init()
中调用main()
函数执行程序的主体功能,main()
函数返回后, 调用halt()(ebreak)
结束运行
整个项目是如何编译的以下是am的makefile(注释详细)
# Makefile for AbstractMachine Kernels and Libraries
### *Get a more readable version of this Makefile* by `make html` (requires python-markdown)
#这个 Makefile 片段定义了一个名为 "html" 的规则,用于生成 HTML 版本的当前 Makefile
html:
cat Makefile | sed 's/^\([^#]\)/ \1/g' | markdown_py > Makefile.html
.PHONY: html
#Makefile 中的变量可以从多个地方获取值:\
Makefile 内部设置: 可能在 Makefile 的其他地方设置了 $(AM_HOME) 变量,但你在当前查看的部分没有找到。\
外部设置: $(AM_HOME) 可能是在调用 Make 命令时通过命令行参数设置的,例如:\
make AM_HOME=/path/to/abstractmachine\
这样会将 $(AM_HOME) 变量设置为 /path/to/abstractmachine。\
Shell 环境变量: Make 可以访问系统环境变量。如果 $(AM_HOME) 没有在 Makefile 中显式设置,它可能会从操作系统的环境变量中获取值。\
## 1. Basic Setup and Checks 基本设置和检查
### Default to create a bare-metal kernel image 默认情况下,如果没有指定构建目标,将创建一个裸机内核映像
#这是一个条件语句,用于检查是否有指定的构建目标。$(MAKECMDGOALS) 是一个特殊的变量,它包含了用户在命令行中指定的构建目标。
ifeq ($(MAKECMDGOALS),)
#如果上述条件为真,即没有指定构建目标,那么这一行会将变量 MAKECMDGOALS 设置为 "image"。
#一般来说,MAKECMDGOALS 用于获取用户在命令行上指定的目标,而 .DEFAULT_GOAL 用于设置默认的构建目标。
#设置它们都是为了确保在没有明确指定目标时,有一个默认的目标可以被构建。
MAKECMDGOALS = image
.DEFAULT_GOAL = image
endif
### Override checks when `make clean/clean-all/html`
#假设用户在命令行中运行以下命令之一:
#make clean: 清理项目,删除临时文件等。
#make clean-all: 清理项目,删除所有生成的文件。
#make html: 生成 HTML 版本的 Makefile。
#那么这段代码将会执行相应的操作。
ifeq ($(findstring $(MAKECMDGOALS),clean|clean-all|html),)
### Print build info message
#$(info ...):$(info) 是一个 Makefile 内置函数,用于输出信息消息到标准输出(通常是终端)。
# Building $(NAME)-$(MAKECMDGOALS) [$(ARCH)]:这是要输出的信息消息的文本内容。它包含了以下变量:
#$(NAME):这是一个在 Makefile 中定义的变量,通常用于表示构建的项目名称。
#$(MAKECMDGOALS):这是一个包含用户在命令行上指定的构建目标的变量。
#$(ARCH):这是另一个在 Makefile 中定义的变量,通常表示目标体系结构或架构。$(ARCH) 的值将是 "riscv32-nemu
$(info # Building $(NAME)-$(MAKECMDGOALS) [$(ARCH)])
### Check: environment variable `$AM_HOME` looks sane
#$(wildcard ...) 是一个 Makefile 函数,用于查找文件系统中的文件。在这里,它用于检查 $(AM_HOME)/am/include/am.h 文件是否存在。
#如果 am.h 文件不存在,条件为真,执行下面的操作。
ifeq ($(wildcard $(AM_HOME)/am/include/am.h),)
$(error $$AM_HOME must be an AbstractMachine repo)
endif
### Check: environment variable `$ARCH` must be in the supported list
### $(shell ls $(AM_HOME)/scripts/*.mk) 会列出 $(AM_HOME)/scripts 目录下的所有 .mk 文件,并返回文件列表。
### $(notdir ...) 会提取这些文件列表中的文件名,去掉路径部分,只留下文件名。
### $(basename ...) 会去掉文件名中的扩展名,只留下文件的基本名称。
### $(filter $(ARCHS), $(ARCH)) 是一个条件判断,用于检查 $(ARCH) 是否在 $(ARCHS) 列表中。
### $(filter ...) 函数返回列表 $(ARCHS) 中包含的值,如果 $(ARCH) 不在列表中,这个函数会返回一个空字符串。
### ifeq 条件语句用于判断 $(filter ...) 的返回值是否为空。如果为空,表示用户指定的 $(ARCH) 不在支持的架构列表中。
ARCHS = $(basename $(notdir $(shell ls $(AM_HOME)/scripts/*.mk)))
ifeq ($(filter $(ARCHS), $(ARCH)), )
$(error Expected $$ARCH in {$(ARCHS)}, Got "$(ARCH)")
endif
### Extract instruction set architecture (`ISA`) and platform from `$ARCH`. Example: `ARCH=x86_64-qemu -> ISA=x86_64; PLATFORM=qemu`
ARCH_SPLIT = $(subst -, ,$(ARCH))### 用空格替换 ARCH 变量中的短横线("-")
ISA = $(word 1,$(ARCH_SPLIT))### riscv32
PLATFORM = $(word 2,$(ARCH_SPLIT))### nemu
### Check if there is something to build SRCS源代码文件的列表
ifeq ($(flavor SRCS), undefined)
$(error Nothing to build)
endif
### Checks end here
endif
## 2. General Compilation Targets 一般编译目标
### Create the destination directory (`build/$ARCH`)
WORK_DIR = $(shell pwd) ### 当前 Makefile 的位置。
DST_DIR = $(WORK_DIR)/build/$(ARCH)
$(shell mkdir -p $(DST_DIR)) ### -p 选项确保如果目录已经存在,就不会触发错误
### Compilation targets (a binary image or archive) 编译目标(二进制映像或存档)包括二进制可执行文件(IMAGE)和静态库文件(ARCHIVE)
IMAGE_REL = build/$(NAME)-$(ARCH)
IMAGE = $(abspath $(IMAGE_REL)) ### $(abspath ...) 函数,将 IMAGE_REL 变量的相对路径转化为绝对路径。它表示最终的可执行文件的完整路径。
ARCHIVE = $(WORK_DIR)/build/$(NAME)-$(ARCH).a
### Collect the files to be linked: object files (`.o`) and libraries (`.a`) 收集要链接的文件,包括目标文件(.o)和静态库文件(.a)
OBJS = $(addprefix $(DST_DIR)/, $(addsuffix .o, $(basename $(SRCS)))) ### addprefix 函数,将 DST_DIR 目录下的目标文件的完整路径列出。它是所有目标文件(.o)的列表。
LIBS := $(sort $(LIBS) am klib) # lazy evaluation ("=") causes infinite recursions 这个变量是用于指定需要链接的库文件的列表,包括 am 和 klib,
LINKAGE = $(OBJS) \
$(addsuffix -$(ARCH).a, $(join \
$(addsuffix /build/, $(addprefix $(AM_HOME)/, $(LIBS))), \
$(LIBS) )) ###LINKAGE:这个变量定义了最终链接时的文件列表,包括目标文件 $(OBJS) 和静态库文件
## 3. General Compilation Flags
### (Cross) compilers, e.g., mips-linux-gnu-g++
AS = $(CROSS_COMPILE)gcc ### 汇编器
CC = $(CROSS_COMPILE)gcc
CXX = $(CROSS_COMPILE)g++
LD = $(CROSS_COMPILE)ld
OBJDUMP = $(CROSS_COMPILE)objdump # 用于反汇编的工具
OBJCOPY = $(CROSS_COMPILE)objcopy # 二进制文件转换的工具
READELF = $(CROSS_COMPILE)readelf # 读取 ELF 文件信息的工具
### Compilation flags ,addsuffix 和 addprefix 是 GNU Make 中的两个常用函数,用于在字符串列表的每个元素前面或后面添加相同的前缀或后缀。
INC_PATH += $(WORK_DIR)/include $(addsuffix /include/, $(addprefix $(AM_HOME)/, $(LIBS)))
INCFLAGS += $(addprefix -I, $(INC_PATH)) #这一行使用 -I 标志将 INC_PATH 中的路径前缀添加到 INCFLAGS 中。这意味着编译器在查找头文件时将搜索这些路径。 -I 标志告诉编译器在这些路径中查找头文件,以便在编译源代码时正确引用依赖的库和头文件。
#CFLAGS:C 编译器选项,包括了 -O2(启用优化级别 2),-MMD(生成依赖关系文件),-Wall(启用所有警告),-Werror(将警告视为错误),$(INCFLAGS)(包含额外的头文件路径),和一系列预处理宏定义,例如 __ISA__、__ARCH__ 和 __PLATFORM__。这些宏定义将用于源代码中的条件编译和控制编译过程。
#CXXFLAGS:C++ 编译器选项,继承自 CFLAGS,但还包括了 -ffreestanding(告诉编译器生成无操作系统环境的代码),-fno-rtti(禁用运行时类型信息),和 -fno-exceptions(禁用 C++ 异常处理)等选项。
#ASFLAGS:汇编器选项,包括了 -MMD(生成依赖关系文件)和 $(INCFLAGS)(包含额外的头文件路径)。
#LDFLAGS:链接器选项,包括了 -z noexecstack(禁止在堆栈上执行代码)。
CFLAGS += -O2 -MMD -Wall -Werror $(INCFLAGS) \
-D__ISA__=\"$(ISA)\" -D__ISA_$(shell echo $(ISA) | tr a-z A-Z)__ \
-D__ARCH__=$(ARCH) -D__ARCH_$(shell echo $(ARCH) | tr a-z A-Z | tr - _) \
-D__PLATFORM__=$(PLATFORM) -D__PLATFORM_$(shell echo $(PLATFORM) | tr a-z A-Z | tr - _) \
-DARCH_H=\"arch/$(ARCH).h\" \
-fno-asynchronous-unwind-tables -fno-builtin -fno-stack-protector \
-Wno-main -U_FORTIFY_SOURCE
CXXFLAGS += $(CFLAGS) -ffreestanding -fno-rtti -fno-exceptions
ASFLAGS += -MMD $(INCFLAGS)
LDFLAGS += -z noexecstack
## 4. Arch-Specific Configurations
### Paste in arch-specific configurations (e.g., from `scripts/x86_64-qemu.mk`)
-include $(AM_HOME)/scripts/$(ARCH).mk
### Fall back to native gcc/binutils if there is no cross compiler
ifeq ($(wildcard $(shell which $(CC))),)
$(info # $(CC) not found; fall back to default gcc and binutils)
CROSS_COMPILE :=
endif
## 5. Compilation Rules
### Rule (compile): a single `.c` -> `.o` (gcc)
$(DST_DIR)/%.o: %.c
@mkdir -p $(dir $@) && echo + CC $<
@$(CC) -std=gnu11 $(CFLAGS) -c -o $@ $(realpath $<)
### Rule (compile): a single `.cc` -> `.o` (g++)
$(DST_DIR)/%.o: %.cc
@mkdir -p $(dir $@) && echo + CXX $<
@$(CXX) -std=c++17 $(CXXFLAGS) -c -o $@ $(realpath $<)
### Rule (compile): a single `.cpp` -> `.o` (g++)
$(DST_DIR)/%.o: %.cpp
@mkdir -p $(dir $@) && echo + CXX $<
@$(CXX) -std=c++17 $(CXXFLAGS) -c -o $@ $(realpath $<)
### Rule (compile): a single `.S` -> `.o` (gcc, which preprocesses and calls as)
$(DST_DIR)/%.o: %.S
@mkdir -p $(dir $@) && echo + AS $<
@$(AS) $(ASFLAGS) -c -o $@ $(realpath $<)
### Rule (recursive make): build a dependent library (am, klib, ...)
$(LIBS): %:
@$(MAKE) -s -C $(AM_HOME)/$* archive
### Rule (link): objects (`*.o`) and libraries (`*.a`) -> `IMAGE.elf`, the final ELF binary to be packed into image (ld)
$(IMAGE).elf: $(OBJS) am $(LIBS)
@echo + LD "->" $(IMAGE_REL).elf
@$(LD) $(LDFLAGS) -o $(IMAGE).elf --start-group $(LINKAGE) --end-group
### Rule (archive): objects (`*.o`) -> `ARCHIVE.a` (ar)
$(ARCHIVE): $(OBJS)
@echo + AR "->" $(shell realpath $@ --relative-to .)
@ar rcs $(ARCHIVE) $(OBJS)
### Rule (`#include` dependencies): paste in `.d` files generated by gcc on `-MMD`
-include $(addprefix $(DST_DIR)/, $(addsuffix .d, $(basename $(SRCS))))
## 6. Miscellaneous
### Build order control
image: image-dep #可执行文件
archive: $(ARCHIVE) #归档文件
image-dep: $(OBJS) am $(LIBS)
@echo \# Creating image [$(ARCH)]
.PHONY: image image-dep archive run $(LIBS)
### Clean a single project (remove `build/`)
clean:
rm -rf Makefile.html $(WORK_DIR)/build/
.PHONY: clean
### Clean all sub-projects within depth 2 (and ignore errors)
CLEAN_ALL = $(dir $(shell find . -mindepth 2 -name Makefile))
clean-all: $(CLEAN_ALL) clean
$(CLEAN_ALL):
-@$(MAKE) -s -C $@ clean
.PHONY: clean-all $(CLEAN_ALL)
通过批处理模式运行NEMU
看nemu的代码,进入sdb循环前有一个这个is_batch_mode的判断,如果参数为true,就会自动执行键入c的cmd_c函数,且不会进入sdb循环,那么在之前就把is_batch_mode=true就可以了,nemu提供了sdb_set_batch_mode()函数来做这个事,在进入sdb之前调用该函数就行
- AM:架构相关的运行时环境,不同架构的AM API实现,目前我们只需要关注NEMU相关得内容即可。
am.h
中列出了AM中的所有API。 - klib:一些架构无关的库函数,方便应用程序开发,kilb是运行在AM上的,所以可以在native测试(native=GUN/Linux运行时环境加它自己的AM),我们这里是要让kilb可以成功运行在(NEMU加它自己的AM)。
kilb/src/string.c
//计算一个字符串的长度
size_t strlen(const char *s) {
if (s == NULL) {
return 0;
}
size_t n = 0;
while(s[n] != '\0') {
++n;
}
return n;
}
//将一个字符串复制到另一个字符串中
char *strcpy(char *dst, const char *src) {
if (src == NULL || dst == NULL) { // 没有所指,直接返回dst
return dst;
}
// 当成指向字符数组处理,所以即使没有空字符,导致内存访问越界,或修改了其他有用的数据也不管,因为这是函数调用者所需要保证的,下面一些string函数都是这样对带非字符串数组
char *res = dst;
do {
*dst = *src;
dst++;
src++;
} while(*src != '\0');
return res;
}
//将源字符串的一部分复制到目标字符串中
char *strncpy(char *dst, const char *src, size_t n) {
size_t i;
for(i = 0; src[i] != '\0'; i++){
dst[i] = src[i];
}
dst[i] = '\0';
return dst;
}
//用于将一个字符串追加到另一个字符串的末尾
char *strcat(char *dst, const char *src) {
size_t i = 0;
while(dst[i] != '\0'){
i++;
}
strcpy(dst + i, src);
return dst;
}
//用于比较两个字符串的内容
int strcmp(const char *s1, const char *s2) {
size_t i = 0;
while(s1[i] != '\0' && s2[i] != '\0')
{
if(s1[i] > s2[i])
return 1;
if(s1[i] < s2[i])
return -1;
i++;
}
if(s1[i] != '\0' && s2[i] == '\0')
return 1;
if(s1[i] == '\0' && s2[i] != '\0')
return -1;
return 0;
}
//比较两个字符串的前几个字符的内容
int strncmp(const char *s1, const char *s2, size_t n) {
while(n--)
{
if(*s1 > *s2)
return 1;
if(*s1 < *s2)
return -1;
s1++;
s2++;
}
return 0;
}
//将指定的内存区域的每个字节都设置为特定的值
void *memset(void *s, int c, size_t n) {
char *ch = (char *) s;
while(n-- > 0)
*ch++ = c;
return s;
}
//用于在内存中移动一块数据。与 memcpy 函数不同,memmove 能够处理源内存区域与目标内存区域重叠的情况,以确保数据正确移动
void *memmove(void *dst, const void *src, size_t n) {
if(dst < src)
{
char *d = (char *) dst;
char *s = (char *) src;
while(n--)
{
*d = *s;
d++;
s++;
}
}
else
{
char *d = (char *) dst + n - 1;
char *s = (char *) src + n - 1;
while(n--)
{
*d = *s;
d--;
s--;
}
}
return dst;
}
//将一个内存区域的内容复制到另一个内存区域
void *memcpy(void *out, const void *in, size_t n) {
char *d = (char *) out;
char *s = (char *) in;
while(n--)
{
*d = *s;
d++;
s++;
}
return out;
}
//用于比较两个内存区域的内容
int memcmp(const void *s1, const void *s2, size_t n) {
char * S1 = (char *)s1;
char * S2 = (char *)s2;
while(n--)
{
if(*S1 > *S2)
return 1;
if(*S1 < *S2)
return -1;
S1++;
S2++;
}
return 0;
}
#endif
输入输出
串口
串口初始化的时候会根据 I/O 编址方式注册端口或者分配空间,对应的回调函数则调用了 serial_putc,根据 CONFIG_TARGET_AM 调用 putch 或 putc
putch -> outb -> serial_io_handler -> serial_putc -> putc,实际上最终还是调用了库函数
实现printf
static char sprint_buf[1024];
/*可变函数在内部实现的过程中是从右向左压入堆栈,从而保证了可变参数的第一个参数始终位于栈顶*/
int printf(const char *fmt, ...)//可以有一个或多个固定参数
{
va_list args; //用于存放参数列表的数据结构
int n;
/*根据最后一个fmt来初始化参数列表,至于为什么是最后一个参数,是与va_start有关。*/
va_start(args, fmt);
n = vsprintf(sprint_buf, fmt, args);
va_end(args);//执行清理参数列表的工作
putstr(sprint_buf);
return n;
}
时钟
i8253计时器初始化时会分别注册0x48
处长度为8个字节的端口, 以及0xa0000048
处长度为8字节的MMIO空间, 它们都会映射到两个32位的RTC寄存器. CPU可以访问这两个寄存器来获得用64位表示的当前时间.这里见nemu/src/device/timer.c
, CPU访问rtc_port_base获取时间,相关MMIO空间CONFIG_RTC_MMIO (0xa0000048)会被映射到rtc_port_base
static void rtc_io_handler(uint32_t offset, int len, bool is_write) {
assert(offset == 0 || offset == 4);
if (!is_write && offset == 4) {
uint64_t us = get_time();
rtc_port_base[0] = (uint32_t)us;
rtc_port_base[1] = us >> 32;
}
}
这里的us通过get_time()获取,get_time()又调用库函数来获取时间,然后把获取的时间返回rtc_io_handler(),并复制给rtc寄存器,也就是MMIO空间
uint64_t get_time() {
if (boot_time == 0) boot_time = get_time_internal();
uint64_t now = get_time_internal();
return now - boot_time;
}
在AM的测试用例中是通过 io_read(AM_TIMER_UPTIME)获取时间
展开 io_read
:
#define io_read(reg) \
({ reg##_T __io_param; \
ioe_read(reg, &__io_param); \
__io_param; })
就会调用到ioe_read()
AM_TIMER_UPTIME_T __io_param;
ioe_read(AM_TIMER_UPTIME, &__io_param);
__io_param;
可以看到ioe_read实际调用了__am_timer_uptime(&__io_param);也就是我们要实现的
typedef void (*handler_t)(void *buf);
static void *lut[128] = {
...
[AM_TIMER_UPTIME] = __am_timer_uptime,
...
}
void ioe_read (int reg, void *buf) { ((handler_t)lut[reg])(buf); }
__am_timer_uptime
的实现通过向 uptime->us 赋值的方式更新抽象寄存器中保存的时间,然后把抽象寄存器中保存的时间返回给io_read()
void __am_timer_init() {
outl(RTC_ADDR, 0);
outl(RTC_ADDR + 4, 0);
}
void __am_timer_uptime(AM_TIMER_UPTIME_T *uptime) {
uptime->us = inl(RTC_ADDR + 4);
uptime->us <<= 32;
uptime->us += inl(RTC_ADDR);
}
RTC_ADDR
定义在 abstract-machine/am/src/platform/nemu/include/nemu.h
:
#if defined(__ARCH_X86_NEMU)
# define DEVICE_BASE 0x0
#else
# define DEVICE_BASE 0xa0000000
#endif
#define SERIAL_PORT (DEVICE_BASE + 0x00003f8)
#define RTC_ADDR (DEVICE_BASE + 0x0000048)
与宏 CONFIG_RTC_MMIO 相对应,也就是RTC时钟在初始化的使用通过回调函数rtc_io_handler()把时间放在了RTC寄存器 ,即CONFIG_RTC_MMIO,然后__am_timer_uptime就会访问这个地址来获取时间赋值给抽象寄存器,AM程序调用的io_read(),读取抽象寄存器中保存的时间然后打印输出
设备访问的踪迹 - dtrace
测试用例编译成二进制指令放在内存中,在NEMU取指令的时候会调用paddr_read()
和paddr_write()访问内存,这两个函数会
判断地址addr
落在物理内存空间还是设备空间, 若落在设备内存空间,就通过map_read()
和map_write()
来访问相应的设备.定义了宏后,只要修改 map.c 即可:
word_t map_read(paddr_t addr, int len, IOMap *map) {
assert(len >= 1 && len <= 8);
check_bound(map, addr);
paddr_t offset = addr - map->low;
invoke_callback(map->callback, offset, len, false); // prepare data to read
word_t ret = host_read(map->space + offset, len);
IFDEF(CONFIG_DTRACE, Log("address = " FMT_PADDR " read " FMT_PADDR " at device = %s", addr, ret, map->name));
return ret;
}
void map_write(paddr_t addr, int len, word_t data, IOMap *map) {
assert(len >= 1 && len <= 8);
check_bound(map, addr);
paddr_t offset = addr - map->low;
host_write(map->space + offset, len, data);
invoke_callback(map->callback, offset, len, true);
IFDEF(CONFIG_DTRACE, Log("address = " FMT_PADDR " write " FMT_PADDR " at device = %s", addr, data, map->name));
}
键盘
keydown
为 true
时表示按下按键,否则表示释放按键。keycode
为按键的断码,没有按键时,keycode
为 AM_KEY_NONE
。
当按下一个键的时候,键盘将会发送该键的通码 (make code)
当释放一个键的时候,键盘将会发送该键的断码 (break code)
KBD_ADDR 中同时编码了通码和断码 ,若最高位为 1,则就是断码,若最高位为 0,则是通码
void __am_input_keybrd(AM_INPUT_KEYBRD_T *kbd) {
int k = AM_KEY_NONE;
k = inl(KBD_ADDR);
kbd->keydown = (k & KEYDOWN_MASK ? true : false);
kbd->keycode = k & ~KEYDOWN_MASK;
}
VGA
__am_gpu_config
用于读取VGA的一些参数,比如屏幕大小等,通过查看nemu框架vga.c,可以知道VGACTL_ADDR
处的4个字节保存屏幕的宽高,因此代码如下
void __am_gpu_config(AM_GPU_CONFIG_T *cfg) {
uint32_t screen_wh = inl(VGACTL_ADDR);
uint32_t h = screen_wh & 0xffff;
uint32_t w = screen_wh >> 16;
*cfg = (AM_GPU_CONFIG_T) {
.present = true, .has_accel = false,
.width = w, .height = h,
.vmemsz = 0
};
}
而负责绘图的__am_gpu_fbdraw
已经实现了同步屏幕的功能,也就是外界调用io_write(AM_GPU_FBDRAW, ... ,true)
时(最后一个参数是sync),用outb输出到抽象寄存器即可。而根据文档提示我们需要实现的是nemu的框架部分,代码也有TODO注释帮助实现:
void vga_update_screen() {
// TODO: call `update_screen()` when the sync register is non-zero,
// then zero out the sync register
uint32_t sync = vgactl_port_base[1];
if (sync) {
update_screen();
vgactl_port_base[1] = 0;
}
}
abstract-machine/am/src/platform/nemu/ioe/gpu.c
void __am_gpu_fbdraw(AM_GPU_FBDRAW_T *ctl) {
int x = ctl->x, y = ctl->y, w = ctl->w, h = ctl->h;
if (!ctl->sync && (w == 0 || h == 0)) return;
uint32_t *pixels = ctl->pixels;
uint32_t *fb = (uint32_t *)(uintptr_t)FB_ADDR;
uint32_t screen_w = inl(VGACTL_ADDR) >> 16;
for (int i = y; i < y+h; i++) {
for (int j = x; j < x+w; j++) {
fb[screen_w*i+j] = pixels[w*(i-y)+(j-x)];
}
}
if (ctl->sync) {
outl(SYNC_ADDR, 1);
}
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)