研究printf的實現,首先來看看printf函數的函數體
int printf(const char *fmt, ...)
{
int i;
char buf[256];
va_list arg = (va_list)((char*)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
代碼位置:D:/~/funny/kernel/printf.c
在形參列表裏有這麼一個token:...
這個是可變形參的一種寫法。
當傳遞參數的個數不肯定時,就能夠用這種方式來表示。
很顯然,咱們須要一種方法,來讓函數體能夠知道具體調用時參數的個數。
先來看printf函數的內容:
這句:
va_list arg = (va_list)((char*)(&fmt) + 4);
va_list的定義:
typedef char *va_list
這說明它是一個字符指針。
其中的: (char*)(&fmt) + 4) 表示的是...中的第一個參數。
若是不懂,我再慢慢的解釋:
C語言中,參數壓棧的方向是從右往左。
也就是說,當調用printf函數的適合,先是最右邊的參數入棧。
fmt是一個指針,這個指針指向第一個const參數(const char *fmt)中的第一個元素。
fmt也是個變量,它的位置,是在棧上分配的,它也有地址。
對於一個char *類型的變量,它入棧的是指針,而不是這個char *型變量。
換句話說:
你sizeof(p) (p是一個指針,假設p=&i,i爲任何類型的變量均可以)
獲得的都是一個固定的值。(個人計算機中都是獲得的4)
固然,我還要補充的一點是:棧是從高地址向低地址方向增加的。
ok!
如今我想你該明白了:爲何說(char*)(&fmt) + 4) 表示的是...中的第一個參數的地址。
下面咱們來看看下一句:
i = vsprintf(buf, fmt, arg);
讓咱們來看看vsprintf(buf, fmt, arg)是什麼函數。
linux
int vsprintf(char *buf, const char *fmt, va_list args)
{
char* p;
char tmp[256];
va_list p_next_arg = args;
for (p=buf;*fmt;fmt++) {
if (*fmt != '%') {
*p++ = *fmt;
continue;
}
fmt++;
switch (*fmt) {
case 'x':
itoa(tmp, *((int*)p_next_arg));
strcpy(p, tmp);
p_next_arg += 4;
p += strlen(tmp);
break;
case 's':
break;
default:
break;
}
}
return (p - buf);
}
咱們仍是先不看看它的具體內容。 想一想printf要左什麼吧 它接受一個格式化的命令,並把指定的匹配的參數格式化輸出。 ok,看看i = vsprintf(buf, fmt, arg); vsprintf返回的是一個長度,我想你已經猜到了:是的,返回的是要打印出來的字符串的長度 其實看看printf中後面的一句:write(buf, i);你也該猜出來了。 write,顧名思義:寫操做,把buf中的i個元素的值寫到終端。 因此說:vsprintf的做用就是格式化。它接受肯定輸出格式的格式字符串fmt。用格式字符串對個數變化的參數進行格式化,產生格式化輸出。 我代碼中的vsprintf只實現了對16進制的格式化。 你只要明白vsprintf的功能是什麼,就會很容易弄懂上面的代碼。 下面的write(buf, i);的實現就有點複雜了 若是你是os,一個用戶程序須要你打印一些數據。很顯然:打印的最底層操做確定和硬件有關。 因此你就必須得對程序的權限進行一些限制: 讓咱們假設個情景: 一個應用程序對你說:os先生,我須要把存在buf中的i個數據打印出來,能夠幫我麼? os說:好的,咱倆誰跟誰,沒問題啦!把buf給我吧。 而後,os就把buf拿過來。交給本身的小弟(和硬件操做的函數)來完成。 只好通知這個應用程序:兄弟,你的事我辦的妥穩當當!(os果真大大的狡猾 ^_^) 這樣 應用程序就不會取得一些超級權限,防止它作一些違法的事。(安全啊安全) 讓咱們追蹤下write吧: write: mov eax, _NR_write mov ebx, [esp + 4] mov ecx, [esp + 8] int INT_VECTOR_SYS_CALL 位置:d:~/kernel/syscall.asm 這裏是給幾個寄存器傳遞了幾個參數,而後一個int結束 想一想咱們彙編裏面學的,好比返回到dos狀態: 咱們這樣用的 mov ax,4c00h int 21h 爲何用後面的int 21h呢? 這是爲了告訴編譯器:號外,號外,我要按照給你的方式(傳遞的各個寄存器的值)變形了。 編譯器一查表:哦,你是要變成這個樣子啊。no problem! 其實這麼說並不嚴緊,若是你看了一些關於保護模式編程的書,你就會知道,這樣的int表示要調用中斷門了。經過中斷門,來實現特定的系統服務。 咱們能夠找到INT_VECTOR_SYS_CALL的實現: init_idt_desc(INT_VECTOR_SYS_CALL, DA_386IGate, sys_call, PRIVILEGE_USER); 位置:d:~/kernel/protect.c 若是你不懂,不要緊,你只須要知道一個int INT_VECTOR_SYS_CALL表示要經過系統來調用sys_call這個函數。(從上面的參數列表中也該可以猜出大概) 好了,再來看看sys_call的實現: sys_call: call save push dword [p_proc_ready] sti push ecx push ebx call [sys_call_table + eax * 4] add esp, 4 * 3 mov [esi + EAXREG - P_STACKBASE], eax cli ret 位置:~/kernel/kernel.asm 一個call save,是爲了保存中斷前進程的狀態。 靠! 太複雜了,若是詳細的講,設計到的東西實在太多了。 我只在意我所在意的東西。sys_call實現很麻煩,咱們不妨不分析funny os這個操做系統了 先假設這個sys_call就一單純的小女孩。她只有實現一個功能:顯示格式化了的字符串。 這樣,若是隻是理解printf的實現的話,咱們徹底能夠這樣寫sys_call: sys_call: ;ecx中是要打印出的元素個數 ;ebx中的是要打印的buf字符數組中的第一個元素 ;這個函數的功能就是不斷的打印出字符,直到遇到:'\0' ;[gs:edi]對應的是0x80000h:0採用直接寫顯存的方法顯示字符串 xor si,si mov ah,0Fh mov al,[ebx+si] cmp al,'\0' je .end mov [gs:edi],ax inc si loop: sys_call .end: ret ok!就這麼簡單! 恭喜你,重要弄明白了printf的最最底層的實現! 若是你有機會看linux的源代碼的話,你會發現,其實它的實現也是這種思路。 freedos的實現也是這樣 好比在linux裏,printf是這樣表示的: static int printf(const char *fmt, ...) { va_list args; int i; va_start(args, fmt); write(1,printbuf,i=vsprintf(printbuf, fmt, args)); va_end(args); return i; } va_start va_end 這兩個函數在個人blog裏有解釋,這裏就很少說了 它裏面的vsprintf和咱們的vsprintf是同樣的功能。 不過它的write和咱們的不一樣,它還有個參數:1 這裏我能夠告訴你:1表示的是tty所對應的一個文件句柄。 在linux裏,全部設備都是被看成文件來看待的。你只須要知道這個1就是表示往當前顯示器裏寫入數據 在freedos裏面,printf是這樣的: int VA_CDECL printf(const char *fmt, ...) { va_list arg; va_start(arg, fmt); charp = 0; do_printf(fmt, arg); return 0; } 看起來彷佛是do_printf實現了格式化和輸出。 咱們來看看do_printf的實現: STATIC void do_printf(CONST BYTE * fmt, va_list arg) { int base; BYTE s[11], FAR * p; int size; unsigned char flags; for (;*fmt != '\0'; fmt++) { if (*fmt != '%') { handle_char(*fmt); continue; } fmt++; flags = RIGHT; if (*fmt == '-') { flags = LEFT; fmt++; } if (*fmt == '0') { flags |= ZEROSFILL; fmt++; } size = 0; while (1) { unsigned c = (unsigned char)(*fmt - '0'); if (c > 9) break; fmt++; size = size * 10 + c; } if (*fmt == 'l') { flags |= LONGARG; fmt++; } switch (*fmt) { case '\0': va_end(arg); return; case 'c': handle_char(va_arg(arg, int)); continue; case 'p': { UWORD w0 = va_arg(arg, unsigned); char *tmp = charp; sprintf(s, "%04x:%04x", va_arg(arg, unsigned), w0); p = s; charp = tmp; break; } case 's': p = va_arg(arg, char *); break; case 'F': fmt++; /* we assume %Fs here */ case 'S': p = va_arg(arg, char FAR *); break; case 'i': case 'd': base = -10; goto lprt; case 'o': base = 8; goto lprt; case 'u': base = 10; goto lprt; case 'X': case 'x': base = 16; lprt: { long currentArg; if (flags & LONGARG) currentArg = va_arg(arg, long); else { currentArg = va_arg(arg, int); if (base >= 0) currentArg = (long)(unsigned)currentArg; } ltob(currentArg, s, base); p = s; } break; default: handle_char('?'); handle_char(*fmt); continue; } { size_t i = 0; while(p[i]) i++; size -= i; } if (flags & RIGHT) { int ch = ' '; if (flags & ZEROSFILL) ch = '0'; for (; size > 0; size--) handle_char(ch); } for (; *p != '\0'; p++) handle_char(*p); for (; size > 0; size--) handle_char(' '); } va_end(arg); } 這個就是比較完整的格式化函數 裏面屢次調用一個函數:handle_char 來看看它的定義: STATIC VOID handle_char(COUNT c) { if (charp == 0) put_console(c); else *charp++ = c; } 裏面又調用了put_console 顯然,從函數名就能夠看出來:它是用來顯示的 void put_console(int c) { if (buff_offset >= MAX_BUFSIZE) { buff_offset = 0; printf("Printf buffer overflow!\n"); } if (c == '\n') { buff[buff_offset] = 0; buff_offset = 0; #ifdef __TURBOC__ _ES = FP_SEG(buff); _DX = FP_OFF(buff); _AX = 0x13; __int__(0xe6); #elif defined(I86) asm { push ds; pop es; mov dx, offset buff; mov ax, 0x13; int 0xe6; } #endif } else { buff[buff_offset] = c; buff_offset++; } } 注意:這裏用遞規調用了printf,不過此次沒有格式化,因此不會出現死循環。 好了,如今你該更清楚的知道:printf的實現了 如今再說另外一個問題: 不管如何printf()函數都不能肯定參數...究竟在什麼地方結束,也就是說,它不知 道參數的個數。它只會根據format中的打印格式的數目依次打印堆棧中參數format後面地址 的內容。 這樣就存在一個可能的緩衝區溢出問題。。。