printf函数实现的深入剖析
来源:互联网 发布:外汇数据行情做单 编辑:程序博客网 时间:2024/05/17 08:25
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;
chartmp[256];
va_listp_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:
moveax, _NR_write
movebx, [esp + 4]
movecx, [esp + 8]
intINT_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
如果你不懂,没关系,你只需要知道一个intINT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。(从上面的参数列表中也该能够猜出大概)
好了,再来看看sys_call的实现:
sys_call:
callsave
pushdword [p_proc_ready]
sti
pushecx
pushebx
call[sys_call_table + eax * 4]
addesp, 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后面地址
的内容。
这样就存在一个可能的缓冲区溢出问题。。。
下次有时间再写^_^
有任何问题可以和我联系讨论:aisan215@126.com
研究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;
chartmp[256];
va_listp_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:
moveax, _NR_write
movebx, [esp + 4]
movecx, [esp + 8]
intINT_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
如果你不懂,没关系,你只需要知道一个intINT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。(从上面的参数列表中也该能够猜出大概)
好了,再来看看sys_call的实现:
sys_call:
callsave
pushdword [p_proc_ready]
sti
pushecx
pushebx
call[sys_call_table + eax * 4]
addesp, 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后面地址
的内容。
这样就存在一个可能的缓冲区溢出问题。。。
下次有时间再写^_^
有任何问题可以和我联系讨论:aisan215@126.com
0
上一篇:嵌入式(linux+ARM)线路图
下一篇:rhel/centos播放mp3文件
相关热门文章
- test123
- 编写安全代码——小心有符号数...
- 使用openssl api进行加密解密...
- 一段自己打印自己的c程序...
- sql relay的c++接口
- linux dhcp peizhi roc
- 关于Unix文件的软链接
- 求教这个命令什么意思,我是新...
- sed -e "/grep/d" 是什么意思...
- 谁能够帮我解决LINUX 2.6 10...
给主人留下些什么吧!~~
评论热议
0 0
- printf函数实现的深入剖析
- printf函数的实现深入剖析
- printf函数实现的深入剖析
- printf函数实现的深入剖析
- printf函数实现的深入剖析
- printf 函数实现的深入剖析[转载]
- printf 函数实现的深入剖析
- 可变参数列表的剖析以及printf函数的实现
- 深入剖析变长参数函数的实现
- 深入剖析变长参数函数的实现
- 深入剖析printf
- 深入剖析printf函数(下):---形参列表和格式化输出是如何做到的?
- 深入剖析printf函数(下):---形参列表和格式化输出是如何做到的?
- 深入剖析printf函数 合集pdf文件
- printf 函数的实现 *
- printf函数的实现
- printf函数的实现
- 剖析printf函数
- VMware SDS 之一 : 什么是VSAN
- Linux下批量修改文件名
- linux kernel的中断子系统之(七):GIC代码分析
- 嵌入式(linux+ARM)线路图
- 对Thread.interrupt()方法很详细的介绍
- printf函数实现的深入剖析
- mysql启动停止
- nodejs在线xlsx转json
- Erlang并发编程
- rhel/centos播放mp3文件
- GRUB(GRand Unified Boot loader)引导加载程序
- NAT DHCP WWW rc.local
- 论文 毕业设计 相关 用语 评语
- 基于EasyDarwin远程视频传输
原创粉丝点击
热门IT博客
热门问题
老师的惩罚
人脸识别
我在镇武司摸鱼那些年
重生之率土为王
我在大康的咸鱼生活
盘龙之生命进化
天生仙种
凡人之先天五行
春回大明朝
姑娘不必设防,我是瞎子
净水器 品牌
净率净水器
净水器芯
净水器供应
净水器制造
哪个净水器好用
全能净水器
台上净水器
台下净水器
净水器售价多少
家用净水器价格表
高端净水器价格
国产净水器品牌
净水器怎么选择
净水器好用
净水器销售
净水器是什么东西
中国净水器
净水器真的有用吗
什么是净水器
净水器哪个好用
净水器怎么样选
韩国净水器
开能净水器
净水器有效果吗
净水器哪种好用
净水器有几种
净水器大概多重
净水器有用么
生产净水器厂家
净水器哪家的好
今日净水器怎么样
易开得净水器多少钱
净水器工作原理示意图
净水器多少钱一台家用
哪个品牌的净水器好
净水器买什么牌子的好
哪种净水器适合家用
美国净水器三大品牌
安利净水器世界排名
天之蓝净水器价格表