字节对齐引发的血案

最近的项目因为引入了合作方的代码,多了很多疑难问题,但这些问题也非常有意思,Debug这些问题的过程给了我很多乐趣。

这篇博客是关于一个由#pragma引起的问题,Debug过程中充满了挑战和惊喜,不停的需找线索的过程,也是一个不断提升自身水平的过程。

Segmentaion Fault

在某次提交代码以后,我们跑回归测试用例,出现了segmentation fault的情况。马上gdb attach上去看进程调用栈。

1
2
3
4
5
Program received signal SIGSEGV, Segmentation fault.
0x08048562 in Component::set (this=0x18000000) at pragma.cpp:7
7 this->flag = 1;
(gdb) bt
#0 0x08048562 in Component::set (this=0x18000000) at pragma.cpp:7

可以看到,第7行的赋值语句导致了SegFault,细看就能看到this指针是一个非法值0x18000000,定位到这一步以后,我们做了第一次推测,很有可能是踩内存了。

为了证明我们的推测,gdb返回上一层堆栈打印Component类的指针,结果出乎我们意料。

1
2
3
4
5
(gdb) f 1
#1 0x08048552 in main () at main.cpp:9
9 board->_com->set();
(gdb) p *board
$3 = {price = 0 '\000', _com = 0x804b018}

这时看到的Component类指针_com是正常的,我一度非常怀疑g++的编译器出问题了,我甚至翻阅了很多关于C++编译成员函数的资料,事实证明:一切问题都应该先从自身上找原因。

所以,我又把board的内存打印出来,看是不是有什么线索。

1
2
3
4
5
(gdb) x/32xb board
0x804b008: 0x00 0x00 0x00 0x00 0x18 0xb0 0x04 0x08
0x804b010: 0x00 0x00 0x00 0x00 0x11 0x00 0x00 0x00
0x804b018: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x804b020: 0x00 0x00 0x00 0x00 0xe1 0x0f 0x02 0x00

细心的话会发现,原本_com的指针为0x804b018,即第一行后4个字节。而this指针为0x18000000,正好是第一行从第二个字节开始的4个字节。似乎有点头绪了…

汇编

从上面的初步分析,我们基本锁定了问题的范围,在函数调用前,指针是正常的,进入函数以后指针变了。难道是函数调用出了问题?为了进一步分析,可以使用gdb的汇编调试功能,如果不熟悉的可以参考Lenky的博客。以下打印出调用set的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(gdb) disassemble main
Dump of assembler code for function main():
0x08048534 <+0>: push %ebp
0x08048535 <+1>: mov %esp,%ebp
0x08048537 <+3>: and $0xfffffff0,%esp
0x0804853a <+6>: sub $0x10,%esp
0x0804853d <+9>: call 0x804858a <Board::init()>
0x08048542 <+14>: mov 0x804a028,%eax
0x08048547 <+19>: mov 0x1(%eax),%eax
0x0804854a <+22>: mov %eax,(%esp)
0x0804854d <+25>: call 0x804855c <Component::set()>
=> 0x08048552 <+30>: mov $0x0,%eax
0x08048557 <+35>: leave
0x08048558 <+36>: ret
End of assembler dump.

在0x08048547行,这一行代码就是获得_com指针,前面我们看到board的偏移4个字节以后取到Component的指针,而这一行汇编代码只偏移1个字节。刹那间,4个字突然跳到我的脑海里 – 字节对齐

我们知道在C语言里,在没有声明字节对齐的情况下,编译器会按4字节或8字节对齐。
Board类的结构如下

1
2
3
4
5
6
7
8
9
class Board
{
public:
Board();
static void init();
char price;
Component *_com;
};

这里没有声明字节对齐的方式,系统是32位的,理应按4字节对齐,这个与我们看到的Board的内存是一致的。可是为什么在调用的地方,汇编代码是按1字节来偏移呢?编译器编译调用点时理解这个结构是1字节对齐,而该结构定义并没有声明1字节对齐,那肯定是别的地方影响了。结果一翻看前面几个头文件,居然有一处这样的代码:

1
2
3
4
#pragma pack(push,1)
struct foo
{
};

这一出#pragma push没有对应的#pragma pop,这个头文件在Board的头文件之前,所以导致Board结构也是按1字节对齐,而由于board申请内存定义的地方没有这个问题,是按4字节对齐,所以才导致了this指针错乱的结果。

总结

终于真相大白了,#pragma引发的血案宣布告破。

对于该编程问题的引入,我就不说什么了。对于该问题的调试,我总结出的结论是:以怀疑一切的眼光看待问题,同时要多方面去考虑问题,这样“问题”就不是什么“问题”了。