MIT 6.828 JOS学习笔记11 Exercise 1.8
Exercise 1.8
解答:
在这个练习中我们首先要阅读以下三个源文件的代码,弄清楚他们三者之间的关系:
三个文件分别为 \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端口。查询上面的链接看到两个端口的定义:
从上面的图片中我们可以知道,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
MIT 6.828 JOS学习笔记11 Exercise 1.8的更多相关文章
- MIT 6.828 JOS学习笔记5. Exercise 1.3
Lab 1 Exercise 3 设置一个断点在地址0x7c00处,这是boot sector被加载的位置.然后让程序继续运行直到这个断点.跟踪/boot/boot.S文件的每一条指令,同时使用boo ...
- MIT 6.828 JOS学习笔记12 Exercise 1.9
Lab 1中Exercise 9的解答报告 Exercise 1.9: 判断一下操作系统内核是从哪条指令开始初始化它的堆栈空间的,以及这个堆栈坐落在内存的哪个地方?内核是如何给它的堆栈保留一块内存空间 ...
- MIT 6.828 JOS学习笔记13 Exercise 1.10
Lab 1 Exercise 10 为了能够更好的了解在x86上的C程序调用过程的细节,我们首先找到在obj/kern/kern.asm中test_backtrace子程序的地址, 设置断点,并且探讨 ...
- MIT 6.828 JOS学习笔记8. Exercise 1.4
Lab 1 Exercise 4 阅读关于C语言的指针部分的知识.最好的参考书自然是"The C Programming Language". 阅读5.1到5.5节.然后下载poi ...
- MIT 6.828 JOS学习笔记9. Exercise 1.5
Lab 1 Exercise 5 再一次追踪一下boot loader的一开始的几句指令,找到第一条满足如下条件的指令处: 当我修改了boot loader的链接地址,这个指令就会出现错误. 找到这样 ...
- MIT 6.828 JOS学习笔记3. Exercise 1.2
这篇博文是对Lab 1中的Exercise 2的解答~ Lab 1 Exercise 2: 使用GDB的'si'命令,去追踪ROM BIOS几条指令,并且试图去猜测,它是在做什么.但是不需要把每个细节 ...
- MIT 6.828 JOS学习笔记2. Lab 1 Part 1.2: PC bootstrap
Lab 1 Part 1: PC bootstrap 我们继续~ PC机的物理地址空间 这一节我们将深入的探究到底PC是如何启动的.首先我们看一下通常一个PC的物理地址空间是如何布局的: ...
- MIT 6.828 JOS学习笔记0. 写在前面的话
0. 简介 操作系统是计算机科学中十分重要的一门基础学科,是一名计算机专业毕业生必须要具备的基础知识.但是在学习这门课时,如果仅仅把目光停留在课本上一些关于操作系统概念上的叙述,并不能对操作系统有着深 ...
- MIT 6.828 JOS学习笔记10. Lab 1 Part 3: The kernel
Lab 1 Part 3: The kernel 现在我们将开始具体讨论一下JOS内核了.就像boot loader一样,内核开始的时候也是一些汇编语句,用于设置一些东西,来保证C语言的程序能够正确的 ...
随机推荐
- SQL Server基线算法(同比和环比)
基线介绍 基线为历史数据统计而成的数据,具有参考价值,并利用基线数据与当前值的对比,通过一定的报警机制,形成实时监控架构.SQL Server计数器采用同比和环比两种方式. 同比:可以计算未来一周的基 ...
- javascript数组array
注意:1.array的length不是只读的.可以从数组的末尾移出项或者向数组中添加新项.看下面例子: var colors = ["red","yellow" ...
- Linux系统启动过程
1. 从BIOS到KERNEL BIOS自检->MBR(GRUB)->KERNEL->KERNEL自解压->内核初始化->内核启动 BIOS自检 当电脑开机的时候,电脑会 ...
- java解析xml的三种方法
java解析XML的三种方法 1.SAX事件解析 package com.wzh.sax; import org.xml.sax.Attributes; import org.xml.sax.SAXE ...
- highlight高亮风格
highlight代码高亮的style有很多个,今天闲着没事,突然想看看各个style的效果.列在这里,以后想用的时候看看. ------------------------------------- ...
- Java Bean、POJO、 Entity、 VO 、PO、DAO
Java Bean.POJO. Entity. VO , 其实都是java 对象,只不过用于不同场合罢了. Java Bean: 就是一个普通的Java 对象, 只不过是加了一些约束条件. 声 ...
- Idea+TestNg配置test-output输出
说明:testNG的工程我是使用eclipse创建的,直接导入到idea中,运行test时不会生产test-output,只能在idea的控制台中查看运行结果,然后到处报告,经过不懈的百度终于找到怎么 ...
- Web Config配置备忘
数据压缩 <httpCompression>节点用于配置静态压缩和动态压缩,<urlCompression>则用于开关 http压缩 <urlCompression do ...
- Merge K Sorted Arrays
This problem can be solved by using a heap. The time is O(nlog(n)). Given m arrays, the minimum elem ...
- C++ 各种基本类型间的转换
常用的转换方法: 流转换 STL标准函数库中函数转换 流转换 流转换主要是用到了<sstream>库中的stringstream类. 通过stringstream可以完成基本类型间的转换, ...