存档

文章标签 ‘linux’

计算机不存在灵异事件

2009年8月1日 2 条评论

再把这条铁律拉出来一次。

今天碰到了一个bug,服务器在运行时会core dump在一个很灵异的地方,排除这个错误的过程,以及最后发现的错误结果很具有典型性,所牵涉到的技术也很多,拿来作为Linux调试的课程挺好的。:-P

整个里面假设读者已经知道怎么用gdb,如果不知道,请参见GDB Manual

首先,很幸运的是,这个问题是可以很容易重现的,而且更重要的,有core dump。
拿到core dump之后,惯例是查看一下调用栈:(为了避免泄漏商业秘密,所有函数名,文件名什么的都用foo啊,bar啊,foobar啊,blabla啊等等代替)。

(gdb) bt
#0 0x000000eb in ?? ()
#1 0x3aa1d941 in ?? ()
#2 0x000001f8 in ?? ()
#3 0x080cf888 in foo (range=10000) at foo/foo.c:18
#4 0x080c1f29 in bar () at bar/bar.c:423
[....]

(gdb) info f 0
Stack frame at 0xbfc42548:
eip = 0xeb; saved eip 0x3aa1d941
called by frame at 0xbfc4254c
Arglist at 0xbfc42540, args:
Locals at 0xbfc42540, Previous frame's sp is 0xbfc42548
Saved registers:
eip at 0xbfc42544

(gdb) f 3
#3 0x080cf888 in foo (range=10000) at foo/foo.c:18
18 return ((u32)random()) % range;

相当的灵异,栈上的0,1,2都是,一个返回地址怎么可能是0x1f8,而且,core dump的原因是因为eip跑飞到了0xeb。到frame 3的时候看起来正常了,但是出错的地方在random这种简单的库函数上。不过既然frame 3往下的部分都是好的,我们有理由认为栈并没有被搞坏。因为GDB在显示调用栈的时候可能会把一些不正确的调用栈也显示出来,我们干脆直接看内存:

(gdb) x/10w $esp
0xbfc42544: 0x3aa1d941 0x000001f8 0x080cf888 0x09582930
0xbfc42554: 0x0833f038 0xbfc42678 0x080c1f29 0x00002710
0xbfc42564: 0x0823747f 0xb7ca168c

看加粗的部分:这里就是frame 3返回地址,而上面的东西,就是bt显示出来的frame 1和frame 2了,而frame 0就是当前的eip了:它跑飞到了0xeb。

GDB果然在显示栈的时候做了手脚。

OK,我们反汇编看看这个返回地址,到底干了什么:

(gdb) disassemble 0x080cf888
Dump of assembler code for function _Z6foom:
0x080cf85c <_Z6foom+0>: push %ebp
0x080cf85d <_Z6foom+1>: mov %esp,%ebp
0x080cf85f <_Z6foom+3>: push %ebx
0x080cf860 <_Z6foom+4>: sub $0x4,%esp
[…]
0x080cf87a <_Z6foom+30>: movl $0x0,0xfffffff8(%ebp)
0x080cf881 <_Z6foom+37>: jmp 0x80cf893 <_Z6foom+55>
0x080cf883 <_Z6foom+39>: call 0x8051f90
0x080cf888 <_Z6foom+44>: mov $0x0,%edx
[…]
0x080cf899 <_Z6foom+61>: pop %ebx
0x080cf89a <_Z6foom+62>: pop %ebp
0x080cf89b <_Z6foom+63>: ret
End of assembler dump.

看起来也没干啥,继续看调用的地址是啥吧:

(gdb) disassemble 0x8051f90
Dump of assembler code for function random@plt:
0x08051f90 : jmp *0x833f140
0x08051f96 : push $0x1f8
0x08051f9b : jmp 0x8051b90 <_init+24>

有点意思啊,random@plt,PLT是什么?PLT是Linux ELF格式可执行文件当中的一个部分,称为Procedure Linkage Table。PLT是程序为了实现Linux共享库的动态迟绑定(Lazy Binding)而引入的一种机制。关于 ELF格式、PLT,和下面要提到的GOT(Global Offset Table)的资料,可以参见下面这些链接:
The ELF Object File Format: Introduction
http://www.linuxjournal.com/article/1060
UNIX ELF File Format (PPT)

好了,言归正传,就算你不知道PLT,你也可以继续往下看。最土的办法,既然jump到一个地方,就看看这个地方是什么吧。

(gdb) p/x *0x833f140
$6 = 0x8051f96

显然这个重新跳回到了PLT当中,看下面:

(gdb) disassemble 0x8051f96
Dump of assembler code for function random@plt:
0x08051f90 : jmp *0x833f140
0x08051f96 : push $0x1f8 // 这是栈上的 0x1f8
0x08051f9b : jmp 0x8051b90 <_init+24>

又一个jmp,这次jmp到了什么地方呢?

(gdb) disassemble 0x8051b90
No function contains specified address.

这是怎么回事?因为没有对应的符号,或者这一段代码不在.text里面。不过我们还可以用其他的方法看到指令:

(gdb) x/10i 0x8051b90
0x8051b90 <_init+24>: pushl 0x833f03c // 这是栈上的另一个东西的来源
0x8051b96 <_init+30>: jmp *0x833f040
0x8051b9c <_init+36>: add %al,(%eax)
0x8051b9e <_init+38>: add %al,(%eax)

又有一个jmp,继续。

(gdb) p/x *0x833f040
$7 = 0xeb

怎么会这样?这个指针被覆写了,Jump到了0xeb,然后就core了。

看08833f040这个地址,属于GOT(Global Offset Table):

(gdb) x/10w 0x833f040
0x833f040 <_GLOBAL_OFFSET_TABLE_+8>: 0x000000eb 0x00000002 0x00000006 0xb7f4401c
0x833f050 <_GLOBAL_OFFSET_TABLE_+24>: 0x08051bd6 0xb7d8e9b0 0xb7d85260 0x08051c06
0x833f060 <_GLOBAL_OFFSET_TABLE_+40>: 0xb7d32ae0 0xb7da88d0

我们从GOT头上开始看:

(gdb) x/10w 0x833f040-8
0x833f038 <_GLOBAL_OFFSET_TABLE_>: 0x00000000 0x3aa1d941 0x000000eb 0x00000002
0x833f048 <_GLOBAL_OFFSET_TABLE_+16>: 0x00000006 0xb7f4401c 0x08051bd6 0xb7d8e9b0
0x833f058 <_GLOBAL_OFFSET_TABLE_+32>: 0xb7d85260 0x08051c06

仍然说Lazy Binding相关的东西,如果你了解ELF的Lazy Binding,你会知道GOT的前面四个字是特殊的,而其中的GOT+8的地方是_dl_runtime_resolve,而上面的最后一个jmp就是跳到这里。但是,这里却变成了0xeb。

错误的原因找到了,但是,究竟是谁干的?我们知道gdb有一个很好的feature叫做watchpoint,这个在VS当中被叫做Data Breakpoint。它们的本质是一样的,都是通过硬件来实现对某个地址的监控,你可以设定当这块内存被读、写、或者被访问(包括读写)的时候触发该断点。很好,我们就在这里加上一个watchpoint。注意watchpoint的语法,你需要在地址前面加一个*。

我们重新启动服务器,然后attach到这个进程,对出现错误的地址 0x0833f040加了watchpoint。为什么可以这么做?因为GOT在内存当中的位置是固定的,我们知道,无论你第几次启动一个程序,GOT都会被加载到同一个地址。

(gdb) watch *0x0833f040
Hardware watchpoint 2 added

然后继续执行

(gdb) c
Continuing.

然后,坐到一边喝茶去吧,等待你看到gdb被断进去。
然后,终于,等到了。

Hardware watchpoint 2: *137621568

Old value = -1208695104
New value = 235
CFoo::foobar (this=0xb1df5810) at foo/foobar.c:477
477 blablabla_func(0, sth->data.id, tmpData);
Current language: auto; currently c++

好了,我们看看这个时候的GOT:

(gdb) x/w 0x833f040
0x833f040 <_GLOBAL_OFFSET_TABLE_+8>: 0x000000eb

这个值的的确确被修改了,那么,我们看看栈。

(gdb) bt
#0 CFoo::foobar (this=0xb1df5810) at foo/foobar.c:477
#1 0x0812fa69 in CFoo::doSomthing(this=0xb1df5810, action=0xbfb93378, event=0xbfb934d0) at foo/foobar.c:1100
[...]
#10 0x08067a60 in main (argc=0, argv=0x3aa1d95a) at server.c:1289

很好,这是我们的代码,过去看看:

对应代码:
LPSOME_DATA_TYPE tmpData;
u64 some_array[MAX_SOME_ARRAY_SIZE];
size_t array_sz = MAX_SOME_ARRAY_SIZE;
[...]
tmpData->iRet = 0;
tmpData->iField01 = someVar;
tmpData->iField02 = SOME_CONSTRANT; // iField02的offset是8,也就是结构的第三个成员。
tmpData->iId = some->unit_id;
foobar(); // line 477

(gdb) p tmpData
0x833f038

OK,因为代码已经被处理过,所以逻辑可能看的不是很清楚,但是很明显,在这里,问题在tmpData,这是一个指针(请参见类型,但是变量名竟然没有任何的表明这个是个指针),没有初始化就用了。非常非常的巧合,这个变量所在的地方正好是GOT的地址,然后,在对iField02变量赋值的时候,_dl_runtime_resolve的地址被覆盖掉了。

至此,凶手已经找到了,但是这个给我们什么启示呢?

计算机没有灵异事件。

标签: , , ,