PA1学习记录
计算机可以没有寄存器吗?
CHATGPT:
在现代计算机体系结构中,寄存器是一个至关重要的组成部分,因为它们存储CPU中需要处理的数据和指令。因此,在现代计算机中,寄存器是必不可少的。
寄存器是CPU内部的存储单元,它们通常比其他存储器(如内存)更快速。寄存器存储CPU正在处理的数据和指令,这些数据和指令用于执行算术、逻辑和控制操作。CPU需要快速地读取和写入这些寄存器中的数据,以便能够快速地处理指令和数据。
虽然理论上,计算机可以没有寄存器,但是在现代计算机中,它们是必不可少的。没有寄存器,CPU将需要不断地从内存中读取和写入数据,这将严重降低计算机的性能。因此,寄存器在现代计算机体系结构中扮演着至关重要的角色。
RTFSC(阅读源码)
Nemu的主要组成:monitor, CPU, memory, 设备
CPU:通过读取寄存器的值,进行运算。
memory:存储信息
monitor:监控客户计算机的运行状态
设备:外设,方便使用
nemu项目的构成:
configs : 预先提供的配置文件
include : 全局使用的头文件
Kconfig : 配置信息管理的规则
Makefile : Makefile构建脚本
scripts : Makefile构建脚本
src : .c 源文件
tools : 工具 如:生成表达式
一个程序是从哪里开始执行的
C语言是从main函数开始执行的
选择RISV 64
通过更改 nemu/tools/kconfig 文件实现更改ISA
为什么全部都是函数?
在使用函数调用的情况下,init_monitor()函数将整个监视器的初始化分解成多个小任务,每个小任务都是由一个独立的函数完成的。这样的设计带来了以下几个好处:
代码复用性:如果需要在其他地方使用这些小任务中的某一个或多个,只需要单独调用对应的函数即可,而不需要复制粘贴函数体。这样的代码重用可以提高代码的可维护性和可扩展性。
可读性:将任务拆分成多个小函数,可以使代码更加模块化,易于理解和调试。
可维护性:当需要修改某个小任务时,只需要修改对应的函数代码,而不需要修改init_monitor()函数的代码。这样的设计可以减少代码修改带来的风险,从而提高代码的可维护性。
可测试性:每个小任务可以单独进行测试,这样可以更容易地发现和定位问题。
综上所述,使用函数调用可以提高代码的可读性、可维护性、可扩展性和可测试性。虽然将函数展开后代码的正确性不受影响,但使用函数调用可以使代码更加优雅、易于理解和管理。
GDB调试
layout src 代码与程序一起调试 (会出现花屏 通过 ctrl x a 输入两次刷新屏幕来解决)
layout split 通过查看汇编一起调试
run:开始运行程序
break:设置断点
break function:在函数function的开头设置断点
break file.c:line:在文件file.c的第line行设置断点
continue:继续运行程序直到下一个断点
next:单步执行一行代码,不进入函数内部
step:单步执行一行代码,进入函数内部
print:打印变量的值
print var:打印变量var的值
print *ptr:打印指针ptr所指向的值
info:显示调试信息
info break:显示所有断点
info locals:显示当前函数的所有局部变量
backtrace:显示函数调用栈
finish:从当前函数返回
quit:退出GDB调试器
优美的退出
uint32_t 当为-1时 实际上为 uint32_t MAX
q退出报错:return !good 改成 return good
返回值大于0
简易调试器
预学习知识Ctags
ctags是一种代码索引工具,可以用来生成源代码的索引文件,支持多种编程语言,包括C、C++、Java、Python等。以下是一些常用的ctags命令:
ctags filename:生成filename文件的索引文件tags。
ctags -R dir:生成目录dir下所有文件的索引文件tags。
ctags -e filename:生成filename文件的索引文件tags,格式为Emacs。
ctags --extra=+q filename:在tags文件中添加每个标识符的类别。
ctags --exclude=pattern dir:在生成tags文件时排除符合模式pattern的文件。
ctags -a filename:将filename文件的标识符添加到现有的tags文件中。
ctags -f tagsfile filenames:将filenames文件生成一个tagsfile索引文件。
ctags --fields=+iaS filename:在tags文件中包含标识符、访问类型、类型和文件名。
ctags --langmap=c:.h filename:将.h文件看作是C文件,以便正确地索引它们。
ctags --verbose filename:在生成tags文件时显示详细信息。
这些命令只是ctags的一部分,可以在终端中输入ctags --help查看所有的命令和选项。
生成文件后可以实现快速跳转函数
推荐使用neovim
具体安装如下:
参考北大 吴同学:https://github.com/ZyWCN1998/MyDevEnvFile
字符串处理常用的函数
字符串处理:ssprintf ssscanf strcmp strcpy strnpy strtok
字符串到数字的转化:atoi strtoul
单步执行
在cpu-exec.c文件中存在如下函数:
void trace_and_difftest(Decode *_this,vaddr_t dnpc) 输出命令在每一次CPU执行时都会执行
void exec once(Decode *s,vaddr t pc) 单步执行
void execute(uint64_t n) 可选择执行多少步
void cpu_exec(uint64 t n) 仿真CPU工作 在单步执行中调用该函数即可
打印寄存器
在文件isa-def.h中存在如下结构体定义,该结构体中gpr存放了32个寄存器的值,以word_t的类型,可以通过在isa_reg_display(void);中调用实现打印寄存器。
#include <common.h>
typedef struct {
word_t gpr[32];
vaddr t pc; }
loongarch32r CPU_state;
typedef MUXDEF(CONFIG ISA64,uint64 t,uint32 t)word t;
扫描内存
初级版本的扫描内存(后面实现表达式可以加进来),通过调用paddr.c文件中的函数
word_t paddr_read(paddr_t addr, int len) {
if (likely(in_pmem(addr))) return pmem_read(addr, len);
IFDEF(CONFIG_DEVICE, return mmio_read(addr, len));
out_of_bound(addr);
return 0;
}
表达式求值
词法分析
"0x80100000+ ($a0 +5)*4 - *( $t1 + 8) + number"
完成表达式求值后能够进行如下的计算
正则表达式
以下是一些常见的正则表达式:
匹配一个数字:\d
匹配一个非数字字符:\D
匹配一个字母或数字:\w
匹配一个非字母或数字字符:\W
匹配一个空格字符:\s
匹配一个非空格字符:\S
匹配一个任意字符(除了换行符):.
匹配字符串的开头:^
匹配字符串的结尾:$
匹配一个或多个前面的字符:+
匹配零个或多个前面的字符:*
匹配零个或一个前面的字符:?
匹配一个特定的字符:[]
匹配一个范围内的字符:[a-z]、[0-9] 等
匹配一个不在特定范围内的字符:[^abc]
匹配一个单词边界:\b
匹配一个非单词边界:\B
匹配一个特定数量的前面的字符:{n}、{n,m} 等
捕获匹配到的字符:()
匹配重复的字符:\1、\2 等
表达式递归求值
通过对下列框架进行增加实现 + - * / == != &&
uint64_t eval(uint64_t p, uint64_t q) {
if(p > q) Assert(0, "Bad expression");
else if(p == q) {
if(Tokens[p].type == TK_Num || Tokens[p].type == TK_RNum || Tokens[p].type == TK_HNum) return strtoul(Tokens[p].str, NULL, 10);
else Assert(0, "This char is not Number!");}
else if(check_parentheses(p, q) == true) return eval(p + 1, q - 1);
else {
uint64_t op = c_main(p, q);
if(Tokens[op].type == TK_Neg) return -1*eval(op+1,q);
else if(Tokens[op].type == TK_Point) {uint64_t num = paddr_read(eval(op + 1,q), sizeof(uint32_t)); return num;}
uint64_t val1 = eval(p, op - 1);
uint64_t val2 = eval(op + 1, q);
switch (Tokens[op].type) {
case TK_Plus : return val1 + val2; break;
case TK_Sub : return val1 - val2; break;
case TK_Mul : return val1 * val2; break;
case TK_Div : Assert(val2 != 0, "The dividend cannot be 0!"); return val1 / val2; break;
case TK_EQ : return val1 == val2; break;
case TK_NEQ : return val1 != val2; break;
case TK_AND : return val1 && val2; break;
default: Assert(0, "eval(p ,q) Operation is Error!"); break;
}
}
}
括号的判断
通过一个标志位从左到右,如果遇到)就-1,(就+1,如果
存在小于0,或者不等于0,该表达式非法,利用特殊判断区分()() 与 (()())
bool check_parentheses(uint64_t a, uint64_t b) {
if(strcmp(Tokens[a].str, "(") != 0 || strcmp(Tokens[b].str, ")") != 0) return false;
int flag = 0;
uint64_t i;
for(i = a; i <= b; i++){ //
if(strcmp(Tokens[i].str, "(") == 0) ++flag;
if(strcmp(Tokens[i].str, ")") == 0) --flag;
if(flag<0) return false;
if(flag == 0 && i != b) return false;
}
if(flag != 0) return false;
return true;
}
实现带有负数的算术表达式的求值 (选做)
可以看文章中监视点有提示,先通过对指定类型进行判断,然后进行特殊处理,注意表达式的优先级。
负号是个单目运算符, 分裂的时候需要注意什么?
需要只分裂一边!
如何测试你的代码
关键思路,利用gen进行字符串的读入,其他的如gen_num以及gen_rand_op都调用gen,需要设置最后一个输入的标志位
void gen(char c) {
if (buf_end_pos < Buf_size - 1) {
buf[buf_end_pos] = c;
++buf_end_pos;
buf[buf_end_pos] = '\0';
} else {
printf("gen Error");
}
}
监视点
需要创建.h文件存结构体以及函数声明,随后进行调用主要考察链表的使用。
WP* new_wp() {
if(free_ == NULL) {
Assert(0,"No_free_wp");
}
//取出空闲节点
WP* new = free_;
free_ = free_-> next;
//将节点放在head上去
new -> next = head;
head = new;
return new;
}