閱讀600 返回首頁    go 阿裏雲 go 技術社區[雲棲]


100個開源C/C++項目中的bugs(二)未定義行為、與運算優先級相關的錯誤

from:https://www.oschina.net/question/1579_45444

未定義行為

首先,一小段理論知識

未定義行為是某些編程語言的特性(尤其在C和C++中),在某些情形下產生的結果將依賴於編譯器的實現或指定的優化選項。換句話說,規範並沒有定義 某情況下該語言的行為,僅僅是說:“在 A 條件下,B 結果是未定義的”。在這種情況下錯誤在你的程序中被認為是允許的,甚至在一些特別的編譯器中執行良好。這樣的程序不能跨平台,並有可能在不同的電腦,不同 的操作係統甚至不同的編譯器設置中導致失敗。

 一個時序點可以是程序中的任意點,它保證在它之前所有運算的副作用已完成,而後繼運算的副作用未開始.學習更多關於時序和跟時序點相關的未定義行為,查看該貼:https://www.viva64.com/en/t/0065/.

例 1. Chromium項目。不正確的使用智能指針。

1 void AccessibleContainsAccessible(...)
2 {
3   ...
4   auto_ptr<VARIANT> child_array(new VARIANT[child_count]);
5   ...
6 }

The error was found through the V554 diagnostic: Incorrect use of auto_ptr. The memory allocated with 'new []' will be cleaned using 'delete'. interactive_ui_tests accessibility_win_browsertest.cc 171

該例子演示了使用智能指針的時候,有可能導致未定義的行為。它可能通過堆破壞,程序崩潰,未完全的對象析構函數或任何其它的錯誤傳達. 該錯誤是:內存被new [] 操作分配,而被‘auto_ptr'類構析函數裏的 delete 操作釋放:

1 ~auto_ptr() {
2   delete _Myptr;
3 }

為修複這類問題,你應為實例使用一個更恰當的類,boost::scoped_array.

例 2. IPP Samples 項目。經典的未定義行為。

01 template<typename T, Ipp32s size> void HadamardFwdFast(...)
02 {
03   Ipp32s *pTemp;
04   ...
05   for(j=0;j<4;j++) {
06     a[0] = pTemp[0*4] + pTemp[1*4];
07     a[1] = pTemp[0*4] - pTemp[1*4];
08     a[2] = pTemp[2*4] + pTemp[3*4];
09     a[3] = pTemp[2*4] - pTemp[3*4];
10     pTemp = pTemp++;
11     ...
12   }
13   ...
14 }

The error was found through the V567 diagnostic: 未定義行為. 'pTemp' 變量被修改且在時序點間使用了兩次. me umc_me_cost_func.h 168

這是一個關於未定義程序行為的經典例子. 各類文章中都拿它來演示未定義行為. ‘pTemp'自增與否是未知的。兩個改變 pTemp 變量值的操作位於一個時序點中.這意味著編譯器可能創建如下的代碼:

pTemp = pTemp + 1;

pTemp = pTemp;

也可能創建另一版本的代碼:

TMP = pTemp;

pTemp = pTemp + 1;

pTemp = TMP;

創建何種版本的代碼依賴於編譯器和優化選項。

例 3.Fennec Media Project 項目。複雜的表達式。

1 uint32 CUnBitArrayOld::DecodeValueRiceUnsigned(uint32 k)
2 {
3   ...
4   while (!(m_pBitArray[m_nCurrentBitIndex >> 5] &
5     Powers_of_Two_Reversed[m_nCurrentBitIndex++ & 31])) {}
6   ...
7 }

The error was found through the V567 diagnostic: 'm_nCurrentBitIndex' 變量被修改並在單個時序點中被使用了兩次.MACLib unbitarrayold.cpp 78

 'm_nCurrentBitIndex' 變量的使用在兩者間並沒有時序點. 意味著標準並未指定該變量何時增長. 情況不同, 該代碼可能工作方式不同,依賴於編譯器和優化選項.

例 4.Miranda IM 項目. 複雜的表達式.

1 short ezxml_internal_dtd(ezxml_root_t root,
2   char *s, size_t len)
3 {
4   ...
5   while (*(n = ++s + strspn(s, EZXML_WS)) && *n != '>') {
6   ...
7 }

The error was found through the V567 diagnostic: 未定義行為. 's' 變量被修改且在時序點間使用了兩次. msne zxml.c

這裏使用了前綴++. 但不代表什麼:無法保證 's' 變量值會在 strspn() 函數調用前增長。 

與運算優先級相關的錯誤

為了更容易理解例子,讓我們先回顧運算符優先級列表。

例 1.MySQL 項目。! 和 & 運算的優先級

1 int ha_innobase::create(...)
2 {
3   ...
4   if (srv_file_per_table
5       && !mysqld_embedded
6       && (!create_info->options & HA_LEX_CREATE_TMP_TABLE)) {
7   ...
8 }

The error was found through the V564 diagnostic: '&' 操作被用到了bool類型的值上. 你很可能忘記加入圓括號或有意的使用'&&'操作。innobase ha_innodb.cc 6789

程序員想用表達式的某部分來檢查 'create_info->options' 變量中的某個特定的比特位等於是否為 0 . 但 '!' 操作的優先級高於 '&' 操作,這就是該表達式使用下麵運算規則的原因:

1 ((!create_info->options) & HA_LEX_CREATE_TMP_TABLE)
2 We should use additional parentheses if we want the code to work properly:
3 (!(create_info->options & HA_LEX_CREATE_TMP_TABLE))

或者,我們發現更好的方式,用下麵的方式編寫代碼:

1 ((create_info->options & HA_LEX_CREATE_TMP_TABLE) == 0)

例 2. Emule 項目. * 和 ++ 優先級.

1 STDMETHODIMP
2 CCustomAutoComplete::Next(..., ULONG *pceltFetched)
3 {
4   ...
5   if (pceltFetched != NULL)
6     *pceltFetched++;
7   ...
8 }

The error was found through the V532 diagnostic:考慮審查 '*pointer++' 部分. 可能的意思是:'(*pointer)++'. emule customautocomplete.cpp 277

如果 'pceltFetched' 不是一個空指針,該函數必須增加該指針指向的ULONG類型變量的值。錯誤是:'++' 運算符的優先級高於 '*' 運算符的優先級(指針解引用)。該 "*pceltFetched++;" 行等同於下麵的代碼:

1 TMP = pceltFetched + 1;
2 *pceltFetched;
3 pceltFetched = TMP;

實際上它僅僅增加了指針的值。為使代碼正確,我們必須添加括號:"(*pceltFetched)++;".

例 3.Chromium 項目。& 和 != 運算符的優先級

1 #define FILE_ATTRIBUTE_DIRECTORY 0x00000010
2  
3 bool GetPlatformFileInfo(PlatformFile file, PlatformFileInfo* info) {
4   ...
5   info->is_directory =
6     file_info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0;
7   ...
8 }

The error was found through the V564 diagnostic: '&' 操作被用到了bool類型的值上. 你很可能忘記加入圓括號或有意的使用'&&'操作。base platform_file_win.cc 216

程序員們很容易忘記 '!=' 的優先級高於 '&' 的優先級。這就在我們的例子中發生了。因此,我們使用下麵的表達式:

1 info->is_directory =
2  file_info.dwFileAttributes & (0x00000010 != 0);

讓我們簡化它:

1 info->is_directory = file_info.dwFileAttributes & (true);

再次簡化它:

1 info->is_directory = file_info.dwFileAttributes & 1;

原來, 我們是測試了第一個比特位而不是第5個比特位. 為修正它,我們需要添加圓括號。

例 4.BCmenu 項目。IF 和 ELSE 混亂。

1 void BCMenu::InsertSpaces(void)
2 {
3   if(IsLunaMenuStyle())
4     if(!xp_space_accelerators) return;
5   else
6     if(!original_space_accelerators) return;
7   ...
8 }

The error was found through the V563 diagnostic: 可能,這裏的 'else' 分支必須與前一個 'if' 關聯。 fire bcmenu.cpp 1853

這不是一個優先級的錯誤,但與它有關。程序員沒有顧及 'else' 分支是於最近的 'if' 操作符關聯。我們有充分的理由認為代碼將以下麵的算法工作:

1 if(IsLunaMenuStyle()) {
2   if(!xp_space_accelerators) return;
3 else {
4   if(!original_space_accelerators) return;
5 }

但實際上它等同於下麵的結構:

1 if(IsLunaMenuStyle())
2 {
3    if(!xp_space_accelerators) {
4      return;
5    else {
6      if(!original_space_accelerators) return;
7    }
8 }

例 5.IPP Samples 項目. ?: 和 | 的優先級。

1 vm_file* vm_file_fopen(...)
2 {
3   ...
4   mds[3] = FILE_ATTRIBUTE_NORMAL |
5            (islog == 0) ? 0 : FILE_FLAG_NO_BUFFERING;
6   ...
7 }

The error was found through the V502 diagnostic: 可能 '?:' 操作不會像期望的那樣工作。'?:' 操作具有比 '|' 操作更低的優先級。 vm vm_file_win.c 393

依賴於 'islog' 變量值,該表達式必須等於 "FILE_ATTRIBUTE_NORMAL" 或 "FILE_ATTRIBUTE_NORMAL"。但這不會發生。'?:' 的優先級比 '|' 優先級低。因此,該代碼會變成下麵這樣:

1 mds[3] = (FILE_ATTRIBUTE_NORMAL | (islog == 0)) ?
2  0 : FILE_FLAG_NO_BUFFERING;

讓我們簡化該表達式:

1 mds[3] = (0x00000080 | ...) ? 0 : FILE_FLAG_NO_BUFFERING;

既然 FILE_ATTRIBUTE_NORMAL等於0x00000080,該條件永遠為真。意味著 0 總是被寫入 mds[3] 中。

例 6.Newton Game Dynamics 項目。?: 和 * 的優先級。

1 dgInt32 CalculateConvexShapeIntersection (...)
2 {
3   ...
4   den = dgFloat32 (1.0e-24f) *
5         (den > dgFloat32 (0.0f)) ?
6           dgFloat32 (1.0f) : dgFloat32 (-1.0f);
7   ...
8 }

The error was found through the V502 diagnostic: 可能 '?:' 操作不會像期望的那樣工作。'?:' 具有比 '*' 運算更低的優先級。physics dgminkowskiconv.cpp 1061

該代碼的錯誤再一次與 '?:' 去處符相關。'?:' 運算符的條件被一個無效的表達式 "dgFloat32 (1.0e-24f) * (den > dgFloat32 (0.0f))"表示了. 添加圓括號將解決該問題.

順便說明,程序員們常常忘記 '?:' 運算符是多麼狡猾。這裏有個關於該主題貼子:"How to make fewer errors at the stage of code writing. Part N2".


最後更新:2017-04-02 22:16:39

  上一篇:go 名字空間和友元函數
  下一篇:go virtualbox如何共享文件