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

研究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後面地址     的內容。         這樣就存在一個可能的緩衝區溢出問題。。。
相關文章
相關標籤/搜索