标签 C 下的文章

也谈’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则是由于内存地址不合法造成的。

一个很有意思的Bug

这个Bug源于昨天凌晨的一次版本升级失败。睡了一大觉后,下午回到公司,重现了这个问题并找到了原因,发现这的确是一个'很有意思的Bug'。

系统在从数据库初始化过程中遇到了问题:在读取数据库数据时,提示ORA-24373错误。手册上对ORA-24373的解释是这样的:
ORA-24373: invalid length specified for statement
Cause: The length specified for the statement is either 0 or too large.
Action: Specify a valid length for the statement.

从错误的提示来看,不应该是数据的问题,但是出于对自己程序的信任,还是首先比对了数据,结果证明了数据是无误的;

看来一定是代码的问题了。首先定位到了返回错误的那个接口OCIStmtPrepare(原型: sword OCIStmtPrepare(OCIStmt *stmtp, OCIError *errhp, OraText *stmt, ub4 stmt_len, ub4 language, ub4 mode)),可是怎么看这块也不该出问题。但从ORA-24373的Cause来看,似乎是传
给OCIStmtPrepare的值不太对劲儿。

我们从数据库读取数据的一般流程:
1、获取符合某一特定条件的某一类数据的条数count;
2、读取count条数据From DB。

为了达到上面的两个目的,我们执行了两次sql操作,分别由两个语句完成,这里不妨成为sql1和sql2。sql1和sql2的内容是在代码里形成的。

其分为两部分:select的字段list和where条件部分;由于where条件部分是动态的,所以每次sql操作之前都是需要先生成sql1和sql2的。

保守的我们认为sql语句的长度是有限的,在我们的代码里我们用了一个宏XX_SQL_MAX_LEN来定义了一条语句内容的最长值,而XX_SQL_MAX_LEN

在我们的系统中被赋予512这个整数值。这样我们很容易得到如下的定义:
char sql1[XX_SQL_MAX_LEN]; //遗憾的是结尾''也被我们算进了XX_SQL_MAX_LEN中了。
char sql2[XX_SQL_MAX_LEN];

一般sql语句的前端的select字段list是固定的,其长度也就是固定的;但是后面的where语句中的条件则是由参数指定。在我们的系统里这个条件很简单,就只有一个索引值(index)。

让我们意想不到的是在我们要读取的那个数据表的操作语句中,select字段list的长度就超出我们预想,达到了508个字节,要知道目前我们还不知道这一事实。我们按照一般数据库操作流程:先读count,再读数据。结果呢?我们读出了若干条数据,然后就在某一条数据上出现了上述错误。这个数据没有什么特殊的,除了其index的值的位数变成了四位之外。

警觉的我们发现:系统在读取index是三位整数的记录时都是没有问题的,偏偏到index是四位整数时才会有问题,而问题偏偏又出在执行sql1上。直觉告诉我们又是内存问题。

分析如下:
sql1和sql2在栈上紧挨着,是否生成sql2的时候污染了sql1的数据呢?我们试图打印出sql1的值,结果发现printf的输出居然是空,这更坚定了内存遭受破坏的想法。遂掰手指头数sql2的长度,发现其长度居然恰好512字节。我们拿一个栈图来说明问题:


栈图

正如上图中所示:sql2的数据多的沿着数据的延伸方向一直越过了边界来到了sql1的境地,在sql1第一个字节上留下一个''就结束了。这就是为什么sql1打印为空的原因。同样由于传给OCIStmtPrepare的参数stmt_len的值为strlen(sql1),导致stmt_len变为0,也就恰好与ORA-24373这个错误码的说明一致了。这简直太巧了,太不可思议了。不多不少恰好512。

程序员的想当然造就了这一切,就好比千年虫一样,给你我带来麻烦。

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