就删了个printf,代码崩了!
The following article is from 原点技术 Author 江南一散人
转自:原点技术
出了一点小意外,今天暂不更新性优化专题系列,改日补上!今天换个口味,临时决定更新一篇有点不太正经的技术文,博君一笑!
本文根据真实历史事件改编,如有雷同,可能不是巧合!
请佛祖给代码开个光吧
某年某月某日,晚十点,程序员张三刚垒完一个BUG,长舒一口气,伸了伸懒腰,心中窃喜:这么快就被我搞定了,真是天纵英才!
稍事休息,给女神刷了几朵小红花,看下时间,快11点了,赶紧跑个测试吧!
root@ubuntu:printf# gcc -g test.c -o test
root@ubuntu:printf# ./test
Segmentation fault
顿感胸闷,恍惚间,似乎看到一万只神兽在屏幕上奔腾而过!
这可咋整?眼看都要11点了,都快赶不上2路汽车了!
突然灵机一动,要么请佛祖来开个光吧!于是乎,整衣敛容,焚香祷告,遂有了下面这段神奇的代码:
再编译运行一下,居然正常运行,Segmentation fault消失了!
大喜!遂提交代码,起身,出门,奔向2路汽车...
一夜无语。
李大仙的高光时刻
第二天,张三和平时一样,手里拿着鸡蛋饼,脚上提拉着人字拖,刚踩着点踏入办公室,明显感觉氛围与往日不同,大家看自己眼神都有一种异样,对,那是发自内心的钦佩与敬畏!
故作淡定,来到自己工位上,刚把电脑打开,旁边美女同事便凑过来,悄声问:“张大仙,你是怎么做到的啊?太厉害了!”
原来,早上有人看到了张三昨晚提交的代码,很是震惊!有点不信邪,非要把那段开光的代码给删掉验证一下,结果可想而知,程序崩了!于是,再把开光代码加上,居然神奇的又好了!那人差点被惊掉下巴!于是,一传十,十传百,整个公司都知道了这个神话!
此时,张三身边已经围了几十个人,纷纷要求大师给传授一下佛法。张大仙清了清嗓子,刚要开口,却听见人群外传来一声:“故弄玄虚,装神弄鬼!”
说此话的,正是公司的技术大拿:尼古拉斯.李四。
尼古拉斯.李四的挑战
“谁装神弄鬼了,不要乱说!”张三不服!
“哼!这么明显的问题,还要搞个什么佛祖开光,真是笑死人了!”李四不屑地说。
“赵大拿,既然如此,来给兄弟们讲讲呗!”, 有人起哄。
“哥渴了,给哥倒杯水去!”
“好嘞!”,那人屁颠屁颠的去了。
李四,喝一口水,开始娓(继)娓(续)道(装)来(B)。
没开光的代码为什么会挂
“首先,咱们来看看,没有开过光的代码为什么会挂!”,李四打开VS Code,把那段开光代码给删掉了:
“我知道了”,小明突然喊道,“bar()中的局部变量idx没初始化,是一个随机值,所以buf[idx] = 'A'这一句内存访问异常了!”。
“这么简单的问题,谁都看得出来!”,李四瞥了小明一眼,继续说道:“问题是,idx真的是一个随机值吗?如果是随机值的话,理论上讲,这个程序应该还是有概率能成功运行的,对吧?可现在为什么会100%每次都segfault呢?”
“请大神赐教!”,小明有点不好意思了。
“咱们先在GDB中看一下idx的值是多少”,李四打开GDB,开始调试:
“idx = 16843009,这看起来不就是个随机值嘛!”,小明有点不服气。
“随机值?你再看!”,李四在GDB中敲了一条命令:
“呃,原来idx = 0x01010101,我明白了”,小明恍然大悟,“这个是在fool()中memset(array, 1, sizeof(array))这一句残留下来的值!”
“是的,说到点子上了”,李四表示赞许,“C语言中未初始化的局部变量,并不是真的随机值,而是当时栈上的残留值,这个值有可能是之前某个函数执行过后残留在栈上的,只不过我们一般不太关心栈上的残留值,所以看起来像是个随机值”,李四继续说,“我们看下fool()中array的地址和bar()中idx的地址就明白了”。
一边说着,李四又在GDB中重新运行程序,并且在第8行和第13行分别设置了一个断点,把array和idx的地址打印了出来:
“看到了吧,array[9]的地址和idx的地址一模一样!”
“原来如此!”,众人恍然大悟!
开了光的代码为什么正常运行
“那为什么开了光之后,程序就又100%运行正常了呢?”,小明还是不解。
“咱们张大仙牛X呗!”,李四有些戏虐地看着张三。
“别整这些没用的,你倒是说说看啊!”,张三有些脸红了。
“So,easy!”,李四有些得意,“佛祖开了光之后,bar()中的idx的值变了呗!”
“我不信!”,张三还在嘴硬!
“Okay,且看哥如何收拾你这妖道”,李四摆出一个降妖伏魔的姿势。
“哈哈哈,张大仙,还不赶紧跑,小心被李天师抓了祭天呀!”,有人起哄。
张三瞪了那人一眼,没有说话。
“李天师,别得瑟了,赶紧拿出真本事,降妖除魔啊!”,那人继续起哄!
“小case,且看哥来祭出法宝,收了那妖道!”,李四有些得意。于是,李四又把佛祖开光的那段代码给它加了回来:
然后,重新编译,在GDB中调试:
“idx = 0, 果然是被printf给改了!”,众人佩服不已,对李四竖起了大拇指!
李天师自己开光 - 翻车了!
“怎么样?服不服?”,李四此时那是相当得意啊!看这张三满脸通红,又得瑟道:“哥不需要请佛祖开光,哥自己就能给这段代码开光,你信不信?”
“我不信!你不许给bar()中的idx赋初值!”,张三不服!
“那当然!”,李四很是自信,又在VSCode里把代码简单修改了一下:
“接下来,是见证奇迹的时刻了!”,尼古拉斯.李四很是自信!重新编译,运行:
“哈哈!翻车了吧!让你得瑟!”,看到程序还是Segmentation fault,张三感觉自己扳回一局!
“是啊,李天师,你这法术不灵啊,道行可不如张大仙啊!哈哈哈”,众人打趣。
“WTF,翻车了!”,李四心里一惊!
李四有点不解,按照自己的理解,那个所谓佛祖开光的代码,肯定是因为printf执行了之后,把fool()残留在栈里的值给改了,idx的值变了,所以bar()里才不会触发内存访问异常了。可是,为什么现在替换成printf("Hello, World!"),就不行了呢?那么现在idx的值又被改成多少了呢?还是先用GDB看下吧:
“呃,原来idx的值还是fool()里的残留值,李天师,看来您这还是得继续修炼啊,开光失败了!哈哈哈!”,众人起哄道。
李四现在也是面红耳赤,突然,灵光一闪!“哥知道了!”,李四大叫一声!
李天师二次开光
于是乎,再打开VSCode,简单修改下代码:
重新编译运行:
居然真的成功了!
“看来李大师还是有两把刷子的!”,众人称赞道!
“不过,你只是在原来打印Hello, World的基础上加了个换行符‘\n’,为什么就成功了呢?”,众人不解。
只见李四露出稍显猥琐的笑容,故作神秘:“想知道啊?”
“想!”,众人一致回答。
“哥,不!告!诉!你!”
“呸!”,一人一口吐沫朝李四喷来!
尼古拉斯.李四,遂溺水,卒!
正经的后记
之前写了几篇关于程序调试的文章,很多童鞋觉得还不错,于是经常收到一些小伙伴私信问一些调试相关的问题。其中好几个小伙伴都提到过这种问题,就是原本很容易出现的问题,加了句printf或其他log之后,问题再也不出现了,导致很难定位。
这类问题,根据我的经验,99%的情况下,printf是不能真正解决问题的,只是把问题隐藏起来了,比如文中这种典型的栈相关的问题。还有比较常见的是多线程时候,加了printf,很有可能会引起线程间的时序逻辑的变化,因为printf是用write()系统调用实现的,而系统调用又是Linux上非常重要的调度点之一,所以很容易触发线程调度等,改变线程时序逻辑。
而另外1%的情况,printf(或printk)或其他log方式,确实是可以解决问题的。比如涉及到内存屏障、指令乱序等相关问题,或者比较典型的是在一些硬件相关的程序中,如bootloader、驱动等。这类问题比较少见,不再赘述。
总之,遇到这类问题,还是要先仔细分析下,printf(或其他log方式)可能会对程序逻辑产生什么样的影响,然后再对症下药!