本帖最后由 michaelchain 于 2021-8-26 11:45 編輯
C項目的文件組織和編譯C項目的代碼, 由頭文件(.h后綴)和C文件(.c后綴)組成 - C語言的函數和變量, 分聲明和定義兩個階段
- 頭文件和C文件是等價的, 相當于C文件的一部分, 其功能由人為劃分, 用于變量和函數的聲明, 頭文件也可以用于變量和函數的定義, 但是這屬于非標準用法, 一般不這么用
- 同一個編譯中, 函數在一處定義, 處處可用(除非使用static關鍵字)
- 在A.c中定義后, 在B.c中用extern聲明這個函數, 就可以調用
- 將A.c中的函數聲明提取到A.h, 在B.c中include A.h, 或者通過B.c include B.h, B.h include A.h, 都可以實現函數引用
- C的編譯, 是按文件編譯的, 每個C文件會編譯為一個目標文件
- 頭文件不單獨編譯, 與include這個頭文件的C文件, 在預編譯階段展開, 之后在C文件中編譯
- 編譯需要知道C文件的列表和頭文件的目錄列表
- 編譯會依次編譯C文件列表中的每個文件, 不管最終是否用到
C項目結構示例定義一個頭文件 inc.h,聲明兩個函數func1和func2, 將定義寫在func1.c和func2.c. 在main.c中通過main.h引用inc.h, 調用這些函數, 程序目錄結構如下 - ├── inc
- │ ├── func1.c
- │ ├── func2.c
- │ └── inc.h
- ├── main.c
- ├── main.h
- └── obj
復制代碼
main.c - #include <stdio.h>
- #include "main.h"
- int main()
- {
- uint8_t a = 0x08;
- uint8_t b = func1(a);
- printf("%X", b);
- return 0;
- }
復制代碼
main.h- #ifndef MAIN_H
- #define MAIN_H
- #include "inc.h"
- #endif
復制代碼
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
復制代碼
func1.c
- #include "inc.h"
- uint8_t func1(uint8_t a)
- {
- a = a << 1;
- return a;
- }
復制代碼
func2.c
- #include "inc.h"
- uint8_t func2(uint8_t a)
- {
- a = a >> 1;
- return a;
- }
復制代碼
gcc的編譯過程
gcc命令其實依次執行了四步操作 - 預處理(Preprocessing),
- 編譯(Compilation),
- 匯編(Assemble),
- 鏈接(Linking)
1.預處理(Preprocessing)預處理用于將所有的#include頭文件以及宏定義替換成其真正的內容,預處理之后得到的仍然是文本文件,但文件體積會大很多。gcc的預處理是預處理器cpp來完成的,你可以通過如下命令對 main.c進行預處理: gcc -E -I./inc main.c -o obj/main.i# or$ cpp main.c -I./inc -o obj/main.i-E是讓編譯器在預處理之后就退出,不進行后續編譯過程; -I指定頭文件目錄, -o指定輸出文件名. 經過預處理之后代碼體積會大很多, main.c只有10行, 但是main.i有749行, 預處理之后的文件可以用文本編輯器查看
2.編譯(Compilation)這一步的編譯將經過預處理之后的程序轉換成特定匯編代碼的過程, 編譯的命令如下: $ gcc -S -I./inc main.c -o obj/main.s-S讓編譯器在編譯之后停止. 這一步會生成程序的匯編代碼, 內容如下: .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)
匯編過程將上一步的匯編代碼轉換成機器碼(machine code),這一步產生了二進制的目標文件, gcc匯編過程通過as命令完成 as obj/main.s -o obj/main.o# porgcc -c obj/main.s -o obj/main.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目錄下已經有main.o, func1.o和func2.o這三個目標文件, 現在需要通過linker將這些目標文以及所需的庫文件(.so等)鏈接成最終的可執行文件(executable file) 命令如下 gcc -o obj/main obj/main.o obj/func1.o obj/func2.o這時候在obj目錄下就會生成可執行文件main
鏈接并不會忽略未使用的目標文件
上面的編譯產生的main文件大小為16824字節, 不管在main中是否調用了func1或者func2.
如果在link中去掉func2.o (因為main中未調用func2, 所以不會產生錯誤), 這樣產生的main文件為16760字節 gcc -o obj/main obj/main.o obj/func1.o如果需要減小尺寸, 可以使用 -fdata-sections -ffunction-sections -Wl --gc-sections -Os等參數優化. 例如 gcc -Os -fdata-sections -ffunction-sections test.cpp -o test -Wl,--gc-sections
頭文件, 靜態庫(.lib, .a) 和動態庫(.dll, .so)靜態庫 vs 動態庫庫文件就是已經預編譯好的目標文件, 只需要link到你的程序里就可以用了, 例如常見的方法 printf() and sqrt(). 庫文件有兩種類型: 靜態庫和動態庫(也叫共享庫). 靜態庫 在Linux下使用擴展名.a, 在Windows下使用擴展名.lib, 當link靜態庫時, 這些對象文件的機器碼會被復制到你的可執行文件中.
動態庫 在Linux下使用擴展每.so, 在Windows下使用擴展名.dll, 當你的程序link靜態庫時, 只會在你的程序可執行文件中添加一個表, 在運行你的程序之前, 操作系統會將這些外部方法的機器碼載入進來. 這種方式可以節約磁盤資源, 讓程序更小, 另外大多數操作系統也運行內存中的一份動態庫在多個運行的程序中共享. 動態庫升級時無需重新編譯執行程序. GCC默認情況下以動態庫方式link. 要查看庫內容, 可以用命令nm filename
編譯中定位包含頭文件和庫文件 (-I, -L and -l)當編譯項目時, 編譯器需要頭文件的信息, linker需要庫文件解決外部依賴.
對于項目中include的頭文件, 編譯器會去搜索相應的路徑, 這些路徑通過 -Idir 參數 ( 或者環境變量 CPATH) 指定, 因為頭文件的文件名是已知的, 所以編譯器只需要知道路徑.
對于linker, 會去搜索庫路徑, 這個通過 -Ldir 參數 (大寫 'L' 后面是路徑) (或者環境變量 LIBRARY_PATH). 另外你需要指定庫名稱. 在Unix系統中, 庫文件 libxxx.a 通過參數 -lxxx 指定 (小寫字符 'l' 不帶lib前綴, 不帶.a擴展名). 在Windows下, 需要提供文件全名, 例如 -lxxx.lib. 路徑和文件名都需要指定.
默認的 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參數開啟verbose mode, 可以了解系統中使用到的庫路徑(-L)以及庫明細(-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, 在標簽頁"Includes", "Library Paths" and "Libraries"下, 設置 include path, library paths 和 libraries.
GCC環境變量GCC 使用下列環境變量: - PATH: 用于搜索可執行文件和運行時的動態鏈接庫(.dll, .so).
- CPATH: 用于搜索頭文件包含路徑. 優先級低于直接用-I<dir>指定的路徑. C_INCLUDE_PATH and CPLUS_INCLUDE_PATH可分別用于指定C和C++的頭文件路徑.
- LIBRARY_PATH: 用于搜索庫文件的路徑, 優先級低于用-L<dir>指定的路徑.
參考
|