天天看點

《C程式設計新思維》一1.3 庫的路徑

本節書摘來自異步社群《c程式設計新思維》一書中的第1章,第1.3節,作者 【美】ben klemens,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視

現在我們有了編譯器,有posix的工具包,還有一個可以用來友善地安裝幾百個庫的包管理器。現在我們着手解決用這些工具來編譯我們程式的問題。

我們必須從編譯器指令行開始,并很快會陷入一團混亂,但是我們還是可以用三個(有時候是三個半)相對簡單的步驟結束。

1. 設定一個編譯器配置變量表。

2. 設定一個要連接配接的庫的變量表。所謂的半個步驟是指,有時你不得不設定一個唯一的變量,用來一邊編譯一邊連接配接;或者有時不得不設定兩個變量,分别用在編譯時和運作時的連接配接。

3. 設定一個根據這些變量來協調編譯的系統。

為了用一個庫,你必須告訴編譯器你将從庫中兩次導入函數:一次是為了編譯,一次是為了連接配接。對于一個在标準位置的庫,這兩次聲明都通過程式中的#include和-l編譯選項發生。

例1-1展示了一個小例子,可以用來做一些神奇的(至少對于我來說是這樣的;如果統計學術語對你來說就像希臘文一樣,其實也沒關系)算數。erf(x)是c99标準的錯誤處理函數,是與平均數為0、均方差為sqrt{2}的從0到x的正則分布的積分緊密相關。這個例子中,我們用erf來驗證一個在統計學家中流行的領域(一個标準大樣本假設的95%置信區間)。我們把這個檔案命名為erf.c。

例1-1 一個對标準庫的一行調用(erf.c)

《C程式設計新思維》一1.3 庫的路徑

對#include行你應該已經很熟悉了。編譯器将把math.h和stdio.h檔案添加在源檔案的這個地方,并是以導入了printf、erf和sqrt的聲明。在math.h中的聲明并沒有erf函數做什麼,隻是說這個函數接收一個double型參數也傳回一個double型值。這些已經足夠編譯器去檢查我們使用的合法性并産生一個源檔案,帶着一個給計算機的标記:一旦你看到這個标記,你可以找到erf函數,并将這個标記用erf的傳回值替代。

而連接配接器的任務是通過确實找到erf來解析那個标記,也就是在你硬碟的某個庫裡。

在math.h中的數學函數分散在他們自己的庫中,你需要通過一個-lm編譯選項告訴連接配接器。這裡,-l是一個用來訓示庫需要連接配接的選項,而本例中的庫有一個用單個字母表示的名字:m。你不用設定任何選項就可以使用printf,因為在連接配接指令行的末尾,有一個隐含的-lc選項來要求連接配接器連接配接标準libc庫。随後,我們将看到glib 2.0通過-lglib-2.0被連接配接進來,gnu科學計算庫也通過-lgsl連接配接進來,依此類推。

是以,如果檔案名為erf.c,那麼完整的gcc編譯器指令行,應該如下所示(這裡包括幾個選項,在後面将會詳細介紹):

《C程式設計新思維》一1.3 庫的路徑

這樣就能告訴編譯器通過程式中的#include包含數學函數,并告訴連接配接器通過指令行中的-lm連接配接數學庫。

-o選項用來給出輸出檔案的名字;否則我們将得到一個預設的可執行檔案名a.out。

在後面你将看到我幾乎每次都用到一些編譯器選項,并且我也建議你使用它們。

-g,表示加入調試符号。沒有這個選項,調試器不會給你變量或者函數的名字。這些調試資訊并不會把程式拖慢,而且我們也不在乎程式增加1k位元組,是以看起來沒啥理由不去用它。該選項對gcc、clang和icc(intel的編譯器)都有效。

-std=gnu11,這是gcc特有的選項,用來提示gcc應該允許符合c11和posix标準的代碼使用。否則,gcc可能将一些目前有效的文法判為非法。與之類似的,一些系統還是在c11之前釋出的,用-std=gnu99。這是gcc獨有的;而其他的編譯器都從很久以前就将c99當作預設配置。posix标準規定你的系統中必須有c99,是以以上這一指令行的與編譯器無關的版本應該是:

《C程式設計新思維》一1.3 庫的路徑

在後文将介紹的makefile中,我通過設定一個變量cc=c99實作了這個效果。

《C程式設計新思維》一1.3 庫的路徑

在mac系統中,c99是一個特别修改的gcc版本,可能并不是你想要的。如果你有一個不是很理想的c99版本,或者它整個就被忽略了,那就自己建立一個。把一個叫做c99的檔案放在你的搜尋路徑中的目錄裡:

《C程式設計新思維》一1.3 庫的路徑

或者如果你願意,就是

《C程式設計新思維》一1.3 庫的路徑

并通過chmod +x c99讓它成為可執行的檔案。

-o3顯示出這裡的優化等級是三級,也就是嘗試已知的所有方式去建立更快的代碼。如果你運作調試器,你會發現太多的變量被優化掉了以至于你都沒辦法跟蹤執行情況,那麼換成-o0。這樣就隻提供随後的cflags變量裡包含的常用方法。該選項對gcc、clang和icc都有效。

-wall添加編譯器警告。該選項對gcc、clang和icc都有效。對于icc使用者,你可能更喜歡用-w1,即在顯示編譯器警告的同時并不顯示備注。

《C程式設計新思維》一1.3 庫的路徑

要堅持使用編譯器警告。你可能已經熟知c語言标準,但是你不可能比你的編譯器更挑剔和更了解c。舊的c教材連篇累牍地警告你注意=和==的差别,或者檢查是否所有的變量在使用前都被初始化了。作為一本更加現代的書的作者,我卻将以平常心來對待,因為我可以把所有的警告總結為一點:永遠都要用你的編譯器警告。

如果編譯器建議你做一個改變,不要懷疑或試圖碰運氣而放棄修改。盡可能去:(1)了解為什麼你得到警告,(2)修改代碼直到不産生任何警告和錯誤。編譯器資訊是出了名的難懂的,是以如果你在第(1)步有困難,把警告資訊貼在搜尋引擎上,就能看到有多少人在你之前也面對了類似的問題。你可能想加上-werror編譯選項,這樣你的編譯器将把警告當作錯誤來處理。

在筆者的硬碟中有超過700 000個檔案,聲明sqrt和erf函數的頭檔案隻是其中之一,而還有一個是包含了這些函數對應的被編譯後的目标檔案(你可以在任何posix标準系統中試一下find /-type f | wc –l來得到一個粗略的檔案數)。編譯器需要知道在哪個目錄中去查找并找到正确的頭檔案和目标檔案,當我們開始使用非标準c庫的時候,這個問題就會變得更加嚴重。

在一個典型的安裝中,庫可能存放的地方至少有三個。

作業系統的廠家可能預定義了一兩個廠家自己用來安裝庫檔案的目錄。

可能存在為本地系統管理者準備的用于安裝包的目錄,并且不能被來自廠家的下一次作業系統更新覆寫。系統管理者可以有一個特殊的破解版本的庫可以用來覆寫系統預設的版本。

使用者一般來說沒有向這些路徑寫操作的權限,但是有從他們的主目錄利用那些庫的權限。

作業系統标準的路徑一般不會引發什麼問題,編譯器也應該知道如何查找那些路徑,并找到标準c庫以及伴随其安裝的任何檔案。posix标準用“通常位置”來指代上面所說的目錄。

但是對于其他的東西,你必須告訴編譯器如何查找。這真是拜占庭風格:沒有一個标準的方法去找到不按标準位置安裝的庫,這一點是人們對c比較惱火的地方。另一方面,編譯器知道如何在通常的位置查找,而庫的提供者傾向于将庫安放在通常位置,是以你可能從來沒有真正手工去配置位址。再者,也有幾種方法使你可以指定路徑。最後,一旦你把非标準庫安裝在系統上,你可以在一個shell腳本或makefiles中配置好然後再也不用去想這個。

假設你在計算機上安裝了一個叫做libuseful的庫,并且你知道與之相關的檔案放在/usr/local/目錄,也就是你的系統管理者的官方主目錄。你已經把#include 放在了你的代碼裡;現在你必須把下一行放在你的指令行中:

《C程式設計新思維》一1.3 庫的路徑

-i添加指定的路徑到搜尋範圍内,也就是編譯器用來搜尋你放在代碼裡面的#included的頭檔案的。

-l添加指定的路徑到庫的搜尋範圍内。

注意順序問題。如果你有一個叫做specific.o的檔案依賴于libbroad庫,而libbroad庫依賴于libgeneral,那麼你應該輸入:

《C程式設計新思維》一1.3 庫的路徑

任何其他順序,比如gcc –lbroad –lgeneral specific.o,都可能失敗。你可能認為連接配接器首先檢視第一個目标——specific.o,将無法解析的函數、結構和變量名記入一個清單。然後連接配接器檢視下一個目标——lbroad,并在這個目标内搜尋清單中仍然缺失的項目,同時有可能在清單中添加新的項目;所有再在-lgeneral查找仍然缺失的項目。如果直到搜尋完最後的目标仍然存在未解析的符号(包括在最後的隐含的-lc),連接配接器将終止運作并向使用者給出最後剩下的未解析項目。

現在回到位置路徑問題:要連接配接的庫到底在哪裡呢?如果它是用你安裝作業系統其他部分同樣的包管理器安裝的,那麼最可能在通常路徑,你也不用去擔心這個。

你可能想不清你自己的本地庫應該放在何處,比如/usr/local還是/sw或者/opt。你無疑可以用硬碟搜尋的方式來查找,比如在你的機器或posix環境中使用

《C程式設計新思維》一1.3 庫的路徑

來搜尋/usr中以libuseful開頭的檔案。當你發現libuseful庫的共享目标檔案在/some/path/lib,那麼幾乎可以肯定對應的頭檔案一定在/some/path/include。

在硬碟裡到處找庫檔案是一件很惱人的事情,于是pkg-config通過将每個包自己報告的用于編譯的配置和位置資訊存在一個知識庫中,解決了這個問題。在指令行中輸入pkg-config;如果你得到一個錯誤提示說“沒有指定包名字”,那麼很好,說明你有pkg-config并且可以用它來做研究。例如,在我個人計算機的指令行上輸入這兩行指令:

《C程式設計新思維》一1.3 庫的路徑

得到下面兩行輸出:

《C程式設計新思維》一1.3 庫的路徑

這些就是我用來編譯gsl和libxml2所需要的所有選項。-l選項揭示出gnu科學計算庫依賴于基本線性數學子程式庫(blas),而gsl的blas庫依賴于标準數學庫。看起來所有這些庫都在通常路徑,因為這裡沒有-l選項,但是-i選項表明libxml2的頭檔案的特殊位置。

回到指令行,shell提供了一個方法,就是當你把一個指令行用反引号包圍時,這個指令行會被其自身的輸出替代。就是說,輸入:

《C程式設計新思維》一1.3 庫的路徑

編譯器看到的是:

《C程式設計新思維》一1.3 庫的路徑

是以pkg-config會為我們做很多工作,但是這并不是标準配置的:我們期望所有人都擁有它,或者每個庫都用它注冊了。如果你沒有pkg-config,你就必須自己研究,比如讀這個庫的手冊,或者像前面那樣搜尋。

《C程式設計新思維》一1.3 庫的路徑

有很多與路徑有關的環境變量,比如cpath、library_path或者c_include_path。你可以在.bashrc或别的使用者定義的環境變量清單中設定它們。它們都不是标準的——連linux和mac中的gcc都分别使用不同的變量,别的編譯器自然也使用自己的變量。我發現在每個項目的makefile或類似機制的基礎上,用-i和-l來設定這些路徑相對更容易。如果你喜歡這些路徑變量,可以在你的編譯器的幫助檔案的末尾查找符合你的情況的相關變量。

即便用pkg-config,我們也顯然需要某種工具幫我們把所有這些自動執行。每個元素都很容易了解,但是這可是一個冗長機械的瑣碎工作。

編譯器連接配接靜态庫的時候,是将庫裡的相關内容直接複制到最終的可執行檔案中的。是以程式本身或多或少是一個獨立的系統。而共享庫與你的程式是在運作時連接配接的,就是說我們在運作時會遇到像編譯器在編譯時尋找庫的路徑那樣的問題。甚至更糟的是,你的程式的使用者也存在同樣的問題。如果你的庫在一個非标準的路徑,那你需要找到一個修改運作時搜尋庫的路徑的方法。有以下選擇。

如果你用autotools打包你的程式,libtools知道如何添加合适的配置,也就是說你不用再操心什麼。

需要更改搜尋路徑的最可能的原因,是由于你沒有root權限,是以你把庫檔案都放在你的個人目錄下。如果你把所有的庫都安裝在libpath中,那麼需要設定環境變量ld_library_path。一般是在shell(.bashrc、.zshrc,或者别的什麼對應物等)啟動腳本中來做這個工作,可使用指令:

《C程式設計新思維》一1.3 庫的路徑

有些人反對過度使用ld_library_path(萬一有人把惡意僞裝的庫放到那個路徑,在你沒有察覺的情況下代替了真正的庫怎麼辦?),但是如果你所有的庫都在一個地方,在你虛拟的控制下添加一個目錄到路徑中就不是不合理的了。

當用gcc、clang或者icc來基于一個在libpath中的庫編譯程式的時候,加入

《C程式設計新思維》一1.3 庫的路徑

到相應的makefile中。-l選項告訴編譯器到哪裡去找到庫以解析符号;-wl選項從gcc/clang/icc傳遞選項到連接配接器,而連接配接器将給定的-r嵌入所連接配接的庫的運作時搜尋路徑。不幸的是,pkg-config經常不知道運作時路徑,是以你可能必須有手工輸入這些資訊。

繼續閱讀