前言
在 C 语言编程中,我们经常会用到printf和sprintf这样强大的函数来进行格式化输出。然而,在实际使用过程中,如果不注意参数类型与格式说明符的匹配,就很容易出现一些意想不到的问题。尤其是当涉及到32位和64位整数时,格式化字符串与实际参数类型不匹配可能会引发各种奇怪的错误,甚至导致程序崩溃。今天,咱们就一起来深入探讨一下这背后的原因以及相关的解决办法。
一、问题现象示例
先来看下面这段代码示例:
void main(void)
{
_int64 a = 1;
char * str = "hello widebright";
printf("%d %s\n",a,str); //这个是打印32位数的,非预期结果
printf("%lld %s\n",a,str); //正确的打印64位数的写法
//上面把64位数当作32位数打印的时候,就把这数的高32位当作第二个参数%s使用了,
//因为这时64位数的高32位还是0,就是空指针,printf程序对空指针专门处理了,
//所以程序不会崩溃,下面改了高32位的,下面还当作32位打印的时候,%s第二个参数就是
//访问0x1地址了,程序就崩溃了。
int * b = (int *)&a;
*++b = 1;
printf("%I64X %s\n",a,str); //正确的打印64位数的写法
printf("%I32d %s\n",a,str); //这个是打印32位数的,程序崩溃
printf("%d %s\n",a,str); //这个是打印32位数的,程序崩溃
printf("%ld %s\n",a,str); //这个是打印32位数的,程序崩溃
}
short b = 1;
// 把short当作32位数打印就不会有问题
printf (“%d %s\n”,a,str);
//把32位int 数当作64位数打印也是不行的,同样导致后面参数解析出错。
int a =1;
char str[] = “hello widebright”;
printf (“%lld %s\n”,a ,str); //把32位整型当作64位打印,打印出来的数字和后面字符都是乱码了。传禁区的str指针应该会被解析成64位数的一部分了。str被解析成后续的未预期内存了
printf (“%I64X %s\n”,a ,str); //把32位整型当作64位打印,打印出来的数字和后面字符都是乱码了。
short b =1;
printf (“%d %s\n”,a ,str); //但把short单做32位数打印就不会有问题,应该是默认都转换成32位数再传给printf了
从这段代码可以看到,当我们将 64 位整数a使用%d(用于 32 位整数的格式说明符)去打印时,就出现了各种不符合预期的情况,甚至导致程序崩溃。而且不仅仅是printf函数,sprintf函数也存在类似的问题,比如:
int a = 1;
char str[] = "hello widebright";
// 把32位整型当作64位打印,打印出来的数字和后面字符都是乱码了
printf ("%lld %s\n",a,str);
printf ("%I64X %s\n",a,str);
二、汇编分析
short b =1;
printf ("%d %s\n",a ,str); //但把short单做32位数打印就不会有问题,应该是默认都转换成32位数再传给printf了
short b =1;
0041158B mov eax,1
00411590 mov word ptr [ebp-4Ch],ax
printf ("%d %s\n",a ,str); //但把short单做32位数打印就不会有问题,应该是默认都转换成32位数再传给printf了
00411594 mov esi,esp
00411596 lea eax,[ebp-40h]
00411599 push eax
0041159A mov ecx,dword ptr [ebp-24h]
0041159D push ecx //还是32数push到栈上的,所以
0041159E push offset string "%d %s\n" (417808h)
004115A3 call dword ptr [__imp__printf (41A400h)]
004115A9 add esp,0Ch
004115AC cmp esi,esp
004115AE call @ILT+415(__RTC_CheckEsp) (4111A4h)
_int64 a = time(NULL);
a =1;
sprintf(buffer,"lX%s\n",a,str);
00411511 push 0
00411513 call time (4116D0h)
00411518 add esp,4
0041151B mov dword ptr [a],eax
0041151E mov dword ptr [ebp-20h],edx
a =1;
00411521 mov dword ptr [a],1
00411528 mov dword ptr [ebp-20h],0
sprintf(buffer,"lX%s\n",a,str);
0041152F mov esi,esp
00411531 mov eax,dword ptr [str]
00411534 push eax
00411535 mov ecx,dword ptr [ebp-20h]
00411538 push ecx //64数的高32位 被push 进栈了
00411539 mov edx,dword ptr [a]
0041153C push edx //64位数的低32位 被push 进栈了
0041153D push offset string "X\n" (417800h)
00411542 mov eax,dword ptr [buffer]
00411545 push eax
00411546 call dword ptr [__imp__sprintf (41A404h)]
0041154C add esp,14h
0041154F cmp esi,esp
00411551 call @ILT+415(__RTC_CheckEsp) (4111A4h)
可以看到 64位的_int64其实是当作两个int被push到栈上了,看起来就像传了多了一个int参数一样,那些可变参数支持的va_list 什么的应该是直接从栈上解析出来这些参数的,所以如果64位和32位格式化参数没有指定好是会导致最后的解析出错的。这种情况应该是32位机器上,64位支持不好导致的? 系统需要用两个int来模拟一个_int64, printf默认又当作32位int来解析,所以就会出现这种问题。
三、问题根源剖析
(一)可变参数函数的参数传递机制
printf和sprintf这类函数属于可变参数函数,它们内部依赖va_list机制来从栈中解析参数。在函数调用时,参数会按照一定顺序被压入栈中,而va_list相关的宏会依据格式字符串里给定的转换说明符去尝试确定每个参数的类型和大小,进而从栈上读取相应字节的数据来完成输出操作。
(二)32 位机器对 64 位数据的处理方式
在 32 位机器环境下,硬件层面的寄存器以及数据总线宽度基本都是 32 位,对于 64 位数据的处理能力是有限的。所以,当我们要处理 64 位整数(比如_int64类型)时,系统往往会采用两个 32 位整数来模拟表示一个 64 位整数。这就意味着,在进行函数调用时,一个 64 位整数实际上是被拆分成两个 32 位整数依次压入栈中的,就好像传递了两个额外的int参数一样。
(三)格式说明符与实际参数不匹配的影响
当我们在代码中使用的格式说明符与实际传入的参数类型不一致时,va_list机制就没办法正确地解析栈上的参数了。例如,我们传入了一个 64 位整数,但是却使用了针对 32 位整数的%d格式说明符,那么printf函数只会从栈上读取前 32 位数据当作要输出的整数部分,而剩下的 32 位数据就会被误当作下一个参数(比如在上面代码中原本对应的字符串参数)来处理,这显然就破坏了参数原本的解析顺序,从而导致输出结果错误甚至程序崩溃。
(四)不同类型数据转换的默认规则影响
像short类型的数据,在作为参数传递给类似printf这样的函数时,会默认被转换成 32 位数再传递到栈上,这也就是为什么把short当作 32 位数打印时通常不会出现像 64 位整数那样严重的参数解析问题的原因所在。
四、总结
在C语言中,使用 printf 等可变参数函数时,必须确保格式化字符串与参数类型严格匹配。特别是32位和64位整数的处理,稍有不慎就会引发错误或程序崩溃。以下是关键点总结:
- 64位整数:
• 使用 %lld 或 %I64d (Windows平台)打印64位整数。
• 在32位系统上,64位整数会被拆分成两个32位整数存储在栈上。 - 32位整数:
• 使用 %d 或 %u 打印32位整数。 - short 类型:
• 在调用 printf 时, short 会被隐式提升为32位整数,因此使用 %d 打印 short 类型是安全的。 - 调试建议:
• 使用编译器警告选项(如 -Wall )来检测潜在的格式化字符串问题。
• 在调试时,检查栈上的参数传递情况,确保参数类型与格式化字符串一致。
通过理解这些原理和注意事项,你可以更好地编写安全、可靠的C语言代码,避免因格式化字符串问题而导致的错误和崩溃。