2007年十一月月 发布的文章

面对'错误'的抉择

大凡写程序者,都会遇到错误;
大凡写程序者也都知道两种错误处理的机制:传统的'错误码返回机制'和'面向对象语言引入的异常处理机制'。

人们常常会在这两种机制之间徘徊不定,难以抉择。但有两类人大可不必为此头痛,他们是坚决只使用'错误码返回机制'的人,和坚决只使用'异常处理机制'的人。而苦就苦了摇摆在中间,思索不定的那些人了。这群人有一个特点就是不停的问:"什么是异常?什么时候该使用错误码返回?什么时候又要用标准的异常处理机制呢?内存不足是不是异常?网络瘫痪是不是异常?用户输入id的超出了允许的长度该如何处理?"等
等诸如此类的问题。

我一直使用C,C没有像C++、Java、Ruby那样提供标准的异常处理机制,C只是提供了setjmp和longjmp这样的粗糙的甚至让很多新人觉得迷惑的调用接口,所以到目前为止,我还没有真正用过"异常处理"来写过代码。直到我看到"David Hanson "的"c interfaces and implementations(C语言接口与实现)"中第4章封装的C的异常处理宏。看完后我有些迷惑。迷惑的不是这组宏有什么精湛技艺,而是他破坏了以前我对错误处理的单一使用错误码返回的想法,他又给了我一个选择:使用异常处理。而多了一种选择之后,我也陷入徘徊不定中。只能反复的一遍又一遍的看和回味Hanson在这章起始的那段关于错误分类的描述和理解,以寻求在返回错误码和使用异常处理之间的平衡。

Hanson将错误分为三类:
用户错误,就是由用户的不正确输入引起的,对于此类错误的态度是:函数必须处理错误并返回错误代码。
//testusererr.c
int main(int argc, char *argv[]) {
if (argc <= 1) {
printf("you should input at leaset an argument!\n");
return 1;
}

int i = atoi(argv[1]);

if (i < 5) {
printf("i should be more than 5.\n");
return 2;
}

return 0;
}

上述的例子仅是一个程序接收用户的输入,并对其输入错误进行处理。

那么对于一个功能接口而言,对其参数是否都要做类似处理呢?通常来说在我们系统中除了针对用户的接口需要进行外,其他的接口都是内部调用的,比如库,库提供了接口同时也隐含了某种约定。我们作为程序员在使用库的时候都会遵守约定。但是这是不是这些库接口就不用对其接口参数进行任何处理了呢。一般我们会在接口的入口处加上断言。这就是我们要说的第二种错误–可检查的运行时错误。

按照Hanson的说法,可检查的运行时错误不是用户错误,上面已经说了,程序员已经按照约定传入了适当的参数,那么一旦出现错误,这个错误是从何而来的呢?可检查的运行时错误恰恰是揭示了程序的漏洞,他们不可预料,通常遇到此类错误,应用程序一般将无法恢复。看下面的例子:

//testcheckedrterr.c
#include
#include

#define MY_MAGIC 0×19210723

struct Foo {
int i;
char id[22];
#ifdef _DEBUG
unsigned int magic;
#endif

};

void check_foo(const struct Foo *foo) {
assert(foo != NULL);
#ifdef _DEBUG
assert(foo->magic == MY_MAGIC);
#endif
;
}

int main() {
struct Foo foo;
foo.i = 13;
#ifdef _DEBUG
foo.magic = MY_MAGIC;
#endif
strcpy(foo.id, "this will cause an overflow");

check_foo(&foo);
}

gcc -D_DEBUG -g *.c
执行的结果:
Assertion failed: foo->magic == MY_MAGIC, file testcheckedrterr.c, line 20
退出 ((主存储器)信息转储)

这里程序的确是按照check_foo的需要的参数提供了参数,只是没有预料到,程序存在栈溢出,对于这种运行时错误,在check_foo中我们使用了断言。断言一般都是无法恢复的,直接的结果就是程序退出。

异常,第三类错误,极少出现,可能不可预测,但有可能从中恢复的错误。如:内存不足、网络瘫痪、磁盘空间满等。按照Hanson的经验,由于异常发生很少,很多可能发生此类异常的函数都是没有返回值的。这时是采用异常处理机制的好时机。

说到这,也许还只是停留在说教上,也许开头提到的那些疑问仍然无法回答。我想什么样的回答都不能令所有人满意,自己在项目中摸索理解吧。其实永远没有绝对的事情,你大可在程序中丝毫不考虑使用异常机制。

遭遇Heap溢出

今天凌晨配合云南移动进行局数据全量升级,本来以为是件很轻松的活计,甚至不需要我动手的事情,结果却又是一次惨痛的教训啊。

这个活计其实真的很简单,就是将数据库中的旧数据全部删除,然后导入新的数据,由于数据量较大需要重启一次我们的系统。问题就在重启系统上。摆在我面前的就是"重启失败",系统dump一个core文件。通过pstack和gdb查看如下:
core 'core' of 7971: xxxxx -s
fe647b38 t_splay (3a71b0, 229, 228, 3a7000, 3ca548, 8000000) + 14
fe6475ec realfree (3ca320, 741f4, 320974, fe6bc000, 0, 3209a5) + c8
fe647e5c cleanfree (0, 7, fe6c29bc, 1a8, 3a7008, 0) + 54
fe646f88 _malloc_unlocked (ea60, 0, ff13de50, fe6bc000, ff184ae6, 0) + f4
fe646e78 malloc (ea60, 3e8, 0, 2, f8e9dacb, 1) + 20
000fa330 我们一业务函数,暂叫A_func吧 (18, 186a0, ffbfe4b0, 30330000, 37, ff00) + 1fc

碰到系统调用malloc出core,简单的初级的想法是:系统资源出现问题。使用df和top查看得知,物理内存居然有12G Free,而且出core的地方位于初始化阶段,系统大量使用内存的地方还未执行呢。

这个问题也真是碰巧了,A_func这个业务层接口恰恰是初始化我们今晨更新的局数据的接口,这又不由得让我去检验数据的有效性,经过回归数据,甚至是清空数据,问题依旧。

曾怀疑过内存对齐问题,但是自己明知道malloc出来的数据是经过编译器对齐的,经过测试后排除。

甚至怀疑这是Solaris的bug,在网上花了半个多小时搜索原因,未遂。每每当应用程序程序员遇到自己解决不了的问题时,都会归咎于操作系统。

在用户的多次催促下(大家知道电信的产品每年的宕机时间是有规定的),无奈下退回到以前的版本,发现居然可以启动。这说明什么呢?首先今晨更新的局数据是没有问题的,系统资源也是没有问题的,操作系统也是无恙的。一个新的思路急至眼前,比对一下新版和旧版功能上的差别,只有一个新增功能。恰好,这个功能(这里暂叫B_func)对应的初始化就在A_func之前调用的。快速转移到B_func内部实现当中,哇,问题
找到了。

问题源于Heap的溢出,原因当然是编码不当,马虎所致,更精确的说是:copy & paste所致。
在B_func中在原生Heap上动态申请了一块内存,内存大小为n * sizeof(A_Struct); 之后在B_func中对这块刚申请的内存进行了一次'清零'操作。就是这次'清零'操作惹下了大祸:memset(ptr_to_mem_alloced, 0, n * sizeof(B_Struct)); 而sizeof(B_Struct) > sizeof(A_Struct),这样大家就清楚了,由于'copy & paste',heap上的'组织网络'被局部的摧毁了。之后再使用heap上的数据单元自然逃离不了灭顶之灾。

这段代码是一个来到公司的新员工所写,这个员工是社招,水平不赖。其实真的不能埋怨这位员工,通过这次事件感觉责任最大的还是我,只怨我当初在评审代码时因此处简单而未加仔细评审,否则这种问题早就会被消灭在萌芽中了,也就不会出现今天的事件了。

这里也要感谢在远方的云南移动的兄弟顶住了投诉的压力,让我有时间找到问题所在啊。

以下是摘自WIKIPEDIA的关于"heap overflow"的描述:A heap overflow is another type of buffer overflow that occurs in the heap data area. Memory on the heap is dynamically allocated by the application at run-time and typically contains program data. Exploitation goes as follows: if an application copies data without first checking to see if it fits into the chunk (blocks of data in the heap), the attacker could supply the application with a piece of data that is too large, overwriting heap management information (metadata) of the next chunk. This allows an attacker to overwrite an arbitrary memory location with four bytes of data. In most environments, this may allow the attacker control over the program execution. 

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! 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