konsole 字符串丢失 bug

有时候小小的设计失误不会带来明显问题,但是谁知道哪个人什么时候会触发它?

大概是在 2024 年 1 月 22 日,我下定决心修复 dolphin 的一个奇怪的bug。到 2024 年 1 月 29 日,它才被我修复。本来想等到官方进行 merge request 的,但是似乎他们都有更大的任务要做,并且似乎触发这个 bug 的人不多(毕竟在 bugzilla 上真找不到同样遇到该问题的),并且,如果你关闭下面的终端面板,该 bug 就神奇的不被触发了!

drawing

查产生 bug 的原因的过程,远比修复该 bug 要有趣多了。要理解项目的大致结构,掌握一些 debug 技巧,还要懂一点动态库知识。

我有一个长路径,假定是(中间省略了一部分,总之是非常长的路径)
/home/afeather/100_101_102_..._149/150_151..._199

不论是 cd 到该路径,或者通过点击文件夹进入到该路径,都会触发该 bug。我在 Bugzilla 上描述该 bug 时,使用的单词是truncate,但实际我找到源头的时候,却发现它实际上是小小的设计失误:

头文件里是这么写的

image

cpp 文件里是这么写的

image

也就是说,当tokenBuffer满的时候,为了防止溢出,原作者的做法就是只写入到最后一个字符。

好巧不巧,偏偏我遇到了。为修复这个 bug,我花了好几天,从文件管理器 dolphin 查到 kparts,再查到 konsole 的。

修复也就改了几行代码,可以在我提交的 merge request 中查到:https://invent.kde.org/utilities/konsole/-/merge_requests/952

2024 年 3 月 18 日更新:已被merge:

image

image

定位报错

猜想 1: dolphin 中有问题

由于一开始我不知道是开启终端面板才会触发该问题,所以我第一个猜想是,这是 dolphin 出了问题。

除了给图形界面的一个报错以外,dolphin 报错的时候,给 stdout 输出了一条

kf.kio.core: "The file or folder /home/afeather/4355a46b19d348dc2f57c046f8e\
f63d4538ebb936000f3c9ee954a27460dd865/4355a46b19d348dc2f57c046\
f8ef63d4538ebb936000f3c9ee954a27460dd865/4355a46b19d348dc2f57c\
046f8ef63d4538ebb936000f3c9ee954a27460dd865/4355a46b19d348dc2f\
57c046f8ef63d4538e5 does not exist."

这一句话,几乎没有什么有用信息。但是我想,如果能够定位到该报错信息,说不定就能够定位到实际的问题了。

于是我尝试搜索 dolphin 的源代码,查找 "dose not exist."

$ rg "dose not exist."
src/tests/kfileitemmodelbenchmark.cpp
203:    // Suppress 'file does not exist anymore' messages from KFileItemPrivate::init().

src/views/viewproperties.cpp
99:    // If the .directory file does not exist or the timestamp is too old,

README.md
48:Options are mandatory as the "average Joe" user does not exist.\
Still it is not the goal of Dolphin to offer options for all kind of\
things. Again the focus is on the possible needs of the target user \
group. Each additional option makes it harder finding other options, \
so the same rules for features are applied to options too.

src/dolphinnavigatorswidgetaction.h
144:     * on both sides. A secondary leading spacing does not exist.

src/settings/contextmenu/servicemenuinstaller/servicemenuinstaller.cpp
111:        fail(i18n("The file does not exist!"));
......

坏消息是,找不到。

我又查找了 kf.kio.core,发现它实际上是 kde 的 kio 输出的内容,它是一个动态库。询问 gpt,他告诉我可以尝试使用 ltrace 查找一下。于是定位到了几个函数调用

$ ltrace -S -o dolphin-trace.txt ./build/bin/dolphin
.....
_ZN7QStringC1EiN2Qt14InitializationE(0x7fff0fd7ede0, 620, 0, 620)                                                        = 0x61fc0f4f4b90
_ZN21QAbstractConcatenable16convertFromAsciiEPKciRP5QChar(0x61fc0e39e4fa, 5, 0x7fff0fd7ed48, 0x78b3e97f6ac0)             = 0x61fc0f4f46c2
memcpy(0x61fc0f4f46c2, "/\0h\0o\0m\0e\0/\0a\0f\0e\0a\0t\0h\0e\0r\0/\01\0"..., 1228)                                      = 0x61fc0f4f46c2
_ZN7QString14toLower_helperERS_(0x7fff0fd7ee38, 0x7fff0fd7ede0, 0x7fff0fd7ede0, 0x61fc0f4f4b90)                          = 0x7fff0fd7ee38
_ZN7QString13toUtf8_helperERKS_(0x7fff0fd7ede0, 0x7fff0fd7ee38, 0x7fff0fd7ee38, 0x61fc0f4f4b90)                          = 0x7fff0fd7ede0
_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEC1EPKcmRKS3_(0x7fff0fd7ee80, 0x61fc0f5178a8, 620, 0x7fff0fd7ed9f)   = 0
_ZN10QArrayData10deallocateEPS_mm(0x61fc0f517890, 1, 8, 0xffffffff)                                                      = 1
_ZStlsIcSt11char_traitsIcESaIcEERSt13basic_ostreamIT_T0_ES7_RKNSt7__cxx1112basic_stringIS4_S5_T1_EE(0x78b3e9a774c0, 0x7fff0fd7ee80, 4113, 0x78b3e97f6b20 <unfinished ...>
......

这里这些_ZN开头的函数,其实就是 GNU C++ 的 ABI 生成的函数名。好在稍微可以读懂。

然后在这几个函数调用周围写了 cout 查看输出内容:

$ rg 'std::cout'        
.....
223:    std::cout << __LINE__ << ": " << __PRETTY_FUNCTION__ << ": " << url.toString().toStdString() << std::endl;
248:    std::cout << __LINE__ << ": " << __PRETTY_FUNCTION__ << ": " << dir.toStdString() << std::endl;
268:    std::cout << "Error cause? " << dir.toStdString() << ("\n cd " + KShell::quoteArg(dir) + '\r').toLower().toStdString() << std::endl;
279:    std::cout << __LINE__ << ": " << __PRETTY_FUNCTION__ << ": " << url.toString().toStdString() << std::endl;
298:    std::cout << __LINE__ << ": " << __PRETTY_FUNCTION__ << ": " << url.toString().toStdString() << std::endl;
......

执行,查看输出内容,于是更加确定了何处报错的,就是 KIO。

猜想 2: KIO 有问题

KIO 是 KDE 的 IO 框架,如果一个完整的字符串发送给它,其内部出现问题,也可能会出现该 bug。

于是下载来 KIO 进行编译,这时需要更改 dolphin 链接的动态库。在此使用了:

export LD_LIBRARY_PATH=../kio/build/bin

之后可以使用ldd来确认程序加载的动态库是否真的被修改了。可是后面使用 gdb 查看调用,实际上传输给 KIO 的内容并没有错误。

到此,只能猜想下一个可能了

猜想 3: dolphin 与 konsole 交互时出现问题之 dolphin 向 konsole 发路径时有问题

这种猜想的来源依然是之前的 ltrace 的结果。里面还有一些函数也显示了错误的路径。

不管是什么情况,只要是二者交互,它一定是遵守这样的流程:

image

也就是说,二者通讯结果就是二者路径一致。不论是发送的路径有问题,还是接受的路径有问题,都会发生路径改变的情况。

在一些带有 input、send 这样的字眼的函数周围写了一些 cout,检查问题。结果却发现,发送过程中,对变量进行检查,没有发现问题;并且所有的发送函数,最终将内容放在 QT 的队列中,也就是 QXcbEventQueue,到此就知道 QT 本身是 Reactor 模型的。到此已经无法继续定位问题了。

基本可以假定 QT 不会出现这样的 bug,原因是很多人都在使用它,假设它是可信的。

猜想 4: dolphin 与 konsole 交互时出现问题之 konsole 向 dolphin 发路径时有问题

随后在大量的输出结果中,定位到了 terminalpanel.cpp 中的 slotKonsolePartCurrentDirectoryChanged 函数。它是一个 slot,QT 中信号的处理函数。对应的信号注册是 connect(m_konsolePart, SIGNAL(currentDirectoryChanged(QString)), this, SLOT(slotKonsolePartCurrentDirectoryChanged(QString))) 。我对 QT 不是很熟悉,当场的猜测,就是 currentDirectoryChanged 和 slotKonsolePartCurrentDirectoryChanged 本质上都是函数,于是通过 rg 去找,在 dolphin 中没有找到 currentDirectoryChanged。

于是对 slotKonsolePartCurrentDirectoryChanged 打断点,到此确定了是回路径的时候出现了问题。到此就开始对 konsole debug。

查找 konsole 的 bug

编译 konsole 的时候,发现编译能通过,链接说找不到符号。后面发现是几年前编译 zlib 的时候,忘记删除它了,所以实际上编译的时候使用的是最新版的 zlib,可是链接的时候,使用的是旧版本的。删除它重新编译即可。

在 konsole 中,依然还有一个 send,其作用是,向真正的 shell 发送数据。所以实际上 konsole 本质上只是一个终端模拟器(到这个时候我才明白为什么它叫做终端模拟器,因为它只负责显示 shell 的输出)。依然是发现发送没问题,接收有问题。

接收的部分叫做 Vt102Emulation.cpp,在它上面,还有一层 Pty。用 gdb 跟踪,就发现了上文说的真正的问题。修改类型为 QVector,重新编译,替换系统内的动态库。到此修复完成。

所以 konsole 整体来看是这样的:

image

其他内容

由于修复该 bug 到我写该文章,中间过了比较长的一段时间,上文漏了一部分内容,并且想不起来它应该是在那一步的了。在此补充

debug info 挤爆内存

由于早期算竞比赛,我关闭了自动下载 debug info 的功能,在此时打开了。但是自动下载导致的问题是,它加载了过多的 debug info,直接用完了我的内存。

每个 debug info 分别属于不同的动态库文件,我们可以使用sharedlibrary,指定需要加载的动态库。虽然这样效率降低了一点,但总比内存用完要好。

gdb 有source命令,可以加载你写的脚本,所以加载指定的动态库 debug info,完全可以写成脚本。如:

sharedlibrary /home/afeather/Resources/kparts/build/bin/libKF5Parts.so.5
sharedlibrary konsole
sharedlibrary /usr/lib/libQt5Gui.so.5
sharedlibrary libQt5Widgets
sharedlibrary libQt5Core
sharedlibrary libxcb.so

gdb replay mode

据说这个模式下,你甚至可以让程序反着跑。但是我实际使用下来,这个功能花费的时间非常多——每次一用这个模式,本来几秒钟跑完的程序,要跑很长时间。

二进制兼容性

可见 KDE 开放的文档,里面给出了大概率是二进制兼容的,以及大概率不兼容的情形:

https://community.kde.org/Policies/Binary_Compatibility_Issues_With_C%2B%2B

由于头文件中没有虚函数,并且实际上 konsole 通过 TerminalInterface 暴露其接口,所以大概率没有兼容性问题。实际情况也是,我替换了我系统内的动态库,如今一切依然正常。

开发环境搭建

在编译的时候,链接器报错,在 libz.a 中找不到符号。一般说找不到符号,大概率是版本问题。我用 pacman -F libz.a,查到的内容是:

$ pacman -F libz.a
core/zlib 1:1.3.1-1 [已安装]
    usr/lib/libz.a
extra/gitlab 16.8.1-1
    usr/share/webapps/gitlab/vendor/bundle/ruby/3.0.0/gems/grpc-1.58.0/src/ruby/ext/grpc/libs/opt/libz.a
archlinuxcn/anaconda 2023.09.0-1
    opt/anaconda/lib/libz.a
    opt/anaconda/pkgs/zlib-1.2.13-h5eee18b_0/lib/libz.a
archlinuxcn/android-ndk r26.b-1
    opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libz.a
    opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libz.a
    opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/i686-linux-android/libz.a
    opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/riscv64-linux-android/libz.a
    opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libz.a
archlinuxcn/miniconda 23.11.0-1
    opt/miniconda/lib/libz.a
    opt/miniconda/pkgs/zlib-1.2.13-h5eee18b_0/lib/libz.a
archlinuxcn/pacman-static 6.0.1-49
    usr/lib/pacman/lib/libz.a

该版本高于 kde 指定的版本,而 libz 是一个比较常见的库,按理说会保证版本兼容的。后面发现它链接的 libz 是 /usr/local 下的,到此我想到了一种可能性,就是过去什么时候我手动编译安装过一个 libz。印象中,只有一个 ns2。打开 ns2 的工程目录,果然被我找到了。

然后尝试在 make 中找一个卸载的,找不到。于是尝试更改 prefix,再次编译,安装,确定了它安装的文件,然后删除。到此,编译成功。

这个时候我才明白,C++ 没有对应的包管理器是一个多么烦人恼火的事情。