PA1:简易调试器
机器永远是对的!
未经过测试的每行代码永远是错误的!
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
打印寄存器状态
同理,定义操作函数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\t 0x%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结构体,它包含两个数据type和str:
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 = NUM和type = 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等变量的时候使用了关键字static,static在此处 的含义是什么? 为什么要在此处使用它?
解答
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 →