[转]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)是什么函数。  
     
   
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后面地址
的内容。
    
    这样就存在一个可能的缓冲区溢出问题。。。
[转]printf 函数实现的深入剖析的更多相关文章
- C 中 关于printf 函数中度剖析
		
题外话 这篇博文主要围绕printf函数分析的,主要讲解printf 使用C的可变参数机制, printf是否可重入(是否线程安全), printf函数的源码实现. 正文 1.C中可变参数机制 我们 ...
 - 可变参数列表与printf()函数的实现
		
问题 当我们刚开始学习C语言的时候,就接触到printf()函数,可是当时"道行"不深或许不够细心留意,又或者我们理所当然地认为库函数规定这样就是这样,没有发现这个函数与普通的函数 ...
 - printf函数
		
printf函数的格式及含义 d 以十进制带符号的形式输出整数(对正数不输出符号) o 以八进制无符号的形式输出整数(不输出 ...
 - Linux Linux下特殊的printf函数和fputs函数
		
Linux下,printf函数必须以'\n'结尾才会立刻输出到屏幕,如果没有'\n'直到输出缓冲区满了以后才会打印到屏幕上(敲击换行也算),如果需要不换行的输出,一般可以使用write函数代替.'\n ...
 - 关于printf函数的所思所想
		
缘起大一下学期,C语言程序设计徐小青老师的随口一提,经娄嘉鹏老师提醒,我觉得应该自己整理清楚这一问题.涉及网上资料将会标明出处. 关于printf函数的所思所想 * printf的定义 printf( ...
 - C语言printf()函数:格式化输出函数
		
C语言printf()函数:格式化输出函数 头文件:#include <stdio.h> printf()函数是最常用的格式化输出函数,其原型为: int printf( char ...
 - 关于printf函数输出先后顺序的讲解!!
		
对于printf函数printf("%d%d\n",a,b);函数的实际输出顺序是这样的先计算出b,然后在计算a,接着输出a,最后在输出b:例子如下:#include<ios ...
 - printf()函数
		
printf()函数是格式化输出函数, 一般用于向标准输出设备按规定格式输出信息. printf()函数的调用格式为: printf("<格式化字符串>", <参 ...
 - printf函数重定向
		
printf函数底层会调用fputc函数 /*重定向c库函数printf到USART1*/ int fputc(int ch, FILE *f) { /*发送一个字节数据USART1 */ USART ...
 
随机推荐
- 我本人一直以来犯的错误,在看了《Think In Java》后才抓了出来(转)
			
也许你是只老鸟,也许你的程序编的很精,但是,在你的程序生活,你也许没有注意到一些“常识性”的问题,因为有些时候我们不需要去注意,我们的程序 照样能够运行得飞快,但是如果那天有一个无聊的人问你一个像这样 ...
 - Android 通过HTTP GET请求互联网数据
			
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); s ...
 - javascript 中字符串之比较
			
<script type="text/javascript"> var string1="apple"; var string2="Ban ...
 - Xcode Coule not launch "aaa" press launch failed:timed out waiting for app launch
			
遇见这个问题 可能是 由于 runapp 的时候设置里面 设置为release了. 解决办法是:见图 build configuration 设置成 debug 状态就OK了. 要是上面的不行就试一下 ...
 - 初识Sencha Touch:面板Panel
			
HTML代码: <!doctype html> <html> <head> <meta charset="utf-8"> <t ...
 - Unicode字符列表
			
注:除非有特别指明,否则以下符号皆属“半角”而非“全角”. 代码 显示 描述 U+0020 空格 U+0021 ! 叹号 U+0022 " 双引号 U+0023 # 井号 U+0024 $ ...
 - 苹果新专利详解Apple Pay和NFC工作原理
			
本周,美国专利商标局公布了苹果一项名为“在移动支付过程中调节NFC的方法”专利申请.专利文件中详细描述了苹果Apple Pay功能以及NFC硬件构架和工作模式. 首先,苹果在专利文件中介绍了其无接触支 ...
 - Http 请求头中的 Proxy-Connection
			
平时用 Chrome 开发者工具抓包时,经常会见到 Proxy-Connection 这个请求头.之前一直没去了解什么情况下会产生它,也没去了解它有什么含义.最近看完<HTTP 权威指南> ...
 - oracle job 定时执行 存储过程
			
oracle job 定时执行 存储过程 一:简单测试job的创建过程案例: 1,先创建一张JOB_TEST表,字段为a 日期格式 SQL> create table JOB_TEST(a ...
 - openNebula libvirt-virsh attach disk device for kvm
			
1,新建文件硬盘 qemu-img create -f qcow2 testdisk.img 2G