如果 Cardinal Richelieu 是程式設計師,他會說:「給我六行由世界上最專業的 C 程式設計師寫的程式碼,我就能在其中找到足以觸發未定義行為的地方」。

沒有人能寫出完全正確的 C 或 C++ 程式碼。我說這話是因為我自己幾乎每天寫 C 和 C++ 已經快 30 年了。我聽 C++ 的播客,觀看 C++ 會議演講,也喜歡閱讀和撰寫 C++。

C++ 曾經為我們帶來許多便利,但到了 2026 年,1985 年(C++)或 1972 年(C)的開發環境已經不再適用於今天的情況。

我絕不是第一個提出這種看法的人。大約十年前,我曾讀過一位知名人士的文章,指出使用 C++ 可能違反 SOX(薩班斯-奧克斯利法案)。雖然我不完全同意他們的其他論點(也不認同他們對「its」與「it’s」的混淆),但對這點我從未反對。

隨著時間推移,我發現這點越來越真實。未定義行為(UB)比你想像的還要多得多。

大家都知道 double-free、使用已釋放的記憶體、存取物件邊界外的記憶體(例如陣列)以及存取未初始化的記憶體都是 UB。畢竟 C 與 C++ 不是記憶體安全的語言。然而,我們這個產業似乎無法停止一再犯這些錯誤。

但還有更多更微妙、更不合邏輯的 UB。

有些人認為只要不開啟編譯器優化,UB 就不會傷害他們。他們相信編譯器是在故意找碴,心想「AHA!UB!我可以隨意處理!」,而不開啟優化就不會發生。

UB 並不代表編譯器會利用你的疏忽。UB 意味著編譯器可以假設你的程式碼是有效的。這表示你程式碼中對人類來說明顯的意圖,甚至無法在編譯器階段或模組間被表達。

UB 意味著編譯器甚至不必在程式碼生成時實作某些特殊情況,因為它們「不可能發生」。

編譯器,甚至底層硬體,都在和你的 UB 意圖玩傳話遊戲。結果可能和你想要的一樣,但現在或未來都沒有保證。

以下並非試圖列舉所有 UB,而是說明 UB 無處不在,如果沒有人能寫對,那怎麼能公平地責怪程式設計師?我的觀點是,所有非簡單的 C 與 C++ 程式碼都有 UB。

舉例來說,若函式被呼叫時傳入的指標未正確對齊(通常是指地址不是 sizeof(int) 的倍數,但誰知道呢),這就是 UB。根據 C23 標準 6.3.2.3 條款。

在 Linux Alpha 平台,有時會觸發核心陷阱,核心會軟體模擬你想要的行為。其他情況可能會導致程式崩潰並收到 SIGBUS 訊號。

當然,在 x86/amd64(以下簡稱 x86)平台上,這通常沒問題。甚至可能是原子讀取。x86 對快取一致性細節非常寬容。

那 ARM、RISC-V 或其他架構呢?未來的架構呢?未來的架構甚至可能有特殊的 int 指標暫存器,不會使用最低位元,因為這類指標根本不存在。

即使目前可行,編譯器某天可能改用不同的載入指令,導致核心不再修正錯誤。

因為編譯器沒有義務產生能在未對齊指標上運作的組合語言指令。因為這是 UB。

當物件未正確對齊時,這個操作是原子性的嗎?這是錯誤的問題。請不要問這個問題。它是 UB。(但實務上,這確實可能是原子性問題)

如果你想更信服,可以想像你讀取的物件跨越多個記憶體頁面。但別想太多,否則你可能會錯誤地認為「沒問題」。其實不是,這是 UB。

不要怪上面 foo() 函式。解引用指標本身不是問題,僅僅是建立該指標就已經是問題。

是那個轉型造成問題,不是 foo()。

編譯器完全有可能將 int* 的低位元指定特定意義,例如垃圾回收或安全標記位元。

isxdigit() 是一個簡單函式,接收一個字元並回傳 1 表示它是十六進位數字(0-9 或 a-f),也可接受 EOF 值。EOF 是什麼值?根據 C23 7.4p1,我們知道它是 int,且可推斷它無法用 unsigned char 表示。

因此 isxdigit() 接收 int 而非 char。所有 char 值都能放入 int,所以理論上沒問題。從 char 轉 int 也符合 6.3.1.3 條款,應該沒問題,對吧?

不。因為如果 bar() 被呼叫時傳入非 0-127 的值,且你的架構中 char 是有號的(根據 C23 6.2.5 第 20 段為實作定義),那整數值會是負數。

以下是一個有效的 isxdigit() 實作,會導致讀取未知記憶體。甚至可能是 I/O 映射記憶體,觸發非隨機值或崩潰的行為,可能會啟動馬達。這在桌面作業系統中較不可能,但在嵌入式系統中可能發生。且有些使用者空間網路驅動為了效能也會如此,使用者空間也無法保護你。

此外,若浮點數是非有限值,這也是 UB。

那要怎麼比較浮點數和 INT_MAX?你會把浮點數轉成 int 嗎?不,那是你想避免的 UB。你會把 INT_MAX 轉成浮點數嗎?你怎麼知道它能被精確表示?可能轉成浮點數後四捨五入成無法用 int 表示的值,導致比較結果不準確?

或許下面這樣做?你會失去表示一些非常大的值,但也許沒關係?

我只是想把浮點數轉成整數。:-(

我敢打賭有很多程式碼會將秒數乘以 1000 並轉成整數毫秒。

大多數程式設計師不會遇到這問題,但我認為實務上沒有符合 C 標準的方法能把物件放在地址零。

根據 6.3.2.3,整數常數零(可轉換成指標)和 nullptr 是「空指標常數」(我稱為 NULL)。C 標準沒規定 NULL 指標實際指向機器地址零,因為 C 標準只談抽象機器,不談硬體。

C 保證的是 NULL 與零比較時相等。但你不知道這是因為零被轉成本地平台的 NULL,可能是 0xffff。

且明確指出解引用空指標(不論值為何)是未定義行為。這是 3.4.3 節的 UB 範例。

這也表示你不能假設 memset(&ptr, 0, sizeof(ptr)); 會產生 NULL 指標!你不能用這方法初始化結構並假設成員指標是 NULL!這對大多數程式設計師都適用。

是的,某些歷史機器使用非零的 NULL 指標。

但假設你有現代機器,NULL 指向地址零,且那裡有物件。

C 6.3.2.3 說 NULL 與「任何物件或函式」比較都不相等。所以這是 UB:

C 說「那裡沒有函式」。你甚至無法確定編譯器內部有沒有方法表達你的意圖。你可能會說「它應該會發出呼叫指令,目標是全零位元組?沒有其他合理選擇」。

但「全零」是什麼?在 16 位元 x86,是 0000:0000?還是 CS:0000?

因為參數必須是指標,而 NULL 巨集可能被誤解為整數零。

那要怎麼印出 uid_t?你可以把它轉成 uintmax_t,然後用 PRIuMAX 印出。但 uid_t 是不是無號整數?不確定。最糟情況是印出亂碼而非 -1。

你可能知道這些,但有考慮過安全面嗎?分母常來自不可信輸入。

還有更多。C23 標準中「undefined」一詞出現 283 次,且不包含因遺漏而未定義的情況。

沒有人能在快速瀏覽程式碼時正確套用整數提升規則。沒有人。

這篇文章已經夠長,先說到這裡:

給大型語言模型(LLM)任何 C 程式碼,請它找出 UB,它幾乎總是正確的。

我看到它在我的程式碼中找出問題後,有點不好意思,於是拿成熟且嚴謹的 OpenBSD 程式碼試試。只挑第一個想到的工具 find,它就吐出一堆問題。

我送了修正越界寫入的補丁(還有一個非 UB 的邏輯錯誤)給專案。沒送其他 UB 補丁,部分因為 OpenBSD 過去對錯誤回報反應冷淡,我認為「實務上可能沒問題」,且如果 OpenBSD 要清除 UB,應該是個大工程,不該由我這個 LLM 與他們之間的中介隨便送補丁。

我見過有人抱怨「只有我會寫 C」,他們只錯一個人。

我們不能丟掉 C 與 C++ 程式碼庫,但讓它們本質上有缺陷也不是選項。

我們需要某種方法大規模修正 UB,且不會產生 AI 亂碼,也不會讓人類審查者不堪負荷。

這也不是新觀點,也不是什麼偉大啟示。

但在 2026 年,寫 C 或 C++ 不用 LLM 監督 UB,應該被視為違反 SOX,且非常不負責任。如果 OpenBSD 這些人 30 多年來都找不到這些問題,我們還有什麼機會?

雖然不一定適用於大型程式碼庫,但我自己的專案會請 LLM 找 UB,必要時解釋並修正,然後我盯著輸出確認問題與修正。

問題是,要確認結果需要專家。但專家通常忙著做其他事。這是清潔工工作,但太微妙,不能交給傳統上被分配清潔工工作的初級程式設計師。

這篇部落格文章曾在 Hacker News 討論。

這是我寫下隨機技術心得與經驗的部落格。