[转]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后面地址
的内容。
这样就存在一个可能的缓冲区溢出问题。。。
转载于:https://www.cnblogs.com/pianist/p/3315801.html
[转]printf 函数实现的深入剖析相关推荐
- printf 函数实现的深入剖析
研究printf的实现,首先来看看printf函数的函数体 int printf(const char *fmt, ...) { int i; char buf[256]; va_l ...
- php 格式化评论量函数,深入剖析PHP中printf()函数格式化使用
printf()函数优点在于可以格式化输出! printf的格式控制的完整格式: % - 0 m.n l或h 格式字符 下面对组成格式说明的各项加以说明: ①%:表示格式说明的起始符号,不可缺少. ② ...
- 剖析printf函数
printf是什么,对于起初学习c语言的同学来说肯定都特别的疑惑.在这里,解答一下:它是一个函数.既然是一个函数的话,想必肯定有返回值和参数吧.那么它的返回值和参数是什么呢? 1.看一下这个例子,可能 ...
- ccs用C语言进行printf时,DSP:CCS V6 TMS320F2812 使用printf函数
使用Code Composer Studio Version: 6.1.1.00022,建立TMS320F2812工程. /* * main.c */ #include int main(void) ...
- C语言printf重入,单片机中printf函数的重映射
单片机中printf函数的重映射 一.源自于:大侠有话说 1.如果你在学习单片机之前学过C语言,那么一定知道printf这个函数.它最最好用的功能 除了打印你想要的字符到屏幕上外,还能把数字进行格式化 ...
- 关于C语言中printf函数“输出歧视”的问题
目录 关于C语言中printf函数"输出歧视"的问题 问题描述 探索问题原因 另一种研究方法 问题结论 关于C语言中printf函数"输出歧视"的问题 问题描述 ...
- c语言自定义char*函数返回值是乱码_[每日C语言」printf()函数的修饰符和返回值...
在上一个小demo<printf()函数(1)>中主要说了一下printf()函数的转换说明符,这些转移说明符是可以被修饰的.我们可以在%d和定义的转义字符之间通过插入修饰符对基本的转换说 ...
- c程序设计语言中printf,编程序 用getchar函数读入两个字符c1和c2,然后分别用putchar和printf函数输出这两个字符。请问...
编程序,用getchar函数读入两个字符给c1和c2,然后分别用putchar和printf函数输出这两个字符.请问: 1. 变量C1,C2应定义为字符型或整型?或二者皆可? 答:在这个题目里,变量C ...
- STM32中关于串口通信的printf()函数重定向问题
在STM32串口通信程序中使用printf()函数发送数据非常方便,但是需要先进行简单的配置,下面介绍两种配置方法. 方法一.对工程属性进行配置 第1步:在main.c中包含"stdio.h ...
- scanf函数和printf函数
C程序实现输出和输入的 主要是printf函数 和 scanf函数,这两个函数是格式输入输出 格式声明由%和格式字符组成 如%d,%f 格式字符: d格式符:用来输出一个有符号的十进制整数 c格式 ...
最新文章
- MIT | 一种可解释的PPI预测模型
- Day8 Python基础之遗漏知识点(六)
- Lfie has sweet
- 电气期刊论文实现:热电联产经济调度【有代码】
- boost::type_erasure::add_assignable相关的测试程序
- json和字符串/数组/集合的互相转换の神操作总结
- Java NIO 下
- DoYourData AppUninser Mac版(Mac应用卸载工具)
- 时间序列分析工具箱——tidyquant
- caddy 作为微服务的 API gateway
- 下载Android kernel
- nginx集群的搭建
- Unity 导入高分辨率图片
- Windows内存体系(4) -- 内存映射文件
- 微信小程序WeUI 前端样式库
- 【Proteus仿真】【51单片机】音乐盒电子琴设计
- 砂土液化判别计算工具
- Nvidia Xavier平台CAN收发控制器调试记录
- 给拯救者Y7000换个固态
- r生成新的dataframe_R语言中数据框的定义与使用