NEMU-PART I

PA1

PA1:简易调试器

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

实现正确的寄存器结构体

寄存器

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
typedef struct {
    struct {
        uint32_t _32;
        uint16_t _16;
        uint8_t _8[2];
    } gpr[8];

    /* Do NOT change the order of the GPRs' definitions. */
    uint32_t eax, ecx, edx, ebx, esp, ebp, esi, edi;
    ... 
} CPU_state;

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
typedef struct {
    union {
        union {
            uint32_t _32;
            uint16_t _16;
            uint8_t _8[2];
        } gpr[8];

        /* Do NOT change the order of the GPRs' definitions. */
        struct {
            uint32_t eax, ecx, edx, ebx, esp, ebp, esi, edi;
        };	
    };
    ...
} 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 删除监视点
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static struct {
    char *name;
    char *description;
    int (*handler) (char *);
} cmd_table [] = {
    ...
    // 定义单步执行操作,关键字为 si,以此类推
    { "si", "Single step execution", cmd_si },
    { "info", "Show register or monitor's infomation", cmd_info },
    { "x", "Scan Memory", cmd_x },
    ...
};

单步执行

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

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

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

打印寄存器状态

同理,定义操作函数cmd_info

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
static int cmd_info(char *args) {
    char *arg = strtok(NULL, " ");  // 获取第二个字符
    int i = 0;

    if (*arg == 'r') {    // 参数为'r'时打印寄存器的值
        for(; i<8; i++) {
            // 打印eax, ecx, edx, ebx, esp, ebp, esi, edi 8个寄存器
            printf("%s\t\t", regsl[i]);     // 打印寄存器名称
            // 先打印16进制值,再打印10进制值(仿GDB)
            // "\t" 为制表符,让输出更美观
            printf("0x%08x\t\t%d\n", cpu.gpr[i]._32, cpu.gpr[i]._32);
        }
        // 打印%eip 寄存器
        printf("eip\t\t0x%08x\t\t%d\n", cpu.eip, cpu.eip);  
    }
    else {
        printf("Illegal input!\n");
    }

    return 0;
}

扫描内存

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static int cmd_x(char *args) {
    char *arg_1 = strtok(NULL, " ");    // 获取第1个参数 N
    char *arg_2 = strtok(NULL, " ");    // 获取第2个参数 EXPR
    int i = 0;
    int j = 0;

    int N;
    swaddr_t address;	

    sscanf(arg_1, "%d", &N);
    sscanf(arg_2, "%x", &address);	// 获取起始内存地址
    
    for(; i<N; i++) {
        // 每行打印4个值
        if (j%4 == 0) {
            printf("0x%x:", address);
        }

        // 每4字节打印一个值
        printf("0x%08x ", swaddr_read(address, 4));
        address += 4;
        j++;

        // 4个值后换行
        if (j%4 == 0) {
            printf("\n");
        }
    }

    printf("\n");
    return 0;
}

表达式求值

词法分析

定义token

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

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

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

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

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

    /* TODO: Add more token types */

};

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

定义正则表达式

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static struct rule {
    char *regex;
    int token_type;
} rules[] = {

    /* TODO: Add more rules.
     * Pay attention to the precedence level of different rules.
     */

    {" +",	NOTYPE},                // spaces 空格

    {"\\+", '+'},                   // plus 运算符
    {"\\-", '-'},
    {"\\*", '*'},
    {"\\/", '/'},

    {"\\$[a-z]+", REGISTER},        // 数据
    {"0x[0-9a-fA-F]+", HEX},
    {"[0-9]+", NUM},

    {"==", EQ},                     // equal
    {"!=", NOTEQ},

    {"&&", AND},                    // 逻辑运算符
    {"\\|\\|", OR},
    {"!", '!'},

    {"\\(", '('},                   // 括号
    {"\\)", ')'},
};

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

识别token

识别token

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static bool make_token(char *e) {
    ...
    while(e[position] != '\0') {
        ...
        for(i = 0; i < NR_REGEX; i ++) {
            ...
            // 清空token值,防止每次运算相互干扰
            int j = 0;
            for(; j < 32; j++) {
                tokens[nr_token].str[j] = '\0';
            }
            
            // tokens.type的赋值函数
            switch(rules[i].token_type) {
                case 256: 
                    break;

                // 输入10进制数、寄存器、16进制数
                case NUM:
                    tokens[nr_token].type = NUM;
                    strncpy(tokens[nr_token].str, &e[position - substr_len], substr_len);
                    nr_token++;
                    break;

                // REGISTER,HEX,EQ,NOTEQ,AND,OR与之类似

                ...
                
                // 输入单运算符
                case '+':
                    tokens[nr_token].type = '+';
                    nr_token++;
                    break;

                // -, *, /, !, (, ) 与之类似

                ...

                default: 
                    assert(0);
            }
            break;
        }
    }
}

以其中一个为例:

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

阶段2:表达式求值**

判断表达式括号匹配

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
bool check_parentheses(int p, int q) {
    int a = 0;          // 记录表达式token下标
    int i = 0, j = 0;   // 分别记录左、右括号总数

    // 检查表达式首尾是否为括号
    if(tokens[p].type == '(' || tokens[q].type == ')') {
        for(a = p; a<=q; a++) {
            if(tokens[a].type == '(') {
                i++;
            }
            if(tokens[a].type == ')') {
                j++;
            }
            if(a != q && i == j) {
                // 排除例如 (a+b)) 这种情况
                return false;
            }
        }

        if(i == j) {
            // 左右括号数量相等,正确
            return true;
        } 
        else {
            // 数量不等,错误
            return false;
        }
    }

    return false;
}

寻找主操作符

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

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

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

参阅此处:分治法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
int dominant_operator(int p, int q) {
    int step = 0;
    int op = -1;
    int i = 0;  	// 记录token下标
    int pri = 0; 	// 记录当前操作符优先级

    for(i = p; i <= q; i++) {
        if(tokens[i].type == '(') {
            step ++;
        }
        else if(tokens[i].type == ')') {
            step --;
        }

        if(step == 0) {
            if(tokens[i].type == OR) {
                if(pri < 51) {
                    op = i;
                    pri = 51;
                }
            }
            else if(tokens[i].type == AND) {
                if(pri < 50) {
                    op = i;
                    pri = 50;
                }
            }
            else if(tokens[i].type == EQ || tokens[i].type == NOTEQ) {
                if(pri < 49) {
                    op = i;
                    pri = 49;
                }
            }
            else if(tokens[i].type == '+' || tokens[i].type == '-') {
                if(pri < 48) {
                    op = i;
                    pri = 48;
                }
            }
            else if(tokens[i].type == '*' || tokens[i].type == '/') {
                if(pri < 46) {
                    op = i;
                    pri = 46;
                }
            }	
        }
        else if(step < 0) {
            return -2;
        }
    }
    return op;
}

递归计算表达式的值

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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
uint32_t eval(int p, int q) {
    int result = 0;
    int op = 0;
    int val1, val2;

    // 表达式左侧超过右侧,错误
    if (p > q) {
        assert(0);
    }

    // 表达式左侧等于右侧,说明为单个数字
    else if (p == q) {
        // 处理10进制数
        if (tokens[p].type == NUM) {
            sscanf(tokens[p].str, "%d", &result);
            return result;
        } 

        // 处理16进制数
        else if (tokens[p].type == HEX) {
            int i = 2;
            while(tokens[p].str[i] != 0) {
                result *= 16;
                if (tokens[p].str[i] <= '9') {
                    // 16进制为0-9
                    result += tokens[p].str[i] - '0'; 	
                }
                else {
                    // 16进制为a-f
                    result += tokens[p].str[i] - 'a' + 10;	
                }
                i++;
            }
            return result;
        } 

        // 处理寄存器
        else if (tokens[p].type == REGISTER) {
            if (!strcmp(tokens[p].str, "$eax")) {
                return cpu.eax;
            } 

            // 剩下的寄存器使用相同的处理办法
            
            ...

            else {
                return 0;
            }
        } 
        else {
            assert(0);
        }
    }

    // 表达式两侧不等,但是括号匹配,去掉括号
    else if (check_parentheses(p, q) == true) {
         return eval(p + 1, q - 1);
    }

    // 正常表达式
    else {
        // 寻找主操作符
        op = dominant_operator(p, q);

        if (op == -2) {
            assert(0);
        } 

        // 处理一元运算符
        else if (op == -1) {
            // 处理逻辑非运算,如 !1
            if (tokens[p].type == '!') {
                sscanf(tokens[q].str, "%d", &result);
                return !result;
            }

            // 之后将在此处实现负数运算和指针解引用

        }

        // 计算主操作数两侧表达式值
        
        val1 = eval(p, op - 1);     // 计算主操作数左侧表达式
        val2 = eval(op + 1, q);     // 计算主操作数右侧表达式

        switch (tokens[op].type) {
            case '+' : 
                return val1 + val2;	

            // -, *, / , OR, AND, EQ, NOTEQ运算以此类推 
             
            ...

            default : 
                assert(0);
        }
    }
    return 0;
}

举例:

分治法求表达式的值

expr函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
uint32_t expr(char *e, bool *success) {
    if(!make_token(e)) {
        *success = false;
        return 0;
    }

    /* TODO: Insert codes to evaluate the expression. */

    int i;
    for (i = 0; i < nr_token; i++){
        if (tokens[i].type == '*' && (i == 0 || (tokens[i - 1].type != NUM && tokens[i - 1].type != HEX && tokens[i - 1].type != ')'))){
            tokens[i].type = POINT;
        }
        if (tokens[i].type == '-' && (i == 0 || (tokens[i - 1].type != NUM && tokens[i - 1].type != HEX && tokens[i - 1].type != ')'))){
            tokens[i].type = NEG;
        }
    }
    return eval(0, nr_token - 1);

    // panic("please implement me");
    return 0;
}

最终实现

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

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

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

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

eval函数中加入以下内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
uint32_t eval(int p, int q) {

    ...

    // 正常表达式
    else {
       // 处理一元运算符
        else if (op == -1) {

            ...

            // 处理带负数的表达式,如 1+ -1
            if (tokens[p].type == NEG) {
                sscanf(tokens[q].str, "%d", &result);
                return -result;
            }

            // 实现指针解引用
            else if (tokens[p].type == POINT) {
                if (!strcmp(tokens[p + 2].str, "$eax")){
                    result = swaddr_read(cpu.eax, 4);
                    return result;
                }

                // 其余寄存器采用类似操作

                ...
            }
        } 
    }  
}

监视点

新建监视点

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 从free_链表中返回一个空闲监视点
WP* new_wp() {
    WP *temp;
    temp = free_;           // free_链表的头节点作为返回值
    free_ = free_->next;    // free_链表的下一个节点成为头节点
    temp->next = NULL;      // 空闲监视点为head链表的尾节点

    // 若head链表为空,返回的节点成为头节点
    if(head == NULL) {
        head = temp;
    }
    // head链表不为空,寻找其尾节点
    else {
        WP *p;
        p = head;
        // 将返回的节点插入head链表的尾部
        while (p->next != NULL) {
            p = p->next;
        }
        p->next = temp;
    }
    return temp;
}

示意图如下:

监视点链表

释放监视点

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

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

    // 待删除节点成为free链表的头节点
    wp->next = free_;
    free_ = wp;

    // 清空待删除节点的内容
    wp->result = 0;
    wp->expr[0] = '\0';
}

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

判断监视点是否触发

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 判断监视点是否触发
bool checkWP() {
    bool check = false;	 // 最终返回值

    bool *success = false;  
    WP *temp = head;    // 从head链表的头节点开始遍历
    int expr_temp;

    while(temp != NULL) {
        expr_temp = expr(temp->expr, success);
        if (expr_temp != temp->result){
            check = true;
            printf ("Hint watchpoint %d at address 0x%08x\n", temp->NO, cpu.eip);
            temp = temp->next;
            continue;
            // 检测到监视点对应的值发生变化
        }
        temp->result = expr_temp;
        temp = temp->next;
    }	
    return check;
}

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

1
2
3
4
5
6
7
8
/* TODO: check watchpoints here. */

// 链接外部函数
extern bool checkWP();
bool change = checkWP();
if (change) {
    nemu_state = STOP;
}

打印和删除监视点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 打印所有监视点
void printf_wp(){
    WP *temp = head;
    if (temp == NULL){
        printf("No watchpoints\n");
    }
    while (temp != NULL){
        printf("Watch point %d: %s\n", temp->NO, temp->expr);
        temp = temp->next;
    }
}

// 删除监视点
WP* delete_wp(int p, bool *key){
    WP *temp = head;
    while (temp != NULL && temp->NO != p){
        temp = temp->next;
    }
    if (temp == NULL){
        *key = false;
    }
    return temp;
}

加入调试器指令

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 打印监视器的值
static int cmd_info(char *args) {
	char *arg = strtok(NULL, " ");  // 获取第二个字符

	...

	else if(*arg == 'w') {
		extern void printf_wp();
		printf_wp();
	}

	...

	return 0;
}

// 设置监视点
static int cmd_w(char *args) {

    extern WP* new_wp();
    WP* temp = new_wp();

    bool *success = false;
    int result = 0;
    result = expr(args, success);

    if(!success) {
        // 若表达式合法,将对应的值赋给temp这个新监视点
        temp->result = result;
        strcpy(temp->expr, args);
    }

    return 0;
}

// 删除监视点
static int cmd_d(char *args) {
    int p = 0;
    bool key = true;
    sscanf(args, "%d", &p);

    // 记得链接外部函数
    extern WP* delete_wp();
    extern void free_wp();
    WP *q = delete_wp(p, &key);

    if (key){
        printf("Delete watchpoint %d: %s\n", q->NO, q->expr);
        free_wp(q);
        return 0;
    } 
    else {
        printf("No found watchpoint %d\n", p);
        return 0;
    }

    return 0;
}

思考题

思考题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 →

网站总访客数:Loading

使用 Hugo 构建
主题 StackJimmy 设计