NEMU-PART I

预备知识与PA1

你说得对,但是NEMU是一个基于X86-64处理器模拟的IA-32操作系统。NEMU运行在一个被称作Docker的容器,在这里,被容器选中的人将被授予gcc,导引C语言之力。你将扮演一位名为Debug的神秘用户,编写众多C语言程序,在调试中找出FAIL的原因,同时逐步发掘Hit Bad Trap的真相。

目标:制作一个32位的操作系统

什么是NEMU

在X86-64处理器的机器上模拟一个32位操作系统(一个用来执行其它程序的程序!),它包括4个连贯的实验内容:

阶段 任务
PA1 简易调试器
PA2 指令系统
PA3 存储管理
PA4 中断与I/O

认识NEMU

NEMU的结构

NEMU的结构

调试器操作指令集

nemu/src/monitor/debug/ui.c中定义了调试器的结构:

 1static struct {
 2    char *name;
 3    char *description;
 4    // 函数指针,可指向*name,*description
 5    int (*handler) (char *);     
 6} cmd_table [] = {
 7    { "help", "Display informations about all supported commands", cmd_help },
 8    { "c", "Continue the execution of the program", cmd_c },
 9    { "q", "Exit NEMU", cmd_q },
10
11    /* TODO: Add more commands */
12    /* 接下来若想定义新的操作,格式为
13    { "name", "description", function},
14    */
15};

几个有用的函数

函数 作用
Log() printf()的升级版,专门用来输出调试信息,同时还会输出使用Log()所在的源文件,行号和函数,当输出的调试信息过多的时候,可以很方便地定位到代码中的相关位置
Assert() assert()的升级版,当测试条件为假时,在assertion fail之前可以输出一些信息
panic() 用于输出信息并结束程序,相当于无条件的assertion fail
swaddr_read() / swaddr_write() 访问模拟的内存
strtok() 一个简单的字符串分割工具,用于解析命令
sscanf() 可以从字符串中读入格式化的内容, 使用它有时候可以很方便地实现字符串的解析

PA1:简易调试器

  1. 机器永远是对的!
  2. 未经过测试的每行代码永远是错误的!
  3. RTFM!(阅读手册!)

实现正确的寄存器结构体

寄存器

/nemu/include/cpu/reg.h中,原寄存器结构体定义为:

 1typedef struct {
 2    struct {
 3        uint32_t _32;
 4        uint16_t _16;
 5        uint8_t _8[2];
 6    } gpr[8];
 7
 8    /* Do NOT change the order of the GPRs' definitions. */
 9    uint32_t eax, ecx, edx, ebx, esp, ebp, esi, edi;
10    ... 
11} CPU_state;

使用结构体,每一个寄存器均独立存在,而X86系统的寄存器是共享的,例如 %eax 的后16位即为 %ax。因此应使用联合体的形式定义寄存器,这样对 %ax 操作时也会相应地改变 %eax 的值。

 1typedef struct {
 2    union {
 3        union {
 4            uint32_t _32;
 5            uint16_t _16;
 6            uint8_t _8[2];
 7        } gpr[8];
 8
 9        /* Do NOT change the order of the GPRs' definitions. */
10        struct {
11            uint32_t eax, ecx, edx, ebx, esp, ebp, esi, edi;
12        };	
13    };
14    ...
15} CPU_state;

实现调试器的功能

回顾nemu/src/monitor/debug/ui.c中对调试器的定义,需增加新的操作指令。

指令名 示例 功能
si si 10 单步执行
info info r 打印寄存器状态
info w 打印监视点状态
x x N EXPR 扫描内存
p p EXPR 表达式求值
w w EXPR 设置监视点
d d N 删除监视点
 1static struct {
 2    char *name;
 3    char *description;
 4    int (*handler) (char *);
 5} cmd_table [] = {
 6    ...
 7    // 定义单步执行操作,关键字为 si,以此类推
 8    { "si", "Single step execution", cmd_si },
 9    { "info", "Show register or monitor's infomation", cmd_info },
10    { "x", "Scan Memory", cmd_x },
11    ...
12};

单步执行

nemu/src/monitor/debug/ui.c中定义新的操作函数:

 1static int cmd_si(char *args) {
 2    char *arg = strtok(NULL, " ");  // 获取第二个字符
 3    int step = 0;   // 待操作步数
 4    int i = 0;
 5   
 6    if (arg == NULL) {
 7        cpu_exec(1);
 8    }
 9    else {
10        sscanf(arg, "%d", &step);  // 将arg转换为整型数
11        if (step <= 0) {
12            printf("Illegal input!");
13        }
14        else {
15            for(; i<step; i++) {
16                cpu_exec(1);
17            }
18        }
19    }
20    return 0;
21}

报错请查看:
  • 使用循环时,循环变量的初始化应在循环结构之前,否则会报错:
1nemu/src/monitor/debug/ui.c:55: error: 'for' loop initial declarations are only allowed in C99 mode
2nemu/src/monitor/debug/ui.c:55: note: use option -std=c99 or -std=gnu99 to compile your code
  • 读取函数应使用 sscanf

打印寄存器状态

同理,定义操作函数cmd_info

 1static int cmd_info(char *args) {
 2    char *arg = strtok(NULL, " ");  // 获取第二个字符
 3    int i = 0;
 4
 5    if (*arg == 'r') {    // 参数为'r'时打印寄存器的值
 6        for(; i<8; i++) {
 7            // 打印eax, ecx, edx, ebx, esp, ebp, esi, edi 8个寄存器
 8            printf("%s\t\t", regsl[i]);     // 打印寄存器名称
 9            // 先打印16进制值,再打印10进制值(仿GDB)
10            // "\t" 为制表符,让输出更美观
11            printf("0x%08x\t\t%d\n", cpu.gpr[i]._32, cpu.gpr[i]._32);
12        }
13        // 打印%eip 寄存器
14        printf("eip\t\t0x%08x\t\t%d\n", cpu.eip, cpu.eip);  
15    }
16    else {
17        printf("Illegal input!\n");
18    }
19
20    return 0;
21}

扫描内存

 1static int cmd_x(char *args) {
 2    char *arg_1 = strtok(NULL, " ");    // 获取第1个参数 N
 3    char *arg_2 = strtok(NULL, " ");    // 获取第2个参数 EXPR
 4    int i = 0;
 5    int j = 0;
 6
 7    int N;
 8    swaddr_t address;	
 9
10    sscanf(arg_1, "%d", &N);
11    sscanf(arg_2, "%x", &address);	// 获取起始内存地址
12    
13    for(; i<N; i++) {
14        // 每行打印4个值
15        if (j%4 == 0) {
16            printf("0x%x:", address);
17        }
18
19        // 每4字节打印一个值
20        printf("0x%08x ", swaddr_read(address, 4));
21        address += 4;
22        j++;
23
24        // 4个值后换行
25        if (j%4 == 0) {
26            printf("\n");
27        }
28    }
29
30    printf("\n");
31    return 0;
32}

表达式求值

阶段1:词法分析

  1. 定义token

/nemu/src/monitor/debug/expr.c中,观察token结构体,它包含两个数据typestr

1typedef struct token {
2    int type;       // 记录token的类型
3    char str[32];   // 记录token的具体数据
4} Token;

容易发现,当token+, -, &等单运算符时,只需要记录它的type即可,因为它们的type唯一标识了各自的具体数据。

/nemu/src/monitor/debug/expr.c的列举类中,定义token的类型。

 1enum {
 2    NOTYPE = 256,
 3    NUM = 1,        // 10进制数
 4    REGISTER = 2,   // 寄存器
 5    HEX = 3,        // 16进制数
 6    EQ = 4,         // 相等
 7    NOTEQ = 5,      // 不相等
 8    OR = 6,         // 或运算
 9    AND = 7,        // 与运算
10    POINT,          // 指针
11    NEG
12
13    /* TODO: Add more token types */
14
15};

定义了枚举类后,相应的字段和数字就确定了唯一对应的关系。例如,type = NUMtype = 1均表示10进制整数。

  1. 定义正则表达式

/nemu/src/monitor/debug/expr.c中,定义正则表达式:

 1static struct rule {
 2    char *regex;
 3    int token_type;
 4} rules[] = {
 5
 6    /* TODO: Add more rules.
 7     * Pay attention to the precedence level of different rules.
 8     */
 9
10    {" +",	NOTYPE},                // spaces 空格
11
12    {"\\+", '+'},                   // plus 运算符
13    {"\\-", '-'},
14    {"\\*", '*'},
15    {"\\/", '/'},
16
17    {"\\$[a-z]+", REGISTER},        // 数据
18    {"0x[0-9a-fA-F]+", HEX},
19    {"[0-9]+", NUM},
20
21    {"==", EQ},                     // equal
22    {"!=", NOTEQ},
23
24    {"&&", AND},                    // 逻辑运算符
25    {"\\|\\|", OR},
26    {"!", '!'},
27
28    {"\\(", '('},                   // 括号
29    {"\\)", ')'},
30};

正则表达式的作用是识别当前表达式的具体内容。例如,对于表达式4 + 3 * ( 2 - 1 ),正则表达式的识别结果应为:

识别token

  1. 识别token

/nemu/src/monitor/debug/expr.c中的make_token函数可以帮助我们实现这一工作。

 1static bool make_token(char *e) {
 2    ...
 3    while(e[position] != '\0') {
 4        ...
 5        for(i = 0; i < NR_REGEX; i ++) {
 6            ...
 7            // 清空token值,防止每次运算相互干扰
 8            int j = 0;
 9            for(; j < 32; j++) {
10                tokens[nr_token].str[j] = '\0';
11            }
12            
13            // tokens.type的赋值函数
14            switch(rules[i].token_type) {
15                case 256: 
16                    break;
17
18                // 输入10进制数、寄存器、16进制数
19                case NUM:
20                    tokens[nr_token].type = NUM;
21                    strncpy(tokens[nr_token].str, &e[position - substr_len], substr_len);
22                    nr_token++;
23                    break;
24
25                // REGISTER,HEX,EQ,NOTEQ,AND,OR与之类似
26
27                ...
28                
29                // 输入单运算符
30                case '+':
31                    tokens[nr_token].type = '+';
32                    nr_token++;
33                    break;
34
35                // -, *, /, !, (, ) 与之类似
36
37                ...
38
39                default: 
40                    assert(0);
41            }
42            break;
43        }
44    }
45}

以其中一个为例:

1case NUM:
2    // token的类型为NUM
3    tokens[nr_token].type = NUM;
4    // 将sub_strlen长度的值存储到tokens.str
5    strncpy(tokens[nr_token].str, &e[position - substr_len], substr_len);
6    // 开始读取下一个token
7    nr_token++;
8    // 跳出switch循环
9    break;

阶段2:表达式求值

  1. 判断表达式括号匹配
 1bool check_parentheses(int p, int q) {
 2    int a = 0;          // 记录表达式token下标
 3    int i = 0, j = 0;   // 分别记录左、右括号总数
 4
 5    // 检查表达式首尾是否为括号
 6    if(tokens[p].type == '(' || tokens[q].type == ')') {
 7        for(a = p; a<=q; a++) {
 8            if(tokens[a].type == '(') {
 9                i++;
10            }
11            if(tokens[a].type == ')') {
12                j++;
13            }
14            if(a != q && i == j) {
15                // 排除例如 (a+b)) 这种情况
16                return false;
17            }
18        }
19
20        if(i == j) {
21            // 左右括号数量相等,正确
22            return true;
23        } 
24        else {
25            // 数量不等,错误
26            return false;
27        }
28    }
29
30    return false;
31}
  1. 寻找主操作符

dominant operator(主操作符)是表达式中最后参与运算的运算符,根据运算符优先级可知:

  • 非运算符的token不是dominant operator
  • 出现在一对括号中的token不是dominant operator。注意到这里不会出现有括号包围整个表达式的情况,因为这种情况已经在check_parentheses()相应的if块中被处理了。
  • dominant operator的优先级在表达式中是最低的。这是因为dominant operator是最后一步才进行的运算符。
  • 当有多个运算符的优先级都是最低时,根据结合性,最后被结合的运算符才是dominant operator。一个例子是1 + 2 + 3,它的dominant operator应该是右边的+

找到主操作符后,表达式的运算可归结为主操作符两侧数的运算。这也就意味着该问题满足了分治的基本条件:一个问题可分解为若干个与原问题结构相同的子问题。

参阅此处:分治法

 1int dominant_operator(int p, int q) {
 2    int step = 0;
 3    int op = -1;
 4    int i = 0;  	// 记录token下标
 5    int pri = 0; 	// 记录当前操作符优先级
 6
 7    for(i = p; i <= q; i++) {
 8        if(tokens[i].type == '(') {
 9            step ++;
10        }
11        else if(tokens[i].type == ')') {
12            step --;
13        }
14
15        if(step == 0) {
16            if(tokens[i].type == OR) {
17                if(pri < 51) {
18                    op = i;
19                    pri = 51;
20                }
21            }
22            else if(tokens[i].type == AND) {
23                if(pri < 50) {
24                    op = i;
25                    pri = 50;
26                }
27            }
28            else if(tokens[i].type == EQ || tokens[i].type == NOTEQ) {
29                if(pri < 49) {
30                    op = i;
31                    pri = 49;
32                }
33            }
34            else if(tokens[i].type == '+' || tokens[i].type == '-') {
35                if(pri < 48) {
36                    op = i;
37                    pri = 48;
38                }
39            }
40            else if(tokens[i].type == '*' || tokens[i].type == '/') {
41                if(pri < 46) {
42                    op = i;
43                    pri = 46;
44                }
45            }	
46        }
47        else if(step < 0) {
48            return -2;
49        }
50    }
51    return op;
52}
  1. 递归计算表达式的值

这里运用的就是分治算法。

  1uint32_t eval(int p, int q) {
  2    int result = 0;
  3    int op = 0;
  4    int val1, val2;
  5
  6    // 表达式左侧超过右侧,错误
  7    if (p > q) {
  8        assert(0);
  9    }
 10
 11    // 表达式左侧等于右侧,说明为单个数字
 12    else if (p == q) {
 13        // 处理10进制数
 14        if (tokens[p].type == NUM) {
 15            sscanf(tokens[p].str, "%d", &result);
 16            return result;
 17        } 
 18
 19        // 处理16进制数
 20        else if (tokens[p].type == HEX) {
 21            int i = 2;
 22            while(tokens[p].str[i] != 0) {
 23                result *= 16;
 24                if (tokens[p].str[i] <= '9') {
 25                    // 16进制为0-9
 26                    result += tokens[p].str[i] - '0'; 	
 27                }
 28                else {
 29                    // 16进制为a-f
 30                    result += tokens[p].str[i] - 'a' + 10;	
 31                }
 32                i++;
 33            }
 34            return result;
 35        } 
 36
 37        // 处理寄存器
 38        else if (tokens[p].type == REGISTER) {
 39            if (!strcmp(tokens[p].str, "$eax")) {
 40                return cpu.eax;
 41            } 
 42
 43            // 剩下的寄存器使用相同的处理办法
 44            
 45            ...
 46
 47            else {
 48                return 0;
 49            }
 50        } 
 51        else {
 52            assert(0);
 53        }
 54    }
 55
 56    // 表达式两侧不等,但是括号匹配,去掉括号
 57    else if (check_parentheses(p, q) == true) {
 58         return eval(p + 1, q - 1);
 59    }
 60
 61    // 正常表达式
 62    else {
 63        // 寻找主操作符
 64        op = dominant_operator(p, q);
 65
 66        if (op == -2) {
 67            assert(0);
 68        } 
 69
 70        // 处理一元运算符
 71        else if (op == -1) {
 72            // 处理逻辑非运算,如 !1
 73            if (tokens[p].type == '!') {
 74                sscanf(tokens[q].str, "%d", &result);
 75                return !result;
 76            }
 77
 78            // 之后将在此处实现负数运算和指针解引用
 79
 80        }
 81
 82        // 计算主操作数两侧表达式值
 83        
 84        val1 = eval(p, op - 1);     // 计算主操作数左侧表达式
 85        val2 = eval(op + 1, q);     // 计算主操作数右侧表达式
 86
 87        switch (tokens[op].type) {
 88            case '+' : 
 89                return val1 + val2;	
 90
 91            // -, *, / , OR, AND, EQ, NOTEQ运算以此类推 
 92             
 93            ...
 94
 95            default : 
 96                assert(0);
 97        }
 98    }
 99    return 0;
100}

举例:

分治法求表达式的值

  1. expr函数
 1uint32_t expr(char *e, bool *success) {
 2    if(!make_token(e)) {
 3        *success = false;
 4        return 0;
 5    }
 6
 7    /* TODO: Insert codes to evaluate the expression. */
 8
 9    int i;
10    for (i = 0; i < nr_token; i++){
11        if (tokens[i].type == '*' && (i == 0 || (tokens[i - 1].type != NUM && tokens[i - 1].type != HEX && tokens[i - 1].type != ')'))){
12            tokens[i].type = POINT;
13        }
14        if (tokens[i].type == '-' && (i == 0 || (tokens[i - 1].type != NUM && tokens[i - 1].type != HEX && tokens[i - 1].type != ')'))){
15            tokens[i].type = NEG;
16        }
17    }
18    return eval(0, nr_token - 1);
19
20    // panic("please implement me");
21    return 0;
22}
  1. 最终实现

/nemu/src/monitor/debug/ui.c中写入cmd_p函数:

1static int cmd_p(char *args) {
2    bool *success = false;
3    int result = 0;
4    result = expr(args, success);
5    if(!success) {
6        printf("%d\n", result); 
7    }
8    return 0;
9}

表达式求值功能正式实现。

选做任务:实现负数运算和指针解引用

eval函数中加入以下内容:

 1uint32_t eval(int p, int q) {
 2
 3    ...
 4
 5    // 正常表达式
 6    else {
 7       // 处理一元运算符
 8        else if (op == -1) {
 9
10            ...
11
12            // 处理带负数的表达式,如 1+ -1
13            if (tokens[p].type == NEG) {
14                sscanf(tokens[q].str, "%d", &result);
15                return -result;
16            }
17
18            // 实现指针解引用
19            else if (tokens[p].type == POINT) {
20                if (!strcmp(tokens[p + 2].str, "$eax")){
21                    result = swaddr_read(cpu.eax, 4);
22                    return result;
23                }
24
25                // 其余寄存器采用类似操作
26
27                ...
28            }
29        } 
30    }  
31}

监视点

  1. 新建监视点

nemu/src/monitor/debug/watchpoint.c中,定义函数new_wp()

 1// 从free_链表中返回一个空闲监视点
 2WP* new_wp() {
 3    WP *temp;
 4    temp = free_;           // free_链表的头节点作为返回值
 5    free_ = free_->next;    // free_链表的下一个节点成为头节点
 6    temp->next = NULL;      // 空闲监视点为head链表的尾节点
 7
 8    // 若head链表为空,返回的节点成为头节点
 9    if(head == NULL) {
10        head = temp;
11    }
12    // head链表不为空,寻找其尾节点
13    else {
14        WP *p;
15        p = head;
16        // 将返回的节点插入head链表的尾部
17        while (p->next != NULL) {
18            p = p->next;
19        }
20        p->next = temp;
21    }
22    return temp;
23}

示意图如下:

监视点链表

  1. 释放监视点

同理,在nemu/src/monitor/debug/watchpoint.c中定义:

 1// 释放监视点至free_链表
 2void free_wp(WP *wp) {
 3    if(wp == NULL) {
 4        assert(0);   // 返回节点为空
 5    } 
 6    else if (wp == head) {
 7        head = head->next;
 8    }
 9    else {
10        WP* temp = head;
11        // 找到head链表待删除节点的前一个节点
12        while(temp != NULL && temp->next != wp) {
13            temp = temp->next;
14        }
15        // 取消待删除节点与其后节点的连接
16        temp->next = wp->next;
17    }
18
19    // 待删除节点成为free链表的头节点
20    wp->next = free_;
21    free_ = wp;
22
23    // 清空待删除节点的内容
24    wp->result = 0;
25    wp->expr[0] = '\0';
26}

释放监视点实际上就是新建监视点的逆操作,换言之,就是将head链表待删除的节点变成free链表的头节点。

  1. 判断监视点是否触发

nemu/src/monitor/debug/watchpoint.c中定义:

 1// 判断监视点是否触发
 2bool checkWP() {
 3    bool check = false;	 // 最终返回值
 4
 5    bool *success = false;  
 6    WP *temp = head;    // 从head链表的头节点开始遍历
 7    int expr_temp;
 8
 9    while(temp != NULL) {
10        expr_temp = expr(temp->expr, success);
11        if (expr_temp != temp->result){
12            check = true;
13            printf ("Hint watchpoint %d at address 0x%08x\n", temp->NO, cpu.eip);
14            temp = temp->next;
15            continue;
16            // 检测到监视点对应的值发生变化
17        }
18        temp->result = expr_temp;
19        temp = temp->next;
20    }	
21    return check;
22}

/nemu/src/monitor/cpu-exec.c中加入以下内容:

1/* TODO: check watchpoints here. */
2
3// 链接外部函数
4extern bool checkWP();
5bool change = checkWP();
6if (change) {
7    nemu_state = STOP;
8}
  1. 打印监视点和删除监视点
 1// 打印所有监视点
 2void printf_wp(){
 3    WP *temp = head;
 4    if (temp == NULL){
 5        printf("No watchpoints\n");
 6    }
 7    while (temp != NULL){
 8        printf("Watch point %d: %s\n", temp->NO, temp->expr);
 9        temp = temp->next;
10    }
11}
12
13// 删除监视点
14WP* delete_wp(int p, bool *key){
15    WP *temp = head;
16    while (temp != NULL && temp->NO != p){
17        temp = temp->next;
18    }
19    if (temp == NULL){
20        *key = false;
21    }
22    return temp;
23}
  1. 加入调试器指令

最后在/nemu/src/monitor/debug/ui.c中加入指令函数:

 1// 打印监视器的值
 2static int cmd_info(char *args) {
 3	char *arg = strtok(NULL, " ");  // 获取第二个字符
 4
 5	...
 6
 7	else if(*arg == 'w') {
 8		extern void printf_wp();
 9		printf_wp();
10	}
11
12	...
13
14	return 0;
15}
16
17// 设置监视点
18static int cmd_w(char *args) {
19
20    extern WP* new_wp();
21    WP* temp = new_wp();
22
23    bool *success = false;
24    int result = 0;
25    result = expr(args, success);
26
27    if(!success) {
28        // 若表达式合法,将对应的值赋给temp这个新监视点
29        temp->result = result;
30        strcpy(temp->expr, args);
31    }
32
33    return 0;
34}
35
36// 删除监视点
37static int cmd_d(char *args) {
38    int p = 0;
39    bool key = true;
40    sscanf(args, "%d", &p);
41
42    // 记得链接外部函数
43    extern WP* delete_wp();
44    extern void free_wp();
45    WP *q = delete_wp(p, &key);
46
47    if (key){
48        printf("Delete watchpoint %d: %s\n", q->NO, q->expr);
49        free_wp(q);
50        return 0;
51    } 
52    else {
53        printf("No found watchpoint %d\n", p);
54        return 0;
55    }
56
57    return 0;
58}

思考题

思考题1:opcode_table到底是一个什么类型的数组?

解答

opcode_table数组是一个函数指针数组。


思考题2(1):在cmd_c()函数中, 调用cpu_exec()的时候传入了参数-1 , 你知道为什么吗?

解答

-1是无符号类型最大的数字,所以函数里的for循环可以执行所有指令。


思考题2(2):框架代码中定义wp_pool等变量的时候使用了关键字staticstatic在此处 的含义是什么? 为什么要在此处使用它?

解答

static在此处的含义是静态全局变量,该变量只能被本文件中的函数调用,并且是全局变量,而不能被同一程序其他文件中的函数调用,使用static是为了避免它被误修改。


思考题3-1:EFLAGS寄存器中的CF位是什么意思?

解答

i386手册里P34页中和参阅附录c提到,CF是进位标志。

EFLAGS寄存器


思考题3-2:ModR/M字节是什么?

解答

P241-243页。ModR/M 由 Mod,Reg/Opcode,R/M 三个部分组成。 Mod 是前两位,提供寄存器寻址和内存寻址, Reg/Opcode为3-5位,如果是Reg表示使用哪个寄存器,Opcode表示对group属性的Opcode进行补充; R/M为6-8位,与mod结合起来会得到8个寄存器和24个内存寻址。

ModR/M


思考题3-3:mov指令的具体格式是怎么样的?

解答

P345页,格式是DEST ← SRC。

mov指令


思考题3-4: 完成 PA1 的内容之后, nemu目录下的所有.c.h和文件总共有多少行代码? 你是使用什么命令得到这个结果的?和框架代码相比, 你在PA1中编写了多少行代码?你可以把这条命令写入Makefile中, 随着实验进度的推进, 你可以很方便地统计工程的代码行数, 例如敲入 make count就会自动运行统计代码行数的命令。再来个难一点的, 除去空行之外, nemu目录下的所有.c.h文件总共有多少行代码?

解答

通过find . -name "*[.h/.c]" | xargs wc -l命令,得到4376行。和框架代码4197行相比, 我在 PA1中编写了606行代码。

通过find . -name "*[.h/.c]" | xargs grep "^." | wc -l命令计算去除空行的所有.c .h文件得到了3900行代码。

make count指令如下:

make count

@find nemu/ -name “.c” -o -name “.h” | xargs cat | grep -v ^$$ | wc -l


思考题3-5:打开工程目录下的Makefile文件, 你会在CFLAGS变量中看到 gcc 的一些编译选项。请解释 gcc 中的-Wall-Werror有什么作用? 为什么要使用-Wall-Werror

解答

-Wall使GCC编译后显示所有的警告信息。-Werror会将将所有的警告当成错误进行处理,并且取消编译操作。使用-Wall-Werror就是为了找出可能存在的错误,尽可能地避免程序运行出错,优化程序。


至此,PA1全部完成

PA2预备知识

Licensed under CC BY-NC-SA 4.0
网站总访客数:Loading

使用 Hugo 构建
主题 StackJimmy 设计