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); }
在上面的代碼中我發現兩點,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 = 0; !(inb(COM1 + COM_LSR) & COM_LSR_TXRDY) && i < 12800; i++) delay(); outb(COM1 + COM_TX, c); }
其中包括了一些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 = 0; !(inb(0x378+1) & 0x80) && i < 12800; i++) delay(); outb(0x378+0, c); outb(0x378+2, 0x08|0x04|0x01); outb(0x378+2, 0x08); }
它的功能在注釋里面已經很清楚了,就是把這個字符輸出給並口設備。為什么這樣做也不清楚。
最后一個程序,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 > 0) { 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, 14); outb(addr_6845 + 1, crt_pos >> 8); outb(addr_6845, 15); outb(addr_6845 + 1, crt_pos); }
這個程序的功能根據名稱就能才出來了,肯定是把字符輸出到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目錄下
1 void 2 vprintfmt(void (*putch)(int, void*), void *putdat, const char *fmt, va_list ap) 3 { 4 register const char *p; 5 register int ch, err; 6 unsigned long long num; 7 int base, lflag, width, precision, altflag; 8 char padc; 9 10 while (1) { 11 while ((ch = *(unsigned char *) fmt++) != '%') { 12 if (ch == '\0') 13 return; 14 putch(ch, putdat); 15 } 16 17 // Process a %-escape sequence 18 padc = ' '; 19 width = -1; 20 precision = -1; 21 lflag = 0; 22 altflag = 0; 23 reswitch: 24 switch (ch = *(unsigned char *) fmt++) { 25 26 // flag to pad on the right 27 case '-': 28 padc = '-'; 29 goto reswitch; 30 31 // flag to pad with 0's instead of spaces 32 case '0': 33 padc = '0'; 34 goto reswitch; 35 36 // width field 37 case '1': 38 case '2': 39 case '3': 40 case '4': 41 case '5': 42 case '6': 43 case '7': 44 case '8': 45 case '9': 46 for (precision = 0; ; ++fmt) { 47 precision = precision * 10 + ch - '0'; 48 ch = *fmt; 49 if (ch < '0' || ch > '9') 50 break; 51 } 52 goto process_precision; 53 54 case '*': 55 precision = va_arg(ap, int); 56 goto process_precision; 57 58 case '.': 59 if (width < 0) 60 width = 0; 61 goto reswitch; 62 63 case '#': 64 altflag = 1; 65 goto reswitch; 66 67 process_precision: 68 if (width < 0) 69 width = precision, precision = -1; 70 goto reswitch; 71 72 // long flag (doubled for long long) 73 case 'l': 74 lflag++; 75 goto reswitch; 76 77 // character 78 case 'c': 79 putch(va_arg(ap, int), putdat); 80 break; 81 82 // error message 83 case 'e': 84 err = va_arg(ap, int); 85 if (err < 0) 86 err = -err; 87 if (err >= MAXERROR || (p = error_string[err]) == NULL) 88 printfmt(putch, putdat, "error %d", err); 89 else 90 printfmt(putch, putdat, "%s", p); 91 break; 92 93 // string 94 case 's': 95 if ((p = va_arg(ap, char *)) == NULL) 96 p = "(null)"; 97 if (width > 0 && padc != '-') 98 for (width -= strnlen(p, precision); width > 0; width--) 99 putch(padc, putdat); 100 for (; (ch = *p++) != '\0' && (precision < 0 || --precision >= 0); width--) 101 if (altflag && (ch < ' ' || ch > '~')) 102 putch('?', putdat); 103 else 104 putch(ch, putdat); 105 for (; width > 0; width--) 106 putch(' ', putdat); 107 break; 108 109 // (signed) decimal 110 case 'd': 111 num = getint(&ap, lflag); 112 if ((long long) num < 0) { 113 putch('-', putdat); 114 num = -(long long) num; 115 } 116 base = 10; 117 goto number; 118 119 // unsigned decimal 120 case 'u': 121 num = getuint(&ap, lflag); 122 base = 10; 123 goto number; 124 125 // (unsigned) octal 126 case 'o': 127 // Replace this with your code. 128 putch('X', putdat); 129 putch('X', putdat); 130 putch('X', putdat); 131 break; 132 133 // pointer 134 case 'p': 135 putch('0', putdat); 136 putch('x', putdat); 137 num = (unsigned long long) 138 (uintptr_t) va_arg(ap, void *); 139 base = 16; 140 goto number; 141 142 // (unsigned) hexadecimal 143 case 'x': 144 num = getuint(&ap, lflag); 145 base = 16; 146 number: 147 printnum(putch, putdat, num, base, width, padc); 148 break; 149 150 // escaped '%' character 151 case '%': 152 putch(ch, putdat); 153 break; 154 155 // unrecognized escape sequence - just print it literally 156 default: 157 putch('%', putdat); 158 for (fmt--; fmt[-1] != '%'; fmt--) 159 /* do nothing */; 160 break; 161 } 162 } 163 }
具體子程序中大致每段代碼中在做什么我在上面的代碼中已經注釋了,這里總結下
這個程序包含4個輸入參數:
(1)void (*putch)(int, void*):
這個參數是一個函數指針,這類函數包含兩個輸入參數int, void*,int參數代表一個要輸出的字符的值。void* 則代表要把這個字符輸出的位置的地址,但是這里void *參數的值並不是這個地址,而是這個地址的值被存放到的存儲單元的地址。比如我想把一個字符值為0x30的字符('0')輸出到地址0x01處,此時我們的程序應該如下圖所示:
1 int addr = 0x01; 2 int ch = 0x30; 3 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',來書寫八進制的,具體原理可以看上面代碼的備注,我填寫代碼如下:
1 ... 2 case 'o': 3 // Replace this with your code. 4 putch('0', putdat); 5 num = getuint(&ap, lflag); 6 base = 8; 7 goto number; 8 ...
注:這個子程序里面涉及到一個非常重要的子函數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),第一個參數為輸出的格式字符串,而后面就是我要輸出的一些參數。
1 int 2 cprintf(const char *fmt, ...) 3 { 4 va_list ap; 5 int cnt; 6 7 va_start(ap, fmt); 8 cnt = vcprintf(fmt, ap); 9 va_end(ap); 10 11 return cnt; 12 }
它是如何實現的呢,我們在它的內部可以看到,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程序的功能,
1 static void 2 putch(int ch, int *cnt) 3 { 4 cputchar(ch); 5 *cnt++; 6 }
它調用了我們最開始分析的子程序,cputchar,這個子程序可以把字符輸出到屏幕上。所以這個putch子程序是滿足vprintfmt子程序的要求的~可以作為參數傳遞給它。
最后再看第二個參數,這個參數在這里就不具備內存地址的含義了,我們看到在putch里面,它只是把字符輸出給屏幕,然后把這個cnt加1,並沒有把字符存放到cnt所指向的地址處,所以這個cnt就變成了一個計數器。記錄已經輸出了多少的字符。
以上就是我對這個練習中涉及到的3個文件的分析~以及練習習題的解答
老規矩,有錯誤歡迎指出,有問題歡迎騷擾~
zzqwf12345@163.com
