前言
记录2020年5月30日,肯哥在群里面分享的一个因为手误带来的bug。
问题描述
肯哥原话:
又到了每天的open话题讨论时刻,一起在摸鱼中学点东西,今天我们来聊一个话题:一不小心的手误,代码有时能跑,有时又会出问题,到时是怎么回事?你是否也遇到过类似的奇葩问题?欢迎大家来聊聊你在实际的学习和工作中遇到的类似问题场景。
又到了每天的open话题讨论时刻,一起在摸鱼中学点东西,今天我们来聊一个话题:一不小心的手误,代码有时能跑,有时又会出问题,到时是怎么回事?你是否也遇到过类似的奇葩问题?欢迎大家来聊聊你在实际的学习和工作中遇到的类似问题场景。
/* 一个信息的记录表 */
static const char *g_msg_tab[] =
{
"SUCESS",
"NO SUCH FILE",
"IO ERROR",
"BAD FILE NUM",
"OUT OF MEM"
"FILE EXIST",
"NO SUCH DEVICE",
"UNKNOWN ERROR",
};
void send_err_msg_out(int err_index)
{
char msg[16] = {0};
snprintf(msg, sizeof(msg), "%s", g_msg_tab[err_index]);
//send out ...
}
问题分析
(1)说实话,这个代码我看了很久没发现问题所在。后面是群里面大佬提醒少了一个逗号,才找到的。
(2)在枚举常量的定义中,少了一个’ ,’ 不应该会报错吗?这个是群里面大佬都在疑惑的一点,但是实操之后发现。居然是可以跑的。
因为肯哥的代码没有main函数,所以无法直接运行。因此我在这里增加了main函数。
这里需要注意一个问题,好像老版本的编译器编译snprintf函数会报错,我懒得处理了,就直接放在Linux中跑了。
#include <stdio.h>
/* 一个信息的记录表 */
static const char *g_msg_tab[] =
{
"SUCESS",
"NO SUCH FILE",
"IO ERROR",
"BAD FILE NUM",
"OUT OF MEM"
"FILE EXIST",
"NO SUCH DEVICE",
"UNKNOWN ERROR",
};
void send_err_msg_out(int err_index)
{
char msg[16] = { 0 };
snprintf(msg, sizeof(msg), "%s", g_msg_tab[err_index]);
//send out ...
}
int main()
{
printf("hello world\r\n");
send_err_msg_out(0);
return 0;
}
问题带来的结果
(1) 在枚举常量中少些了一个’,’ 我们发现并没有产生报错,所以我决定将枚举常量的第四个参数打印出来,看看是什么结果。
(2)后面发现,第四个字符串变成了两个字符串的叠加!
(3)肯哥后面揭晓答案的时候说,,编译器远比我们想象的更聪明,这里面的问题点是第5行字符串少了一个逗号,虽然编译没有问题,但是编译器会把第5行字符串和第6行字符串合并成一个字符串,存放在好g_msg_tab[4]的位置。
如何写能够揭露这个问题?
bug解决方案
(1) 肯哥后面说,因为这里定义字符串数组很巧妙,用了*xxx[ ]这种自适应元素个数的方法,即[ ]里面没有定个数,由编译器在编译的时候自行决定空间。
(2)而如果使用标准的二维数组来定义,可能这个错误问题早就暴露了,即形如 xxx[8][16]。
(3)可能这个时候有些人就有疑问了,为啥写成二维数组就能够暴露问题呢?
(4)首先,我们需要知道一个基本概念,C语言的字符串是什么?C语言的字符串本质上就是一个指针,这个指针指向一个.rodata段,.rodata段是一个特殊的段,用于存储只读的数据,如字符串常量和全局常量。
(5) g_msg_tab[]就是一个指针数组,用于存放这些字符串。而我们如果用二维数组来存放,二维数组的不也可以进似理解为一个指针数组吗?(注意,有区别!)不过这个指针指向的区域大小有规定,以xxx[8][16]为例,这个指针数组指向的区域大小最多16个字节。而 g_msg_tab[]中的指针指向的区域没有大小规定,只要别超出.rodata段存储大小即可。
(6)知道指针数组和二维数组的区别之后,我们应该不难得出结论。这个问题如何暴露呢?通过限制指针指向的区域大小。
这个方式真的有效吗
(1)可能已经有人想到了,这种方法真的能够杜绝这样的问题吗?既然二维数组能够规定存入位置的指定字符个数,那么我就让存放的数据恰好等于指定的字符个数不就行了吗?
(2)于是我做了如下更改,本来 "OUT OF MEM"和"FILE EXIST"合并之后的数据是大于16个字符的。我现在让他恰好等于16个字符试试。
(3)结果发现,编译通过了。但是却多打印了一行。这个又是什么问题呢?
(4)经过思考之后,我突然想到。我printf使用的是%s。而%s的工作原理是检测到“\0”才停止。我这里因为恰好为16个字符,所以“\0”被覆盖了,只有多的哪一行才有。因此这个二维数组实际上指向的区域只能存放15个字符。
总结
(1)其实这个就是一个C语言的小知识点,只不过很容易让人忽略。至于对于这个问题如何避免,我没有想到合适的方法,所以我只能建议各位一定要细心。
(2)这个bug恶心在于,编译器无法提示,所以如果出现莫名奇妙的问题。只能提醒各位检查检查枚举常量是否忘记写了一个’,'。