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 文書處理器