600
阿裏雲
技術社區[雲棲]
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(...)
|
4
|
auto_ptr<VARIANT>
child_array( new VARIANT[child_count]);
|
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 操作釋放:
為修複這類問題,你應為實例使用一個更恰當的類,boost::scoped_array.
例 2. IPP
Samples 項目。經典的未定義行為。
01
|
template < typename T,
Ipp32s size> void HadamardFwdFast(...)
|
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];
|
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)
|
4
|
while (!(m_pBitArray[m_nCurrentBitIndex
>> 5] &
|
5
|
Powers_of_Two_Reversed[m_nCurrentBitIndex++
& 31])) {}
|
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,
|
5
|
while (*(n
= ++s + strspn (s,
EZXML_WS)) && *n != '>' )
{
|
The error was found through the V567 diagnostic:
未定義行為. 's' 變量被修改且在時序點間使用了兩次. msne zxml.c
這裏使用了前綴++. 但不代表什麼:無法保證 's' 變量值會在 strspn() 函數調用前增長。
為了更容易理解例子,讓我們先回顧運算符優先級列表。
例 1.MySQL 項目。!
和 & 運算的優先級
1
|
int ha_innobase::create(...)
|
6
|
&&
(!create_info->options & HA_LEX_CREATE_TMP_TABLE)) {
|
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 項目.
* 和 ++ 優先級.
2
|
CCustomAutoComplete::Next(..., ULONG *pceltFetched)
|
5
|
if (pceltFetched
!= NULL)
|
The error was found through the V532 diagnostic:考慮審查
'*pointer++' 部分. 可能的意思是:'(*pointer)++'. emule customautocomplete.cpp 277
如果 'pceltFetched' 不是一個空指針,該函數必須增加該指針指向的ULONG類型變量的值。錯誤是:'++' 運算符的優先級高於 '*' 運算符的優先級(指針解引用)。該 "*pceltFetched++;" 行等同於下麵的代碼:
1
|
TMP
= pceltFetched + 1;
|
實際上它僅僅增加了指針的值。為使代碼正確,我們必須添加括號:"(*pceltFetched)++;".
例 3.Chromium 項目。&
和 != 運算符的優先級
1
|
#define
FILE_ATTRIBUTE_DIRECTORY 0x00000010
|
3
|
bool GetPlatformFileInfo(PlatformFile
file, PlatformFileInfo* info) {
|
6
|
file_info.dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY != 0;
|
The error was found through the V564 diagnostic:
'&' 操作被用到了bool類型的值上. 你很可能忘記加入圓括號或有意的使用'&&'操作。base platform_file_win.cc 216
程序員們很容易忘記 '!=' 的優先級高於 '&' 的優先級。這就在我們的例子中發生了。因此,我們使用下麵的表達式:
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 )
|
4
|
if (!xp_space_accelerators) return ;
|
6
|
if (!original_space_accelerators) return ;
|
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 ;
|
4
|
if (!original_space_accelerators) return ;
|
但實際上它等同於下麵的結構:
3
|
if (!xp_space_accelerators)
{
|
6
|
if (!original_space_accelerators) return ;
|
例 5.IPP
Samples 項目. ?: 和 | 的優先級。
1
|
vm_file*
vm_file_fopen(...)
|
4
|
mds[3]
= FILE_ATTRIBUTE_NORMAL |
|
5
|
(islog
== 0) ? 0 : FILE_FLAG_NO_BUFFERING;
|
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 (...)
|
4
|
den
= dgFloat32 (1.0e-24f) *
|
5
|
(den
> dgFloat32 (0.0f)) ?
|
6
|
dgFloat32
(1.0f) : dgFloat32 (-1.0f);
|
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