exercise 1.8
要求:
补充printfmt.c line 208处的代码,让程序能够打印出八进制形式数。
- 解释printf.c和console.c之间的接口,特别是,console提供了什么函数?这个函数是怎样被printf.c使用的?
- 解释下面这段来自console.c的代码片段。
if (crt_pos >= CRT_SIZE) {
int i;
memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
crt_buf[i] = 0x0700 | ' ';
crt_pos -= CRT_COLS;
}
- 单步追踪下面的代码运行。
int x = 1, y = 3, z = 4;
cprintf("x %d, y %x, z %d\n", x, y, z);
- 在对cprintf()调用中,fmt指向什么?ap指向什么?
- 按照执行顺序列出每一个对cons_putc,va_arg,和vcprintf的调用。对于cons_putc,列出它的变量。对于va_arg,列出调用前和调用后ap指向什么。对于vcprintf,列出它的两个参数的变量。
- 运行下面的代码。
unsigned int i = 0x00646c72;
cprintf("H%x Wo%s", 57616, &i);
输出是什么?用上一个练习的方法,解释输出是怎样实现的。需要ASCII码表。
- 在下面的代码中,在’y='的后面将会打印什么?(注意:答案不是一个明确的数。)为什么这会发生?
cprintf("x=%d y=%d", 3);
- GCC改变了它调用习惯,它将变量按声明顺序压栈,所以最后一个变量被最后压栈。你应该怎样改变cprintf或者它的接口,才可以仍能传递可变个数个参数?
解答:
填代码:那个位置处的上下文如下:
// unsigned decimal
case 'u':
num = getuint(&ap, lflag);
base = 10;
goto number;
// (unsigned) octal
case 'o':
// Replace this with your code.
putch('X', putdat);
putch('X', putdat);
putch('X', putdat);
break;
这打印的三个叉就让我很迷。如果照着上文的格式填,这仨叉就不管了。或许这就对了吧。
case 'o':
num = getuint(&ap, lflag);
base = 8;
goto number;
putch('X', putdat);
putch('X', putdat);
putch('X', putdat);
break;
下面是其他习题答案。
-
用vscode很容易找到调用关系。接口是void cputchar(int c)。console提供了这个接口,以及接口调用的cons_putc()。printf通过将要输出的字符ASCII码作为整型参数传递给cputchar,在console.c内调用cons_putc()等完成输出工作。
-
这段函数的意思要结合上下文,以及存在与其他位置的定义来理解。部分显示输出的知识,来自之前读的《x86汇编语言:从实模式到保护模式》。
if (crt_pos >= CRT_SIZE) { int i; memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t)); for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++) crt_buf[i] = 0x0700 | ' '; crt_pos -= CRT_COLS; }
下面给出一些关键量的定义,都来自引用的其他代码。讲真,下面这些量都明白了,这段代码的意思就明白了。
参数:
crt_pos:当前输出位置指针,指向内存区中对应输出映射地址。
CRT_SIZE:是CRT_COLS和CRT_ROWS的乘积,即2000=80*25,是不翻页时一页屏幕最大能容纳的字数
crt_buf:输出缓冲区内存映射地址
CRT_COLS:默认输出格式下整个屏幕的列数,为80
CRT_ROWS:默认输出格式下整个屏幕的行数,为25
unit16_t:typedef unsigned short 正好两字节,可以分别用来表示当前要打印的字符ASCII码和打印格式属性。
函数:
memmove(): memmove(void *dst, const void *src, size_t n).意为将从src指向位置起的n字节数据送到dst指向位置,可以在两个区域重叠时复制。
好了,有了上述说明,函数意图就很明确了:若当前输出位置大于屏幕最大输出范围,则将缓冲区整体前移一行(整体前移80字,覆盖之前的内容),之后将最后一行置为全0x0,最后将当前输出位置上移一行(也是80字),从这一页之后移到这一页内。
还是很抽象是吧,其实就是完成了向下滚动一行的操作。。。不信细读读,看看是不是。
- 分问题回答
- fmt指向字符串"x %d, y %x, z %d\n",它本省就是一个指向字符串的指针。ap指向后面的变参。变参就是参数的个数可变,牵扯到标准库中的变量类型va_list,限于时间原因我没有继续研究,留待以后。
- cons_putc:调用顺序为cprintf->vcprintf->vprintfmt->putch->cputchar->cons_putc。cons_putc的作用是输出一个字符到控制台。参数是一个整型表示的ASCII码。
- va_arg:在vprintfmt中不是每个switch分支都调用了它,用vscode查看引用操作就能看到,太多了。在switch分支中有一个借口getint,也调用了va_arg。它的作用是将指针指向变参的下一个参数。则调用前,ap指向x,调用后,ap指向y。va_start宏识别并指向第一个变参,va_arg一个一个依次指向接下来的变参。详细概念可自行上网搜索这个结构体.这篇博客很有帮助 va_list/va_start/va_arg/va_end深入分析。
- vcprintf:只在cprintf被调用了一次。两个调用参数是常数字符型指针fmt,指向printf的字符串;va_list型变量ap,用于控制变参指针。
- 下面是输出结果
He110 World
要求运行代码,但是我花了接近两个小时都没有在原来的代码上运行出来,最后把cprintf换成了printf,打出来了。
输出很有意思。其实如果学了计组,这题不用做的。57616用十六进制打印符%x打印出来就是110。x86采用小端方式,这是计组的内容,不说了,科班的没理由不会。0x00646c72按字符串打印,64,6c,72分别代表字符d,l,r。小端方式低位字节放在低地址。输出时从低到高读,就是rld。
如果是大端方式的处理器,就改成0x72626400。57616不用改,处理器应该是自动识别整型变量并自动按自己的大小端方式存储。
- 显示的是一个变化的值,应该是个随机的地址之类的。因为之前已知,va_arg控制指向参数的指针,每次向后移动一个参数。这里后面没有正确的指向参数的指针了,则应该移动到了一个野指针,所以显示乱七八糟的值。
- 其实办法很简单,只需要我们手动把所有参数指针都倒置一遍就行了。。。
至于最后那个挑战,其实在《x86汇编语言:从实模式到保护模式》有很详细的讲解,讲了字节控制文字颜色显示等,照着书就能做出来。