存档

文章标签 ‘debugging’

计算机不存在灵异事件

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的地址被覆盖掉了。

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

计算机没有灵异事件。

标签: , , ,

都是Intel无线网卡驱动惹得祸

2007年10月27日 2 条评论

连续一段时间在公司使用无线网络上网,一直非常不爽的就是其不稳定——非常不稳定,时断时续。后来仔细研究之后发现,每次经过十几分钟,无线网卡的指示灯就灭掉了,紧接着就是无线网络断掉。过了几秒钟,无线网卡指示灯重新亮起,然后无线网络重新连接——就这样,无限网络不断的通断。

既然已经发现了问题的症结,接下来就是看看为什么会造成这种情况。我的无线网卡是ipw2200 (Intel PRO/Wireless 2200BG),应该是非常常见和通用的一款无线网卡,至少在Linux下面一直以来驱动不成问题。但是看了一下驱动程序里面的选项,发现驱动程序的时间比较久远了,所以首先还是来更新一下驱动程序吧。

于是到Google上搜索,顺利的找到了Intel的驱动官方站点。从上面的列表当中看,有两个版本的驱动程序可供选择:11.1.1.11/9.0.4.36 版本的Driver Only包,和11.1.1.0 版本的Network Connection Software。我一直觉得硬件这个东西还是有个硬件厂商提供的软件来协助管理比较好,于是没有多想,就把两个都下载了下来。之后运行安装Network Connection Software,安装过程当中笔记本的喇叭(喇叭,不是音箱)突然连续响了N声,直觉告诉我有点问题,但是也没多考虑,安装完毕之后就重新启动了。

不想重新启动之后,先是登录界面上面输入账户密码之后,登录界面好像死掉了一样,等待了大约两分钟才启动到桌面,而且登录进去之后也马上发现不对劲:任务栏上面的快速启动无法显示。虽然之前也出现过类似的问题,但是都是把快速启动删除然后再重新添加就好了。但这次直接报错:无法添加工具栏。

当然我知道,任务栏当中的图标是一组快捷方式,存放在C:\Documents and Settings\<Your User Name>\Application Data\Internet Exploerer\Quick Lunch下面。于是转过去找,竟然发现Application Data下面根本没有Internet Explorer的目录!再试图打开FireFox,竟然直接跳出首次运行的配置导入向导——一切迹象都表明:Application Data目录失踪了!

幸好我知道sysinternal有一个工具叫做Junction,用来建立目录软链接。所谓的目录软链接好像快捷方式一样,可以让你从一个位置指向另一个位置。但是目录软链接对于上面的应用程序是透明的,只要软链接已经建立好了,比如把C:\test重定向到D:\test,在应用程序看来这两个目录就是完全一样的了,对于C:\test下面的文件访问,也都会被操作系统直接重定向到D:\test下面的相应文件。

幸好我知道这个东西,而且是之前就知道。当年当我发现你根本无法阻止很多程序往我狭窄的C盘上写文件的时候,我就不得不想办法把尽可能多的东西迁移到别的盘上去。最早被我看上的就是Documents and Settings下面的目录:无数的程序把配置文件放在这里(也应该放在这里)。所以我就用软链接把C:\Documents and Settings\<My user name>\Application Data目录重定向到了另一个磁盘分区上,顺便一起被重定向的还有几个同级的目录。这种解决方案的一个坏处就是,如果某个软件作了什么不该做的事情,删掉这个软链接如同删掉一个空目录一样简单:一个RemoveDirectory的系统API调用足以。

那么是不是谁把这个软链接给删掉了呢?用资源管理器是没有办法区分的,但是用命令提示符可以。所以我立即用命令行转到这个目录去看,果然, Application Data已经变成了<DIR>,而不是软链接(<JUNCTION>)了(可惜这里忘记截图留下现场了)。

那么实际的目录内容呢?我立即转到软链接指向的目录观察,万幸的事情:所有的配置文件都还在。

接下来的事情就很简单了,换用另外一个账户登录,使用命令提示符移动到C:\Documents and Settings\<my user name>下面,删除原来的Application Data目录,然后执行junction "Application Data" "E:\path\to\Application Data",建立了一个新的目录软链接。

重新用自己的账户登录,所有的配置文件都回来了,打开FireFox看到布满插件的状态栏,放心许多。

回顾一下我之前作过的事情,只能让我相信,确实是Intel的安装程序把这个目录删掉了:计算机喇叭的响声也许就是因为有程序发现自己的 Application Data目录被删掉了。而当时在做的事情,就是在安装Intel的应用程序而已。

一个问题已经解决了,但是还有另外一个问题:登录仍然非常的慢,为什么呢?

你看到的登录界面其实是叫做WinLogon的进程所显示的。而这个WinLogon当中是可以加入许多的自定义模块的。但是这些模块分布在各处,查找起来非常不便。幸运的是sysinternal仍然有一个工具替我们做了这些事情:Autoruns。这个工具运行之后,会有一个WinLogon标签页,当中显示了所有WinLogon注册的组件。请注意图中用红线圈出的部分,这些就是Intel安插在 WinLogon当中的。而登录的时候,WinLogon使用GINA DLL来进行身份认证。从图中显然可以看出,Intel的无线网卡程序使用它自己的GINA DLL替换掉了Windows自己的GINA DLL。

那么Windows自己的GINA DLL是什么呢?我们在system32目录下面运行dir *GINA.DLL,得到一个列表,如图中右下角所示。从名字上面看,msgina.dll应该就是微软的GINA DLL了。所以在IWPDGINA上面双击,Autoruns自动打开注册表编辑器,并定位到该注册表项。将它改成msgina.dll。然后把剩下的两个和Intel相关的项目前面的勾去掉。之后重启系统,输入密码之后,登录立即完成,并且很快的看到桌面,一切恢复正常。

而这个时候回头看看我最初想要做的事情:我的无线网卡的驱动正确更新了:

更新之后的无线网卡驱动确实提高了电源管理的能力:在我写这篇文章的整个1个小时当中,无线网络没有再断。

但是,仅仅是一个无线网卡,为什么要去修改GINA,而且是这么敏感的组件:我的登录用户名和密码与无线网络有关系么?而且,为什么会毁掉我的 Application Data目录?如果不是我之前知道一点Windows
东西,也许只能重装系统了,于是两年多的用户习惯,首选配置,甚至邮件都可能丢失。

而我,本来只是为了更新一下驱动程序罢了。