程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> [轉]printf 函數實現的深入剖析

[轉]printf 函數實現的深入剖析

編輯:關於C語言

研究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後面地址
    的內容。
   
    這樣就存在一個可能的緩沖區溢出問題。。。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved