Letting va_list re-entraint by va_copy()
之前在這篇《Non-standard behavior of (v)snprintf, on Visual C++》探究過 strprintf() 的實作方法。那時我以為已經找到終極解法,僅需為每個不符合標準的平台,製作 work around 即可。不過最近將這些程式搬到 Mac OS X 上面,卻連連接到 Segment Fault,才知道其實我的 strprintf() 還有缺陷,導因於我的無知與疏忽。
拿該篇最後一個版本的程式,在 Mac OS X 10.6 (Snow Leopard) 上面編譯執行,會得到下列的輸出結果:
SHELL> ./a.out
[DEBUG] pbuf_size == 16
[DEBUG] len == 13
[DEBUG] {13} Hello, sign!
Hello, sign!
[DEBUG] pbuf_size == 16
[DEBUG] len == 17
[DEBUG] pbuf_size == 18
[DEBUG] len == 26
[DEBUG] pbuf_size == 27
[DEBUG] len == 15
[DEBUG] {15} Hello, (null)!
[DEBUG] free pbuf
Hello, (null)!
[DEBUG] pbuf_size == 16
[DEBUG] len == 36
[DEBUG] pbuf_size == 37
[DEBUG] len == 9
[DEBUG] {9} Hello, !
[DEBUG] free pbuf
Hello, !
這個結果,很顯然地,並不正確。起先我以為是 Mac OS X 的 vsnprintf() 和 Windows 平台一樣,不符合 C99 標準。不過驗證的結果卻非如此。幾經波折,再加上空閒很少,拖了好久我才注意到,上列輸出裡,第二輪預期結果為 "Hello, jeffhung!" 的測試,其 len 值起先為正確的 17,後來卻變成了 26 與 15。這個差異,讓我注意到,我這段程式,居然只有一個 va_start(),而沒有 va_end()。
見鬼,真是見鬼!!居然犯這種低級錯誤,我羞愧到極點,想要鑽到土裡去。不過,還好趕緊檢查所有我寫的相關程式庫,都沒有這種少掉 va_end() 的錯誤,只能算是當初寫文章時,準備範例程式不夠嚴謹。
但是將 va_end() 補在函式後面後,卻仍然產生同樣的錯誤輸出,讓我百思不得其解。測試後發現,要將 va_start() 與 va_end() 包在 vsnprintf() 前後才行。也就是說,每次將 va_list 變數送進去給 vsnprintf() 用之前,都要重新用 va_start() 初始化一次。這不禁讓我懷疑,va_list 變數,是 non-reentraint 的。
翻查 C99 標準,在 7.15.3 節裡找到這句:
The object
apmay be passed as an argument to another function; if that function invokes theva_argmacro with parameterap, the value ofapin the calling function is indeterminate and shall be passed to theva_endmacro prior to any further reference toap.212)212) It is permitted to create a pointer to a
va_listand pass that pointer to another function, in which case the original function may make further use of the original list after the other function returns.
這證實了我的想法。另外,原來還有 va_copy() 這個 macro,以便在自己寫的,直接收一個 va_list 變數而非 ... 的 v 系列函式,複製 va_list 變數好重覆使用。再依據 C99 標準裡第 7.15.1.1 節的說法:
Each invocation of the
va_startandva_copymacros shall be matched by a corresponding invocation of theva_endmacro in the same function.
得知自製 v 系列函式時,若收到的 va_list 變數會不只用到一次,如再傳給 vsnprintf() 很多次,則最好每次都用 va_copy() 製作複本,使用這個複本,並馬上利用 va_end() 銷毀之。
雖然說我之前寫的相關程式庫,並沒有犯少了 va_end() 的低級錯誤,但一樣還是沒有注意到這個 reentraint 的問題。還好在所 target 的平台,實際上並不會造成影響,隨附的 test cases 的成功執行,也都確保了這點。不過,這種 non portable issue,還是地雷一枚。寫出這種程式,慚愧啊慚愧。



Post a Comment