同事最近遇到一個神奇問題,簡單來說就是他修改了一個會 release 給客戶或其他產品的 library,在裡面新增了一個 API。但照理說這個剛 release 的 API 還沒有人用到才對,卻在一系列的 regression 中發現它竟然被 call 到,而且造成程式 crash?! 從此展開他的 debug 之旅...
往下開始敘述問題前,先來看看可以重現這狀況的範例吧:reproduce code example.
假設客戶 (這邊就是另一個公司的產品,假設叫 P) 寫的程式是 main.cpp 那個,基於各種原因,這邊採用 dynamic load 的方式在 runtime 才去抓 shared library libtestlib.so。同事修改的 library 就是 libtestlib.so,其中它的實作放在 testlib.cpp,header 是 testlib.h。
而這神奇狀況的起因是這樣的:產品 P 跟這個 library 因為每天 build code 時間不一定,所以為了可以 compile 產品 P 先把 testlib.h 複製了一份過去到他們自己的目錄,但是 regression 跑的時候會到最新目錄下抓剛編好的新 library。這造成甚麼結果呢?
首先,同事進 code 前產品 P 複製的那份 testlib.h 假設是範例中的 testlib_old.h,同事進 code 後的 testlib.h 假設是範例中的 testlib_new.h。所以產品 P 的狀況是編譯時拿 testlib_old.h,跑產品 P 的 regression 時用的是最新版本且內容跟 testlib_old.h 不同的 libtestlib.so。
在這個範例中,差異僅僅只是新增了一個 member function hidden() 到 class A 中其餘不變,然而 main.cpp 第 14 行的結果卻會完全不同:
- 如果 main.cpp 編譯時是用 testlib_new.h,則一切行為如預期:
print function in B - 如果 main.cpp 編譯時適用 testlib_old.h,則輸出會變成:
hidden function in B
同事遇到的就是第二種狀況。
原因簡單來說發生在 call virtual function 時因為會先去 virtual table 拿到真正的 function pointer ,這個例子下的 virtual table 利用 gdb 的 info vtbl 會看到長這樣:
vtable for 'A' @ 0x7ffff73b4170 (subobject @ 0x6016f0):
[0]: 0x7ffff71b3df8 <B::~B()>
[1]: 0x7ffff71b3e3c <B::~B()>
[2]: 0x7ffff71b3d38 <B::hidden()>
[3]: 0x7ffff71b3d1e <B::print()>
理想上,看到的則應該要是下面這樣:
vtable for 'A' @ 0x7ffff73b4170 (subobject @ 0x6016f0):
[0]: 0x7ffff71b3df8 <B::~B()>
[1]: 0x7ffff71b3e3c <B::~B()>
[2]: 0x7ffff71b3d1e <B::print()>
從 testlib_old.h 中會認為要 call B::print() 這個 function 要從 virtual table 中拿位置是 [2] 的 function pointer (也就是下面的 virtual table),然而實際上在新編譯好的 libtestlib.so 中所看到的卻會是上面的 virtual table,這也就導致執行時去拿 [2] 的 function pointer 實際上卻會拿到 B:::hidden()。
workaround 很簡單,把 hidden() 的宣告移到 print() 下面就好。當然最根本的解決方案是打從一開始就不要有這種不一致出現。
備註: 這個問題因為是發生在 virtual table resolve function pointer 的階段發生的,雖然這例子是使用 dynamic load 的方式載入 shared library,但實際上就算用 dynamic link 也不影響,non-virtual function 也因為 resolve symbol 的機制不同,所以也不影響。
沒有留言:
張貼留言