900字范文,内容丰富有趣,生活中的好帮手!
900字范文 > [转]printf 函数实现的深入剖析

[转]printf 函数实现的深入剖析

时间:2020-01-29 08:47:01

相关推荐

[转]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后面地址

的内容。

这样就存在一个可能的缓冲区溢出问题。。。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。