本实验的网站链接:MIT 6.828 Lab 1 Exercise 8。
题目
解答
补全打印八进制整数的代码
参考打印10进制和16进制整数的代码即可。
case 'o':
num = getuint(&ap, lflag);
base = 8;
goto number;
问题1:解释printf.c和console.c之间的接口关系
解答:printf.c中的putch函数调用了console.c中的cputchar函数,具体调用关系:cprintf -> vcprintf -> putch -> cputchar
。
问题2:解释console.c的以下代码
1 if (crt_pos >= CRT_SIZE) {
2 int i;
3 memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
4 for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
crt_buf[i] = 0x0700 | ' ';
5 crt_pos -= CRT_COLS;
6 }
解答:联系代码上下文,可以理解这段代码的作用。首先,CRT(cathode ray tube)是阴极射线显示器。根据console.h文件中的定义,CRT_COLS是显示器每行的字长(1个字占2字节),取值为80;CRT_ROWS是显示器的行数,取值为25;而#define CRT_SIZE(CRT_ROWS * CRT_COLS)
是显示器屏幕能够容纳的字数,即2000。当crt_pos大于等于CRT_SIZE时,说明显示器屏幕已写满,因此将屏幕的内容上移一行,即将第2行至最后1行(也就是第25行)的内容覆盖第1行至倒数第2行(也就是第24行)。接下来,将最后1行的内容用黑色的空格塞满。将空格字符、0x0700进行或操作的目的是让空格的颜色为黑色。最后更新crt_pos的值。总结:这段代码的作用是当屏幕写满内容时将其上移1行,并将最后一行用黑色空格塞满。
问题3:逐步跟踪以下代码并回答问题
int x = 1, y = 3, z = 4;
cprintf("x %d, y %x, z %d\n", x, y, z);
解答:
fmt指向格式化字符串"x %d, y %x, z %d\n"的内存地址(这个字符串是存储在栈中吗?),ap指向第一个要打印的参数的内存地址,也就是x的地址。
列出每次调用cons_putc, va_arg和vcprintf的状态:
- cprintf首先调用vcprintf,调用时传入的第1个参数fmt的值为格式化字符串"x %d, y %x, z %d\n"的地址,第2个参数ap指向x的地址。
- vcprintf调用vprintfmt,vprintfmt函数中多次调用va_arg和putch,putch调用cputchar,而cputchar调用cons_putc,putch的第一个参数最终会传到cons_putc.接下来按代码执行顺序列出每次调用这些函数的状态。
- 第1次调用cons_putc: printfmt.c第95行,参数为字符'x'.
- 第2次调用cons_putc: printfmt.c第95行,参数为字符' '.
- 第1次调用va_arg: printfmt.c第75行,lflag=0,调用前ap指向x,调用后ap指向y.
- 第3次调用cons_putc: printfmt.c第49行,参数为字符'1'.此处传给putch的第一个参数的表达方式比较新奇、简洁:
"0123456789abcdef"[num % base]
。注意双引号及其内部实际上定义了一个数组,其元素依次为16进制的16个字符。 - 第4次调用cons_putc: printfmt.c第95行,参数为字符','.
- 第5次调用cons_putc: printfmt.c第95行,参数为字符' '.
- 第6次调用cons_putc: printfmt.c第95行,参数为字符'y'.
- 第7次调用cons_putc: printfmt.c第95行,参数为字符' '.
- 第2次调用va_arg: printfmt.c第75行,lflag=0,调用前ap指向y,调用后ap指向z.
- 第8次调用cons_putc: printfmt.c第49行,参数为字符'3'.
- 第9次调用cons_putc: printfmt.c第95行,参数为字符','.
- 第10次调用cons_putc: printfmt.c第95行,参数为字符' '.
- 第11次调用cons_putc: printfmt.c第95行,参数为字符'z'.
- 第12次调用cons_putc: printfmt.c第95行,参数为字符' '.
- 第3次调用va_arg: printfmt.c第75行,lflag=0,调用前ap指向z,调用后ap指向z的地址加4的位置。
- 第13次调用cons_putc: printfmt.c第49行,参数为字符'4'.
- 第14次调用cons_putc: printfmt.c第95行,参数为字符'\n'.
问题4:判断以下代码的输出
unsigned int i = 0x00646c72;
cprintf("H%x Wo%s", 57616, &i);
解答:
输出为“He110 World”。哈哈,这道题很有意思,不过我看见H和Wo就基本猜到是“Hello World”了,不过疑惑l和o不是16进制字符,怎么能表示出来呢?结果是使用1来代替l,使用0来代替o,有创意!简单解释一下:57616转换成16进制就是0xe110.根据ASCII码,i的4个字节从低到高依次为'r', 'l', 'd', '\0'.这里要求主机是小端序才能正常打印出“World”这个单词。
如果是大端字节序,i的值要修改为0x726c6400,而57616这个值不用修改。因为根据printnum函数实现,打印整数时,总是按照从高位到低位来打印的。
问题5:当打印的参数数目小于格式化字符串中需要的参数个数时会怎样?
cprintf("x=%d y=%d", 3);
解答:打印出来的y的值应该是栈中存储x的位置后面4字节代表的值。因为当打印出x的值后,va_arg函数的ap指针指向x的最后一个字节的下一个字节。因此,不管调用cprintf传入几个参数,在解析到"%d"时,va_arg函数就会取当前指针指向的地址作为int型整数的指针返回。
问题6:如果将GCC的调用约定改为参数从左到右压栈,为支持参数数目可变需要怎样修改cprintf函数?
解答:有两种方法。一种是程序员调用cprintf函数时按照从右到左的顺序来传递参数,这种方法不符合我们的阅读习惯、可读性较差。第二种方法是在原接口的最后增加一个int型参数,用来记录所有参数的总长度,这样我们可以根据栈顶元素找到格式化字符串的位置。这种方法需要计算所有参数的总长度,也比较麻烦。。。
疑问
- 做问题3有个疑问:cprintf的第一个参数(也就是格式化字符串)存储在内存中的哪个位置?