程序編譯運行時頭文件或動態鏈接庫的查找

          轉載請註明來源:http://blog.csdn.net/dlutxie/article/details/6776936

          當考慮怎樣總結這個頭文件及動態鏈接庫的查找問題時,我想到了一個程序從生到死的歷程。寫過很多程序,編譯過很多程序,也運行過很多程序,對一個程序的從生到死,感覺很簡單,也就沒有做更多的或者說深入的思考與研究。也許我們習慣了在windows環境下的編程,在那裏我們有很好的IDE,它能把一個工程組織得很好,直接點編譯生成一個可執行文件,然後直接雙擊這個.exe文件或者創建一個快捷方式運行這個程序。以前可能我們也聽說過,源碼先要編譯,然後鏈接,然後裝載、運行,可我們很少去考慮這背後到底都發生了些什麼,似乎也不用考慮那麼多,因爲我們的IDE實在是太智能了,因爲我們已經很習慣了使用windows環境,因爲在windows下安裝個軟件,我們幾乎只需要點擊下一步就可以了。

        這段時間在做linux系統下opencv2.0到ARM開發板的移植,在這裏,我有很多問題不得不考慮,問題如下:

             程序在編譯時,源碼所需要的庫(靜態庫和動態庫)及頭文件編譯器是去哪找的?(庫及頭文件的查找)

             當輸入一個命令時,系統時如何找到這個命令的?(命令的查找)

             程序在運行時,它所需要的庫是去哪找的?(動態鏈接庫的查找)

       這就是一個程序的由生到死的過程中需要考慮的幾個問題!

       在linux系統下,我們常常要自己通過源碼安裝一些庫,裝一些軟件,第一件事該想到的,編譯生成後的頭文件,庫或者程序我們該放到哪,是放到/lib  /usr/lib  /usr/include  /usr/bin  /usr/local/lib  /usr/local/include    /usr/local/bin等目錄下嗎?可能有些書會說自己安裝的程序一般放在/usr/local目錄下,放到這下面我可以省心的不用去修改一些環境變量或配置文件了,但接下來我們可能會想,要是我以後想刪除我前面安裝的軟件呢?你還能想起你以前安裝這個軟件時它到底安裝了哪些文件了嗎?幾乎不可能吧!

         所以當我想安裝一個軟件時我希望像在windows下一樣,把這個軟件安裝在一個單獨的目錄下,比如說我要安裝opencv2.0,那麼我就在/usr/local目錄下創建一個目錄opnecv2.0,然後把所有相關的都安裝到/usr/local/opencv2.0目錄下,這樣如果我以後不想要這個庫時我就可以直接刪掉這個文件夾就可以了。可在這裏我們就有些問題不得不考慮了:如果我們把opencv安裝到了/usr/local/opencv2.0這個目錄下了,那編譯器在編譯包含有opencv2.0的庫或頭文件時,編譯器能找着這些頭文件和庫嗎?如果這是一個可運行文件,我在其它目錄下運行這個文件,系統能找到這個文件嗎?它是如何找到這個文件的呢?當其它包含有opencv有關函數的程序時,它是如何找到這些庫的呢?由這就引發了我上面提到的三個問題。

         下面我們先來看第一個問題:程序在編譯時,源碼所需要的庫及頭文件編譯器是去哪找的?

         在這裏,其實有庫的查找,和頭文件的查找,下面先來講頭文件的查找。我們在寫一個比較大型的程序時,總是喜歡把這些函數還有一些數據結構的聲明放在一個文件中,我們把這種文件稱爲頭文件,文件名以.h後綴結尾。在一些源文件裏,我們可能要包含自己寫的頭文件,還有一些標準庫的頭文件比如說stdio.h等等。在編譯的預處理階段,預處理程序會將這些頭文件的內容插到相應的include指令處,現在的問題是編譯器是如何找到這些頭文件的。

         1. 在編譯時,我們可以用-I(i的大寫)選項來指定頭文件所在的目錄,如:

test.h內容如下:

Struct student
{
    int  age;
};

main.c 內容如下:

#include<stdio.h>
#include<test.h>
int main()
{
    structstudent st;
    st.age= 25;
    printf(“st.age=%d\n”,st.age);
    return0;
}

         可以把test.h放在與main.c同一個目錄下,編譯命令如下:

        xgy@ubuntu:~/tmp/workSpace/testincludedir$  gcc main.c -I./

        如果把test.h放在/usr/include/xgytest目錄下,注意,xgytest是我自己建的一個目錄

       編譯命令如下:xgy@ubuntu:~/tmp/workSpace/testincludedir$  gcc main.c –I/usr/include/xgytest

       注意:在-I後可以有空格也可以沒有空格,另外也可以指定多個目錄,例如,tesh.h放在當前文件夾下,還有一個teacher.h放在 ./include目錄下,則可以這樣編譯:

      xgy@ubuntu:~/tmp/workSpace/testincludedir$  gcc main.c -I ./ -I ./include/

           2. 設置gcc的環境變量C_INCLUDE_PATH、CPLUS_INCLUDE_PATH 、CPATH。

        C_INCLUDE_PATH編譯 C 程序時使用該環境變量。該環境變量指定一個或多個目錄名列表,查找頭文件,就好像在命令行中指定 -isystem 選項一樣。會首先查找 -isystem 指定的所有目錄。

         CPLUS_INCLUDE_PATH編譯 C++ 程序時使用該環境變量。該環境變量指定一個或多個目錄名列表,查找頭文件,就好像在命令行中指定 -isystem 選項一樣。會首先查找 -isystem 指定的所有目錄。

          CPATH 編譯 C 、 C++ 和 Objective-C 程序時使用該環境變量。該環 境變量指定一個或多個目錄名列表,查找頭文件,就好像在命令行中指定-l 選項一樣。會首先查找-l 指定的所有目錄。

          假設test.h放在/usr/include/xgytest,則對C_INCLUDE_PATH做如下設置:

export C_INCLUDE_PATH=$C_INCLUDE_PATH:/usr/include/xgytest

詳細請況可以參考如下文章:

http://blog.csdn.net/katadoc360/article/details/4151286

 http://blog.csdn.net/dlutxie/article/details/8176164

3. 查找默認的路徑/usr/include   /usr/local/include等

 

總結一下gcc在編譯源碼時是如何尋找所需要的頭文件的:

   1.  首先gcc會從-Idir   -isystem dir   -Bprefix    -sysroot  dir     --sysroot=dir    -iquote dir選項指定的路徑查找(這些選項先指定的會先搜索,有特例的情況請參考前面的鏈接)

    2. 然後找gcc的環境變量:C_INCLUDE_PATH、CPLUS_INCLUDE_PATH 、CPATH、GCC_EXEC_PREFIX等。(這些環境變量搜索的先後順序不確定,有待確認)

   3. 然後查找GCC安裝的目錄(可以通過gcc  -print-search-dirs查詢)

   4.  然後再按照下面列出的順序查找系統默認的目錄:/usr/include      /usr/local/include

         

程序在編譯時,編譯器又是如何查找所需要的庫的呢?這裏的庫既包括靜態庫又包括動態庫。在這裏,我們得先了解兩個概念:庫的鏈接時路徑和運行時路徑。

        現代連接器在處理動態庫時將鏈接時路徑(Link-time path)和運行時路徑(Run-time path)分開,用戶可以通過-L指定連接時庫的路徑,通過-R(或-rpath)指定程序運行時庫的路徑,大大提高了庫應用的靈活性。

我們來看幾個例子:

pos.c文件的內容如下:

#include<stdio.h>
void pos()
{
    printf("the directory is .//n");
}

main.c文件的內容如下:

#include<stdio.h>
intmain()
{
    pos();
    return 0;
}

接下來看如下執行的命令:

我們來分析下上面圖片中的命令:生成的動態鏈接庫libpos.so放在了當前的路徑下,接着用gcc main.c  –lpos 來鏈接這個庫卻發現ld找不着這個庫!然後我加了一個-L選項,指出這個庫在當前路徑下,結果編譯通過,可在運行剛編譯生成的a.out時又出現了錯誤!這就是運行是的鏈接錯誤!運行時的鏈接問題在後面將有介紹。用ldd命令可以查看一個可執行文件依懶於哪些庫。注意-lpos, 這裏的-l是L的小寫,另外也可以寫成-l  pos即中間有一個空格,但有沒有空格是有一點區別的,有空格的只搜索與POSIX兼容的庫,一般建議使用沒有空格的。

         另外我們可以把剛纔編譯生成的libpos.so拷到默認的路徑/lib  /usr/lib /usr/local/lib路徑下,然後直接執行gcc main.c –lpos也可以通過編譯。

         在這裏補充說明一點:Linux下 的庫文件在命名時有一個約定,那就是庫文件應該以lib三個字母開頭,由於所有的庫文件都遵循了同樣的規範,因此在用-l(L的小寫字母)選項指定鏈接的庫文件名時可以省去 lib三個字母,也就是說GCC在對-lfoo進行處理時,會自動去鏈接名爲libfoo.so的文件。

每個共享庫也有一個實名,其真正包含有庫的代碼,組成如下:

so+.+子版本號+.+發佈號(最後的句點和發佈號是可選項。)

另外,共享庫還有一個名稱,一般用於編譯連接,稱爲連名(linkername),它可以被看作是沒有任何版本號的so名。在上面的討論中,我一直是以動態庫(或者說共享庫)爲例的,其實對於靜態庫也一樣,只是在這裏又有一個問題,如果在同一個目錄下既有動態庫,又有靜態庫,且它倆的文件名也一樣,只是後綴不一樣,那鏈接器在鏈接時是鏈接動態庫還是鏈接靜態庫呢?如果我要指定鏈接動態庫或者靜態庫又該如何做呢?

           讓我們來看看下面執行的命令(注意下,爲了方便,我開了兩個終端):

通過上面的這些命令,也許就能回答我剛提出的兩個問題了。

在這裏,我還想看下編譯時gcc是否會查LD_LIBRARY_PATH環境變量,還有/etc/ld.so.conf文件指定的路徑,命令如下:

從上面的命令可以看出,編譯時,編譯器不會查找LD_LIBRARY_PATH,還有/etc/ld.so.conf文件中指定的路徑。下面來總結下:

 

程序在編譯鏈接時,編譯器是按照如下順序來查找動態鏈接庫(共享庫)和靜態鏈接庫的:

1.  gcc會先按照-Ldir    -Bprefix選項指定的路徑查找

2. 再找gcc的環境變量GCC_EXEC_PREFIX

3. 再找gcc的環境變量LIBRARY_PATH

4. 然後查找GCC安裝的目錄(可以通過gcc  -print-search-dirs查詢)

5.  然後查找默認路徑/lib

6.  然後查找默認路徑/usr/lib

7.  最後查找默認路徑/usr/local/lib

8.  在同一個目錄下,如果有相同文件名的庫(只是後綴不同),那麼默認鏈接的是動態鏈接庫,可以用-static選項顯示的指定鏈接靜態庫。

 

 第二個問題:當輸入一個命令時,系統時如何找到這個命令的?(命令的查找)

    如果我們輸入一個命令時帶入路徑時一般是不會不什麼疑問的,因爲此時我們執行的就是指定路徑下程序。當我們只輸入一個命令名時會發生什麼情況呢?

當我們鍵入命令名時,linux系統更確切的說應該是shell按照如下順序搜索:

1.  Shell首先檢查命令是不是保留字(比如for、do等)

2.  如果不是保留字,並且不在引號中,shell接着檢查別名表,如果找到匹配則進行替換,如果別名定義以空格結尾,則對下一個詞作別名替換,接着把替換的結果再跟保留字表比較,如果不是保留字,則shell轉入第3步。

3.  然後,shell在函數表中查找該命令,如果找到則執行。

4.  接着shell再檢查該命令是不是內部命令(比如cd、pwd)

5.  最後shell在PATH中搜索以確定命令的位置

6.  如果還是找不到命令則產生“command not found”錯誤信息。

這裏要注意一點:系統在按PATH變量定義的路徑搜索文件時,先搜到的命令先執行。例如,我的PATH變量如下:

root@ubuntu:~# echo $PATH

/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:

如果在不同的目錄中有兩個ls文件,例如/usr/local/sbin/ls, /usr/local/bin/ls,那麼在使用ls的時候,會執行/usr/local/sbin/ls,因爲在PATH中哪個目錄先被查詢,則哪個目錄下的文件就會先執行。

 

第三個問題:程序在運行時,它所需要的庫是去哪找的?(動態鏈接庫的查找)

        在這裏我沒有提到頭文件的查找,因爲頭文件只在編譯的時候纔會用到,編譯完後就不需要頭文件了!另外,這裏的庫指的是動態鏈接庫,靜態鏈接庫在鏈接後是不需要了的,因爲鏈接時鏈接器會把靜態庫中的代碼插入到相應的函數的調用處,所以程序在運行時不再需要靜態庫,而對於動態庫來說,鏈接時,並沒有將動態庫中的任何代碼或數據拷貝到可執行文件中,而只是拷貝了一些重定位與符號表信息!所以程序在運行時才需要鏈接時所使用的動態鏈接庫以執行動態鏈接庫中的代碼!這個可以參考《深入理解計算機系統》第七章。

 

程序運行時動態庫的搜索路徑搜索的先後順序是:

1.編譯目標代碼時指定的動態庫搜索路徑(指的是用-wl,rpath或-R選項而不是-L);

example: gcc -Wl,-rpath,/home/arc/test,-rpath,/lib/,-rpath,/usr/lib/,-rpath,/usr/local/lib test.c

2.環境變量LD_LIBRARY_PATH指定的動態庫搜索路徑;

3.配置文件/etc/ld.so.conf中指定的動態庫搜索路徑;

4.默認的動態庫搜索路徑/lib;

5.默認的動態庫搜索路徑/usr/lib。

在上述1、2、3指定動態庫搜索路徑時,都可指定多個動態庫搜索路徑,其搜索的先後順序是按指定路徑的先後順序搜索的。

 

上面這個的具體內容可以參考:

http://hi.baidu.com/kkernel/blog/item/ce31bb34a07e6b46251f14cf.html

         在這裏補充說明下:gcc的-Wl,rpath選項可以設置動態庫所在路徑,也就是編譯生成的該程序在運行時將到-Wl,rpath所指定的路徑下去尋找動態庫,如果沒找到則到其它地方去找,並且這個路徑會直接寫在elf文件(就是生成的可執行文件)中,這樣可以免去設置LD_LIBRARY_PATH。注意,gcc參數設定時-Wl,rpath,/path/to/lib, 中間不能有空格。

gcc -o pos main.c -L. -lpos -Wl,-rpath,./

上面這個命令的意思是:編譯main.c時在當前目錄下查找libpos.so這個庫,生成的文件名爲pos,當執行pos這個文件時,在當前目錄下查找所需要的動態庫文件。

可以像下面這個命令一樣指定查找多個路徑:

gcc -Wl,-rpath,/home/arc/test,-rpath,/lib/,-rpath,/usr/lib/,-rpath,/usr/local/libtest.c

更改/etc/ld.so.conf文件後記得一定要執行命令:ldconfig!該命令會將/etc/ld.so.conf文件中所有路徑下的庫載入內存中。

 

下面對編譯時庫的查找與運行時庫的查找做一個簡單的比較:

1. 編譯時查找的是靜態庫或動態庫,而運行時,查找的只是動態庫。

2. 編譯時可以用-L指定查找路徑,或者用環境變量LIBRARY_PATH,而運行時可以用-Wl,rpath或-R選項,或者修改/etc/ld.so.conf文件或者設置環境變量LD_LIBRARY_PATH.

3. 編譯時用的鏈接器是ld,而運行時用的鏈接器是/lib/ld-linux.so.2.

4. 編譯時與運行時都會查找默認路徑:/lib  /usr/lib

5. 編譯時還有一個默認路徑:/usr/local/lib,而運行時不會默認找查該路徑。

           如果安裝的包或程序沒有放在默認的路徑下,則使用mancommand查找command的幫助時可能查不到,這時可以修改MANPATH環境變量,或者修改/etc/manpath.config文件。如果使用了pkg-config這個程序來對包進行管理,那麼有可能要設置PKG_CONFIG_PATH環境變量,這個可以參考:http://www.linuxsir.org/bbs/showthread.php?t=184419

 

寫在最後的話

    一個程序的從生到死會發生很多很多的故事,在這裏,我只是從一個角度探討了其中的冰山一角,還有許許多多的問題需要去理解,比如說:編譯鏈接時,各個文件是如何鏈接到一起的?程序運行時,動態庫已經被加載到內存中,程序又是如何準確找到動態庫在內存中的位置的?動態庫的鏈接器/lib/ld-linux.so.2自己本身也是一個動態庫,那麼它又是如何被載入內存的呢?更深入的想一下,可以認爲ld-linux.so.2是隨內核一起載入內存的,那內核又是如何載入內存的呢?如果說內核是由bootloader載入的,那bootloader又是如何載入內存的呢?也許你該想到BIOS了。其中的一些問題可以參考《深入理解計算機系統》這本書。

 

                                                                                                                                                                               整理於2011-9-15     作者:seamus

轉載請註明來源:http://blog.csdn.net/dlutxie/article/details/6776936

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章