研究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)是什麼函數。
我們還是先不看看它的具體內容。
想想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後面地址
的內容。
這樣就存在一個可能的緩沖區溢出問題。。。