« 让 GNU Make 把中间文件放到独立目录 | 返回首页 | 编程的首要原则 »

Freebsd 下 glx 的一点问题

最近特别忙,经常连续工作超过 10 小时。所以情绪也有点不稳定,有的应该在半小时处理完的问题,会拖上几个小时。比如昨天就碰到一个。

由于底层代码由我一个人做了大规模重构(在不改变中间层的基础上做的),大约修改了一万多行代码,上百个 C 文件,其中重写了数千行。这样的变动实在太大,以至于出了罕见的问题后,很让人头痛。新的代码在 Windows 和 Ubuntu 上都一切正常,惟独在 Freebsd 上,程序退出的时候会引起一个 core dump 。

我对 gdb 调试器的高阶运用显然经验不足,加上这几年写代码也不怎么调试。(IMHO ,与其增加调试能力,不如提高设计能力和编写高质量代码的能力;让 bug 不容易出现,或是把 bug 限制在很小的范围,比解决 bug 更重要)在尝试过初步的调试后,我还是使用大脑做静态分析。确定问题出在 so 文件的显式关闭 (主动调用 dlclose)的环节。

根据常规推断,如果在 dlclose 后,程序异常,最可能是其它模块引用了其中的代码段,或静态数据段。实际现象中,进程崩溃在 C 的 main 函数返回之后。而我的系统整个是用 C 构建的,不会有 C++ 里那么多的跟这些有关的相关问题。所以可以推断是进程隐式加载的 so 最后卸载时发生的问题。

这个合乎逻辑的推理最终也基本得到证实,看起来很简单。不知道昨天我怎么就吃错药了,足足浪费了 4 小时来得到结论。

实际调试过程是这样的。我用二分法去掉某些代码,想看看把测试程序缩减到什么程度,问题会消失。这显然是个不肯动脑子的傻方案。鉴于精神不佳,姑且原谅一下。凡是愚蠢的方法,在有效的同时,一定是件体力活。干的时间久点就可以理解了。最终我发现,在 X Window 创建之后,为了隐藏鼠标光标,我调用了一个 X 的 API :XCreateBitmapFromData 。如果不调这个 API ,系统结束后就没有问题。但这个 API 本身调用是成功的。

在没有经过大脑的状态下,我拼命的往这个小洞里钻。又是怀疑 API 传入的参数,又是怀疑 XLib 的工作方式。接着,我又发现了另外几个 X 调用会引起问题。全部的现象都是,调用时成功,程序结束时崩溃。当然具体情况也分两类,一类是毫无朕兆,另一类是,调整一些次序,会提前得到 X Server 发过来的错误。这后一类近一步的加深了我的错觉。


最终,我发现,即使不改变代码,修改最终编译的链接参数也会引起问题。

如果程序只链接 GL 库,就一切正常。但是多链接一个 GLU 或是别的跟 openGL 有关的库,例如 GLEW ,就会在程序退出的时候崩溃。即使,我缩减后的测试代码里没有用到任何 GLU 或 GLEW 里的函数也一样。

当然,测试代码里使用了 glx 的一个 API ,必须链接 GL 库。

暂时还不能确定引起进程崩溃的 bug 直接的罪魁祸首在哪个位置。还是多休息一下,精力充沛时再回头来看比较好。好在 freeBSD 上有全部相关源码,我的桌面也是 buildworld 出来的,等周一再仔细看看吧。


虽然程序世界里,因果关系往往都非常明显,远没有现实世界那么复杂。但偶而也会遇到一些棘手的问题。当然是否棘手取决于你对系统的犄角旮旯的了解程度。或许你难以理解的问题,换个具有相关知识的人就变的一览无遗。就好象 Windows 上的木马病毒泛滥,许多用户在中招后毫无察觉;但 Windows Geek 们不装所谓杀毒软件,一有异向就会立刻察觉到。

链接,其实是个很复杂的过程。有经验的程序员一不小心也会中招。我曾经帮同事查过一个跟 lua 有关的问题,最终就是因为错误的链接导致进程内同时存在了两份 lua core 的代码。导致某些静态变量有了两份。这种问题往往会被隐藏很久,最终会以奇怪的形式暴露出来,而超出一般程序员的调试能力。

btw, 我认为,这次遇到的问题以前就存在,只是这次重构暴露出来而已。因为早先的代码,我自己完成了链接和二进制模块加载的过程。这次我想简化这个环节,更依赖系统提供的相关机制。


3 月 25 日补充:

问题的根本原因见:libstdc++ 卸载问题

Comments

教你一招,用 git 的 biselect 可以直接确定是哪次提交导致的bug 每次提交最多几十行代码,很容易定位的。哈哈。 我自己就成功搞定了一些难缠的bug
@赵中 我碰到这个问题,堆栈已经被破坏掉。
命令 ldd -v 执行程序名 可用来查看可执行文件的动态链接库的依赖关系
进程意外退出会在当前目录下产生形如‘core.数字’的文件比如‘core.1234’ 使用命令 gdb 运行程序名 core.数字 进入gdb然后使用bt命令 可以查看进程意外退出前函数调用的堆栈 比如 [root@voip-config imserver]# gdb IMServer core.1234 GNU gdb Red Hat Linux (6.1post-1.20040607.52rh) Copyright 2004 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i386-redhat-linux-gnu"...(no debugging symbols found)...Using host libthread_db library "/lib/tls/libthread_db.so.1". Core was generated by `./IMServer'. Program terminated with signal 11, Segmentation fault. Reading symbols from /etc/libcwait.so...(no debugging symbols found)...done. Loaded symbols for /etc/libcwait.so Reading symbols from /lib/i686/libpthread.so.0...(no debugging symbols found)...done. Loaded symbols for /lib/i686/libpthread.so.0 Reading symbols from /usr/lib/mysql/libmysqlclient.so.10...(no debugging symbols found)...done. Loaded symbols for /usr/lib/mysql/libmysqlclient.so.10 Reading symbols from /usr/local/lib/libmysqlpp.so.4...done. Loaded symbols for /usr/local/lib/libmysqlpp.so.4 Reading symbols from /data/oracle/9.2.0/lib/libclntsh.so.9.0...done. Loaded symbols for /oracle/9.2.0/lib/libclntsh.so.9.0 Reading symbols from /data/imserver/lib/libACE.so.5.5.0...done. Loaded symbols for ./lib/libACE.so.5.5.0 Reading symbols from /data/imserver/lib/libIce.so.30...done. Loaded symbols for ./lib/libIce.so.30 Reading symbols from /data/imserver/lib/libIceUtil.so.30...done. Loaded symbols for ./lib/libIceUtil.so.30 Reading symbols from /data/imserver/lib/libDBManager.so...done. Loaded symbols for ./lib/libDBManager.so Reading symbols from /lib/i686/libc.so.6...done. Loaded symbols for /lib/i686/libc.so.6 Reading symbols from /usr/lib/libstdc++.so.5...done. Loaded symbols for /usr/lib/libstdc++.so.5 Reading symbols from /lib/ld-linux.so.2...done. Loaded symbols for /lib/ld-linux.so.2 Reading symbols from /usr/lib/libz.so.1...done. Loaded symbols for /usr/lib/libz.so.1 Reading symbols from /lib/libcrypt.so.1...done. Loaded symbols for /lib/libcrypt.so.1 Reading symbols from /lib/libnsl.so.1...done. Loaded symbols for /lib/libnsl.so.1 Reading symbols from /lib/i686/libm.so.6...done. Loaded symbols for /lib/i686/libm.so.6 Reading symbols from /data/oracle/9.2.0/lib/libwtc9.so...done. Loaded symbols for /oracle/9.2.0/lib/libwtc9.so Reading symbols from /lib/libdl.so.2...done. Loaded symbols for /lib/libdl.so.2 Reading symbols from /lib/i686/librt.so.1...done. Loaded symbols for /lib/i686/librt.so.1 Reading symbols from /lib/libgcc_s.so.1...done. Loaded symbols for /lib/libgcc_s.so.1 Reading symbols from /usr/lib/libbz2.so.1...done. Loaded symbols for /usr/lib/libbz2.so.1 Reading symbols from /lib/libnss_files.so.2...done. Loaded symbols for /lib/libnss_files.so.2 #0 0x08081ef1 in std::_Rb_tree_base_iterator::_M_increment () (gdb) bt #0 0x08081ef1 in std::_Rb_tree_base_iterator::_M_increment () #1 0x081583be in std::_Rb_tree_iterator, std::pair&, std::pair*>::operator++ () #2 0x081989c2 in IMServerJabberManager::AddNodeProc () #3 0x08198278 in IMServerJabberManager::iksHookFunction () #4 0x08071369 in tagHook () #5 0x0806f859 in sax_core () #6 0x080703d9 in iks_parse () #7 0x08071834 in iks_recv () #8 0x08197c99 in IMServerJabberManager::processParser () #9 0x08153a12 in IMClientHandler::process () #10 0x081524a5 in IMClientHandler::handle_input () #11 0x081a1961 in IMThreadPool::svc () #12 0x00227c97 in ACE_Task_Base::svc_run (args=0x8e81894) at Task.cpp:254 #13 0x0022801a in ACE_Thread_Adapter::invoke_i (this=0x10081b04) at Thread_Adapter.cpp:151 #14 0x00227f81 in ACE_Thread_Adapter::invoke (this=0x8e60d58) at Thread_Adapter.cpp:95 #15 0x001c5f7f in ace_thread_adapter (args=0x31393331) at Base_Thread_Adapter.cpp:137 #16 0x00aa3e21 in pthread_start_thread () from /lib/i686/libpthread.so.0 #17 0x0039bbda in clone () from /lib/i686/libc.so.6 (gdb) quit [root@voip-config imserver]# 据此可能比较容易找出导致进程意外退出的原因并纠正
昨天,我又试了一下,原因在于静态库和动态库的不同。静态库的话,如果没有用到某个函数,它是不会去查找定义的,而动态库则不同。而那个静态库中,多包含了一个只对对方公司调试有用的.o文件,所以改成了.so文件后,会发生找不到定义的情况,删除那个.o文件就OK了
云风,你好,我试图安装你的方法进行操作,但是我用gcc -o C.so C.a -shared的方式生成的.so文件,发现里面export出来符号只有一些so文件必要的东西(__do_global_ctors_aux等),其他的函数名,全局变量都没有输出。 所以我就使用ar x来把所有的.o文件都展开到一个目录下,然后再生成.so文件。 就当我以为一切OK的时候,最后和可执行文件链接的时候,神奇的事情发生了,竟然说有一个函数找不到定义。我找遍了所有的.o文件,用nm -C的方法来查找,竟然真的没有,那为什么.a文件的时候link就OK的呢??? 难道有什么神秘的方法,可以防止别人来展开.a文件? 继续调查中……
@mikeshi, 首先我的问题最终查清楚是系统里的 so 链接了其它同一 so 的不同版本导致的。所以本质上和你的问题非常相似。 不过你的问题更明显一些。 原则上:所有库要么都是静态连接到一起,要么全部是动态连接。这样做的目的是:所有静态变量在进程中只有一份。 否则,从不同的代码出发,会访问到不同的静态变量的地址。虽然在功能逻辑上它们应该是同一份东西。 你的问题,只是 C.a 是第 3 方提供的,所以非常容易解决。 简单的封装一个 C.so 静态连接 C.a ,然后其它所有程序都连接 C.so 即可。
云风,你好。我这两天也遇到了同样的问题,但是和你的有些不同。先把问题描述如下。 有执行文件A.exe,动态链接库B.so,还有一个静态库C.a。 原来呢,是隐式加载的,在生成A.exe时,把B和C都link进去了。但是由于B里的功能并非所有时间都会使用,是针对特定的客户的,所以,领导决定把它改成显式加载的。 现在的问题是,改成了显式加载后,在main函数退出时,会段错误,有警告信息称,是发生了double free。但是同样的操作,Windows版却没有任何问题。 我看了你的博客,然后就想,估计也是全局变量的问题,因为在C这个静态库里,是有一些全局/静态变量的,是不是这个原因呢? 当然,最大的问题是,这个C,是客户提供的,我们都没有source,是不是就是说,没法使用显式加载了呢?
佩服云风,我现在能做到写完大段代码一次编译通过就乐得屁颠屁颠了。最好的调试就是读代码,呵呵。经常查一些外网的游戏bug,都是难以重现的,只能靠着逻辑去查,挺锻炼自己的逻辑思维的。《UNIX编程艺术》是对我影响最大的一本书,KISS也是从中学到的,改变了自己的很多编程观念。
写代码不怎么调试 个人认为不调试不是写的代码没有错误而是根据运行时所表现的错误(错误表像)而立即能够定位到该错误所出现在源代码中的大概位置,要做到这一点模块化的细分,功能的明确,乃至于整个工程的Kiss构建是不可分割的.不调试大概指得是不用调试工具(动态调试)而仅仅通过肉眼来审核源码吧.
与其增加调试能力,不如提高设计能力和编写高质量代码的能力 这句我很赞同
@analyst 我认为证明的是 KISS 原则。 这是我现在的最高准则。
偶曾经也遇到过不少不能靠调试解决的bug。一次,一个C#写的服务器在.net虚拟机里crash,crash极难重现,而且也没有虚拟机源代码,无从调试,后来google了crash的出错代码的地址,发现是.net虚拟机的一个BUG,打了补丁就解决了。另一次,一个游戏在物理引擎里crash,在极其复杂的物理计算代码里跟踪了2天,发现是浮点数出现异常值,导致另外一个地方的内存分配错误,但是对浮点数为何出现异常值仍然一筹莫展,后来突然想到D3D初始化的时候修改了浮点数精度,保留精度以后BUG消失。 在软件开发过程中,难免会碰上一些棘手的BUG,需要通过一些非常规的手段来解决,大家不妨这样的案例拿出来交流一下,或许对别人是个启发。
错 牛逼人都会调试
>因为早先的代码,我自己完成了链接和二进制模块加载的过程。这次我想简化这个环节,更依赖系统提供的相关机制。 再次证明了不要重复发明轮子。
小心身体啊。。
1c1 当然是否棘手取决于你对系统的犄角旮旯的了解程度 可能是个人语感问题吧,算不上bug,但还是提交一个 patch,抛砖引玉,呵呵
犄里旯旮 方言。 中文拼音:ji jiao ga la(都是第一声) 角角落落。
牛逼人是不是都不调试的 - -。。
“写代码不怎么调试”,不愧是大哥啊。

Post a comment

非这个主题相关的留言请到:留言本