大代码时代里的一个大陷阱

321aa031e1629d1ac0de59c41e53b3eb.jpeg

大代码时代里的一个大陷阱

七月初,格蠹办公室里来了一批实习生。他们从不同的地方聚集到上海,最南的来自广东,最北的来自秦皇岛,最西的来自古都西安。他们有一个共同的特点——年轻,都是00后。

1

陆君首战告捷

对于来格蠹实习的每一位实习生,我都尽可能为他们量身定制最适合他们的任务,不能太难,避免挫伤了他们尚未丰满的羽翼,也不能太浅,以免他们觉得没有学到东西,虚度了时光。但是,这个难度并不太好掌握,特别是刚开始的一两周,双方还都不太了解。以来自江西上饶的陆君(实名隐去)为例,上周的一个难题差点把他打倒,准备放弃。

6月15日下午,我和陆君在一个直聘平台上相遇,看到他的学校在江西上饶,让我不禁想起了著名的鹅湖书院。和他初步聊了几句后,我在五点半的时候发给他一份编程题目,两个小时后,他发来了答案。今天的大学生,大多都缺少编码实践。陆君能快速完成编程题目,技术方面就算过了关。当晚,我和他语音聊了几分钟,双方都觉得合适,当即便约定好了实习计划。7月3日一早,有人敲门,我大声说请进。门被慢慢推开后,一位白面书生走进了格蠹的办公室,他就是陆君。

3b5eba567ee95fdc4e2772c3ef09d965.jpeg

在欢迎实习生的短会上,我问陆君是否去过鹅湖书院。他说不知道。鹅湖书院位于上饶的铅山县鹅湖镇鹅湖山麓,可能距离陆君的校园比较远。

我给陆君安排的第一个任务是改进“刘姥姥”驱动。先编译老的代码,再增加新的功能。

陆君很少说话,但是遇到问题时,会两手抱着笔记本电脑走到我的座位提问。他的笔记本并不是很重,但是他每次都是伸长双臂,像是抱着千斤重物一样。

用了大约一周时间,他顺利完成了第一个任务,不仅完成了代码,还写了两篇文章。一篇是《整理刘姥姥驱动的基本用法》,另一篇是《比较处理硬件差异的两种不同做法:条件编译和动态检测》。在后一篇文章的末尾,他引用苏轼的名句——“横看成岭侧成峰,远近高低各不同”表达感慨之情,意思是同一件事情,可以有多种做法,效果不同,风景不同。

开源项目

2

陆君的第二个任务是关于debuginfod的,是个开源项目,用来搭建调试符号服务器,分成服务端和客户端两个部分。整个设施的功能与微软的调试符号服务器很类似。

下载了开源代码后,陆君有点茫然,感觉文件很多,代码量很大,不知道应该从哪里下手进行编译。我浏览了他下载的代码后,果断指出了方向,只需要关注和编译debuginfod一个目录,其它目录的都是基础库,可以使用动态链接方法,不需要自己编译。

我建议他自己写个小的Makefile,单独编译debuginfod目录里的文件。目录里的文件不算多,只有几个头文件,一个C++源文件,2个c文件(下图中test.c是后来新增的)。

6cfc8728e7ae4adb9c5859903f82bc76.png

按照我的建议,陆君写了个Makefile,处理了一些编译错误后,遇到了一大堆链接错误。对于这些链接错误,他不知道哪个函数应该链接哪个库了。我当时比较忙,便嘱咐格蠹的一位老员工帮助他,在Makefile中增加-l选项,指定动态库,解决链接问题。

在老员工的帮助下,陆君成功编译出两个可执行文件,debuginfod和debuginfod-find。前者是服务端,后者是客户端。

接下来是搭建一个模拟的服务器,测试整套符号设施,包括增加符号文件,下载文件等。debuginfod支持如下三类调试文件:

可执行文件

交给CPU执行的程序,通常用在分析转储文件时。

符号文件

一般是只供调试的符号信息,也就是所谓的分立的符号文件(separate debug info)。

源文件

源程序文件,.c, .h等。

针对以上三类调试文件,作为服务程序的debuginfod有如下两大功能:

功能一

接收客户端的下载请求,下载对方所需的文件。

功能二

按配置间隔自动扫描软件工程的工作目录,将新增的调试文件增加到自己的SQLite数据库中。

其中,第二个功能默认是关闭的,可以使用-F选项打开: 

-F

激活ELF/DWARF文件扫描。

例如:

./debuginfod -F /home/geduer/testfile

这便会扫描/home/geduer/testfile文件夹下所有的ELF/DWARF文件。

值得说明的是,对于源代码文件,debuginfod的处理逻辑并不是在工程目录里直接扫描.c这样的文件,而是解析符号文件中的源文件表,这样做有多个好处:既可以避免把不需要的源文件增加到库中,又可以避免文件路径表达方式与符号文件中的不一致。

但这样做就需要解析DWARF格式的符号表,正是在尝试这个功能时,陆君遇到了拦路虎。

3

拦路虎

7月12日,我意识到陆君挺长时间没有抱着本子来找我提问,我便主动询问他是否顺利。他用手指屏幕,慢条斯理地说:“一运行就崩掉了”。

我看是著名的段错误,建议他使用gdb。他说:“不熟悉啊……”我说:“多用用就熟悉了。”

在我的敦促下,上了GDB后,发现崩溃发生在一个名为 _dwarf_load_section 的函数中。

执行bt观察调用栈,执行经过如下:

2a560b79b0b2e4a5a2825eaf1024b422.png

从调用栈看,这个线程正式debuginfod用来扫描调试文件的工作线程。它在扫描源文件,调用dwarf_extract_source_paths提取源文件路径。

调用一层层深入,通过_dwarf_load_debug_info加载符号信息,符号信息是以节的形式保存的,所以又调用_dwaf_load_section加载一个节。

但正是在这个_dwaf_load_section函数中,程序崩溃了,崩溃的位置在1939行,即:

res = o->methods->load_section(
        o->object, section->dss_index,
        &section->dss_data, &err);
    if (res == DW_DLV_ERROR) {
        DWARF_DBG_ERROR(dbg, err, DW_DLV_ERROR);
    }

C程序的最常见崩溃就是段错误,段错误的最常见原因就是空指针。在崩溃这一行,有两次指针引用:

o->methods->load_section

观察变量o,不为空,继续观察o->methods,果真为空。

如此看来,问题就出在这个o->methods成员上,根据多年经验,这像是一个函数表。

空指针

4

初步搜索源代码,这个函数表有很多地方使用。

f816869c44b5848b9421e54795041eeb.png

寻找对它赋值的地方,发现三处(赋空除外):

5239d6fd89dfc237d240d5dec0381c5b.png

观察其中一处:

2e8f0c70e808ff172e65232ed7044959.png

果然是使用的函数表方法。赋值的函数以init结束,表明是在初始化阶段做的。既然如此,应该是没有做好初始化工作。于是我建议陆君检查初始化有关的代码,确认是否调用了这个初始化函数。花了几分钟时间,给陆君指了个方向后,我便忙别的事去了。

一天后的周四上午,我又意识到陆君很久没有抱着本子来找我了,于是我走过去询问情况,昨天的问题还没有解决。而且,他似乎没有按照我的建议定位init函数溯源。他按自己的想法在逐级分析_dwaf_load_section的父函数。并且说:“父函数里没有任一个会调用init……”我说:你在崩溃线程的调用栈里是看不到init,init可能调用过,返回了,栈上已经不见了。他满脸问号,对我的话,可能是没懂,也可能是充满怀疑(后来知道)。

周四下午,我又问他情况,他再次说:“父函数里没有任一个会调用init……”我知道,他没明白我前面的话,给他做了个比喻:“你春天是去大学读书,放假先回到家里,从家里又到上海。因此,在查询你从家到上海的这段旅程时,中间是没有学校这个点的。”我觉得这个比喻挺贴切的,但是他似乎并没有听懂,因为他可能事实上一放假就来实习,是从学校直接到上海的,和我比喻的情况不同。

周四下午五点多,陆君向我发了下面这个截图,我看了一下,感觉和崩溃问题没什么关系。

c5f0d798e07e95234d7b2ed2c8521af9.png

下午六点到了,小伙伴们大多准时下班回家了,陆君也下班了。

第二天是周五,一上午和下午,陆君仍然在和这个问题战斗。要下班时,我约他到会议室里面对面交谈,向他讲职业软件开发和大学里写代码的不同。

5

久攻不下

两天周末,我到合肥见几位老朋友。

周一一整天,陆君继续分析这个问题。他有了一个发现,如果在编译被扫描的程序时,增加一个-Og的选项,那么就可以避免崩溃。但是在我的启发下,他意识到了,这个选项的作用是不产生源文件表,因此debuginfod运行时不会执行会崩溃的_dwaf_load_section函数,但这也失去了这件事本来的意义。

周一下午五点左右,我意识到,陆君的耐力快用完了。我问他情况时,他开始抱怨开源代码质量不好,怀疑是这个项目本身有问题,本来代码就有BUG。我说:“这是个著名的项目,全世界的用户已经使用几年了。”他说:“那也可能有问题呢。”

我说:“怀疑精神是好的。连CPU都可能有BUG,但是被我们遇到的概率几乎是不可能的。”

以物为师

6

多日来,几次陪着陆君看代码,虽然每次时间不长,但是我也逐渐建立了一些对这个代码的理解。debuginfod的核心逻辑是使用C++写的,而且是现代C++。

对于这个问题,我始终觉得关键点就在于methods这个空指针,这个指针为什么没有初始化?在我的多次推动下,陆君证实给methods赋值的几个初始化函数根本没有调用过。

这个结论,有点出乎我的预料。我本来的推测是,debuginfod的代码里调用了init函数,但是因为某个条件,走了错误的分支,没能给methods指针赋值。现在陆君证明说init函数根本没有被调用,那么就说明debuginfod的源代码里根本没有这个init逻辑。浏览了一下debuginfo的源代码,和陆君的说法一致。

于是,我给出了另一个建议,安装Ubuntu官方仓库里的debuginfod。陆君是使用幽兰做这个项目,开发工具和仓库设置都是现成的,几秒钟之后官方的debuginfo便装好了。使用它加-F选项扫描符号文件,尝试重现问题,没有崩溃。这证明了我说的这个软件是可靠的。

接下来上调试器,对init函数设置断点,r命令让程序重新运行,断点也没有命中。这证明了陆君说的“init函数根本没有被调用”是对的。

这时,我想起陆君曾提起的库版本问题,建议他比较两种情况下使用的库有何不同。崩溃的_dwaf_load_section函数位于libdwarf.so中,在有问题的情况,它的路径为:

/lib/aarch64-linux-gnu/libdwarf.so.1

在没有问题的官方进程里寻找libdwarf.so.1时,有个惊人的发现,里面根本没有这个so。

(gdb) info shared
From                To                  Syms Read   Shared Object Library
0x0000007ff7fbeec0  0x0000007ff7fdac34  Yes         /lib/ld-linux-aarch64.so.1
0x0000007ff7e4f300  0x0000007ff7f39fb0  Yes         /lib/aarch64-linux-gnu/libsqlite3.so.0
0x0000007ff7df2ec0  0x0000007ff7e07fc8  Yes         /lib/aarch64-linux-gnu/libelf.so.1
0x0000007ff7d41d30  0x0000007ff7dabf1c  Yes         /lib/aarch64-linux-gnu/libcurl.so.4
0x0000007ff7ce42c0  0x0000007ff7cfc9d0  Yes         /lib/aarch64-linux-gnu/libmicrohttpd.so.12
0x0000007ff7c00020  0x0000007ff7c86980  Yes         /lib/aarch64-linux-gnu/libarchive.so.13
0x0000007ff7b430a0  0x0000007ff7b9fdf0  Yes         /lib/aarch64-linux-gnu/libdw.so.1
0x0000007ff7960750  0x0000007ff7a82650  Yes         /lib/aarch64-linux-gnu/libstdc++.so.6
0x0000007ff7882f00  0x0000007ff78945ac  Yes         /lib/aarch64-linux-gnu/libgcc_s.so.1
0x0000007ff76f6a00  0x0000007ff78055f0  Yes         /lib/aarch64-linux-gnu/libc.so.6
0x0000007ff762ca50  0x0000007ff7671f80  Yes         /lib/aarch64-linux-gnu/libm.so.6
0x0000007ff75e2140  0x0000007ff75f24a0  Yes         /lib/aarch64-linux-gnu/libz.so.1
0x0000007ff7594b30  0x0000007ff75a7820  Yes         /lib/aarch64-linux-gnu/libnghttp2.so.14
0x0000007ff7551300  0x0000007ff7555588  Yes         /lib/aarch64-linux-gnu/libidn2.so.0
0x0000007ff75249d0  0x0000007ff753318c  Yes         /lib/aarch64-linux-gnu/librtmp.so.1
0x0000007ff749e490  0x0000007ff74df8d0  Yes         /lib/aarch64-linux-gnu/libssh.so.4
0x0000007ff74512b0  0x0000007ff7452dd0  Yes         /lib/aarch64-linux-gnu/libpsl.so.5
0x0000007ff73af530  0x0000007ff74050d0  Yes         /lib/aarch64-linux-gnu/libssl.so.3
0x0000007ff7022000  0x0000007ff7249240  Yes         /lib/aarch64-linux-gnu/libcrypto.so.3
0x0000007ff6f0c790  0x0000007ff6f3d244  Yes (*)     /lib/aarch64-linux-gnu/libgssapi_krb5.so.2
0x0000007ff6e8e550  0x0000007ff6ec5420  Yes         /lib/aarch64-linux-gnu/libldap.so.2
0x0000007ff6e529e0  0x0000007ff6e59cdc  Yes         /lib/aarch64-linux-gnu/liblber.so.2
0x0000007ff6d93da0  0x0000007ff6e14f18  Yes         /lib/aarch64-linux-gnu/libzstd.so.1
0x0000007ff6d60bb0  0x0000007ff6d66b2c  Yes         /lib/aarch64-linux-gnu/libbrotlidec.so.1
0x0000007ff6b836c0  0x0000007ff6cb8710  Yes         /lib/aarch64-linux-gnu/libgnutls.so.30
0x0000007ff6aeb0d0  0x0000007ff6b1165c  Yes         /lib/aarch64-linux-gnu/libnettle.so.8
0x0000007ff6ab15e0  0x0000007ff6ab52c8  Yes         /lib/aarch64-linux-gnu/libacl.so.1
0x0000007ff6a63020  0x0000007ff6a80834  Yes         /lib/aarch64-linux-gnu/liblzma.so.5
0x0000007ff6a22200  0x0000007ff6a3a6dc  Yes         /lib/aarch64-linux-gnu/liblz4.so.1
0x0000007ff69f1390  0x0000007ff69fd4c4  Yes         /lib/aarch64-linux-gnu/libbz2.so.1.0
0x0000007ff681e9d0  0x0000007ff6975118  Yes         /lib/aarch64-linux-gnu/libxml2.so.2
0x0000007ff6630600  0x0000007ff666d570  Yes         /lib/aarch64-linux-gnu/libunistring.so.2
0x0000007ff65b80a0  0x0000007ff65caf3c  Yes         /lib/aarch64-linux-gnu/libhogweed.so.6
0x0000007ff6519280  0x0000007ff656dec4  Yes         /lib/aarch64-linux-gnu/libgmp.so.10
0x0000007ff6450680  0x0000007ff64a8440  Yes (*)     /lib/aarch64-linux-gnu/libkrb5.so.3
0x0000007ff63e3870  0x0000007ff63fc3e0  Yes (*)     /lib/aarch64-linux-gnu/libk5crypto.so.3
0x0000007ff63b1190  0x0000007ff63b1d98  Yes         /lib/aarch64-linux-gnu/libcom_err.so.2
0x0000007ff6382d70  0x0000007ff638815c  Yes (*)     /lib/aarch64-linux-gnu/libkrb5support.so.0
0x0000007ff6342ea0  0x0000007ff6351a40  Yes         /lib/aarch64-linux-gnu/libsasl2.so.2
0x0000007ff6300740  0x0000007ff6300ccc  Yes         /lib/aarch64-linux-gnu/libbrotlicommon.so.1
0x0000007ff61d9740  0x0000007ff627c2dc  Yes         /lib/aarch64-linux-gnu/libp11-kit.so.0
0x0000007ff6172b90  0x0000007ff617eb44  Yes         /lib/aarch64-linux-gnu/libtasn1.so.6
0x0000007ff5fc6fc0  0x0000007ff60b3150  Yes         /lib/aarch64-linux-gnu/libicuuc.so.72
0x0000007ff5f316f0  0x0000007ff5f32920  Yes         /lib/aarch64-linux-gnu/libkeyutils.so.1
0x0000007ff5f032b0  0x0000007ff5f0a348  Yes         /lib/aarch64-linux-gnu/libresolv.so.2
0x0000007ff5ed1a20  0x0000007ff5ed66dc  Yes         /lib/aarch64-linux-gnu/libffi.so.8
0x0000007ff40e0480  0x0000007ff40e0564  Yes (*)     /lib/aarch64-linux-gnu/libicudata.so.72
(*): Shared library is missing debugging information.

发现这个事实后,我们俩都很兴奋,有“山穷水尽疑无路,柳暗花明又一村”的感觉,离答案应该很近了。

我提议打开Makefile,去掉里面的-ldwarf,再此编译运行,果然问题不见了,进程也不崩溃了。

看到这个场景,我们俩都站了起来,发自内心的喜悦之情上升到脸上。此时是周一晚上七点,已过了下班时间,格蠹的其它小伙伴都下班了,办公室里只剩我们两个人。

问题解决了,我们也很快都下班了。

在回家路上,我收到陆君的信息:“谢谢张老师”,外加一个笑脸。

69da2935283480f4b7c8ebe53a9c43ca.png

7

复盘

回过头看,这个问题的根源在于有两个开源库都实现了非常类似的功能,都有相同的导出函数dwarf_offdie。

(gdb) info func dwarf_offdie
All functions matching regular expression "dwarf_offdie":


File ../backends/i386_unwind.c:
74:     Dwarf_Die *dwarf_offdie(Dwarf *, Dwarf_Off, Dwarf_Die *);
81:     Dwarf_Die *dwarf_offdie_types(Dwarf *, Dwarf_Off, Dwarf_Die *);


File /build/dwarfutils-RGIFmM/dwarfutils-20210528/libdwarf/dwarf_abbrev.c:
2814:   int dwarf_offdie(Dwarf_Debug, Dwarf_Off, Dwarf_Die *, Dwarf_Error *);
2822:   int dwarf_offdie_b(Dwarf_Debug, Dwarf_Off, Dwarf_Bool, Dwarf_Die *, Dwarf_Error *);

这两个库的名字也很类似,一个叫libdwarf.so,一个叫libdw.so。故障情况使用的是libdwarf,正常情况使用的是libdw。

从时间上看,libdwarf很古老,差不多是解析DWARF格式的最早实现。根据DaveA的描述,它始于1990年代,源自SGI。

Format is of interest to programmers working on compilers and debuggers (and anyone interested in reading or writing DWARF information). It was developed by a committee (known as the PLSIG at the time) starting around 1991. Starting around 1991 SGI got involved with the committee and then developed the libdwarf and dwarfdump tools for SGI-internal use and as part of SGI IRIX developer tools. From around 1993 dwarfdump and libdwarf were shipped (as an executable and archive respectively, not source) with every release of the SGI MIPS/IRIX C compiler. In 1994 (I think the correct year) SGI agreed (at my request) to open-source libdwarf (and in 1999 to open-source dwarfdump) so anyone could use them.

https://www.prevanders.net/dwarf.html

Dave的个人网站里有很多关于DWARF的第一手资料。

a46dd38930776fb6bed57d8fdfdb58e5.png

相对而言,libdw要年轻一些,始于新世纪初,是elfutils的一部分,源于红帽公司(redhat)。根据源代码包里的ChangLog文件,libdw的主要作者有两位:他们都与glibc密切相关,一位是Ulrich Drepper,是glibc目前的维护者,另一位是Roland McGrath,glibc的最初维护者和主要开发者,他做了30年的glibc后,把接力棒传给了Ulrich Drepper。

下面是调用正确版本时的情景:

Thread 5 "debuginfod" hit Breakpoint 1.1, dwarf_offdie (dbg=0x7fdc00f3c0, offset=12, result=0x7ff28ad7b8) at /usr/src/elfutils-0.188-2.1/libdw/dwarf_offdie.c:43
43        if (dbg == NULL)
(gdb) bt
#0  dwarf_offdie (dbg=0x7fdc00f3c0, offset=12, result=0x7ff28ad7b8) at /usr/src/elfutils-0.188-2.1/libdw/dwarf_offdie.c:43
#1  0x0000005555567440 in dwarf_extract_source_paths (elf=0x7fdc026b70, debug_sourcefiles=std::set with 0 elements) at debuginfod.cpp:2844
#2  0x0000005555567f48 in elf_classify (fd=10, executable_p=@0x7ff28addbe: true, debuginfo_p=@0x7ff28addbf: true, buildid="e135d39ce2bebc779e80bbc7c84766d04aba4a3e",
    debug_sourcefiles=std::set with 0 elements) at debuginfod.cpp:3027
#3  0x0000005555568284 in scan_source_file (rps="/home/geduer/debuginfod/testabc", st=..., ps_upsert_buildids=..., ps_upsert_files=..., ps_upsert_de=..., ps_upsert_s=..., ps_query=...,
    ps_scan_done=..., fts_cached=@0x7ff28ae148: 22, fts_executable=@0x7ff28ae14c: 0, fts_debuginfo=@0x7ff28ae150: 0, fts_sourcefiles=@0x7ff28ae154: 0) at debuginfod.cpp:3100
#4  0x000000555556b6b0 in thread_main_scanner (arg=0x0) at debuginfod.cpp:3622
#5  0x0000007ff774e814 in start_thread (arg=0x7fffffed47) at ./nptl/pthread_create.c:444
#6  0x0000007ff77b7d5c in thread_start () at ../sysdeps/unix/sysv/linux/aarch64/clone.S:79

1a9f8066cf30f5a7fe389a910b59ecde.png

我在读大学时,曾买过一本很厚的书,是用C语言实现的函数库。那时,一些比较基础的函数还缺少实现。

今天,形势大不一样,即使是符号解析这样只有编译工具和调试工具才使用的库也有多种实现。多种实现带来的问题是一旦选错了库,就可能误入歧途,掉入陷阱。

5b36277cdc2c427ececa0f9f8f3130e4.jpeg

这个问题发生在幽兰代码本上,所有幽兰客户可以在兰友群里获得完整的源代码包用以重现和调试这个问题。

幽兰官网:https://nanocode.cn/#/yl/

e20913d0a94c64bbc79f8ea963beaa7b.jpeg

【盛格塾】

正心诚意,格物致知

人文情怀审视软件,以软件技术改变人生

9dec32cc1af6927bf9f0ebc0b0708271.png

格友公众号

eae5b01012564a6d4a406fe7c87bd618.png

盛格塾小程序

扫描上方二维码或在微信中搜索“盛格塾”小程序

可以阅读更多文章和有声读物

往期推荐

《学活Linux》第一讲——系统调用和VFS

【小白学编程5】我的房子在哪儿?理解类型和变量

软件工程师的“硬功夫”

LINUX平台高级调试和优化(上海站)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值