# 編譯、連結 library ## GCC預設的參數有什麼?代表什麼意思 ```bash gcc --help // 可以看到可用的參數與其解釋 ``` ### **gnu編譯過程**  ### 1. 預處理 將 include 、 macro 擴展成真正的內容,經過這一步驟後,檔案會變大許多。 使用 GCC 進行預處理的方式如下: ```bash gcc -E -I./src main.c -o main.i ``` - -E 會讓 gcc 僅對 c 程式做預處理 - -I 用來指定自定義標頭檔的優先搜尋目錄,若你定義的標頭檔與程式碼同目錄,就可以忽略該參數。 - -o 指定輸出檔案的檔名與格式 :::info ***巨集(Macro)*** 在編譯指令中宣告或是在程式碼中使用#define宣告 ``` bash // define gcc -D DEBUG -o main main.c // cancel defination gcc -U DEBUG -o main main.c ``` 可以使用巨集宣告定值、進行有條件編譯甚至是撰寫擬函式 ```c // 宣告定值 #define SIZE 10 // 撰寫擬函式 #define MAX(a, b) ((a) > (b) ? (a) : (b)) // 有條件編譯 #ifndef SOMETHING_H #define SOMETHING_H /* Declare some data types and public functions. */ #endif /* SOMETHING_H */ // 有條件編譯 #ifdef DEBUG fprintf(stderr, "Some message\n"); #endif ``` ::: ### 2. 編譯 將預處理過的程式碼編譯成組合語言,而組合語言又會因為不同的處理器架構出現差異。 使用 GCC 進行編譯的方法: ```bash gcc -S -I./src main.c -o main.s ``` - -S 僅輸出 C 程式對應的組合語言 :::info ***交叉編譯*** 假設要編譯出能在arm架構上執行的程式碼,通常因為效能和時間成本的關係不會直接在arm平台上編譯,而是可以先在x86先驗證程式碼(當然這邊是先編譯x86版本),接著再編譯出ARM(aarch64)的檔案部署到機器上整合測試 使用Toolchain內提供的交叉編譯器以及資料庫(Library),Toolchain提供了個階段的交叉編譯工具。無論你使用gcc或是Make或是CMaker,只要設定好路徑跟環境,整個交叉編譯的程序就可以直接在Host啟動  reference: https://allenshaing.wordpress.com/2021/08/21/cross-compiler-101/ ::: ### 3. 組譯 經過編譯後,我們會得到組合語言檔案,也就是 *.s 檔案。不過,若要讓電腦能夠執行我們的程式碼,我們仍需要將其轉換成機器碼。 ```bash gcc -c main.s -o main.o ``` - -c 僅編譯 C 程式,但不連結,即輸出對應的目標碼 ### 4. 連結 連結會將多個目標文件或是 library 連結成可執行檔。 ```bash gcc -o main main.o ``` 如果有多個機器碼最後會被連結成一個可執行檔案,也可以這麼做: ```bash gcc -o main a.o b.o c.o ``` 這樣一來,如果開發者更動了其中一個檔案,我們只需對有更動的檔案進行編譯再連結起來。可以大幅節省編譯的時間。 :::info **static linking** 靜態函式庫(static library)就是由一些物件檔案(object files)所構成的封裝檔,通常其檔案名稱都會以 lib 開頭,而副檔名則為 .a。 使用靜態連結函式庫的好處就是所有的程式都包裝在執行檔中,不會因為缺少函式庫的檔案而不能執行,不過缺點就是這樣的執行檔大小會比較大,而如果函式庫有更新的話,整個執行檔也要跟著重新編譯。 ```bash! gcc -o main main.c sum.c // static linking gcc -c -o sum.o sum.c ar -rcs libsum.a sum.o // linking 第一種寫法 gcc main.c -L. -lsum -o main_static // linking 第二種寫法 gcc -o main_static main.c libsum.a ``` ::: :::success **ar指令參數** - r => 把參數中所有的檔案插入 library 檔案中,若該檔案已存在於 library 中,覆蓋掉原本的內容 - c => 新增一個 library 檔案 - s => 在 library 中寫入 object file 的 index ::: :::info **shared library** 共享函式庫(shared library)是在程式實際開始執行時,才會被載入的函式庫,執行檔本身與共享函式庫是分離的,這樣可以讓執行檔的大小比較小,而且未來共享函式庫在更新之後,執行檔也不需要重新編譯,而缺點則是執行檔在執行時就會需要共享函式庫的檔案,如果缺少了共享函式庫的檔案,就會無法執行。 ```bash! gcc -shared -o libsum.so sum.o gcc -o main_dynamic main.c libsum.so LD_LIBRARY_PATH= . ./main_dynamic // LD_LIBRARY_PATH代表環境變數的設置 // '.'代表目前所在位置 ``` **底下以openssl為例子:** 使用shared library  使用static library  ```bash! du -sh openssl // 1.2M openssl => shared library // 7.5M openssl => static library ``` ::: :::info ***GNU Debugger*** 使用GDB進行除錯 ```bash gcc -Wall -g main main.c ``` - -Wall: 顯示所有警告信息 - -g: 使gcc要盡可能告知資訊給GDB幫助除錯 ::: ## 理解C++有不同標準版本:C++11, C++14, C++17, C++20。 - GCC如何指定不同版本?(不同版本可能在參數上不一樣) ```bash gcc -std=c11 -o main main.c ``` - 使用library前先閱讀最低版本需求,否則可能編譯失敗 ## GCC有哪些效能優化的參數,如果編譯指令有多個參數,會依照哪一個為準? :::warning 如果使用多個級別的優化選項,會以最後一個為準。 ::: ### **程式最佳化** **-O1** => 編譯器會在不做任何將編譯時間拉很長的優化的前提下,降低程式碼的大小與執行時間 -fdefer-pop -fmerge-constants -fthread-jumps -fif-conversion -fif-conversion2 -fdelayed-branch -fguess-branch-probability -fcprop-registers **-O2** => 不會做loop unrolling 或是 function inlining,並且相對於O1而言,增加了編譯的時間與生成出的組合語言的效能。 -fforce-mem -foptimize-sibling-calls -fstrength-reduce -fcse-follow-jumps -fcse-skip-blocks -frerun-cse-after-loop -frerun-loop-opt -fgcse -fgcse-lm -fgcse-sm -fgcse-las -fdelete-null-pointer-checks -fexpensive-optimizations -fregmove -fschedule-insns -fschedule-insns2 -fsched-interblock -fsched-spec -fcaller-saves -fpeephole2 -freorder-blocks -freorder-functions -fstrict-aliasing -funit-at-a-time -falign-functions -falign-jumps -falign-loops -falign-labels -fcrossjumping :::info ***loop unrolling: 一種犧牲程式的大小來加快程式執行速度的最佳化方法*** - 優點 - branch prediction的失敗次數減少。 - 如果循環結構內語句沒有data dependency,增加了平行執行的機會。 - 缺點 - 程式碼膨脹。 - 循環結構內若包含遞迴可能會降低循環展開的效益。 ```c for (i = 1; i <= 60; i++) a[i] = a[i] * b + c; ``` ```c for (i = 1; i <= 58; i+=3) { a[i] = a[i] * b + c; a[i+1] = a[i+1] * b + c; a[i+2] = a[i+2] * b + c; } ``` ::: :::info ***inline function*** 一般而言,當我們撰寫函數,並呼叫使用,電腦的機器語言指令會紀錄目前工作階段的記憶體位址,然後跳至函數的記憶體位置處理完程序後,並回到原先的位址上,而這樣來回會造成時間上的額外負擔。若開啟function inlining的功能,編譯時便會把函數中的程式直接展開。 ```c int pred(int x) { if (x == 0) return 0; else return x - 1; } ``` Before inlining: ```c int func(int y) { return pred(y) + pred(0) + pred(y+1); } ``` After inlining: ```c int func(int y) { int tmp; if (y == 0) tmp = 0; else tmp = y - 1; /* (1) */ if (0 == 0) tmp += 0; else tmp += 0 - 1; /* (2) */ if (y+1 == 0) tmp += 0; else tmp += (y + 1) - 1; /* (3) */ return tmp; } ``` ::: **-O3** => 相對於O2而言,增加了以下優化的選項 -finline-functions -fweb -frename-registesr -funswitch-loops reference: [1]https://gcc.gnu.org/onlinedocs/gcc-3.4.6/gcc/Optimize-Options.html#Optimize-Options [2]https://en.wikipedia.org/wiki/Loop_unrolling [3]https://en.wikipedia.org/wiki/Inline_expansion ## 什麼是glibc?他和musl libc有什麼差別? glibc為GNU C Library,glibc於設計上有諸多不足之處,例如全靜態連結因NSS機制而有狀況、內部機制甚多而難以維護/移植、組語太多、pthread實做有bug......,因此Rich Felker重新設計出一個新的C Library - musl。 musl 被設計為更輕量級和高效的函式庫,特別適用於資源受限的環境,如嵌入式系統。musl傾向於嚴格遵循POSIX標準,這可能使得使用musl編譯的程式更容易在不同系統上實現二進制文件的可移植性。並且使用shared library的時候不支援lazy binding。 :::info **二進位可移植性** 將保證任何程式在符合標準的給定硬體平台上一旦編譯通過,可以在符合同樣標準的任何其他硬體平台上以編譯後的形式執行。 **lazy binding** 在library call 真正被呼叫時再去載入。 ::: reference: https://wiki.musl-libc.org/functional-differences-from-glibc.html#:~:text=glibc%20allows%20the%20usage%20of,in%20the%20regular%20symbol%20tables. ## 什麼CXXABI?他與GCC及glibc的版本有什麼關係? - Application Binary Interface(ABI) 當c語言程式碼編譯成組合語言後,透過ABI將組合語言轉換成機器語言 - CXXABI為c++的ABI,不同版本的GNU C Library支援不同的CXXABI - gcc -> glibc(GNU C Library) -> ABI ```bash! // 先找尋目前正在使用的GNU C Library版本 ll /usr/lib/x86_64-linux-gnu/libstdc* ```  ```bash! // 找可以支援的CXXABI strings /usr/lib/x86_64-linux-gnu/libstdc++.so.6|grep CXXABI ```  :::warning 若出現 Error: /lib/libstdc++.so.6: version 'CXXABI_1.3.8' not found 類似的錯誤碼,可以考慮更換GNU C Library的版本,並使用ln指令更換新的連結 ::: reference: https://www.xdull.cn/cxxabi.html ## 一個Linux作業系統裡面可能有好幾個stdio.h,我該如何確認GCC會引用哪個stdio.h  ```bash! gcc -H -c your_source.c ``` - -H => 顯示在編譯過程中每個header file的搜索路徑  ## 如果編譯過程中出現undefined reference該怎麼辦 1. 確定引用函數時的格式是否錯誤,或是該函數屬於的標頭檔是否有引入 2. 確定library是否有被正確連接上並檢查版本相容性 - 如果你在使用外部的library,確保被正確連接到編譯過程中,需要使用 -l 選項指定庫的名稱,並且 -L 選項指定庫的搜索路徑。 ```bash! // 這裡 -lmylibrary 指定了要使用的library,而 -L/path/to/library 指定了library的路徑。 gcc -o myprogram myprogram.c -lmylibrary -L/path/to/library ``` 3. 確定include file的路徑正確 - 如果你在標頭檔中聲明了某個函數或變數,確保其路徑正確。你可以使用 -I 選項指定標頭檔的路徑。 ```bash! gcc -o myprogram myprogram.c -I/path/to/headers ``` 4. 檢查編譯器和library的相容性: - 確保你使用的編譯器和library是相容的,有時不同版本的編譯器或library之間可能存在不同的ABI。 ## 如果我確定已經安裝某個library,但仍然編譯失敗,出現undefined reference,有哪些方法可以讓GCC找到我安裝的library 1. 檢查環境變數 LD_LIBRARY_PATH: 確保環境變數 LD_LIBRARY_PATH 包含了庫文件的搜索路徑。這個環境變數告訴運行時連接器(runtime linker)在哪裡找shared library。 ```bash! export LD_LIBRARY_PATH=/path/to/library:$LD_LIBRARY_PATH gcc -o myprogram myprogram.c -lmylibrary -L/path/to/library -Wl,-rpath=/path/to/library ``` ## 如果編譯過程中出現no such file "ooxx.h"該怎麼辦 1. 確認標頭檔的位置與權限: 2. 指定正確的標頭檔路徑 - 確定include file的路徑正確 - 如果你在標頭檔中聲明了某個函數或變數,確保其路徑正確。你可以使用 -I 選項指定標頭檔的路徑。 ```bash! gcc -o myprogram myprogram.c -I /path/to/headers ``` ## 使用apt或yum安裝library和我自己下載原始碼編譯有什麼不同 1. 安裝/卸載: 通過一條命令安裝套件及其相依項目,省去額外的步驟,包括解壓縮、配置、編譯、安裝與一些相依性問題。在卸載時也解決相依性的問題同時保持系統的整潔。 2. 版本管理: 不僅可以解決不同套件之間版本衝突的問題,通過一條指令可以將套件升級或退回到特定版本。 3. 系統整合: 安裝的套件其文件放置在系統預定的目錄中,並且可以被其他系統工具發現。
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up