1. 程式人生 > >Linux動態連結那點事兒(`cmake find_package,linux shared library`路徑詳解)

Linux動態連結那點事兒(`cmake find_package,linux shared library`路徑詳解)

Motivation

經常在Linux下面寫C++程式,尤其是需要整合各種第三方庫的工程,肯定對find_package指令不陌生。

這是條很強大的指令。可以直接幫我們解決整個工程的依賴問題,自動把標頭檔案和動態連結檔案配置好。比如說,在Linux下面工程依賴了OpenCV,只需要下面幾行就可以完全配置好:

add_executable(my_bin src/my_bin.cpp)
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})
target_link_libraries(my_bin, ${OpenCV_LIBS})

工作流程如下:

  1. find_package在一些目錄中查詢OpenCV的配置檔案。
  2. 找到後,find_package會將標頭檔案目錄設定到${OpenCV_INCLUDE_DIRS}中,將連結庫設定到${OpenCV_LIBS}中。
  3. 設定可執行檔案的連結庫和標頭檔案目錄,編譯檔案。

到現在為止出現了第一個問題。那就是:
find_package會在哪些目錄下面尋找OpenCV的配置檔案?

find_package目錄

為什麼我們要知道這個問題呢?因為很多庫,我們都是自己編譯安裝的。比如說,電腦中同時編譯了OpenCV2OpenCV3,我該如何讓cmake知道到底找哪個呢?

其實這個問題在CMake官方文件中有非常詳細的解答。

首先是查詢路徑的根目錄。我把幾個重要的預設查詢目錄總結如下:

<package>_DIR
CMAKE_PREFIX_PATH
CMAKE_FRAMEWORK_PATH
CMAKE_APPBUNDLE_PATH
PATH

其中,PATH中的路徑如果以binsbin結尾,則自動回退到上一級目錄。
找到根目錄後,cmake會檢查這些目錄下的

<prefix>/                                               (W)
<prefix>/(cmake|CMake)/                                 (W)
<prefix>/<name>*/                                       (W)
<prefix>/<name>*/(cmake|CMake)/                         (W)
<prefix>/(lib/<arch>|lib|share)/cmake/<name>*/          (U)
<prefix>/(lib/<arch>|lib|share)/<name>*/                (U)
<prefix>/(lib/<arch>|lib|share)/<name>*/(cmake|CMake)/  (U)

括號裡的W代表Windows,U代表Unix。但是經過實測,Windows下的目錄Linux也會檢查。

cmake找到這些目錄後,會開始依次找<package>Config.cmakeFind<package>.cmake檔案。找到後即可執行該檔案並生成相關連結資訊。

現在回過頭來看查詢路徑的根目錄。我認為最重要的一個是PATH。由於/usr/bin/PATH中,cmake會自動去/usr/(lib/<arch>|lib|share)/cmake/<name>*/尋找模組,這使得絕大部分我們直接通過apt-get安裝的庫可以被找到。

另外一個比較重要的是<package>_DIR。我們可以在呼叫cmake時將這個目錄傳給cmake。由於其優先順序最高,因此cmake會優先從該目錄中尋找,這樣我們就可以隨心所欲的配置cmake使其找到我們希望它要找到的包。如我在3rd_parties目錄下編譯了一個OpenCV,那麼執行cmake時可以使用

OpenCV_DIR=../../3rd-party/opencv-3.3.4/build/ cmake .. 

另一種方式是使用
cmake -D CMAKE_PREFIX_PATH=../../3rd-party/opencv-3.3.4/build/
這種做法比第一種優先順序還要高,而且要更常用一些。

這樣做以後,cmake會優先從該目錄尋找OpenCV

配置好編譯好了以後,我感興趣的是另一個問題:
我現在編譯出了可執行檔案,並且這個可執行檔案依賴於opencv裡的動態庫。這個動態庫是在cmake時顯式給出的。那麼,

  1. 該執行檔案在執行時是如何找到這個動態庫的?
  2. 如果我把可執行檔案移動了,如何讓這個可執行檔案依然能找到動態庫?
  3. 如果我把該動態庫位置移動了,如何讓這個可執行檔案依然能找到動態庫?
  4. 如果我把可執行檔案複製到別的電腦上使用,我該把其連結的動態庫放到新電腦的什麼位置?

可執行檔案如何尋找動態庫

在ld的官方文件中,對這個問題有詳盡的描述。

The linker uses the following search paths to locate required
shared libraries:

   1.  Any directories specified by -rpath-link options.

   2.  Any directories specified by -rpath options.  The difference
       between -rpath and -rpath-link is that directories specified by
       -rpath options are included in the executable and used at
       runtime, whereas the -rpath-link option is only effective at
       link time. Searching -rpath in this way is only supported by
       native linkers and cross linkers which have been configured
       with the --with-sysroot option.

   3.  On an ELF system, for native linkers, if the -rpath and
       -rpath-link options were not used, search the contents of the
       environment variable "LD_RUN_PATH".

   4.  On SunOS, if the -rpath option was not used, search any
       directories specified using -L options.

   5.  For a native linker, the search the contents of the environment
       variable "LD_LIBRARY_PATH".

   6.  For a native ELF linker, the directories in "DT_RUNPATH" or
       "DT_RPATH" of a shared library are searched for shared
       libraries needed by it. The "DT_RPATH" entries are ignored if
       "DT_RUNPATH" entries exist.

   7.  The default directories, normally /lib and /usr/lib.

   8.  For a native linker on an ELF system, if the file
       /etc/ld.so.conf exists, the list of directories found in that
       file.

   If the required shared library is not found, the linker will issue
   a warning and continue with the link.

最重要的是第一條,即rpath。這個rpath會在編譯時將動態庫絕對路徑或者相對路徑(取決於該動態庫的cmake)寫到可執行檔案中。chrpath工具可以檢視這些路徑。

>>> chrpath extract_gpu
extract_gpu: RPATH=/usr/local/cuda/lib64:/home/dechao_meng/data/github/temporal-segment-networks/3rd-party/opencv-3.4.4/build/lib

可以看到,OpenCV的動態庫的絕對路徑被寫到了可執行檔案中。因此即使可執行檔案的位置發生移動,依然可以準確找到編譯時的rpath

接下來的問題:如果我把可執行檔案複製到了別人的電腦上,或者我的動態庫檔案的目錄發生了改變,怎樣讓可執行檔案繼續找到這個動態庫呢?其實是在第五條:LD_LIBRARY_PATH。只要將儲存動態庫的目錄加入到LD_LIBRARY_PATH中,可執行檔案就能正確找到該目錄。

這種做法十分常見,比如我們在安裝CUDA時,最後一步是在.bashrc中配置

export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH

這樣做之後,依賴cuda的可執行檔案就能夠正常運行了。

總結

寫這篇文章是因為從我第一次使用cmake以來,經常因為動態連結的問題而耽誤很長時間。清楚理解find_package的執行機制在Linux的C++開發中是非常重要的,而相關的資料網上又比較稀少。其實官網上解釋的非常清楚,不過之前一直沒有認真查。做事情還是應該一步一個腳印,將原理搞清楚再放心使用。

Reference

  1. https://cmake.org/cmake/help/v3.0/command/find_package.html
  2. https://unix.stackexchange.com/questions/22926/where-do-executables-look-for-shared-objects-at-runtime
  3. https://codeyarns.com/2017/11/02/how-to-change-rpath-or-runpath-of-executable/