MIT 6.828 JOS学习笔记11 Exercise 1.8

时间:2023-03-09 04:29:02
MIT 6.828 JOS学习笔记11 Exercise 1.8

Exercise 1.8

      我们丢弃了一小部分代码---即当我们在printf中指定输出"%o"格式的字符串,即八进制格式的代码。尝试去完成这部分程序。

解答:

  在这个练习中我们首先要阅读以下三个源文件的代码,弄清楚他们三者之间的关系:

  三个文件分别为 \kern\printf.c,\kern\console.c, \lib\printfmt.c

  首先大致浏览三个源文件,其中粗略的观察到3点:

     1.\kern\printf.c中的cprintf,vcprintf子程序调用了\lib\printfmt.c中的vprintfmt子程序。

       2.\kern\printf.c中的putch子程序中调用了cputchar,这个程序是定义在\kern\console.c中的。

     3.\lib\printfmt.c中的某些程序也依赖于cputchar子程序

    所以得出结论,\kern\printf.c\lib\printfmt.c两个文件的功能依赖于\kern\console.c的功能。所以我们就先探究一下\kern\console.c

  1.\kern\console.c

   这个文件中定义了如何把一个字符显示到console上,即我们的显示屏之上,里面包括很多对IO端口的操作。

   其中我们最感兴趣的自然就是cputchar子程序了。下面是这个程序的代码

// `High'-level console I/O.  Used by readline and cprintf.
void
cputchar(int c)
{
cons_putc(c);
} // output a character to the console
static void
cons_putc(int c)
{
serial_putc(c);
lpt_putc(c);
cga_putc(c);
}

cputchar

   在上面的代码中我发现两点,1.cputchar代码的注释中说:这个程序时最高层的console的IO控制程序,2.cputchar的实现其实是通过调用cons_putc完成的。

   cons_putc程序的功能在它的备注中已经被叙述的很清楚了,即输出一个字符到控制台(计算机的屏幕)。所以我们就知道了cputchar的功能也是向屏幕上输出一个字符。

   下面我们具体看下cons_putc子程序,这段如果不感兴趣可以略过,直接看对\lib\printfmt.c文件的分析。

   cons_putc子程序中包含3个子程序,我们分别看下,首先是serial_putc子程序:

#define COM1        0x3F8
#define COM_TX 0 // Out: Transmit buffer (DLAB=0)
#define COM_LSR 5 // In: Line Status Register
#define COM_LSR_TXRDY 0x20 // Transmit buffer avail static void
serial_putc(int c)
{
int i; for (i = ;
!(inb(COM1 + COM_LSR) & COM_LSR_TXRDY) && i < ;
i++)
delay(); outb(COM1 + COM_TX, c);
}

serial_putc

  其中包括了一些IO端口程序,通过代码中的宏定义我们知道它是在控制0x3f8端口,这个端口我们在 http://bochs.sourceforge.net/techspec/PORTS.LST 中查询可以看到,它是属于控制计算机中的串口的。我们在观察一下子程序中的inb指令和outb指令,他们分别控制了两个端口,COM1 + COM_LSR = 0x3f8+5 = 0x3fd端口和COM1 + COM_TX = 0x3f8+0 = 0x3f8端口。查询上面的链接看到两个端口的定义:

  MIT 6.828 JOS学习笔记11 Exercise 1.8

  MIT 6.828 JOS学习笔记11 Exercise 1.8

  从上面的图片中我们可以知道,inb指令是读取0x3fd端口,即line status registers,的内容,并且判断它的bit5是否为1,即发送数据缓冲寄存器是否为空。如果为空,则计算机可以发送下一个数据给端口。

  而outb指令则是把要发送的数据c,发送给0x3f8,从上图中可见,当0x3f8端口被写入值时,他是作为发送数据缓冲寄存器的,里面存放要发送给串口的数据。

  所以serial_putc子程序的功能是把一个字符输出给串口。至于为什么要这么做我还没有想明白。

再考虑下一个子程序,lpt_putc,代码如下:

/***** Parallel port output code *****/
// For information on PC parallel port programming, see the class References
// page. static void
lpt_putc(int c)
{
int i; for (i = ; !(inb(0x378+) & 0x80) && i < ; i++)
delay();
outb(0x378+, c);
outb(0x378+, 0x08|0x04|0x01);
outb(0x378+, 0x08);
}

lpt_putc

  它的功能在注释里面已经很清楚了,就是把这个字符输出给并口设备。为什么这样做也不清楚。

  最后一个程序,cga_putc:

static void
cga_putc(int c)
{
// if no attribute given, then use black on white
if (!(c & ~0xFF))
c |= 0x0700; switch (c & 0xff) {
case '\b':
if (crt_pos > ) {
crt_pos--;
crt_buf[crt_pos] = (c & ~0xff) | ' ';
}
break;
case '\n':
crt_pos += CRT_COLS;
/* fallthru */
case '\r':
crt_pos -= (crt_pos % CRT_COLS);
break;
case '\t':
cons_putc(' ');
cons_putc(' ');
cons_putc(' ');
cons_putc(' ');
cons_putc(' ');
break;
default:
crt_buf[crt_pos++] = c; /* write the character */
break;
} // What is the purpose of this?
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;
} /* move that little blinky thing */
outb(addr_6845, );
outb(addr_6845 + , crt_pos >> );
outb(addr_6845, );
outb(addr_6845 + , crt_pos);
}

cga_putc

  这个程序的功能根据名称就能才出来了,肯定是把字符输出到cga设备上面,即计算机的显示屏。至于里面的代码的含义,也比较好理解,它定义了一个缓冲区,缓冲区的当且显示内容的最后一个字符的指针就是crt_pos,所以当你新输入一个字符时,你必须根据字符值的值,来输出正确的内容给这个缓冲区,然后缓冲区的内容才能正确的显示在屏幕上。

  比如第8行当c为'\b'时,代表是输入了退格,所以此时要把缓冲区最后一个字节的指针减一,相当于丢弃当前最后一个输入的字符。当c为'\t'时,我要输出5个空格给缓冲区。如果不是特殊字符,那么就把字符的内容直接输入到缓冲区。

  而switch之后的if判断语句的功能应该是保证缓冲区中的最后显示出去的内容的大小不要超过显示的大小界限CRT_SIZE。

  最后四句则是把缓冲区的内容输出给显示屏。

  以上就是对cputchar子程序和console.c文件的分析。

  2.\lib\printfmt.c

  首先看一下这个文件刚开头的注释:

  "打印各种样式的字符串的子程序,经常被printf,sprintf,fprintf函数所调用,这些代码是同时被内核和用户程序所使用的。"

  通过这个注释我们知道,这个文件中定义的子程序是我们能在编程时直接利用printf函数向屏幕输出信息的关键。

  那么我们把目光锁定到被其他文件依赖的vprintfmt子程序,下面的这个版本是我已经加过注释,并且补充了一部分的版本,你还可以在lab/Lab1目录下找到它,名字为printfmt.c,而原来没修改过没备注的在lab/lib目录下

 void
vprintfmt(void (*putch)(int, void*), void *putdat, const char *fmt, va_list ap)
{
register const char *p;
register int ch, err;
unsigned long long num;
int base, lflag, width, precision, altflag;
char padc; while () {
while ((ch = *(unsigned char *) fmt++) != '%') {
if (ch == '\0')
return;
putch(ch, putdat);
} // Process a %-escape sequence
padc = ' ';
width = -;
precision = -;
lflag = ;
altflag = ;
reswitch:
switch (ch = *(unsigned char *) fmt++) { // flag to pad on the right
case '-':
padc = '-';
goto reswitch; // flag to pad with 0's instead of spaces
case '':
padc = '';
goto reswitch; // width field
case '':
case '':
case '':
case '':
case '':
case '':
case '':
case '':
case '':
for (precision = ; ; ++fmt) {
precision = precision * + ch - '';
ch = *fmt;
if (ch < '' || ch > '')
break;
}
goto process_precision; case '*':
precision = va_arg(ap, int);
goto process_precision; case '.':
if (width < )
width = ;
goto reswitch; case '#':
altflag = ;
goto reswitch; process_precision:
if (width < )
width = precision, precision = -;
goto reswitch; // long flag (doubled for long long)
case 'l':
lflag++;
goto reswitch; // character
case 'c':
putch(va_arg(ap, int), putdat);
break; // error message
case 'e':
err = va_arg(ap, int);
if (err < )
err = -err;
if (err >= MAXERROR || (p = error_string[err]) == NULL)
printfmt(putch, putdat, "error %d", err);
else
printfmt(putch, putdat, "%s", p);
break; // string
case 's':
if ((p = va_arg(ap, char *)) == NULL)
p = "(null)";
if (width > && padc != '-')
for (width -= strnlen(p, precision); width > ; width--)
putch(padc, putdat);
for (; (ch = *p++) != '\0' && (precision < || --precision >= ); width--)
if (altflag && (ch < ' ' || ch > '~'))
putch('?', putdat);
else
putch(ch, putdat);
for (; width > ; width--)
putch(' ', putdat);
break; // (signed) decimal
case 'd':
num = getint(&ap, lflag);
if ((long long) num < ) {
putch('-', putdat);
num = -(long long) num;
}
base = ;
goto number; // unsigned decimal
case 'u':
num = getuint(&ap, lflag);
base = ;
goto number; // (unsigned) octal
case 'o':
// Replace this with your code.
putch('X', putdat);
putch('X', putdat);
putch('X', putdat);
break; // pointer
case 'p':
putch('', putdat);
putch('x', putdat);
num = (unsigned long long)
(uintptr_t) va_arg(ap, void *);
base = ;
goto number; // (unsigned) hexadecimal
case 'x':
num = getuint(&ap, lflag);
base = ;
number:
printnum(putch, putdat, num, base, width, padc);
break; // escaped '%' character
case '%':
putch(ch, putdat);
break; // unrecognized escape sequence - just print it literally
default:
putch('%', putdat);
for (fmt--; fmt[-] != '%'; fmt--)
/* do nothing */;
break;
}
}
}

vprintfmt

  具体子程序中大致每段代码中在做什么我在上面的代码中已经注释了,这里总结下

  这个程序包含4个输入参数:

  (1)void (*putch)(int, void*):

  这个参数是一个函数指针,这类函数包含两个输入参数int, void*,int参数代表一个要输出的字符的值。void* 则代表要把这个字符输出的位置的地址,但是这里void *参数的值并不是这个地址,而是这个地址的值被存放到的存储单元的地址。比如我想把一个字符值为0x30的字符('0')输出到地址0x01处,此时我们的程序应该如下图所示:

 int addr = 0x01;
int ch = 0x30;
putch(ch, &addr);

  之所以这样做,就是因为这个子程序能够实现,把值存放到这个地址后,地址数自动增加1,即上面的代码执行完后,0x01内存处的值变为0x30,addr的值变为0x02.

  (2)void *putdat

  这个参数就是输入的字符要存放在的内存地址的指针,就是和上面putch函数的第二个输入参数是一个含义。

  (3)const char *fmt

  这个参数代表你在编写类似于printf这种格式化输出程序时,你指定格式的字符串,即printf函数的第一个输入参数,比如printf("This is %d test", n),这个子程序中,fmt就是"This is %d test"。

  (4)va_list ap

  这个参数代表的是多个输入参数,即printf子程序中从第二个参数开始之后的参数,比如("These are %d test and %d test", n, m),那么ap指的就是n,m

  

  那么这个函数的执行过程主要是一个while循环,分为以下几个步骤:

  (1)(源文件中第92~96行) 首先一个一个的输出格式字符串fmt中所有'%'之前的字符,因为它们就是要直接输出的,比如"This is %d test"中的"This is "。当然如果在把这些字符一个个输出中遇到结束符'\0',则结束输出。

  (2)(源文件中第98~243行) 剩余的代码都是在处理'%'符号后面的格式化输出,比如是%d,则按照十进制输出对应参数。另外还有一些其他的特殊字符比如'%5d'代表显示5位,其中的5要特殊处理。具体的含义在上面的代码中有备注。

   而这个程序也是正是这个练习让我们补充的地方,在源程序的第207行~212行,这里是要处理显示八进制的格式的时候的代码:

  我们可以参照上面显示无符号十进制的情况'u',或者十六进制的'x',来书写八进制的,具体原理可以看上面代码的备注,我填写代码如下:

 ...
case 'o':
// Replace this with your code.
putch('', putdat);
num = getuint(&ap, lflag);
base = ;
goto number;
...

  注:这个子程序里面涉及到一个非常重要的子函数va_arg(),其实与这个函数类似的还有2个,va_start(),va_end(),以及一个数据类型va_list。这个4个东西是为了计算机能够处理输入参数不固定的程序。比如下面这种程序的声明方式

     void fun(int arg_num, ...)

  其中arg_num,代表这个程序输入参数的个数(不包含arg_num本身),而后面的省略号则指代后续所有的输入参数,我们可以在程序中调用,如下

     fun(3, 10, 20, 30);

   这种能够处理可变个数输入参数的功能就是有va_list, va_arg(), va_start(), va_end()来实现的,大家可以看这篇博文 http://www.cnblogs.com/justinzhang/archive/2011/09/29/2195969.html 学习一下~

  

  3. \kern\printf.c

    下面查看一下最后一个文件,这个文件中定义的就是我们在编程中会用到的最顶层的一些格式化输出子程序,比如printf,sprintf等等。而在这个文件中定义了三个子程序。

     首先看一下最下面的cprintf子程序,它的输入是最接近于我们在编程中使用格式化输出子程序时的输入了,比如printf("This is %d test", n),第一个参数为输出的格式字符串,而后面就是我要输出的一些参数。

 int
cprintf(const char *fmt, ...)
{
va_list ap;
int cnt; va_start(ap, fmt);
cnt = vcprintf(fmt, ap);
va_end(ap); return cnt;
}

cprintf

     它是如何实现的呢,我们在它的内部可以看到,va_list,va_arg(), va_start(), va_end()这组操作的使用,前面我们刚刚说过,他们专门是来处理这种输入参数的个数不确定的情况。你最好还是先弄懂这个几个操作是如何配合使用的~

     在cprintf中我们发现,它利用va_list,va_arg(), va_start(), va_end()这些操作,把cprintf的fmt之后的输入参数都转换为va_list类型的一个参数,然后把fmt,和这个新生成的ap作为参数传递给vcprintf

     在vcprintf中我们发现,它就是调用了我们在上面仔细分析过的vprintfmt子程序,回顾一下,介绍vprintfmt子程序时,我们说过它有4个参数,如下

     (1)void (*putch)(int, void*):

     这个参数是一个函数指针,这类函数包含两个输入参数int, void*,int参数代表一个要输出的字符的值。void* 则代表要把这个字符输出的位置的地址

    (2)void *putdat

        这个参数就是输入的字符要存放在的内存地址的指针,就是和上面putch函数的第二个输入参数是一个含义。

    (3)const char *fmt

    这个参数代表你在编写类似于printf这种格式化输出程序时,你指定格式的字符串,即printf函数的第一个输入参数,比如printf("This is %d test", n),这个子程序中,fmt就是"This is %d test"。

     (4)va_list ap

     这个参数代表的是多个输入参数,即printf子程序中从第二个参数开始之后的参数,比如("These are %d test and %d test", n, m),那么ap指的就是n,m

    我们可以发现,刚刚得到的fmt和ap正好可以被放在第3和第4个输入参数处!

    另外再看头两个参数,第一个参数是一个函数指针,这个函数必须能够实现把一个字符输出到某个地址处的功能。再看一下vcprintf中它赋给vprintfmt子程序的第一个参数是这个文件中的第一个子程序putch。

    我们再看一下这个putch程序的功能,

 static void
putch(int ch, int *cnt)
{
cputchar(ch);
*cnt++;
}

putch

    它调用了我们最开始分析的子程序,cputchar,这个子程序可以把字符输出到屏幕上。所以这个putch子程序是满足vprintfmt子程序的要求的~可以作为参数传递给它。

    最后再看第二个参数,这个参数在这里就不具备内存地址的含义了,我们看到在putch里面,它只是把字符输出给屏幕,然后把这个cnt加1,并没有把字符存放到cnt所指向的地址处,所以这个cnt就变成了一个计数器。记录已经输出了多少的字符。

    

  以上就是我对这个练习中涉及到的3个文件的分析~以及练习习题的解答

  

  老规矩,有错误欢迎指出,有问题欢迎骚扰~

    zzqwf12345@163.com