The most difficult bugs to fix are the ones that don’t exist.
qe 是類似 dos pe2 或是
漢書 的文字編輯器。在 google 搜尋已經找不到這個軟體的資訊, 所以想寫一篇來紀錄。
原作者是 Jiann-Ching Liu,
https://github.com/descent/qe 忘記從那個地方 clone 來, readme 提到的網址 http://www.cc.ncu.edu.tw/~center5/product/qe/ 已經無法連上, 所以只貼我 clone 的 source code。
readme 提到的網站
http://www.cc.ncu.edu.tw/~center5/product/qe/ 當然已經連不上。
按下2次 ESC, 會切到下方的命令列, 打 quit 可以離開 qe。在剛從 dos 轉到 linux 時, 還不太會用 vi 時, 短暫用過 qe。
目前的版本在 gcc 14 編譯會有 double free 的問題, 無法正常執行, 我嚇傻了, 之前用還好好的, 加上 sanitizer -fsanitize=undefined -fsanitize=address 後可以正常執行, 參考 fig 1。
身為軟體工程師, 看到這棘手的問題, 很想找出問題, 用了 valgrind, sanitizer 來查 double free, 不過沒什麼進展。由於是用 ncurses 寫的, 在除錯印出 debug message 上會有點麻煩, 增加點除錯困擾。
追了一下 code, 這應該是早期的 c++, 還沒有容器和 std::string, 要不然應該不會自己寫 linked list 和 string。
fig 1. qe
dirbuffer.cc
180 int dirbuffer::refreshdir(const char *path) {
183 linebuffer *tmpptr, *ptr;
208 sprintf(linebuf, "[ %s ]", pathname.getString());
209 filename = linebuf;
210
211 if ((dirp = opendir(pathname.getString())) != NULL) {
212
213 for (ptr = head->next; ptr != tail; ptr = current) {
214 current = ptr->next;
215 ptr->~linebuffer(); // 引發 double free
216 delete ptr;
217 }
找了很久, 本來以為是 linebuffer 的 linked list 出問題, 結果是 qeString 引發, 並不是 linebuffer 這個 class, qeString 是類似 std::string 的東西, 我把動態 malloc 改成固定 array。
char str[10000];
就解決這個問題。
class qeString {
protected:
- char *str;
+ char str[10000];
有點奇怪, 感覺還是沒找到關鍵問題。
20251014 終於找到原因了, 的確和 qeString 有關。qe.cc 是簡化的版本, 這個就會得到 free(): double free detected in tcache 2 錯誤訊息。
qe.cc
1 int main(int argc, char *argv[])
2 {
3 linebuffer *head, *tail, *current;
4
5 head = new linebuffer("=== Top of file ===");
6 tail = new linebuffer("=== Bottom of file ===");
7 current = new linebuffer("");
8 head->previous = NULL;
9 head->next = current;
10 current->previous = head;
11 current->next = tail;
12 tail->previous = current;
13 tail->next = NULL;
14
15 current->~linebuffer();
16 printf("current: %p\n", current);
17 delete current;
18 }
原因是 qe.cc L15, linebuffer 繼承 qeString, 當手動喚起 linebuffer 解構函式時, 一併發動 qeString::~qeString(), 會 delete [] str, 而 qe.cc L17, delete current 會再次發動 qeString::~qeString(), 所以又再一次 delete [] str, 造成 double free。
delete current; 其實就會執行 current->~linebuffer();
是不是早期 c++ delete current 不會執行 current->~linebuffer();
qestring.cc
1 qeString::~qeString(void) {
2 if (str != NULL) delete [] str;
3 str = NULL;
4 buflen = len = 0;
5 }
參考 qe_df.log L9, L12, str 被重複 delete。
list 5. qe_df.log
1 descent@deb64:qe$ ./qe
2 init s: === Top of file ===, slen: 19
3 str: 0x5595b8b29700
4 init s: === Bottom of file ===, slen: 22
5 str: 0x5595b8b29750
6 init s: , slen: 0
7 str: 0x5595b8b297a0
8 next: 0x5595b8b29720, previous: 0x5595b8b292c0
9 ~ str: 0x5595b8b297a0
10 current: 0x5595b8b29770
11 next: (nil), previous: (nil)
12 ~ str: 0x5595b8b297a0
13 free(): double free detected in tcache 2
14 Aborted
可是為什麼會這樣, qestring.cc L3 在 delete 之後有把 str 設定為 NULL, 照理來說 delete NULL 是不會有問題的, 後來發現是 -O2 影響的, 把 -O2 拿掉就正常。
list 6. no -O2
1 descent@deb64:qe$ ./qe
2 init s: === Top of file ===, slen: 19
3 str: 0x55bd3ec88700
4 init s: === Bottom of file ===, slen: 22
5 str: 0x55bd3ec88750
6 init s: , slen: 0
7 str: 0x55bd3ec887a0
8 next: 0x55bd3ec88720, previous: 0x55bd3ec882c0
9 ~ str: 0x55bd3ec887a0
10 current: 0x55bd3ec88770
11 next: (nil), previous: (nil)
12 ~ str: (nil)
13 next: 0x55bd3ec88770, previous: (nil)
14 ~ str: 0x55bd3ec88700
15 next: (nil), previous: 0x55bd3ec88770
16 ~ str: 0x55bd3ec88750
list 6 沒有 -O2, 就沒遇到 delete 同個位址 str 的問題, 看來是 -O2 最佳化引起的。
gcc 7 也會編譯出有問題的 code, 一樣要拿掉 -O2。
list 7, 比較有無 -O2 差異
1 descent@deb64:qe$ ./qe
2 init s: === Top of file ===, slen: 19
3 str: 0x55bd3ec88700
4 init s: === Bottom of file ===, slen: 22
5 str: 0x55bd3ec88750
6 init s: , slen: 0
7 str: 0x55bd3ec887a0
8 next: 0x55bd3ec88720, previous: 0x55bd3ec882c0
9 ~ str: 0x55bd3ec887a0
10 current: 0x55bd3ec88770
11 next: (nil), previous: (nil)
12 ~ str: (nil)
13 next: 0x55bd3ec88770, previous: (nil)
14 ~ str: 0x55bd3ec88700
15 next: (nil), previous: 0x55bd3ec88770
16 ~ str: 0x55bd3ec88750
17
18 no -O2, 觸發 4次 ~str,
19
20 descent@deb64:qe$ ./qe
21 init s: === Top of file ===, slen: 19
22 str: 0x55b33169b700
23 init s: === Bottom of file ===, slen: 22
24 str: 0x55b33169b750
25 init s: , slen: 0
26 str: 0x55b33169b7a0
27 next: 0x55b33169b720, previous: 0x55b33169b2c0
28 ~ str: 0x55b33169b7a0
29 current: 0x55b33169b770
30 next: (nil), previous: (nil)
31 ~ str: 0x55b33169b7a0
32 free(): double free detected in tcache 2
33
34 -O2 觸發 2 次 ~str, 的確有最佳化,只是最佳化錯了, delete L28, L31 同樣位址
這個問題, 如果只貼 code 給 ai 看, 是找不出 -O2 的問題, 因為程式碼本身並沒有錯誤。
中文處理, 中文處理比我想像的還要難上不少, qe 支援的是 big5, 目前 linux 已改為 utf8, 顯示方面做了一些修正, 可以正常顯示 utf8 中文, 輸入中文的部份就還有問題, 而且看起來不好改, 暫時不動。
移動游標也不正常, 中文字移動游標需要2次左鍵, 覺得很不爽, 但還不知道怎麼改比較好。
ref:
從 CLE 移植過來的新套件:qe-0.0.34 仿 PE2 文書處理器