字符串问题

Posted by 婷 on January 23, 2023 本文总阅读量

前言

最近碰到一个字符串的问题,将问题记录下来。

问题背景

写了段代码,主要是实现,从一个文件中读取一个raw_value:000076083这样的字符串出来,然后根据index,表示要提取哪三位当做给一个int类型的变量final_val。比如index = 0,那么final_val = 0index = 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_val7683,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 17dest_temp_str一直是奇怪的数值,比如076a000076083这种,但是这种情况下,最后一步的sscanf的时候因为是取十进制的关系,所以解析出来还是正常的076。而解析出错的时候,dest_temp_str的数值是类似于07683a000076083这种,然后最后解析出来就是异常的7683这种数值了。

问题一

一开始用上面的demo单独在ARM Linux设备上运行,无论怎么测试复现,都没有出现异常的情况。编译demo程序的时候也没有任何的warning相关信息。

image-20230105234224059

image-20230105234555073

后面因为调试原因,直接在自己的ubuntu上测试这段代码,用命令gcc string_parse.c -o string_parse直接编译,有个warning

image-20230105235635950

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输入的时候,估计全部就输入了。

image-20230105235833682

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];

image-20230106001304699

#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;
}

再次编译不再报错,执行结果如下

image-20230106001436366

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的问题。

问题二

既然有乱码,那么最直接的想法就是初始化,清零。虽然从代码上分析,没必要清零,但是还是加上去试试,结果发现,数据都是正常的了。

image-20230107111556287

image-20230107111540660

这确实是最终的解决方法,但是为什么清零就可以了呢,第一个粗略的想法当然是,这些操作都跟字符串有关系,可能跟终止符有关系。第二个想法是,会不会是栈上的一些内容有关系?

问题三

为了验证想法,特地在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;
}

结果如下

image-20230123160229987

按照打印出来的地址,画出分布图:

1

而从打印dest_temp_str:076a000076083判断,如果不清零,确实栈上的变量会影响。如果test_test初始化不是字符a,比如是字符1,那么解析结果一定会出现问题

image-20230123164215343

image-20230123164246809

但是还有个疑问,打印line:26 dest_temp_str这一行,为什么dest_temp_str打印出的结果是把dest_temp_str前面的四个字节跟raw_string全部的十个字节拼在一起了,dest_temp_str后面的六个字节哪去了?

问题四

还有一个地方,一开始自己没反应过来,我把test函数的内容拆开放到main里面,这几个变量的地址分布情况也会发生改变,不会有重叠的部分,就算parse_func函数对局部变量不清零也不会出现问题。这是因为栈是要有递归调用才能体现出来的。。。

image-20230123165621963

image-20230123165706938

#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;
}

链接