记录自己做PA的思路,之前做的时候总是忙其他的事情,导致做到PA3由于对框架代码的不熟练寸步难行,打算重头开始,绝不偷懒,熟悉框架代码。这里是做到后面回头写的所以不怎么详细,之后关于PA的博客就非常详细了!!!这里是做到后面回头写的所以不怎么详细,之后关于PA的博客就非常详细了!!!这里是做到后面回头写的所以不怎么详细,之后关于PA的博客就非常详细了!!!
NEMU是一个模拟器,用来模拟硬件的行为
看NEMU的main函数代码
int main(int argc, char *argv[]) {
/* Initialize the monitor. */
#ifdef CONFIG_TARGET_AM
am_init_monitor();
#else
init_monitor(argc, argv);
#endif
/* Start engine. */
engine_start();
return is_exit_status_bad();
}
首先是加载Monitor(监视器)模块,它是为了方便地监控客户计算机的运行状态而引入的. 它除了负责与GNU/Linux进行交互(例如读入客户程序)之外, 还带有调试器的功能, 为NEMU的调试提供了方便的途径. 看其中代码,都做了一些初始化的操作
void init_monitor(int argc, char *argv[])
{
/* Perform some global initialization. */
/* Parse arguments. *///这个函数用于解析命令行参数,argc是参数计数,argv是参数数组。它会解析并记录命令行参数,以便在后续的初始化和运行过程中使用。
parse_args(argc, argv);
/* Set random seed. */
init_rand();
/* Open the log file. */
init_log(log_file);
/* Initialize memory. *///初始化内存,为程序运行提供了内存空间
init_mem();
/* Initialize devices. */
IFDEF(CONFIG_DEVICE, init_device());
/* Perform ISA dependent initialization. */
init_isa();
/* Load the image to memory. This will overwrite the built-in image. */
long img_size = load_img();
/* Initialize differential testing. */
init_difftest(diff_so_file, img_size, difftest_port);
/* Initialize the simple debugger. */
init_sdb();
#ifndef CONFIG_ISA_loongarch32r
IFDEF(CONFIG_ITRACE, init_disasm(
MUXDEF(CONFIG_ISA_x86, "i686",
MUXDEF(CONFIG_ISA_mips32, "mipsel",
MUXDEF(CONFIG_ISA_riscv32, "riscv32",
MUXDEF(CONFIG_ISA_riscv64, "riscv64", "bad")))) "-pc-linux-gnu"));
#endif
/* Display welcome message. */
welcome();
}
其中init_isa()是把客户程序放在内存(用一个大数组模拟)的位置,并调用restart()将虚拟计算机的状态重置,包括将程序计数器设置为起始地址 RESET_VECTOR,并将通用寄存器的第一个寄存器([0])设置为零。相关代码如下
//这些指令将被加载到虚拟计算机的内存中以执行。
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 void restart() {
/* Set the initial program counter. */
// = RESET_VECTOR:设置程序计数器()的值为 RESET_VECTOR。程序计数器存储下一条要执行的指令的地址。
= RESET_VECTOR;
/* The zero register is always 0. */
//[0] = 0:将通用寄存器组()的第一个寄存器(通常是零寄存器)的值设置为零。通用寄存器通常用于存储程序执行过程中的数据。
[0] = 0;
}
void init_isa() {
/* Load built-in image. */
//把客户程序读入到一个固定的内存位置RESET_VECTOR. RESET_VECTOR的值在nemu/include/memory/中定义.s
//guest_to_host接受虚拟地址返回物理地址,把客户程序加载到真正的物理地址
memcpy(guest_to_host(RESET_VECTOR), img, sizeof(img));
/* Initialize this virtual computer system. */
//restart():调用 restart 函数,将虚拟计算机的状态重置,包括将程序计数器设置为起始地址 RESET_VECTOR,并将通用寄存器的第一个寄存器设置为零。
restart();
}
Monitor的初始化工作结束后, main()
函数会继续调用engine_start()
函数, 代码会进入简易调试器的主循环sdb_mainloop(),
简易调试器是monitor的核心功能, 我们可以在命令提示符中输入命令,它通过rl_gets()函数获取我们输入的命令并解析成不同的段(命令和参数),相关代码如下
void sdb_mainloop() {
//检查是否处于批处理模式
if (is_batch_mode) {
cmd_c(NULL);
return;
}
//rl_gets()函数获取用户在命令行中输入的文本。这个文本通常包括一个命令和可能的参数。
//用户输入的命令以字符串形式存储在str中,然后使用strtok()函数从字符串中提取第一个标记作为命令(cmd)。
for (char *str; (str = rl_gets()) != NULL; ) {
char *str_end = str + strlen(str);
/* extract the first token as the command */
char *cmd = strtok(str, " ");
if (cmd == NULL) { continue; }
/* treat the remaining string as the arguments,
* which may need further parsing
*/
char *args = cmd + strlen(cmd) + 1;
//如果args的起始位置超过了输入文本的末尾位置(args >= str_end),则将参数设为NULL。这可能发生在用户只输入了命令而没有参数的情况下。
if (args >= str_end) {
args = NULL;
}
#ifdef CONFIG_DEVICE
extern void sdl_clear_event_queue();
sdl_clear_event_queue();
#endif
int i;
for (i = 0; i < NR_CMD; i ++) {
//如果找到匹配的命令,它会调用相应的处理函数,并将参数传递给它。如果处理函数返回小于0的值,表示需要退出,此时循环结束,程序退出。
if (strcmp(cmd, cmd_table[i].name) == 0) {
if (cmd_table[i].handler(args) < 0) { return; }
break;
}
}
if (i == NR_CMD) { printf("Unknown command '%s'\n", cmd); }
}
}
其中if (cmd_table[i].handler(args) < 0) { return; }表明键入命令后处理函数返回值小于0的话(键入q处理函数会返回-1),程序返回退出
键入c程序会执行之前加载的客户程序
优美地退出
sdb_mainloop()
返回之后会执行is_exit_status_bad(),这里是解决"优美地退出"的关键,从代码中可以看到在没有任何干预的情况下会返回1,所以报错,需要返回0,从条件上看,我们需要让nemu_state.state == NEMU_QUIT这个条件成立,那么就在之前键入q的处理函数(cmd_q)中加上这一行代码就行了
int is_exit_status_bad() {
int good = (nemu_state.state == NEMU_END && nemu_state.halt_ret == 0) ||
(nemu_state.state == NEMU_QUIT);
return !good;
}
实现单步执行, 打印寄存器, 扫描内存
实现基础设施,基础设施的作用是加快开发效率,帮助debug
扫描内存注意以 4 为步长来递增 startAddress,程序的一条指令类型是uint32_t,
在这里,startAddress
是一个 uint32_t
类型的整数,因此 4
会被隐式提升为 uint32_t
类型。这样,它们的类型就匹配了,你可以将 4
加到 startAddress
上,而不会出现类型不匹配的错误。
//单部执行
static int cmd_si(char *args)
{
int n;
if (args==NULL){
n=1;
}
else sscanf(args,"%d",&n);
cpu_exec(n);
return 0;
}
//打印程序状态
static int cmd_info(char *args){
if (args==NULL){
printf("\"r\"-Print register status or \"w\"-Print watchpoint information\n");
}
else if (strcmp(args, "r") == 0){
isa_reg_display();
}
else if (strcmp(args,"w")==0)
{
//打印监视点状态
info_watchpoint();
}
return 0;
}
//扫描内存
static int cmd_x(char *args){
if (args == NULL) {
printf("Wrong Command!\n");
return 0;
}
int N;
uint32_t startAddress;
sscanf(args,"%d%x",&N,&startAddress);
for (int i = 0;i < N;i ++){
printf("%x\n", paddr_read(startAddress,4));
//C语言会自动执行类型提升以匹配表达式的操作数的类型。所以,4 被转换为 uint32_t,
startAddress += 4;
}
return 0;
}
实现算术表达式的词法分析 , 扩展表达式求值的功能
键入p的处理函数
static int cmd_p(char *args){
bool success=true;
int32_t res = expr(args, &success);
if (!success)
{
printf("invalid expression\n");
} else
{
printf("%d\n", res);
}
return 0;
}
给出一个待求值表达式, 我们首先要识别出其中的token, 进行这项工作的是make_token()
函数. make_token()
函数的工作方式十分直接, 它用position
变量来指示当前处理到的位置, 并且按顺序尝试用不同的规则来匹配当前位置的字符串. 当一条规则匹配成功, 并且匹配出的子串正好是position
所在位置的时候, 我们就成功地识别出一个token。
思路文档中描述清楚,以下是文件代码
enum {
TK_NOTYPE = 256, TK_EQ, TK_NUMBER,TK_NEGATIVE,TK_NOEQ,TK_AND,TK_POINTER_DEREF,TK_REG,TK_HEX,
/* TODO: Add more token types */
};
static struct rule {
const char *regex;
int token_type;
} rules[] = {
/* TODO: Add more rules.
* Pay attention to the precedence level of different rules.
*/
{" +", TK_NOTYPE}, // spaces
{"\\+", '+'}, // plus
{"==", TK_EQ}, // equal
{"!=",TK_NOEQ}, // NO equal
{"&&",TK_AND}, // and
{"-",'-'}, // minus
{"\\*",'*'}, // multiplication
{"/",'/'}, // divisions
{"\\b[0-9]+\\b", TK_NUMBER}, //number
{"\\(", '('}, // 左括号
{"\\)", ')'}, // 右括号
//{"[A-Za-z]+",TK_STRING},//字符串
{"\\$(\\$0|ra|[sgt]p|t[0-6]|a[0-7]|s([0-9]|1[0-1]))", TK_REG},//寄存器
{"0[xX][0-9a-fA-F]+",TK_HEX}, //十六进制
};
//ARRLEN(rules) 是一个宏函数,用于计算数组的长度。NR_REGEX 是一个宏定义,用于计算规则数组 rules 的元素个数。
#define NR_REGEX ARRLEN(rules)
/*这个数组用于存储编译后的正则表达式,struct re_pattern_buffer (regex_t)是一个用于存储编译后正则表达式模式的数据结构*/
static regex_t re[NR_REGEX] = {};
/* Rules are used for many times.
* Therefore we compile them only once before any usage.
*/
//这些规则会在简易调试器初始化的时候通过init_regex()被编译成一些用于进行pattern匹配的内部信息, 这些内部信息是被库函数使用的
//通过这个函数,规则数组中的每个正则表达式都会被编译成内部表示形式,存储在 re 数组的相应位置,供后续使用。
void init_regex() {
int i;
char error_msg[128];
int ret;
for (i = 0; i < NR_REGEX; i ++) {
ret = regcomp(&re[i], rules[i].regex, REG_EXTENDED);
if (ret != 0) {
regerror(ret, &re[i], error_msg, 128);
panic("regex compilation failed: %s\n%s", error_msg, rules[i].regex);
}
}
}
//使用Token结构体来记录token的信息
typedef struct token {
int type;
char str[32];
} Token;
/*展示了两个全局变量的定义:tokens 和 nr_token。
tokens 是一个 Token 结构体类型的静态数组,长度为 32。Token 结构体用于表示词法分析中的标记(token),
包含一个整数类型的 type 字段和一个长度为 32 的字符串 str 字段。通过声明 Token tokens[32],我们创建了一个可以存储
最多 32 个标记的数组。
nr_token 是一个整数类型的静态变量,用于记录已识别的标记数量。nr_token 的初始值为 0。
__attribute__((used)) 是一个编译器特定的属性(attribute),在给定的上下文中,它用于告知编译器保留这些变量即使它们没有被显式
地使用。这样做是为了防止编译器优化掉这些变量,确保它们在链接阶段能够正确地被访问到。*/
static Token tokens[32] __attribute__((used)) = {};
static int nr_token __attribute__((used)) = 0;
make_tokenu函数//
/*make_token()函数的工作方式十分直接, 它用position变量来指示当前处理到的位置,
并且按顺序尝试用不同的规则来匹配当前位置的字符串. 当一条规则匹配成功, 并且匹配出的子串正好
是position所在位置的时候, 我们就成功地识别出一个token, Log()宏会输出识别成功的信息.*/
static bool make_token(char *e) {
int position = 0;
int i;
//可以准确地记录正则表达式匹配的子字符串在原始字符串中的位置信息。
regmatch_t pmatch;
nr_token = 0;
while (e[position] != '\0') {
/* Try all rules one by one. */
for (i = 0; i < NR_REGEX; i ++) {
if (regexec(&re[i], e + position, 1, &pmatch, 0) == 0 && pmatch.rm_so == 0) {
char *substr_start = e + position;
int substr_len = pmatch.rm_eo;
Log("match rules[%d] = \"%s\" at position %d with len %d: %.*s",
i, rules[i].regex, position, substr_len, substr_len, substr_start);
position += substr_len;
/* TODO: Now a new token is recognized with rules[i]. Add codes
* to record the token in the array `tokens'. For certain types
* of tokens, some extra actions should be performed.
*/
Token token;
switch (rules[i].token_type)
{
case TK_NOTYPE:
break;
default:
strncpy(, substr_start, substr_len);
[substr_len] = '\0';
=rules[i].token_type;
tokens[nr_token++] = token;
break;
}
break;
}
}
if (i == NR_REGEX) {
printf("no match at position %d\n%s\n%*.s^\n", position, e, position, "");
return false;
}
}
return true;
}
/**********************************************check_parentheses函数判断括号是否合法*******************************************************************/
bool check_parentheses(word_t p,word_t q)
{
bool flag=false;
if(tokens[p].type=='(' && tokens[q].type == ')')
{
for(int i =p+1;i<q;)
{
if (tokens[i].type==')')
{
break;
}
else if (tokens[i].type=='(')
{
while(tokens[i+1].type!=')' )
{
i++;
if(i==q-1)
{
break;
}
}
i+=2;
}
else i++;
}
flag=true;
}
return flag;
}
/********************************************find_major函数寻找主运算符**********************************************************************/
word_t find_major(word_t p,word_t q)
{
word_t ret=0;
word_t par=0;//括号的数量
word_t op_type=0; //当前找到的最高优先级的运算符类型
word_t tmp_type=0; //相应运算符类型的等级
for(word_t i=p;i<=q;i++)
{
if (tokens[i].type=='-')//负号处理
{
if(i==p)
{
tokens[i].type=TK_NEGATIVE;
return i;
}
}
//指针解引用处理
for (int i = 0; i < nr_token; i ++)
{
if (tokens[i].type == '*' && (i == 0 || (tokens[i - 1].type !=')' || tokens[i - 1].type !=TK_NUMBER)) )
{
tokens[i].type = TK_POINTER_DEREF;
return i;
}
}
//数字
if (tokens[i].type ==TK_NUMBER)
{
continue;
}
else if (tokens[i].type=='(')
{
par++;
continue;
}
else if (tokens[i].type==')')
{
if (par==0)
{
return -1;
}
par--;
}
else if (par>0)
{
continue;
}
else
{
switch (tokens[i].type)
{
case '*': case '/': tmp_type = 1; break;
case '+': case '-': tmp_type = 2; break;
case TK_EQ:case TK_NOEQ:tmp_type=3;break;
case TK_AND:tmp_type=4;break;
//cas
default: assert(0);
}
if (tmp_type>=op_type)
{
op_type=tmp_type;
ret=i;
}
}
}
if(par>0)
{return -1;}
return ret;
}
/**********************************************eval求值函数*******************************************************************/
int32_t eval(word_t p,word_t q)
{
if(p>q)
{
printf("The input is wrong p>q\n");
assert(0);
}
else if (p==q)
{
if (tokens[p].type == TK_REG)
{
word_t num;
bool t = true;
num = isa_reg_str2val(tokens[p].str, &t);
if (!t)
{
num = 0;
}
return num;
}
else if (tokens[p].type==TK_NUMBER)
{
word_t num;
sscanf(tokens[p].str,"%d",&num);
return num;
}
else if (tokens[p].type==TK_HEX)
{
return strtol(tokens[p].str, NULL, 16);
}
else
{
printf("false when p==q");
return 0;
}
}
else if (check_parentheses(p, q) == true)
{
/* The expression is surrounded by a matched pair of parentheses.
* If that is the case, just throw away the parentheses.
*/
return eval(p + 1, q - 1);
}
else
{
word_t op=find_major(p,q); //主运算符的索引
int32_t val2 = eval(op + 1, q); //可能是负数,所以先计算val2
//负数处理
if(tokens[op].type==TK_NEGATIVE)
{
val2=-val2;
return val2;
}
//指针解引用
if(tokens[op].type==TK_POINTER_DEREF)
{
//word_t* ptr = (word_t*)tokens[op+1].str;
//return *ptr;
return paddr_read(val2,4);
}
int32_t val1 = eval(p, op - 1);//写在后是为了防止op是负数导致eval传入的p>q
switch (tokens[op].type)
{
case '+': return val1 + val2;
case '-': return val1 - val2;
case '*': return val1 * val2;
case '/': return val1 / val2;
case TK_EQ: return val1==val2;
case TK_NOEQ:return val1!=val2;
case TK_AND:return val1&&val2;
default: assert(0);
}
}
}
//成功解析命令行并存放在tokens中,再调用eval函数z求值
int32_t expr(char *e, bool *success) {
if (!make_token(e)) {
*success = false;
return 0;
}
return eval(0,nr_token-1);
}
实现表达式生成器
结合文档的思路,完成
typedef uint32_t word_t;
// this should be enough
static char buf[65536] = {};
static char code_buf[65536 + 128] = {}; // a little larger than `buf`
static char *code_format =
"#include <>\n"
"int main() { "
" unsigned result = %s; "
" printf(\"%%u\", result); "
" return 0; "
"}";
//生成0——n-1之间的数
static word_t choose(word_t n)
{
return rand()%n;
}
//生成一个随机数字
static void gen_num() {
word_t num = rand() % 9 + 1;; // 生成0到9之间的随机数
char num_str[2]; //临时缓冲区
snprintf(num_str, sizeof(num_str), "%d", num);//将数字转化为字符串
if(strlen(buf)+strlen(num_str)<sizeof(buf))
{
strcat(buf, num_str);//添加到buf末尾
}
else{return;}
}
// 生成一个随机操作符
static void gen_rand_op() {
char ops[] = {'+', '-', '*', '/'}; // 定义操作符集合
word_t op_index = choose(4); // 随机选择一个操作符的索引
char op_str[2] = {ops[op_index], '\0'}; // 创建一个包含操作符的字符串
if(strlen(buf)+strlen(op_str)<sizeof(buf))
{
strcat(buf, op_str); // 将选中的操作符存储到缓冲区中
}
else {return;}
}
// 生成随机表达式
static void gen_rand_expr() {
switch (choose(3)) {
case 0:
if( buf[strlen(buf) - 1]!=')')
{
gen_num(); // 生成随机数字
}
else
{
gen_rand_expr();
}
break;
case 1:
// 避免在操作数之后立即插入左括号,而是在操作符之后插入左括号
if (buf[0] != '\0' && strchr("+-*/", buf[strlen(buf) - 1]))
{
strcat(buf, "("); // 将左括号添加到缓冲区末尾
gen_rand_expr(); // 递归生成随机表达式
strcat(buf, ")"); // 将右括号添加到缓冲区末尾
}
else
{
gen_rand_expr(); // 递归生成随机表达式
}
break;
default:
gen_rand_expr(); // 递归生成随机表达式
gen_rand_op(); // 生成随机操作符
gen_rand_expr(); // 递归生成随机表达式
break;
}
}
// 检查表达式中的除零行为
static int check_division_by_zero() {
char *p = buf;
while (*p) {
if (*p == '/' && *(p + 1) == '0') {
return 1; // 表达式中存在除零行为
}
p++;
}
return 0; // 表达式中不存在除零行为
}
int main(int argc, char *argv[]) {
int seed = time(0);
srand(seed);
int loop = 1;
if (argc > 1) {
sscanf(argv[1], "%d", &loop);//如果命令行参数中指定了循环次数,则将其读取并存储到 loop 变量中。
}
int i;
for (i = 0; i < loop; i ++)
{
gen_rand_expr();
// if (check_division_by_zero)
// {
// memset(buf, '\0', sizeof(buf));//将所有元素设置为空字符可以将数组重置为空
// i--;
// continue;
// }
sprintf(code_buf, code_format, buf);//使用生成的随机表达式按照之前format格式填充 code_buf 缓冲区
FILE *fp = fopen("/tmp/.", "w");
assert(fp != NULL);
fputs(code_buf, fp);
fclose(fp);
int ret = system("gcc /tmp/. -o /tmp/.expr");
if (ret != 0) continue;
fp = popen("/tmp/.expr", "r");
assert(fp != NULL);
int result;
ret = fscanf(fp, "%d", &result);
pclose(fp);
printf("%u %s\n", result, buf);
}
return 0;
}
实现监视点
键入命令的处理函数
//设置监视点
static int cmd_w(char *args){
if (!args)
{
printf("Usage: w EXPR\n");
return 0;
}
bool success;
int32_t res = expr(args, &success);
if (!success)
{
printf("invalid expression\n");
}
else
{
wp_set(args, res);
}
return 0;
}
//删除序列号为N的监视点
static int cmd_d(char *args){
char *arg = strtok(NULL, "");
if (!arg) {
printf("Usage: d N\n");
return 0;
}
int no = strtol(arg, NULL, 10);
wp_remove(no);
return 0;
}
nemu/src/monitor/sdb/代码如下
监视点池的操作和链表操作有关,插入可以使用头插法or尾插法
// wp_pool 数组可以存储 NR_WP 个监视点结构体对象
#define NR_WP 32
typedef struct watchpoint {
int NO;
struct watchpoint *next;
char *expression; // 监视点的表达式
int value; // 监视点的上一个值
/* TODO: Add more members if necessary */
} WP;
static WP wp_pool[NR_WP] = {};
//head用于组织使用中的监视点结构, free_用于组织空闲的监视点结构
static WP *head = NULL, *free_ = NULL;
void init_wp_pool() {
int i;
for (i = 0; i < NR_WP; i ++) {
wp_pool[i].NO = i;
wp_pool[i].next = (i == NR_WP - 1 ? NULL : &wp_pool[i + 1]);
}
head = NULL;
free_ = wp_pool;
}
/* TODO: Implement the functionality of watchpoint */
//new_wp()从free_链表中返回一个空闲的监视点结构
WP* new_wp();
//free_wp()将wp归还到free_链表中
void free_wp(WP *wp);
WP* new_wp()
{
if(free_==NULL)
{
printf("free_没有空闲监视点\n");
assert(0);
}
WP *pos=free_;
free_++;
pos->next=head;
head=pos;
return pos;
}
void free_wp(WP *wp)
{
if(wp==head)
{
head=head->next;
}
else
{
WP *pos=head;
while(pos && pos->next!=wp)
{
pos++;
}
if (!pos)
{
printf("输入的监视点不在head链表中\n");
assert(0);
}
pos->next=wp->next;
}
wp->next=free_;
free_=wp;
}
void info_watchpoint()
{
WP *pos=head;
if(!pos)
{
printf("NO watchpoints");
return;
}
printf("%-8s%-8s\n", "No", "Expression");
while (pos) {
printf("%-8d%-8s\n", pos->NO, pos->expression);
pos = pos->next;
}
}
void wp_set(char *args, int32_t res)
{
WP* wp = new_wp();
strcpy(wp->expression, args);
wp->value=res;
printf("Watchpoint %d: %s\n", wp->NO, wp->expression);
}
void wp_remove(int no)
{
if(no<0 || no>=NR_WP)
{
printf("N is not in right\n");
assert(0);
}
WP* wp = &wp_pool[no];
free_wp(wp);
printf("Delete watchpoint %d: %s\n", wp->NO, wp->expression);
}
void wp_difftest()
{
WP* pos = head;
while (pos) {
bool _;
word_t new = expr(pos->expression, &_);
if (pos->value != new) {
printf("Watchpoint %d: %s\n"
"Old value = %d\n"
"New value = %d\n"
, pos->NO, pos->expression, pos->value, new);
pos->value = new;
nemu_state.state=NEMU_STOP;
}
pos = pos->next;
}
}
在nemu/Kconfig
中为监视点添加一个开关选项, 最后通过menuconfig打开这个选项, 从而激活监视点的功能. 当你不需要使用监视点时, 可以在menuconfig中关闭这个开关选项来提高NEMU的性能.
config WATCHPOINT
bool "Enable watchpoint"
default n
help
Enable watchpoint feature to monitor memory access.
总是使用-Wall
和-Werror
开启 -Wall
和 -Werror
参数,以启用所有警告并将它们转化为错误。在 Makefile 中找到 CFLAGS
变量,并将它配置为包含这些参数。下面是一些示例配置,你可以根据你的项目结构和需要进行相应的修改。
CFLAGS += -Wall -Werror
将上述行添加到 Makefile 的适当位置,通常在编译选项部分。