問題現象
我們實現了一個名叫libilvrfplugin.so的lib,該lib鏈接了libiubsntconflib.so, 而libiubsntconflib.so 又鏈接了libipconflib.so, libipconflib.so裏面實現了一個方法check_vrf_r()用於檢查VRF的合法性。
簡單點來說,A lib鏈接了B lib,而B lib又鏈接了C lib,C lib實現了方法check_vrf_r().
某些場景下,系統會動態加載A lib, 但是A lib根本沒用到方法check_vrf_r()。注意這裏是動態加載的lib庫,也就是lib的使用方運行時會使用dlopen()加載該lib。
在新發布的版本里,我們發現該lib竟然沒起做用,好像該lib 根本就不存在一樣。
我們在syslog裏找到下面一條log:
Apr 17 15:46:27.718075 info CFPU-0 Validator: CPluginManager : Unable to load plugin libilvrfplugin.so Error:/opt/nokiasiemens/lib64/libiubsntconflib.so: undefined symbol: check_vrf_r
從syslog 裏可以看出動態加載libilvrfplugin.so(也就是 A lib)時失敗,原因是找不到符號check_vrf_r。
疑惑一:這幾個lib在上一個版本里工作正常,當前版本里沒有任何改動,爲什麼會出錯呢?
疑惑二:libilvrfplugin.so 中根本沒用到符號check_vrf_r,爲什麼會報找不到符號呢?
查找問題
根據syslog裏的線索,定位到出事的代碼:
...
lib_handle=dlopen(ep[count]->d_name,RTLD_LAZY);
if(!lib_handle)
{
error = dlerror();
TRACER(TRC_INFO)<<"CPluginManager : Unable to load plugin "
<<ep[count]->d_name << " Error:"<<error<<std::endl;
free(ep[count]);
continue;
} // if
...
這裏使用dlopen()來加載動態鏈接庫,並設置flag爲RTLD_LAZY,該flag控制了dlopen()加載lib時的解析方式,加載時只解析用庫裏用到的符號。本例中符號check_vrf_r就屬於只聲明(include進來)而未使用的符號。
順便說一句,dlopen()還有一種解析方式是RTLD_NOW,這就要求所有的符號都需要解析到地址,不管該符號有沒有用到。
但是本例中dlopen()解析方式是正確的,我們期望不去解析符號check_vrf_r,可是爲什麼還是去解析了呢?
原因可能是glibc的實現有變,有個bug也說不準,還有可能是哪裏改變了dlopen的實現。順便說一句由於C語言沒有命名空間的概念,所以你可以定義一個與系統函數同名的函數以覆蓋系統函數,大多數情況應該避免這麼做。
經查glibc在我們發佈的兩個版本中沒有變化,那麼很可能是哪裏改變了或影響了dlopen()。不得已,只能去翻看我們這個版本中所有代碼變化。
我們驚奇的發現,在某個腳本里新增了這麼一行代碼:
export LD_PRELOAD=/opt/nokiasiemens/SS_FConfigure/lib/libdlopeninterceptor.so
從字面意義上看跟dlopen有關,"dlopen劫持者",好霸氣的名字!接着看這個代碼會起什麼作用。
這裏export了環境變量LD_PRELOAD,該環境變量聲明瞭應用程序加載前優先加載的動態鏈接庫,換句話說如果這裏的動態鏈接庫實現了與系統函數同名的函數的話,那麼將覆蓋系統函數。
懷着激動的心情查看該動態lib的實現:
#include <dlfcn.h>
#include <syslog.h>
#include <stdlib.h>
#ifdef __cplusplus__
extern "C" {
#endif
typedef void* (*dlopen_func_t)(const char* filename, int flag);
static dlopen_func_t _glibc_dlopen = NULL;
void* dlopen(const char* filename, int flag)
{
int realflag = flag;
if (NULL == _glibc_dlopen) {
_glibc_dlopen = (dlopen_func_t)dlsym(RTLD_NEXT, "dlopen");
if (NULL == _glibc_dlopen) {
syslog(LOG_CRIT, "dlopeninterceptor:Failed to resolve dlopen, got error:%s", dlerror());
return NULL;
}
}
if (realflag & RTLD_LAZY) {
realflag = realflag & ~RTLD_LAZY;
realflag = realflag | RTLD_NOW;
syslog(LOG_DEBUG, "dlopeninterceptor:Changing dlopen flag from to %d to %d when opening %s", flag, realflag, filename);
}
return _glibc_dlopen(filename, realflag);
}
#ifdef __cplusplus__
}
#endif
驚奇的發現該Lib確實重寫了dlopen(), 如果dlopen()指定的flag時RTLD_LAZY將強制轉換成RTLD_NOW。找到root cause了,鬆一口氣。
最後
查找到root cause後就簡單了,給相應的組織或部門報個issue,將你的分析結果放上去就OK了,問題很快就解決了。後來聽說是某個同事誤操作在腳本中加了一行代碼讓dlopen()劫持者生效了。
類似這樣的問題,root cause是很簡單的,去fix花費的effort也不大。稍有困難的是如何在龐雜的系統中逐步定位問題,而且需要去查看整個系統的代碼實現,由於對系統其他模塊的不熟悉,必要時還需要給矛支持。感謝在此過程中給予支持的同事,也很欣慰公司有種很好的機制或氛圍,讓你在必要時都能得到給力的support。