标签 编译器 下的文章

遭遇“死循环”

昨天看了“外刊IT评论”上的一篇名为《软件编程21法则》的文章,文章中提到的一条法则是:“软件直到被变成产品运行至少6个月后,它最严重的问题才会被发现”,当时表示认同。不过仅仅相隔一天,这条法则就变成了眼前的现实。

今天上午我们的某版本系统在某省出现了故障,该版本在这个省上线恰好将近6个月^_^,系统上线以来一直运行良好,直到这次故障。故障现象为"挂死":所有进程都挂死在某一把锁的lock上。以前出现这种情况多为某个进程加锁后,在锁内异常退出,未能释放锁而导致其他进程挂死。这种"挂死"多是代码中访问非法内存地址导致的,一般都会有core文件dump出来。不过这次出现挂死后,我们并未找到core文件的影子。查看系统运行日志也无果。通过脚本将所有该应用的子进程的运行栈快照收集到一个文件中,然后对这个数据庞大的文件进行分析,以试图找到一些蛛丝马迹。

分析发现绝大多数子进程都挂起了,其运行栈栈顶多为:
lwp_mutex_timedlock (f1444c28, 0)

不过只有一个进程与众不同,它的栈顶是一个我们自己实现的函数,这里暂且称这个函数为foo_func吧。迅速查看foo_func的源码实现,发现一个while循环,第一时间想到:是不是foo_func进入while死循环了?在故障应用主机上用top查看一下系统运行状态,发现确有一个进程占用cpu很高,而且持续很高。pstack一下这个进程,栈顶端果真就是foo_func,“死循环”的推论是正确的。 即使这个进程死循环了,怎么会连累其他进程也停止工作了呢?原因就在于这个死循环是在这个子进程获得锁之后发生的,因为死循环了,导致无法释放这把锁,其他子进程干着急也无可奈何!

foo_func为何能进入死循环?仔细斟酌一下foo_func的代码也不难得出:代码中混用了int和unsigned char,导致数组下标值变为负数,数组访问溢出,读取到的值是随机值,所以死循环也是随机发生的(之前几个月运行都良好也是因为这个原因)。 C语言不是强类型的,int和unsigned char两个宽度不同的类型可以放在一起使用。C编译器帮你做隐式转换,转换规则虽不十分复杂(C99引入了rank概念后,转换规则略就显复杂了),但也很容易犯错,这也是我们常说的C陷阱之一。另外foo_func代码本身的功能逻辑也有漏洞,这里就不细说了。

视警告为错误

每当你Build Project代码的时候,如果看到的是满屏的Warning,那么提醒你小心了,不妨看看《高效程序员的45个习惯》中对Warning的态度和处理方式。该书中的第34个习惯讲的是“警告就是错误”! 当然这个“习惯”所阐述的内容并不是这本书首创,在很多经典的传授编程之道的书中也都提到过。

将警告作为错误来处理,说起来容易,可作起来可并不那么简单。这不仅仅只是一个态度的问题,有时候还需要有技术手段或技巧去帮助你完成。《高效程序员的45个习惯》一书的作者也在该习惯所对应的“平衡的艺术”中提到:“如果确实没有应对之策的话,那就不要再浪费更多时间了。但类似这种情况很少发生”。另外他建议应该经常使用编译器的directive(指示器),将一些实在无法避免但又确定不是潜在缺陷的警告进行提示,告知编译器这个地方的警告可以不去理会。

将警告视为错误首先需要的就是勇气,大胆的在你的Makefile中将-Werror赋予给Gcc吧。Gcc则为你提供了一定的技术手段来帮你处理面对无法避免的警告时“左右为难”的情况。

Gcc为我们提供的技术手段就是Pragmas,虽然Gcc手册中建议我们一般情况下不要显式的使用Pragmas,但必要时还是需要这个工具的帮助的。

#pragma directives这个指示器嵌入在源码中,用于在源码编译时给GCC编译器提供一些指示信息。Pragmas有多种分类,这里我们需要的是Diagnostic Pragmas。简单的说,嵌入在源码中的Diagnostic Pragmas给我们提供了如下一些能力:
-> 将某类编译警告按编译错误处理 (如:#pragma GCC diagnostic error "-Wformat")
-> 将某类编译错误按编译警告处理 (如:#pragma GCC diagnostic warning "-Wformat")
-> 忽略某类编译警告 (如:#pragma GCC diagnostic ignored "-Wformat")

Gcc对Diagnostic Pragmas的支持是随着版本进化而逐渐增强的,在gcc 3.4.6版本下Diagnostic Pragmas是不被accepted的,Gcc只accept五类Pragmas;到了gcc 4.4.4版本,Gcc已经可以支持12类Pragmas,这其中就包括Diagnostic Pragmas

通过实际的测试也可以证实以上说明。

e.g.
/* testpragma.c , gcc -Wall testpragma.c */
#include

#pragma GCC diagnostic error "-Wformat"
int main() {
    printf("%d\n", "Diagnostic Pragmas Test");
    return 0;
}

在Sparc Solaris 10上使用gcc 3.4.6编译结果如下:
gcc -Wall testpragma.c
testpragma.c:3: warning: ignoring #pragma GCC diagnostic
testpragma.c: In function `main':
testpragma.c:5: warning: int format, pointer arg (arg 2)
编译器提示忽略了diagnostic pragma指示。

而在Ubuntu 10.04 Gcc 4.4.3版本下编译结果如下:
gcc -Wall testpragma.c
testpragma.c: In function ‘main’:
testpragma.c:5: error: format ‘%d’ expects type ‘int’, but argument 2 has type ‘char *’
编译器正确执行了我们给出的指示^_^。

#pragma directive的作用范围也很好理解:
首先肯定是在同一编译单元范围内有效,在A编译单元中设置的#pragma directive,在B编译单元是无效的。比如我另外编写一个utils.c,其内容如下:
#include

void foo() {
    printf("%d\n", "In another compile unit");
}
我们将utils.c与testpragma.c一起编译,gcc -Wall testpragma.c utils.c,得到的结果是:
testpragma.c: In function ‘main’:
testpragma.c:5: error: format ‘%d’ expects type ‘int’, but argument 2 has type ‘char *’
utils.c: In function ‘foo’:
utils.c:4: warning: format ‘%d’ expects type ‘int’, but argument 2 has type ‘char *’
在testpragma.c这个编译单元,#pragma directive生效,但是在utils.c这个编译单元并不生效,依旧被诊断为Warning。

其次,在同一个编译单元中,#pragma directive影响的范围是其所在行之后的代码,直到下一次修改针对同样warning option的#pragma被放置。还是举例说明,我们改造一下testpragma.c:
/* testpragma.c */
#include

#pragma GCC diagnostic error "-Wformat"
void foo() {
    printf("%d\n", "Diagnostic Pragmas Scope Test");
}

int main() {
    printf("%d\n", "Diagnostic Pragmas Test");
    return 0;
}
编译命令执行后,Gcc给出的输出结果是:
testpragma.c: In function ‘foo’:
testpragma.c:5: error: format ‘%d’ expects type ‘int’, but argument 2 has type ‘char *’
testpragma.c: In function ‘main’:
testpragma.c:9: error: format ‘%d’ expects type ‘int’, but argument 2 has type ‘char *’
#pragma directive从头置尾一直发挥作用,两个format Warning都被当作Error报告了。

现在我们对main中的问题放松要求,将源码变为:
/* testpragma.c */
#include

#pragma GCC diagnostic error "-Wformat"
void foo() {
    printf("%d\n", "Diagnostic Pragmas Scope Test");
}

#pragma GCC diagnostic ignored "-Wformat"
int main() {
    printf("%d\n", "Diagnostic Pragmas Test");
    return 0;
}
执行编译命令后,Gcc的提示变为:
testpragma.c: In function ‘foo’:
testpragma.c:5: error: format ‘%d’ expects type ‘int’, but argument 2 has type ‘char *’
显然对-Wformat按照Error处理的作用范围仅限于foo这个函数,因为在foo之后我们修改了对-Wformat的指示!

有了编译器的支持,我们将更有信心去养成“视警告为错误”的习惯了。实际工作中,#pragma directive应用应该不多,因为多数情况下的警告都是可以通过正常的代码完善消除掉的。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats