本期小结:
printf函数的实现,大致是这样的:
int printf(const char *fmt, ...)
{
char printf_buf[1024];
va_list args;
int printed;
va_start(args, fmt);
printed = vsprintf(printf_buf, fmt, args);
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,指针指向的内容一定要可控,千万不要用计算机自动生成的内容。
下期预告:strcpy函数是C标准库里面的一个常用的字符串拷贝函数。如果让你来写一个,你会写成什么样子呢? |