3.5 关于printf的典型隐藏性错误

调试的时候,碰到一个问题,从表面上看非常正常,但是运行就是不正常。幸亏以前也碰到过相同的问题,才会迅速的联想起来。很有代表性的一个问题,大家都可以看看:

1、问题描述。

是这样一个问题:产品进程从底层gb驱动,读取一个计数结构体,然后再打印出来。代码简化如下:

typedef struct __GB_CHANNEL_INTERNAL__INFOSTAT{


    unsigned long long ChannelNo;               /*总通道数 288:64K - 0~255 ;2M - 256~287*/
    unsigned long long tx_call_num;             /*mtp 某个通道发送调用次数*/
    unsigned long long tx_pack_num;             /*mtp 某通道发送数据包成功次数*/
    unsigned long long tx_error_num;            /*mtp 某通道发送错误次数  */
    unsigned long long rx_call_num;             /*mtp 某通道接收调用次数*/
    :
    :
}GBchannelintInfo;


GBchannelintInfo * pInfo;
  • step1:代码首先从驱动读取计数,存入到pInfo变量当中。理论上说读出的结构体各成员的值应该如下:
pInfo->ChannelNo = 1;
pInfo->tx_call_num= 2;
pInfo->tx_pack_num= 3;
pInfo->tx_error_num= 4;
pInfo->rx_call_num= 5;
  • step2:现象一,产品进程1使用如下方式打印结构体成员:
printf("ChannelNo = %d, tx_call_num = %d, tx_pack_num= %d, tx_error_num = %d,rx_call_num = %d\n",
    pInfo->ChannelNo, pInfo->tx_call_num, pInfo->tx_pack_num, pInfo->tx_error_num, pInfo->rx_call_num);

期望的结果为:ChannelNo = 1, tx_call_num = 2, tx_pack_num= 3, tx_error_num = 4,rx_call_num = 5
实际的结果为:ChannelNo = 1, tx_call_num = 0, tx_pack_num= 2, tx_error_num = 0,rx_call_num = 3

  • step3:现象二,产品进程2使用如下方式打印结构体成员:
printf("ChannelNo = %d\n",pInfo->ChannelNo);
printf("tx_call_num = %d\n",pInfo->tx_call_num );
printf("tx_pack_num= %d\n",pInfo->tx_pack_num);
printf("tx_error_num = %d\n",pInfo->tx_error_num );
printf("rx_call_num = %d\n",pInfo->rx_call_num );

这种方式的实际打印结果倒是和期望的是一致的:

ChannelNo = 1
tx_call_num = 2
tx_pack_num= 3
tx_error_num = 4
rx_call_num = 5
  • step4:刚开始的时候,对现象一百思不得其解,以为是接口数据拷贝上出了什么问题,造成了结构成员的数据混乱。后面想起了类似的可变参数函数的问题,对这个现象才恍然大悟。

2、问题原因和解决。

  • 1、原因:

printf是一个参数可变的函数,它的定义原型:int printf( const char* format, …);

这类可变参数的函数,对函数参数的取值和正常函数并不一样。函数的参数值,在函数调用时,都是放在堆栈之中的,类似于临时变量。对参数固定的函数来说,访问参数值的位置在堆栈中的访问位置是固定,所以基本不会出错。对于参数可变的函数来说,根据每次函数调用传递的参数不同,参数在堆栈中的位置分布也是不一样的。那怎么样每次都能正确的解析出相应的参数值呢?一般的做法就是在第一个参数中带入格式化字符串,解析格式化字符串,得到可变参数的个数和类型。再根据解析结果,算出每个参数在堆栈中的偏移值,再来得到参数的实际位置。

现象一中我们所犯的错误就是,格式字符中带的可变参数的类型和实际带入的参数的类型不一致,造成提取参数值失败:
格式字符中的变量类型是”%d”,指定变量为int类型的,而实际调用printf时,带入的参数类型是unsigned long long。所以在打印时其实使用int的类型去unsigned long long中提取数值。ChannelNo = %d提取了pInfo->ChannelNo的下半部分int的值,tx_call_num = %d提取了pInfo->ChannelNo的上半部分int的值。

现象二中,其实用法也是不对,但是由于每次只传入一个参数,所以每次都是刚好获取unsigned long long的下半部分int值,刚好是相等的。

  • 2、解决方法:

正确的用法是使用”%lld”打印unsigned long long类型变量:

printf(“ChannelNo = %lld, tx_call_num = %lld, tx_pack_num= %lld, tx_error_num = %lld,rx_call_num = %lld\n”,
pInfo->ChannelNo, pInfo->tx_call_num, pInfo->tx_pack_num, pInfo->tx_error_num, pInfo->rx_call_num);

  • 3、风险:

从上看来,操作可变参数的函数完全是自定义操作,你对参数和堆栈的使用完全是编码人员自己控制。如果使用不善就会造成内存使用错误,堆栈溢出等等。而且在编译的时候不会提示错误,只是有相关告警。如果你还对可变参数进行了写操作,那么一旦出错后果就会更加严重。

3、参考资料:

posted @ 2017-10-12 20:42  pwl999  阅读(111)  评论(0)    收藏  举报