前言
最近碰到一个字符串的问题,将问题记录下来。
问题背景
写了段代码,主要是实现,从一个文件中读取一个raw_value:000076083
这样的字符串出来,然后根据index
,表示要提取哪三位当做给一个int
类型的变量final_val
。比如index = 0
,那么final_val = 0
;index = 1
,那么final_val = 76;
;index = 2
,那么final_val = 83
。换句话就是说,输入index
,将字符串raw_value
分割,得到final_val
。这里我们设定要取的是76
,也即是index=1
。
开始的代码如下(如果单纯实现上面的效果,肯定是有更好的代码,这里只是抽象出来,把问题暴露出来)
#include <stdio.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
void parse_func(void)
{
char raw_string[9],dest_temp_str[3];
int raw_file_value = 76083;//000076083从文件中读出来的int类型
int index = 1;
int final_value;//最后的输出结果
sprintf(raw_string, "%09d", raw_file_value);//对齐 不足位补零
memcpy(dest_temp_str, raw_string+(2-index)*3, 3);
sscanf(dest_temp_str,"%d",&final_value);
printf("func:%s 1ine:%d final_value:%d\n",__func__,__LINE__,final_value);
}
int main(void)
{
parse_func();
return 0;
}
在ARM Linux
设备上运行这段代码大概有半年,一直没有出现问题。直到有一天,发现解析出来的final_val
有7683
,7681
,8176
各种乱七八糟的数值。
复现过程
问题并不是复现的,而是偶发的,但是频率也不低。复现手段也不难,让设备运行这段代码后就设备reboot
重启,如此大概一两个小时即可复现。复现的时候代码中对于解析函数加了相关打印。当然实际运行的时候main
函数里面肯定不止只有一个parse_func
函数。
#include <stdio.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
void parse_func(void)
{
char raw_string[9],dest_temp_str[3];
int raw_file_value = 76083;//000076083从文件中读出来的int类型
int index = 1;
int final_value;//最后的输出结果
sprintf(raw_string, "%09d", raw_file_value);//对齐 不足位补零
printf("func:%s 1ine:%d raw_string:%s\n",__func__,__LINE__,raw_string);
memcpy(dest_temp_str, raw_string+(2-index)*3, 3);
printf("func:%s 1ine:%d dest_temp_str:%s\n",__func__,__LINE__,dest_temp_str);
sscanf(dest_temp_str,"%d",&final_value);
printf("func:%s 1ine:%d final_value:%d\n",__func__,__LINE__,final_value);
}
int main(void)
{
parse_func();
return 0;
}
添加了相关打印后,收集日志,发现Line 17
的dest_temp_str
一直是奇怪的数值,比如076a000076083
这种,但是这种情况下,最后一步的sscanf
的时候因为是取十进制的关系,所以解析出来还是正常的076
。而解析出错的时候,dest_temp_str
的数值是类似于07683a000076083
这种,然后最后解析出来就是异常的7683
这种数值了。
问题一
一开始用上面的demo
单独在ARM Linux
设备上运行,无论怎么测试复现,都没有出现异常的情况。编译demo
程序的时候也没有任何的warning
相关信息。
后面因为调试原因,直接在自己的ubuntu
上测试这段代码,用命令gcc string_parse.c -o string_parse
直接编译,有个warning
,
string_parse.c: In function ‘parse_func’:
string_parse.c:14:30: warning: ‘sprintf’ writing a terminating nul past the end of the destination [-Wformat-overflow=]
14 | sprintf(raw_string, "%09d", raw_file_value);//对齐 不足位补零
| ^
string_parse.c:14:5: note: ‘sprintf’ output between 10 and 12 bytes into a destination of size 9
14 | sprintf(raw_string, "%09d", raw_file_value);//对齐 不足位补零
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
而且最后运行的结果是直接就歪了(x86
更能暴露出问题,在ARM Linux
设备单独运行确实是没出现问题),而且dest_temp_str
也是异常的数值,全是十进制数,所以后面sscanf
输入的时候,估计全部就输入了。
root@iZwz9fp23rp007gktdtkhaZ:~/cexpert/ch6# ./string_parse
func:parse_func 1ine:15 raw_string:000076083
func:parse_func 1ine:17 dest_temp_str:076000076083
func:parse_func 1ine:19 final_value:-1309335245
root@iZwz9fp23rp007gktdtkhaZ:~/cexpert/ch6#
先从waring
入手,最后在这个链接,大概知道报错的原因了。就是000076083
这个字符串在sprintf
处理的时候,还要一个最后的字符串终止符。所以修改代码,将代码从char raw_string[9],dest_temp_str[3];
改成了char raw_string[10],dest_temp_str[4];
#include <stdio.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
void parse_func(void)
{
char raw_string[10],dest_temp_str[4];
int raw_file_value = 76083;//000076083从文件中读出来的int类型
int index = 1;
int final_value;//最后的输出结果
sprintf(raw_string, "%09d", raw_file_value);//对齐 不足位补零
printf("func:%s 1ine:%d raw_string:%s\n",__func__,__LINE__,raw_string);
memcpy(dest_temp_str, raw_string+(2-index)*3, 3);
printf("func:%s 1ine:%d dest_temp_str:%s\n",__func__,__LINE__,dest_temp_str);
sscanf(dest_temp_str,"%d",&final_value);
printf("func:%s 1ine:%d final_value:%d\n",__func__,__LINE__,final_value);
}
int main(void)
{
parse_func();
return 0;
}
再次编译不再报错,执行结果如下
root@iZwz9fp23rp007gktdtkhaZ:~/cexpert/ch6# ./go.sh
root@iZwz9fp23rp007gktdtkhaZ:~/cexpert/ch6# ./string_parse
func:parse_func 1ine:15 raw_string:000076083
func:parse_func 1ine:17 dest_temp_str:076V000076083
func:parse_func 1ine:19 final_value:76
root@iZwz9fp23rp007gktdtkhaZ:~/cexpert/ch6#
但是中间还是乱码 ,所以运行久了还是会有可能解析出来是7683
的问题。
问题二
既然有乱码,那么最直接的想法就是初始化,清零。虽然从代码上分析,没必要清零,但是还是加上去试试,结果发现,数据都是正常的了。
这确实是最终的解决方法,但是为什么清零就可以了呢,第一个粗略的想法当然是,这些操作都跟字符串有关系,可能跟终止符有关系。第二个想法是,会不会是栈上的一些内容有关系?
问题三
为了验证想法,特地在parse_func
前面再加一个函数,然后故意不清零。顺便打印出一些关键变量的地址。
#include <stdio.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
void parse_func(void)
{
char raw_string[10],dest_temp_str[4];
int raw_file_value = 76083;//000076083从文件中读出来的int类型
int index = 1;
int final_value;//最后的输出结果
// memset(raw_string,0,sizeof(raw_string));
// memset(dest_temp_str,0,sizeof(dest_temp_str));
//打印局部变量的地址
printf("func:%s line:%d raw_string[0]:%p raw_string[9]:%p\n",__func__,__LINE__,&raw_string[0],&raw_string[9]);
printf("func:%s line:%d dest_temp_str[0]:%p dest_temp_str[3]:%p\n",__func__,__LINE__,&dest_temp_str[0],&dest_temp_str[3]);
sprintf(raw_string, "%09d", raw_file_value);//对齐 不足位补零
printf("func:%s 1ine:%d raw_string:%s\n",__func__,__LINE__,raw_string);
memcpy(dest_temp_str, raw_string+(2-index)*3, 3);
printf("func:%s 1ine:%d dest_temp_str:%s\n",__func__,__LINE__,dest_temp_str);
sscanf(dest_temp_str,"%d",&final_value);
printf("func:%s 1ine:%d final_value:%d\n",__func__,__LINE__,final_value);
}
void test()
{
char test_test[1024];
memset(test_test,97,sizeof(test_test));//97在ASICII字符表中就代表字符'a'
printf("func:%s line:%d test_test[0]:%p test_test[1023]:%p\n",__func__,__LINE__,&test_test[0],&test_test[1023]);
}
int main(void)
{
test();
parse_func();
return 0;
}
结果如下
按照打印出来的地址,画出分布图:
而从打印dest_temp_str:076a000076083
判断,如果不清零,确实栈上的变量会影响。如果test_test
初始化不是字符a
,比如是字符1
,那么解析结果一定会出现问题
但是还有个疑问,打印line:26 dest_temp_str
这一行,为什么dest_temp_str
打印出的结果是把dest_temp_str
前面的四个字节跟raw_string
全部的十个字节拼在一起了,dest_temp_str
后面的六个字节哪去了?
问题四
还有一个地方,一开始自己没反应过来,我把test
函数的内容拆开放到main
里面,这几个变量的地址分布情况也会发生改变,不会有重叠的部分,就算parse_func
函数对局部变量不清零也不会出现问题。这是因为栈是要有递归调用才能体现出来的。。。
#include <stdio.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
void parse_func(void)
{
char raw_string[10],dest_temp_str[4];
int raw_file_value = 76083;//000076083从文件中读出来的int类型
int index = 1;
int final_value;//最后的输出结果
// memset(raw_string,0,sizeof(raw_string));
// memset(dest_temp_str,0,sizeof(dest_temp_str));
//打印局部变量的地址
printf("func:%s line:%d raw_string[0]:%p raw_string[9]:%p\n",__func__,__LINE__,&raw_string[0],&raw_string[9]);
printf("func:%s line:%d dest_temp_str[0]:%p dest_temp_str[3]:%p\n",__func__,__LINE__,&dest_temp_str[0],&dest_temp_str[3]);
sprintf(raw_string, "%09d", raw_file_value);//对齐 不足位补零
printf("func:%s 1ine:%d raw_string:%s\n",__func__,__LINE__,raw_string);
memcpy(dest_temp_str, raw_string+(2-index)*3, 3);
printf("func:%s 1ine:%d dest_temp_str:%s\n",__func__,__LINE__,dest_temp_str);
sscanf(dest_temp_str,"%d",&final_value);
printf("func:%s 1ine:%d final_value:%d\n",__func__,__LINE__,final_value);
}
void test()
{
char test_test[1024];
memset(test_test,49,sizeof(test_test));//49在ASICII字符表中就代表字符'1'
printf("func:%s line:%d test_test[0]:%p test_test[1023]:%p\n",__func__,__LINE__,&test_test[0],&test_test[1023]);
}
int main(void)
{
// test();
char test_test[1024];
memset(test_test,49,sizeof(test_test));//49在ASICII字符表中就代表字符'1'
printf("func:%s line:%d test_test[0]:%p test_test[1023]:%p\n",__func__,__LINE__,&test_test[0],&test_test[1023]);
parse_func();
return 0;
}