【转载】printf函数的内涵以及造成的严重内存问题 转载来源:liangkaiming的专栏

源地址:https://blog.csdn.net/liangkaiming/article/details/5895052

问题的引出:在某型号的核心网络交换机设备中,为调试方便,实现了一个把内存内容从终端打印的功能,也就是dump memory,屏幕的左边显示十六进制,右边以ASCII形式显示,用过UltraEdit的十六进制编辑功能朋友应该都很了解。显示ASCII码部分的实现,是这样的:
printf (string_ascii); /* string_ascii指向待显示的内存段 */
结果,这个函数调用引起了一个致命的问题,原本用于调试的功能,居然造成了整机重启。

 

问题解决讨论:

1,问题出在对printf函数的细节理解上。大家都知道调用printf函数要include ,但是有谁看过stdio.h里面对printf函数的原型声明?
一句话,想成为职业C程序员,就必须多看源码,特别是对一些细节的推敲。找出答案的一个重要线索,就藏在stdio.h里面的printf函数原型里。

int __cdecl printf(const char *, ...);

2,printf 的参数应该是字符串,也就是从那个参数指针指向的第一个非零数据一直打印直到遇到0x00。大概是指向的内存段一直都没有0x00这个值吧。

 

3,没错,第一个参数是一个字符串,但它并非一般的字符串,而是格式化字符串。提醒一下,如果这个字符串里面不含有转义字符,如%d, %x这样的符号,那么会打印出字符串本身。但是如果含有这些符号但是后面没有参数来指明这个符号呢?如果这个符号代表指针呢?

比如:printf("%d,%s");后面根本没有参数来指明%d和%s。

 

4,应该说,如果是有%s,那么会在这个地方插入一个字串,该字串的起始地址是printf的下一个参数。如果没有这个参数,会读取一个随机的地址,这个地址很可能是非法的,从而导致系统的崩溃。

 

5,函数看到fmt这个字串里面的转义字符%,就去寻找下一个参数。如果调用函数的时候并没有传递这个参数,函数会取得一个随机值对应这个转义字符。问题是,这个随机值从哪里取出来的?

 

6,printf函数的实现,大致是这样的:

int printf(const char *fmt, ...)
{
char printf_buf[1024];
va_list args;
int printed;

va_start(args, fmt);//调用va_start后args将指向第一个可变参数
printed = vsprintf(printf_buf, fmt, args);//调用vsprintf将fmt字符串按照args指向的第一个可变参数开始格式化输出,并将由args替换fmt中的格式化字符串保存在printf_buf缓冲中。如printf("kevin %d,%f,%s",num,fnum,string),则printf_buf中保存的是kevin 44,3.450000,abc
va_end(args);

puts(printf_buf);

return printed;
}

 

我们可以看到,它是调用vsprintf函数,将第二个参数以后的参数,按格式化字串输出,然后在终端显示出来。如果格式化字串中有多余的%字符,而没有传递相应的参数,会获得一个随机数。
这里有两个问题:首先,如果这个多余的%转义字符是一个“%s”,后果是怎样的呢?后果就是访问了一个没有初始化的指针“野指针”。对于8051/ARM7这样没有内存处理单元(MMU)的处理器,会显示不可预料的内容。但对于MIPS/X86这样有MMU的处理器,CPU内部的硬件线路会判断这个指针是否合法。如果是一个非法指针,会引发系统抛出一个异常(Exception),从而导致进程崩溃(process crash),也就是Windows下常见的“该程序执行了非法操作”。而在操作系统内核中发生了这种错误,会引发内核panic,类似Windows的蓝屏死机的概念,系统彻底崩溃。前面提到的,出现这个问题的核心交换机上,就是由于出现内核panic而死机重启的。

那么,随机数是从哪里获得的呢?
这个问题和体系结构密切相关了。对于x86,我们知道,参数是通过堆栈传递的,那么也就是从没有初始化的堆栈存储区取得的。而对于MIPS/ARM这样的RISC处理器,系统会占用几个寄存器作为传递参数用,如果不够用再通过堆栈传递。因此,在RISC处理器上,错误的参数还有可能从寄存器传递过来。8051是CISC处理器中的一个异类,C51编译器的函数参数传递,使用了R4到R7的四个寄存器,和RISC类似,这点要注意。
最后,请大家吸取一个教训,printf的第一个参数char *fmt,指针指向的内容一定要可控,千万不要用计算机自动生成的内容。

 

另外还有一篇讨论类似问题的文章:https://blog.csdn.net/qq_27312943/article/details/53152244

posted @ 2018-07-19 13:55  Abraham_Xu  阅读(793)  评论(0编辑  收藏  举报