标签 Unix 下的文章

也谈’SIGBUS和SIGSEGV’

SIGBUS和SIGSEGV也许是我们在平时遇到的次数最多的两个内存错误信号。内存问题一直是最令我们头疼的事情,弄清楚两个信号的发生缘由对我们很好的理解程序的运行是大有裨益的。

我们来看两段程序:
//testsigsegv.c
int main() {
        char *pc = (char*)0×00001111;
        *pc = 17;
}

//testsigbus.c
int main() {
        int *pi = (int*)0×00001111;
        *pi = 17;
}

上面的代码那么的相似,我们也同样用gcc编译(加上-g选项,便于gdb调试;平台Solaris Sparc),执行结果也都是dump core。但通过GDB对core进行观察,你会发现细微的不同。第一个例子出的core原因是:Program terminated with signal 11, Segmentation fault. 而第二个例子的core则提示:Program terminated with signal 10, Bus error. 两者有什么不同呢?这两段代码的共同点都是将一个非法地址赋值给指针变量,然后试图写数据到这个地址。

如果要说清楚这个问题,我们就要结合汇编码和一些计算机的体系结构的知识来共同分析了。

先来看testsigsegv.c的汇编码:
… …
main:
        !#PROLOGUE# 0
        save    %sp, -120, %sp
        !#PROLOGUE# 1
        sethi   %hi(4096), %i0
        or      %i0, 273, %i0
        st      %i0, [%fp-20]
        ld      [%fp-20], %i1
        mov     17, %i0
        stb     %i0, [%i1]
        nop
        ret
        restore
… …

我们关注的是这句:stb     %i0, [%i1]
从计算机底层的执行角度来说,过程是如何的呢?%i0寄存器里存储的是立即数17,我们要将之存储到寄存器%i1的值指向的内存地址。这一过程对于CPU来说其指挥执行的正常过程是:将寄存器%i0中的值送上数据总线,将寄存器%i1的值送到地址总线,然后使能控制总线上的写信号完成这一向内存写1 byte数据的过程。

我们再看testsigbus.c的汇编码:
… …
main:
        !#PROLOGUE# 0
        save    %sp, -120, %sp
        !#PROLOGUE# 1
        sethi   %hi(4096), %i0
        or      %i0, 273, %i0
        st      %i0, [%fp-20]
        ld      [%fp-20], %i1
        mov     17, %i0
        st      %i0, [%i1]
        nop
        ret
        restore
… …

同样最后一句:st      %i0, [%i1],CPU执行的过程与testsigsegv.c中的一致(只是要存储数据长度是4字节),那为什么产生错误的原因不同呢?一个是SIGSEGV,而另一个是SIGBUS。这里涉及到的就是对内存地址的校验的问题了,包括对内存地址是否对齐的校验以及该内存地址是否合法的校验。

我们假设如果首先进行的内存地址是否合法的校验(是否归属于用户进程的地址空间),那么我们回顾一下,这两个程序中的地址0×00001111显然都不合法,按照这种流程,两个程序都应该是SIGSEGV导致的core才对,但是事实并非如此。那难道是先校验内存地址的对齐?我们再看这种思路是否合理?

testsigsegv.c中,0×00001111这个地址值被赋给了char *pc;也就是告诉CPU通过这个地址我们要存取一个字节的值,对于一个字节长度的数据,无所谓对齐,所以该地址通过对齐校验;并被放到地址总线上了。而在testsigbus.c里,0×00001111这个地址值被赋给了int *pi;也就是告诉CPU通过这个地址我们要存取一个起码4个字节的值,那么对于长度4个字节的对象,其存放地址起码要被4整除才可以,而0×00001111这个值显然不能满足要求,也就不能通过内存对齐的校验。也就是说SIGBUS这个信号在地址被放到地址总线之后被检查出来的不符合对齐的错误;而SIGSEGV则是在地址已经放到地址总线上后,由后续流程中的某个设施检查出来的内存违法访问错误。

一般我们平时遇到SIGBUS时总是因为地址未对齐导致的,而SIGSEGV则是由于内存地址不合法造成的。

面对'错误'的抉择

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

人们常常会在这两种机制之间徘徊不定,难以抉择。但有两类人大可不必为此头痛,他们是坚决只使用'错误码返回机制'的人,和坚决只使用'异常处理机制'的人。而苦就苦了摇摆在中间,思索不定的那些人了。这群人有一个特点就是不停的问:"什么是异常?什么时候该使用错误码返回?什么时候又要用标准的异常处理机制呢?内存不足是不是异常?网络瘫痪是不是异常?用户输入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的经验,由于异常发生很少,很多可能发生此类异常的函数都是没有返回值的。这时是采用异常处理机制的好时机。

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

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 Go语言精进之路1 Go语言精进之路2 Go语言第一课 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