1. GCC編譯基礎(chǔ)

        共 7976字,需瀏覽 16分鐘

         ·

        2021-05-16 07:05

        點擊上方小白學(xué)視覺”,選擇加"星標(biāo)"或“置頂

        重磅干貨,第一時間送達(dá)

        本文轉(zhuǎn)自:AI算法與圖像處理


        資料準(zhǔn)備


        ?

        為了方便演示和講解,在這里提前準(zhǔn)備好幾個簡單的文件:test.cpp test.h main.cpp  文件內(nèi)容如下:

        ?

        main.cpp

        #include "test.h"

        int main (int argc, char **argv)
        {
            Test t;
            t.hello();
            return 0;
        }

        test.h

        //test.h
        #ifndef _TEST_H_ 
        #define _TEST_H_ 

        class Test
        {

        public:
            Test();
            void hello();
            ~Test();
        };
        #endif  //TEST

        test.cpp

        //test.cpp
        #include "test.h"
        #include <iostream>
        using namespace std;

        Test::Test()
        {

        }

        void Test::hello()
        {
            cout << "hello" << endl;
        }

        Test::~Test()
        {

        }


        C++的編譯過程


        一個完整的C++編譯過程(例如g++ a.cpp生成可執(zhí)行文件),總共包含以下四個過程:

        • 編譯預(yù)處理,也稱預(yù)編譯,可以使用命令g++ -E執(zhí)行
        • 編譯,可以使用g++ -S執(zhí)行
        • 匯編,可以使用as 或者g++ -c執(zhí)行
        • 鏈接,可以使用g++ xxx.o xxx.so xxx.a執(zhí)行
        ?

        可以通過添加g++ --save-temps參數(shù),保存編譯過程中生成的所有中間文件 下面對這四個步驟進(jìn)行逐一講解

        ?

        1、編譯預(yù)處理階段:主要對包含的頭文件(#include )和宏定義(#define,#ifdef … )還有注釋等進(jìn)行處理。

        可以使用g++ -E 讓g++ 在預(yù)處理之后停止編譯過程,生成 *.ii(.c文件生成的是*.i) 文件。因為上面寫的main.cpp中沒有任何預(yù)編譯指令,所以預(yù)編譯生成與源文件幾乎沒有差別。這里預(yù)編譯一下test.cpp文件

        g++ -E test.cpp test.h -o test.ii

        可以打開test.ii查看,剛剛的main.cpp文件預(yù)編譯完成后的內(nèi)容:

        預(yù)編譯完成后,#include引入的內(nèi)容 被全部復(fù)制進(jìn)預(yù)編譯文件中,除此之外,如果文件中有使用宏定義也會被替換處理。

        ?
        • 預(yù)編譯過程最主要的工作,就是宏命令的替換
        • #include命令的工作就是單純的導(dǎo)入,這里其實并不限制導(dǎo)入的類型,甚至可以導(dǎo)入.cpp、.txt等等。
        • 感興趣的同學(xué)可以預(yù)編譯一個包含Qt中信號的文件,會看到預(yù)編譯之后:
          • emit直接成了空。發(fā)射信號實質(zhì)就是一次函數(shù)調(diào)用;
          • 頭文件中的signals:也被替換成了protected:(Qt5被替換為public:
          • 以及Qt中其他的宏定義都在預(yù)編譯時被處理如:Q_OBJECT Q_INVOKEABLE
        ?

        2、g++ 編譯階段:C++ 語法錯誤的檢查,就是在這個階段進(jìn)行。在檢查無誤后,g++ 把代碼翻譯成匯編語言。

        可以使用-S 選項進(jìn)行查看,該選項只進(jìn)行編譯而不進(jìn)行匯編,生成匯編代碼。

        g++ -S main.ii -o main.s

        匯編代碼中生成的是和CPU架構(gòu)相關(guān)的匯編指令,不同CPU架構(gòu)采用的匯編指令集不同,生成的匯編代碼也不一樣:

        3、g++ 匯編階段:生成目標(biāo)代碼 *.o

        有兩種方式:

        • 使用 g++ 直接從源代碼生成目標(biāo)代碼 g++ -c *.s -o *.o
        • 使用匯編器從匯編代碼生成目標(biāo)代碼 as *.s -o *.o

        到編譯階段,代碼還都是人類可以讀懂的。匯編這一階段,正式將匯編代碼生成機器可以執(zhí)行的目標(biāo)代碼,也就是二進(jìn)制碼。

        # 編譯
        g++ -c main.s -o main.o
        # 匯編器編譯
        as main.s -o main.o

        也可以直接使用as *.s, 將執(zhí)行匯編、鏈接過程生成可執(zhí)行文件a.out, 可以像上面使用-o 選項指定輸出文件的格式。

        4、g++ 鏈接階段:生成可執(zhí)行文件;Windows下生成.exe

        修改main.cpp的內(nèi)容,引用Test

        #include "test.h"

        int main (int argc, char **argv)
        {
            Test t;
            t.hello();
            return 0;
        }

        生成目標(biāo)文件:

        • g++ test.cpp -c -o test.o
        • g++ main.cpp -c -o main.o

        鏈接生成可執(zhí)行文件:

        g++ main.o test.o -o a.out

        鏈接的過程,其核心工作是解決模塊間各種符號(變量,函數(shù))相互引用的問題,更多的時候我們除了使用.o以外,還將靜態(tài)庫和動態(tài)庫鏈接一同鏈接生成可執(zhí)行文件。

        對符號的引用本質(zhì)是對其在內(nèi)存中具體地址的引用,因此確定符號地址是編譯,鏈接,加載過程中一項不可缺少的工作,這就是所謂的符號重定位。本質(zhì)上來說,符號重定位要解決的是當(dāng)前編譯單元如何訪問「外部」符號這個問題。

        接下來我們先講解如何將源文件編譯成動態(tài)庫和靜態(tài)庫,然后再講述如何在鏈接時鏈接我們編譯好的庫。

        編譯動態(tài)庫和靜態(tài)庫

        大型項目中不可能使用一個單獨的可執(zhí)行程序提供服務(wù),必須將程序的某些模塊編譯成動態(tài)或靜態(tài)庫:

        編譯生成靜態(tài)庫

        使用ar命令進(jìn)行“歸檔”(.a的實質(zhì)是將文件進(jìn)行打包)

        ar crsv libtest.a test.o 
        • r 替換歸檔文件中已有的文件或加入新文件 (必要)
        • c 不在必須創(chuàng)建庫的時候給出警告
        • s 創(chuàng)建歸檔索引
        • v 輸出詳細(xì)信息

        編譯生成動態(tài)庫

        使用g++ -shared 命令指定編譯生成的是一個動態(tài)庫

        g++ test.cpp -fPIC -shared -Wl,-soname,libtest.so -o libtest.so.0.1
        • shared:告訴編譯器生成一個動態(tài)鏈接庫
        • -Wl,-soname:指示生成的動態(tài)鏈接庫的別名(這里是libtest.so
        • -o:指示實際生成的動態(tài)鏈接庫(這里是libtest.so.0.1
        • -fPIC
          • fPIC的全稱是 Position Independent Code, 用于生成位置無關(guān)代碼(看不懂沒關(guān)系,總之加上這個參數(shù),別的代碼在引用這個庫的時候才更方便,反之,稍不注意就會有各種亂七八糟的報錯)。
          • 使用-fPIC選項生成的動態(tài)庫,是位置無關(guān)的。這樣的代碼本身就能被放到線性地址空間的任意位置,無需修改就能正確執(zhí)行。通常的方法是獲取指令指針的值,加上一個偏移得到全局變量/函數(shù)的地址。
          • 關(guān)于PIC參數(shù)的詳細(xì)解讀:點此鏈接
        ?

        在gcc中,如果指定-shared不指定-fPIC會報錯,無法生成非PIC的動態(tài)庫,不過clang可以。

        ?

        庫中函數(shù)和變量的地址是相對地址,不是絕對地址,真實地址在調(diào)用動態(tài)庫的程序加載時形成。動態(tài)庫的名稱有別名(soname),真名(realname)和鏈接名(linker name)。

        • 真名是動態(tài)庫的真實名稱,一般總是在別名的基礎(chǔ)上加上一個小的版本號,發(fā)布版本構(gòu)成 別名由一個前綴lib,然后是庫的名字加上.so構(gòu)成,例如:libQt5Core.5.7.1
        • 鏈接名,即程序鏈接時使用的庫的名字,例如:-lQt5Core
        • 在動態(tài)鏈接庫安裝的時候總是復(fù)制庫文件到某個目錄,然后用軟連接生成別名,在庫文件進(jìn)行更新的時候僅僅更新軟連接即可。
        ?

        「注意:」

        • 生成的庫文件總是以libXXX開頭,這是一個約定,因為在編譯器通過-l參數(shù)尋找?guī)鞎r,比如-lpthread會自動去尋找libpthread.solibpthread.a
        • 如果生成的庫并沒有以lib開頭,編譯的時候仍然可以連接到,不過只能以顯示加在編譯命令參數(shù)里的方式鏈接。例如g++ main.o test.so
        ?


        靜態(tài)編譯和動態(tài)編譯

        鏈接階段,會將匯編生成的目標(biāo)文件.o與引用到的庫一起鏈接打包到可執(zhí)行文件中。這種稱為靜態(tài)編譯,靜態(tài)編譯中使用的庫就是靜態(tài)庫(*.a*.lib)生成的可執(zhí)行文件在運行時不需要依賴于鏈接庫。

        • 優(yōu)點:
          • 代碼的裝載速度快,執(zhí)行速度也比較快
          • 不依賴其他庫執(zhí)行,移植方便
        • 缺點:
          • 程序體積大
          • 更新不方便,如果靜態(tài)庫需要更新,程序需要重新編譯
          • 如果多個應(yīng)用程序使用的話,會被裝載多次,浪費內(nèi)存。
        g++ main.o libtest.a

        編譯完成后可以運行a.out查看效果,通過ldd命令查看運行a.out所需依賴,可以看到靜態(tài)編譯的程序并不依賴libtest庫。

        動態(tài)編譯

        動態(tài)庫在程序編譯時并不會被連接到目標(biāo)代碼中,而是在程序運行時才被載入。不同的應(yīng)用程序如果調(diào)用相同的庫,那么在內(nèi)存里只需要有一份該共享庫的實例,規(guī)避了空間浪費問題。

        動態(tài)編譯中使用的庫就是動態(tài)庫(*.so*.dll

        動態(tài)庫在程序運行時才被載入,也解決了靜態(tài)庫對程序的更新、部署和發(fā)布頁會帶來麻煩。用戶只需要更新動態(tài)庫即可,增量更新。

        動態(tài)庫在鏈接過程中涉及到加載時符號重定位的問題,感興趣的同學(xué)參看鏈接:動態(tài)編譯原理分析

        • 優(yōu)點:
          • 多個應(yīng)用程序可以使用同一個動態(tài)庫,而不需要在磁盤上存儲多個拷貝
          • 動態(tài)靈活,增量更新
        • 缺點:
          • 由于是運行時加載,多多少少會影響程序的前期執(zhí)行性能
          • 動態(tài)庫缺失會導(dǎo)致文件無法運行
        g++ main.o libtest.so

        編譯完成后可以運行a.out查看效果,通過ldd命令查看運行a.out所需依賴

        gcc鏈接參數(shù) -L、-l、-rpath、-rpath-link

        從上面的截圖中,我們已經(jīng)看到了剛才的程序運行報錯,原因是找不到動態(tài)鏈接庫libtest.so

        這個報錯的解決方案有很多例如:

        • LD_LIBRARY_PATH=. ./a.out

        那么明明編譯成功,運行時為什么會找不到庫?為了弄清這個問題,我們需要對鏈接動態(tài)庫的過程有一個更深入的理解。

        我們在main.cpp中明確引用到了Test類,所以在編譯進(jìn)行到最后階段,鏈接的時候。如果在所有參與編譯的文件中沒能檢索到Test這個符號,則會報錯未定義的引用。
        所以在編譯過程中必須能夠找到包含Test符號的文件,可以是.o、.a、或者.so。
        如果是.o或者.a,也就是靜態(tài)鏈接,那么它會將.o或者.a中的內(nèi)容一起打包到生成的可執(zhí)行文件中,生成的可執(zhí)行文件可以獨立運行不受任何限制。
        而如果是.so這種動態(tài)鏈接庫,就比較麻煩了。鏈接器將不會把這個庫打包到生成的可執(zhí)行文件里,而僅僅只會在這里記錄一個地址,告訴程序,如果遇到Test符號,你就去文件libtest.so的第三行第五列(打個比方,實際是一個相對的內(nèi)存地址)找它的定義。

        綜上所述

        • 編譯鏈接main.cpp的時候,必須能夠找到libtest.so的動態(tài)庫,記錄下Test符號的偏移地址。
        • 運行的時候,程序必須找到libtest.so,然后尋址找到Test

        編譯時鏈接庫

        -L-l 鏈接器參數(shù),就是指定鏈接時去(哪里)找(什么)庫。

        • -l,代表鏈接哪個庫,會自動檢索lib開頭的對應(yīng)庫名。例如-lpthread,-lQt5Core。會自動檢索libpthread.so,libpthread.a,libQt5Core.so,libQt5Core.a
          • 如果靜態(tài)庫動態(tài)庫同時存在,優(yōu)先鏈接動態(tài)庫
        • -L,指定去哪里找?guī)煳募@缰付ǎ?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: 'Operator Mono', Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);display: inline-block;padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">-L/home/threedog/test,則在編譯時會優(yōu)先檢索/home/threedog/test/libpthread.so等文件。
        • 鏈接庫最直接的辦法是不用任何參數(shù),直接寫庫的路徑加載編譯參數(shù)里。
        • 查找順序
          • 如果直接寫的庫的全路徑,則會直接去找到庫,不走下面的順序檢索。
          • -L,優(yōu)先級最高
          • 然后是系統(tǒng)的環(huán)境變量LIBRARY_PATH
          • 最后再找內(nèi)定目錄 /lib /usr/lib /usr/local/lib 這是當(dāng)初編譯 gcc時寫在程序內(nèi)的
          • 如果都找不到,會報錯找不到文件或找不到-lxxxx

        所以以上的編譯命令,可以通過多種方式通過編譯:

        • g++ main.o libtest.so,或g++ main.o ./libtest.so
        • g++ main.o /home/threedog/test/libtest.so
        • g++ main.o -ltest -L.,或g++ main.o -ltest -L/home/threedog/test/
        • LIBRARY_PATH=. g++ main.o -ltest,或LIBRARY_PATH=/home/threedog/test/ g++ main.o -ltest
        • 或者把libtest.so拷貝到/usr/lib目錄下去。

        運行時鏈接庫

        通過上面的方法編譯出的a.out,運行會報錯,通過ldd命令查看,發(fā)現(xiàn)編譯時鏈接的libtest.so成了not found
        這就引出了第二個問題:如何讓程序運行的時候能夠找到對應(yīng)的庫。

        -Wl,-rpath就是做這個事情的:-Wl代表后面的這個參數(shù)是一個鏈接器參數(shù),-rpath+庫所在的目錄,會給程序明確指定去哪里找對應(yīng)的庫。
        手動將一個目錄指定成了ld的搜索目錄。

        在這里插入圖片描述

        另外,也可以通過在環(huán)境變量LD_LIBRARY_PATH里添加路徑的方式成功運行

        在這里插入圖片描述

        運行時庫的查找順序:

        1. 編譯目標(biāo)代碼時指定的動態(tài)庫搜索路徑(-rpath);
        2. 環(huán)境變量LD_LIBRARY_PATH指定的動態(tài)庫搜索路徑;
        3. 配置文件/etc/ld.so.conf中指定的動態(tài)庫搜索路徑;
        4. 默認(rèn)的動態(tài)庫搜索路徑/lib;
        5. 默認(rèn)的動態(tài)庫搜索路徑/usr/lib.

        rpath與rpath-link

        ?

        其實rpath和rpath-link都是鏈接器ld的參數(shù),不是gcc的。

        ?

        rpath-linkrpath只是看起來很像,可實際上關(guān)系并不大,rpath-link-L一樣也是在鏈接時指定目錄的。rpath-link的作用,在我們的這個實例中體現(xiàn)不出來。例如你上述的例子指定的需要libtest.so,但是如果 libtest.so 本身是需要 xxx.so 的話,這個 xxx.so 我們你并沒有指定,而是 libtest.so 引用到它,這個時候,會先從 -rpath-link 給的路徑里找。rpath-link指定的目錄與并運行時無關(guān)。

        C++頭文件的搜索原則

        上面提到了編譯時鏈接庫的查找順序和運行時動態(tài)庫的檢索順序,順便再提一下C++編譯時頭文件的檢索順序:

        • #include<file.h>只在默認(rèn)的系統(tǒng)包含路徑搜索頭文件
        • #include"file.h"首先在當(dāng)前目錄以及-I指定的目錄搜索頭文件, 若頭文件不位于當(dāng)前目錄, 則到系統(tǒng)默認(rèn)的包含路徑搜索

        順序:

        1. 先搜索當(dāng)前目錄
        2. 然后搜索-I指定的目錄
        3. 再搜索gcc的環(huán)境變量CPLUS_INCLUDE_PATH(C程序使用的是C_INCLUDE_PATH
        4. 最后搜索gcc的內(nèi)定目錄
          • /usr/include
          • /usr/local/include
          • /usr/lib/gcc/x86_64-redhat-linux/4.1.1/include

        以上,就是對gcc參數(shù)的一些詳細(xì)總結(jié),下面根據(jù)上面的講解解決幾個常遇到的疑問:

        問題1:-l鏈接的到底是動態(tài)庫還是靜態(tài)庫?

        • 如果鏈接路徑下同時有 .so 和 .a  那優(yōu)先鏈接 .so

        問題2:如果路徑下同時有靜態(tài)庫和動態(tài)庫如何鏈接靜態(tài)庫?

        • 最好的辦法,是參數(shù)里直接寫上靜態(tài)庫的全路徑。
        • 另一個辦法,可以使用-static參數(shù),會強制鏈接靜態(tài)庫。這種方式生成的文件可以執(zhí)行,但是文件的elf頭會有問題,使用ldd,readelf -d查看會顯示不是動態(tài)可執(zhí)行文件。

        問題3:如果文件中沒有使用對應(yīng)的庫,編譯器是否仍然會進(jìn)行鏈接?

        • 這個取決于編譯器的類型和版本,我本地gcc5.4,如果沒有用到的庫,即使寫了-l也不會鏈接。而我本地的clang,則會明確鏈接對應(yīng)的庫即使我沒有用到它。

        參考鏈接:

        • https://www.cnblogs.com/king-lps/p/7757919.html
        • https://blog.csdn.net/abcdu1/article/details/86083295
        • https://blog.csdn.net/weixin_40240269/article/details/86702090
        • https://www.jianshu.com/p/b2f611acba3d
        • https://www.cnblogs.com/youxin/p/5357614.html
        • https://blog.csdn.net/benpaobagzb/article/details/51277960


        下載1:OpenCV-Contrib擴(kuò)展模塊中文版教程
        在「小白學(xué)視覺」公眾號后臺回復(fù):擴(kuò)展模塊中文教程,即可下載全網(wǎng)第一份OpenCV擴(kuò)展模塊教程中文版,涵蓋擴(kuò)展模塊安裝、SFM算法、立體視覺、目標(biāo)跟蹤、生物視覺、超分辨率處理等二十多章內(nèi)容。

        下載2:Python視覺實戰(zhàn)項目52講
        小白學(xué)視覺公眾號后臺回復(fù):Python視覺實戰(zhàn)項目即可下載包括圖像分割、口罩檢測、車道線檢測、車輛計數(shù)、添加眼線、車牌識別、字符識別、情緒檢測、文本內(nèi)容提取、面部識別等31個視覺實戰(zhàn)項目,助力快速學(xué)校計算機視覺。

        下載3:OpenCV實戰(zhàn)項目20講
        小白學(xué)視覺公眾號后臺回復(fù):OpenCV實戰(zhàn)項目20講即可下載含有20個基于OpenCV實現(xiàn)20個實戰(zhàn)項目,實現(xiàn)OpenCV學(xué)習(xí)進(jìn)階。

        交流群


        歡迎加入公眾號讀者群一起和同行交流,目前有SLAM、三維視覺、傳感器、自動駕駛、計算攝影、檢測、分割、識別、醫(yī)學(xué)影像、GAN、算法競賽等微信群(以后會逐漸細(xì)分),請掃描下面微信號加群,備注:”昵稱+學(xué)校/公司+研究方向“,例如:”張三 + 上海交大 + 視覺SLAM“。請按照格式備注,否則不予通過。添加成功后會根據(jù)研究方向邀請進(jìn)入相關(guān)微信群。請勿在群內(nèi)發(fā)送廣告,否則會請出群,謝謝理解~


        瀏覽 44
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 亚洲精品视频免费观看 | 中国老太卖婬视频播放 | 精品人妻一区二区三区四区在线看 | 午夜欧美性爱视频 | 欧美大肚子孕妇做爰 |