zoukankan      html  css  js  c++  java
  • printf函数实现的深入剖析

    http://blog.tianya.cn/blogger/post_show.asp?BlogID=462085&PostID=8363874

    研究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)是什么函数。
      
      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字符数组中的第一个元素
       ;这个函数的功能就是不断的打印出字符,直到遇到:''
       ;[gs:edi]对应的是0x80000h:0采用直接写显存的方法显示字符串
       xor si,si
       mov ah,0Fh
       mov al,[ebx+si]
       cmp al,''
       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 != ''; 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 '':
       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 != ''; 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! ");
       }
       if (c == ' ')
       {
       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后面地址
      的内容。
      
      这样就存在一个可能的缓冲区溢出问题。。。
      下次有时间再写^_^
      
      有任何问题可以和我联系讨论:aisan215@126.com
    <script>window._bd_share_config={"common":{"bdSnsKey":{},"bdText":"","bdMini":"2","bdMiniList":false,"bdPic":"","bdStyle":"0","bdSize":"16"},"share":{}};with(document)0[(getElementsByTagName('head')[0]||body).appendChild(createElement('script')).src='http://bdimg.share.baidu.com/static/api/js/share.js?v=89860593.js?cdnversion='+~(-new Date()/36e5)];</script>
    阅读(890) | 评论(0) | 转发(0) |
    给主人留下些什么吧!~~
    评论热议
  • 相关阅读:
    echarts中图表过于靠左或靠右的情况解决办法。
    C#语法糖大汇总【转发】
    近期对于windows服务的理解
    解决echarts中X轴文字过长的问题。【转】
    两个页面之间通过后台处理,调用父窗体方法。
    docker
    docker php
    webpack
    jwt 解密
    阿里云服务器 ECS Linux 主机删除文件后磁盘空间显示不变(转载https://www.zhanqunfuwuqi.com/archives/5293)
  • 原文地址:https://www.cnblogs.com/ztguang/p/12647664.html
Copyright © 2011-2022 走看看