本帖最后由 michaelchain 于 2021-8-26 11:45 編輯
C項目的文件組織和編譯C項目的代碼, 由頭文件(.h后綴)和C文件(.c后綴)組成 - C語言的函數(shù)和變量, 分聲明和定義兩個階段
- 頭文件和C文件是等價的, 相當(dāng)于C文件的一部分, 其功能由人為劃分, 用于變量和函數(shù)的聲明, 頭文件也可以用于變量和函數(shù)的定義, 但是這屬于非標(biāo)準(zhǔn)用法, 一般不這么用
- 同一個編譯中, 函數(shù)在一處定義, 處處可用(除非使用static關(guān)鍵字)
- 在A.c中定義后, 在B.c中用extern聲明這個函數(shù), 就可以調(diào)用
- 將A.c中的函數(shù)聲明提取到A.h, 在B.c中include A.h, 或者通過B.c include B.h, B.h include A.h, 都可以實現(xiàn)函數(shù)引用
- C的編譯, 是按文件編譯的, 每個C文件會編譯為一個目標(biāo)文件
- 頭文件不單獨編譯, 與include這個頭文件的C文件, 在預(yù)編譯階段展開, 之后在C文件中編譯
- 編譯需要知道C文件的列表和頭文件的目錄列表
- 編譯會依次編譯C文件列表中的每個文件, 不管最終是否用到
C項目結(jié)構(gòu)示例定義一個頭文件 inc.h,聲明兩個函數(shù)func1和func2, 將定義寫在func1.c和func2.c. 在main.c中通過main.h引用inc.h, 調(diào)用這些函數(shù), 程序目錄結(jié)構(gòu)如下 - ├── inc
- │ ├── func1.c
- │ ├── func2.c
- │ └── inc.h
- ├── main.c
- ├── main.h
- └── obj
復(fù)制代碼
main.c - #include <stdio.h>
- #include "main.h"
- int main()
- {
- uint8_t a = 0x08;
- uint8_t b = func1(a);
- printf("%X", b);
- return 0;
- }
復(fù)制代碼
main.h- #ifndef MAIN_H
- #define MAIN_H
- #include "inc.h"
- #endif
復(fù)制代碼
inc.h
- #ifndef INC_H
- #define INC_H
- typedef unsigned char uint8_t;
- uint8_t func1(uint8_t a);
- uint8_t func2(uint8_t a);
- #endif
復(fù)制代碼
func1.c
- #include "inc.h"
- uint8_t func1(uint8_t a)
- {
- a = a << 1;
- return a;
- }
復(fù)制代碼
func2.c
- #include "inc.h"
- uint8_t func2(uint8_t a)
- {
- a = a >> 1;
- return a;
- }
復(fù)制代碼
gcc的編譯過程
gcc命令其實依次執(zhí)行了四步操作 - 預(yù)處理(Preprocessing),
- 編譯(Compilation),
- 匯編(Assemble),
- 鏈接(Linking)
1.預(yù)處理(Preprocessing)預(yù)處理用于將所有的#include頭文件以及宏定義替換成其真正的內(nèi)容,預(yù)處理之后得到的仍然是文本文件,但文件體積會大很多。gcc的預(yù)處理是預(yù)處理器cpp來完成的,你可以通過如下命令對 main.c進(jìn)行預(yù)處理: gcc -E -I./inc main.c -o obj/main.i# or$ cpp main.c -I./inc -o obj/main.i-E是讓編譯器在預(yù)處理之后就退出,不進(jìn)行后續(xù)編譯過程; -I指定頭文件目錄, -o指定輸出文件名. 經(jīng)過預(yù)處理之后代碼體積會大很多, main.c只有10行, 但是main.i有749行, 預(yù)處理之后的文件可以用文本編輯器查看
2.編譯(Compilation)這一步的編譯將經(jīng)過預(yù)處理之后的程序轉(zhuǎn)換成特定匯編代碼的過程, 編譯的命令如下: $ gcc -S -I./inc main.c -o obj/main.s-S讓編譯器在編譯之后停止. 這一步會生成程序的匯編代碼, 內(nèi)容如下: .file "main.c"
.text
.section .rodata
.LC0:
.string "%X"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movb $8, -2(%rbp)
movzbl -2(%rbp), %eax
movl %eax, %edi
call func1@PLT
movb %al, -1(%rbp)
movzbl -1(%rbp), %eax
movl %eax, %esi
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
3.匯編(Assemble)
匯編過程將上一步的匯編代碼轉(zhuǎn)換成機器碼(machine code),這一步產(chǎn)生了二進(jìn)制的目標(biāo)文件, gcc匯編過程通過as命令完成 as obj/main.s -o obj/main.o# porgcc -c obj/main.s -o obj/main.o這一步需要給每一個源文件產(chǎn)生一個目標(biāo)文件, 以便后面link gcc -c -I./inc inc/func1.c -o obj/func1.ogcc -c -I./inc inc/func2.c -o obj/func2.o
4.鏈接(Linking)通過上面的步驟, 在obj目錄下已經(jīng)有main.o, func1.o和func2.o這三個目標(biāo)文件, 現(xiàn)在需要通過linker將這些目標(biāo)文以及所需的庫文件(.so等)鏈接成最終的可執(zhí)行文件(executable file) 命令如下 gcc -o obj/main obj/main.o obj/func1.o obj/func2.o這時候在obj目錄下就會生成可執(zhí)行文件main
鏈接并不會忽略未使用的目標(biāo)文件
上面的編譯產(chǎn)生的main文件大小為16824字節(jié), 不管在main中是否調(diào)用了func1或者func2.
如果在link中去掉func2.o (因為main中未調(diào)用func2, 所以不會產(chǎn)生錯誤), 這樣產(chǎn)生的main文件為16760字節(jié) gcc -o obj/main obj/main.o obj/func1.o如果需要減小尺寸, 可以使用 -fdata-sections -ffunction-sections -Wl --gc-sections -Os等參數(shù)優(yōu)化. 例如 gcc -Os -fdata-sections -ffunction-sections test.cpp -o test -Wl,--gc-sections
頭文件, 靜態(tài)庫(.lib, .a) 和動態(tài)庫(.dll, .so)靜態(tài)庫 vs 動態(tài)庫庫文件就是已經(jīng)預(yù)編譯好的目標(biāo)文件, 只需要link到你的程序里就可以用了, 例如常見的方法 printf() and sqrt(). 庫文件有兩種類型: 靜態(tài)庫和動態(tài)庫(也叫共享庫). 靜態(tài)庫 在Linux下使用擴(kuò)展名.a, 在Windows下使用擴(kuò)展名.lib, 當(dāng)link靜態(tài)庫時, 這些對象文件的機器碼會被復(fù)制到你的可執(zhí)行文件中.
動態(tài)庫 在Linux下使用擴(kuò)展每.so, 在Windows下使用擴(kuò)展名.dll, 當(dāng)你的程序link靜態(tài)庫時, 只會在你的程序可執(zhí)行文件中添加一個表, 在運行你的程序之前, 操作系統(tǒng)會將這些外部方法的機器碼載入進(jìn)來. 這種方式可以節(jié)約磁盤資源, 讓程序更小, 另外大多數(shù)操作系統(tǒng)也運行內(nèi)存中的一份動態(tài)庫在多個運行的程序中共享. 動態(tài)庫升級時無需重新編譯執(zhí)行程序. GCC默認(rèn)情況下以動態(tài)庫方式link. 要查看庫內(nèi)容, 可以用命令nm filename
編譯中定位包含頭文件和庫文件 (-I, -L and -l)當(dāng)編譯項目時, 編譯器需要頭文件的信息, linker需要庫文件解決外部依賴.
對于項目中include的頭文件, 編譯器會去搜索相應(yīng)的路徑, 這些路徑通過 -Idir 參數(shù) ( 或者環(huán)境變量 CPATH) 指定, 因為頭文件的文件名是已知的, 所以編譯器只需要知道路徑.
對于linker, 會去搜索庫路徑, 這個通過 -Ldir 參數(shù) (大寫 'L' 后面是路徑) (或者環(huán)境變量 LIBRARY_PATH). 另外你需要指定庫名稱. 在Unix系統(tǒng)中, 庫文件 libxxx.a 通過參數(shù) -lxxx 指定 (小寫字符 'l' 不帶lib前綴, 不帶.a擴(kuò)展名). 在Windows下, 需要提供文件全名, 例如 -lxxx.lib. 路徑和文件名都需要指定.
默認(rèn)的 Include-paths, Library-paths 和 Libraries可以通過cpp -v命令列出: > cpp -v......#include "..." search starts here:#include <...> search starts here: /usr/lib/gcc/x86_64-pc-cygwin/6.4.0/include /usr/include /usr/lib/gcc/x86_64-pc-cygwin/6.4.0/../../../../lib/../include/w32api在編譯時, 加入-v參數(shù)開啟verbose mode, 可以了解系統(tǒng)中使用到的庫路徑(-L)以及庫明細(xì)(-l) > gcc -v -o hello.exe hello.c......-L/usr/lib/gcc/x86_64-pc-cygwin/6.4.0-L/usr/x86_64-pc-cygwin/lib-L/usr/lib-L/lib-lgcc_s // libgcc_s.a-lgcc // libgcc.a-lcygwin // libcygwin.a-ladvapi32 // libadvapi32.a-lshell32 // libshell32.a-luser32 // libuser32.a-lkernel32 // libkernel32.aEclipse CDT 在 Eclipse CDT 中, 可以在項目上右鍵, 點擊project ⇒ Properties ⇒ C/C++ General ⇒ Paths and Symbols, 在標(biāo)簽頁"Includes", "Library Paths" and "Libraries"下, 設(shè)置 include path, library paths 和 libraries.
GCC環(huán)境變量GCC 使用下列環(huán)境變量: - PATH: 用于搜索可執(zhí)行文件和運行時的動態(tài)鏈接庫(.dll, .so).
- CPATH: 用于搜索頭文件包含路徑. 優(yōu)先級低于直接用-I<dir>指定的路徑. C_INCLUDE_PATH and CPLUS_INCLUDE_PATH可分別用于指定C和C++的頭文件路徑.
- LIBRARY_PATH: 用于搜索庫文件的路徑, 優(yōu)先級低于用-L<dir>指定的路徑.
參考
|