计算机系统基础PA1
在开始愉快的PA之旅之前
不来玩一下吗?
不得不说,要仔细看清楚来,是根据 fceux-am/README.md中的内容进行操作
这里我们可以拿到一个压缩包
咋解压?
解压完后,比如我想要将这个文件夹下的全部文件移动到当前文件夹下,咋办?
如将 a/b/下的文件全部移动搭配 a/下
mv a/b/* a/
解决编译有点慢的问题
lscpu查看系统有多少个cpu
因为make 可以多线程运行,但是默认情况下是单线程运行
使用 make -j? (?是查到的cpu个数)可以多线程运行
使用Ccache
为了配置Ccache 我们来看看man中对与 Ccache是咋说的吧!
我们就用第一种方法吧
这个时候就有个难题了,什么叫做 just make sure that ccache is in your PATH,应该怎么做?
PATH是什么?
环境变量是什么?
我们咋做?
通过which ccache 可以知道cccahe安装在哪里
我们在~/.bashrc中找到PATH并改成
export PATH="/usr/bin/ccache:$PATH"
.bashrc是什么?
好博客<-----
NEUM是什么?
neum是可以运行程序的程序
ISA是什么?
开天辟地的篇章
RTFSC
nemu 就是我们用程序模拟出来的电脑
当我们运行程序时,nemu能够达到如上效果
nemu中模仿了计算机,那nemu中的结构时咋样的?
Makefile入门资料<-----
C语言条件编译#if入门资料<----
C语言中为何需要.h文件,其作用是什么?<------
当然是main开始
我们先捋清我们启动这个nemu的步骤:
1.首先我们需要设置nemu的配置:
2.然后我们需要编译:
3.最后我们可以启动nemu了:
更多关于配置和编译上代码的细节需要看RTFSC中的 配置系统和项目构建
我们直接讨论一下make run 后发生了什么:
如下为nemu-main.c的代码:
可以看到其是要初始化monitor
那么monitor是什么?他要干什么?
即monitor其初始化是为运行客户程序做准备
在init_monitor()中 一系列初始化的函数:
在init_isa()函数中:
1.第一项工作就是将一个内置的客户程序读入到内存中.
2.第二项任务是初始化寄存器, 这是通过restart()
函数来实现的.
初始化寄存器的一个重要工作就是设置cpu.pc
的初值, 我们需要将它设置成刚才加载客户程序的内存位置,
这样就可以让CPU从我们约定的内存位置开始执行客户程序了.
运行第一个客户程序
Monitor的初始化工作结束后, main()
函数会继续调用engine_start()
函数(在nemu/src/engine/interpreter/init.c
中定义).
代码会进入简易调试器(Simple Debugger)的主循环sdb_mainloop()
(在nemu/src/monitor/sdb/sdb.c
中定义),
并输出NEMU的命令提示符:
至此,我们的第一个客户程序便运行起来了
cmd_c()函数在~/ics2022/nemu/src/monitor/sdb/sdb.c 中
cpu_exec()函数在/ics2022/nemu/src/cpu/cpu-exec.c中
在cpu_exec()中主要是会调用execute()函数
注意这里的n是uint64_t 即无符号int数
当传值为-1时,根据补码规则,-1是最大的无符号整数,即这里会循环完整个无符号整数的范围
看来并不是return 后程序就结束了,还需要一些函数来做一些善后处理
对于调试有用的宏
下面用蓝色字体打印出来的为调用Log()输出的,而白色字体的为普通调用C库函数的printf等
下面是使用Log的源码:
可以看出确实使用Log()可以自动打印出在哪个代码下的哪个函数的哪一行中
想要看懂Log实现的源代码不得不学一下宏是咋用的了
宏与条件编译
非常不错的宏入门教程<-----
现在来看看Log实现的源码:
#define Log(format, ...) \ _Log(ANSI_FMT("[%s:%d %s] " format, ANSI_FG_BLUE) "\n", \
__FILE__, __LINE__, __func__, ## __VA_ARGS__)
更多的可变长参数在上面的博客中
优美地退出
我的经历比较曲折,当时没办法了,我想知道如何直接从nemu-main.c开始调试,但是不知道咋整
但是不知道从哪个文件看到的(上面讲义提到过),应该是在一个.mk或者Makefile上看到的
make gdb可以从nemu-main.c开始调试
于是我开始了调试之旅
出现 Error 1这样的报错 我开始看的是一头雾水,但是随着调试的进行
出现了return is_exit_status_bad()这个函数
于是gdb step 跟进
发现这个函数导致main最后函数返回1
即return 1
我们是return 0才是正常退出
所以一定是这个is_exit_status_bad()函数有问题了
我们发现其源码中有几个常量,这些常量哪里来的?
发现在这个源码中其引入了utils.h这个头文件
这个头文件在nemu中有!
可以发现其是通过枚举enum来定义
关于enum的基础教程<-----
反正我们是想要return 0,那么结合着我们是退出
应该让nemu.state.state==NEMU_QUIT
gdb print nemu.state.state 为1
接下来知道咋做了吧!
就是这么简单
基础设施: 简易调试器
为了实现下面的功能不得不看的函数
字符串分割函数: strtok()<----
字符串转换为数字的函数:atoi() <-----
字符串转换为数字的函数更好的函数:sscanf()<-----
打印寄存器
既然他说了,那么我们就去isa_reg_display()函数下看看吧!
发现他这里给出了寄存器数组
我们不妨来看看他引入的头文件:
发现这两个函数
一个应该是从cpu中获得寄存器的值,一个应该是获得寄存器的名字
还记得吗,寄存器是在哪里初始化的,这是谁做的?
那我们就去isa-def.h下看看吧!
确实发现了CPU中寄存器结构的描述
同时也验证了上面gpr(idx)这个操作是取寄存器中的值
word_t和vaddr_t是什么类型?
不知道,现在还找不到
然后让我们去cpu-exec.c看看吧!
可以看到这个定义的全局变量
在这文件下引入的头文件common.h中我们可以发现word_t 和 vaddr_t的定义:
万事具备,让我们去实现isa_reg_display()函数吧!
我们要打印什么?
看看gdb咋打印的
看起来是打印了寄存器的名字,其中的值
好像第一个值是值的16进制,第二个值是值的10进制?
话说:寄存器中保存的不就是立即数或者是地址吗?
show you code:
/*@cilinmengye:implement the info r*/
void isa_reg_display() {
ba int n = sizeof(regs)/sizeof(regs[0]);
for (int i = 0 ; i < n ; i++ ) {
printf ("%s\t 0x%x\t %u\n",reg_name(i,0),gpr(i),gpr(i));
}
}
小小的隐患:
这里打印%u我不知道对不对(或者其他地方哪里错了)
返回值是uint32_t 类型
具体像 uint32_t , uint_64_t是什么看:这个<-----
扫描内存
gdb如何打印?<-----
不如先来看看我们的内存是如何定义的
那我们去paddr.c下看看吧!
发现我们这个数组是以uint8_t为单位的
在init_mem()中发现使用pmem的一个案例
我们再去paddr.h下看看是如何定义RESET_VECTOR的吧!
这个CONFIG_MBASE是0x80000000,PMEM_LEFT以uint32_t即int的形式保存下来,
这个CONFIG_PC_RESET_OFFSET是0x0,
则RESET_VECTOR是0x80000000并且以uint32_t的形式保存下面
在初始化内存后,因为要将客户程序装入到内存中,一定会有咋使用内存的案例!
去看看!
已经很明确了,我们要使用guest_to_host这个函数访问内存
而且这个函数返回的是一个uint8_t*的指针
在我们实现cmd_x下的sdb.c文件下,需要引入
#include <memory/paddr.h>
因为在这个头文件下定义了如下函数:
然后再我们使用的时候编译器会知道我们这里需要使用guest_to_host()函数
会为我们将实现了guest_to_host()函数的.c文件 链接起来
然后我们遇到的问题是:
在存放数据的时候,系统是按照大端模式还是小端模式?
我们的内存数组是以uint8_t为单位的,我们如何按照讲义的要求将结果作为起始内存
地址, 以十六进制形式输出连续的N
个4字节?
我们输入地址时是字符串的形式,如何将字符串转化为16进制下的数据?
解决这些问题的最好方式是:
从源代码中找到各个定义,并简化后尝试一下:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string.h>
using namespace std;
#define CONFIG_MSIZE 0x8000000
static const uint32_t img[] = {
0x800002b7, // lui t0,0x80000
0x0002a023, // sw zero,0(t0)
0x0002a503, // lw a0,0(t0)
0x00100073, // ebreak (used as nemu_trap)
};
static uint8_t pmem[CONFIG_MSIZE] __attribute((aligned(4096)));
int main()
{
memcpy(pmem, img, sizeof(img));
uint32_t value = (pmem[3] << 24) |
(pmem[2] << 16) |
(pmem[1] << 8) |
pmem[0];
for (int i=0;i<16;i++)
{
printf("%x ",pmem[i]);
if ((i+1)%4==0)
cout<<endl;
}
return 0;
}
可以发现系统保存数据到我们的内存数组时是按照小端模式
同时将我们可以通过位运算的方式将uint8_t得到一个uint32_t 连续4个字符的数据
使用sscanf函数可以轻松将字符串转化为16进制下的数据
sscanf使用方法<-----
show you code
//@cilinmengye:Implement x(print memory)
static int cmd_x(char *args){
char *arg=strtok(NULL," ");
if (arg==NULL){
return inputIncomplete((char*)"x");
}else{
int n=atoi(arg);
arg=strtok(NULL," ");
//memory addree not given,default start from RESET_VECTOR
paddr_t addr=RESET_VECTOR;
if (arg!=NULL)
sscanf (arg,"%x",&addr);
for (int i=1;i<=n;i++){
printf ("0x%x:\t0x%08x\n",addr,
*guest_to_host(addr+3)<<24|*guest_to_host(addr+2)<<16|
*guest_to_host(addr+1)<<8|*guest_to_host(addr));
addr+=4;
}
}
return 0;
}
提升!开发效率
还记得环境变量吗?
在我们的~/.bashrc中有上面两个环境变量
其中NEMU_HOME十分好用
因为我们在完成实验的使用总要到nemu下进行make run
然而经常的cd 十分烦人!
我们可以cd $NEMU_HOME直接到nemu
然后cd - 可以快速回到原理的路径上