目錄
一. GCC命令語句大全
二. GCC編譯4個階段
三. makefile的使用
四. CMake
五.?GNU工具鏈開發流程圖?
六. Keil中的地址段
七. 靜態庫和動態庫
一. GCC命令語句大全
-c | 只編譯源文件,生成目標文件(.o ?文件),不進行鏈接。 |
-o <file> | 指定輸出文件的名稱。 |
-g | 生成調試信息,用于調試程序 |
-Wall | 打開所有警告提示。 |
-I <dir> | 添加頭文件搜索路徑。 |
-L <dir> | 添加庫文件搜索路徑。 |
-l <library> | 鏈接指定的庫文件。 |
-std=<standard> | 指定使用的語言標準,如?-std=c99 。 |
-0<level> | 優化級別,如?-O0 (無優化)、-O1 (基本優化)、-O2 (更高級別優化)。 |
-D<macro> | 定義預處理宏。 |
-E | 只進行預處理,輸出預處理結果。 |
-S | 只進行編譯,生成匯編代碼。 |
-shared | 生成共享庫(動態鏈接庫)。 |
-static | 生成靜態可執行文件,使用靜態鏈接。 |
-pthread | 鏈接 POSIX 線程庫。 |
-lm | 鏈接數學庫。 |
-fopenmp | 啟用 OpenMP 并行編程支持。 |
二. GCC編譯4個階段
GCC不僅僅是一個編譯器,它還包括預處理器、匯編器以及鏈接器,可以處理從代碼編寫到可執行程序生成的整個流程。
三. makefile的使用
先新建一個main.c
#include "stdio.h"int add(int a, int b);
int main()
{printf("num:%d\r\n",add(1,2));return 0;}
再新建一個math.c?
int add(int a, int b)
{return (a + b);
}
編譯永遠都是以單個源文件為單位的。這里我們先編譯一下mian.c文件。編譯生成的.o文件是一個二進制文件,文件格式是ELF(Linux下所有可執行文件的通用格式),Windows使用的是PE格式,(都是對二進制代碼的封裝)
我們可以在文件頭部找到可執行文件的基本信息比如支持的操作系統,機器類型等。
查看一系列的區塊,.text是代碼區,.data是數據區(里面保存了我們初始化的全局變量,局部靜態變量等等),
查看main.o這個目標文件中的內容,這里call指令是之前調用的printf和add函數,但是它們的跳轉地址都被設為了0,而這里的0會在后面鏈接的時候被修正。
另外為了讓編譯器能夠定位到這些需要被修正的地址,在代碼塊中我們還可以找到一個重定位表,比如在.text區塊中,找到被重定位的兩個函數printf和add。
接下來編譯math.c且連同main.o一起鏈接生成一個獨立的可執行文件
gcc main.o math.o -o main
鏈接其實就是將編譯之后的所有目標文件,連同用到的一些靜態庫,運行時庫,組合拼裝成一個獨立的可執行文件,其中就包括之前說到的地址修正,在這個時候,連接器會根據我們的目標文件或者靜態庫中的重定位表,找到那些需要被重定位的函數,全局變量,從而修正它們的地址。
但如果我們每次都要手動編譯和鏈接就不高效,所以我們就得使用makefile,而makefile的核心是對"依賴"的管理,所以makefile其實就是在定義一顆依賴樹。
新建一個makefile文件,具體makefile可以查看二. MakeFile-CSDN博客
堆makefile文件進行一定優化
1. 使用makefile變量
2. “%”表示長度任意的非空字符串,比如“%.c”就是所有的以.c 結尾的 文件,類似與通配符,a.%.c 就表示以 a.開頭,以.c 結束的所有文件。如 %.o : %.c
3. makefile自動化變量
4.?Makefile 偽目標
自動化變量 | 描述 |
$@ | 規則中的目標集合,在模式規則中,如果有多個目標的話,“$@”表示匹配模 式中定義的目標集合。 |
$% | 當目標是函數庫的時候表示規則中的目標成員名,如果目標不是函數庫文件, 那么其值為空。 |
$< | 依賴文件集合中的第一個文件,如果依賴文件是以模式(即“%”)定義的,那么 “$<”就是符合模式的一系列的文件集合。 |
$?? | 所有比目標新的依賴目標集合,以空格分開。 |
$^ | 所有依賴文件的集合,使用空格分開,如果在依賴文件中有多個重復的文件, “$^”會去除重復的依賴文件,值保留一份。 |
$+ | 和“$^”類似,但是當依賴文件存在重復的話不會去除重復的依賴文件。 |
$* | 這個變量表示目標模式中"%"及其之前的部分,如果目標是 test/a.test.c,目標模 式為 a.%.c,那么“$*”就是 test/a.test。 |
all:main#我們最終需要的mian文件
objects = main.o math.o
main:$(objects) #main文件是由main.o math.o而來gcc -o main $(objects)%.o : %.cgcc -c $<#main.o:main.c #main.o由main.c而來
# gcc -c main.c
#math.o:math.c #math.math.c而來
# gcc -c math.c.PHONY : cleanclean:rm *.orm main
接下來我們直接make就能生成最后的可執行文件.
四. CMake
CMake主要功能
1.配置和生成各大平臺的工程
2.生成makefile文件
因為makefile的配置與當前系統有關,如果要開發一個跨平臺的軟件所以我們使用cmake。
CMakeList.txt文件
cmake_minimum required(VERSION 3.10)
project(hello)
add_executable(hello main.cpp factorial.cpp printhello.cpp)
cmake . 之后,也能生成makefile文件。這時候我們再make,就能生成可執行文件了。
五.?GNU工具鏈開發流程圖?
GNU工具鏈包括了一系列重要的組件,如GCC(GNU編譯器集合)、GNU Binutils、GNU Make和GDB等。
編譯:編譯器是armcc和armasm ,每個c/c++和匯編源文件編譯成對應的以“.o”為后綴名的對象文件。
鏈接:鏈接器armlink把各個.o文件及庫文件鏈接成一個映像文件“.axf”或“.elf”;
格式轉換:一般來說Windows或Linux系統使用鏈接器直接生成可執行映像文件elf后,內核根據該文件的信息加載后,就可以運行程序了,但在單片機平臺上,需要把該文件的內容加載到芯片上,所以還需要對鏈接器生成的elf映像文件利用格式轉換器fromelf轉換成“.bin”或“.hex”文件,交給下載器下載到芯片的FLASH或ROM中。
六. Keil中的地址段
程序組件 | 所屬類別 |
機器代碼指令 | Code |
常量 | RO-data(Read Only data) |
初值非0的全局變量 | RW-data(Read Write data) |
初值為0的全局變量 | ZI-data(Zero Initialie data) |
局部變量 | ZI-data的棧空間 |
使用malloc動態分配的空間 | ZI-data的堆空間 |
初值非0的全局變量和初值為0的全局變量其實就是數據區,而值為0的全局變量區又稱為bss段。
七. 靜態庫和動態庫
簡單的來說二者的區別:
-
靜態庫:就是在編譯的時候直接將需要的代碼連接進可執行程序中去;
-
動態庫:就是在需要調用其中的函數時,根據函數映射表找到該函數然后調入堆棧執行。當動態庫被加載到內存之后,一旦它的內存地址被確定,我們就可以去修正動態庫中的那些函數跳轉地址,也就是重定位,這些地址在程序加載之前不過只是一堆占位符而已,如果我們直接去修改代碼段中的跳轉地址,由于代碼段的內容被修改,自然就不能被其他進程所共享了,因為我們需要在內存中保存多個不同的副本,這剛好和節約內存的目標就背道而馳了,為了解決這個問題,動態鏈接采用了一種聰明的做法,不再修改代碼段,而是在數據段中專門預留一片區域用來存放函數的跳轉地址,它也被叫做全局偏移表
查看全局偏移表readelf -S ./libmath.so
got里面專門用來存放全局變量和函數的跳轉地址,于是我們在調用函數的時候,會首先查表,然后根據表中的地址來進行跳轉,這些地址會在動態庫加載的時候,被修改成為真正的地址,而查表的過程也很容易實現,由于全局偏移表與代碼段的相對位置是固定的,我們完全可以利用CPU的相對尋址來實現,有了全局偏移表,我們不再需要修改代碼段,因此代碼可以被所有進程共享,而全局偏移表雖然在每一個進程中保留一份副本,但由于占用空間很小,所以完全沒有問題,采用這種方式實現的動態鏈接也被叫做 PIC(地址無關代碼),換句話說我們的動態庫不需要做任何修改,被加載到任意內存地址都能夠正常運行,并且能夠被所有進程共享,這也是為什么之前我們給編譯器指定 -fPIC 參數的原因,另一方面由于動態鏈接在程序加載的時候需要對大量函數進行重定位,這一步是非常耗時的,為了進一步降低開銷,我們的操作系統還做了一些其他的優化,比如延遲綁定或者也叫PLT,與其在一開始就對所有函數進行重定位,不如將這個過程推遲到函數第一次被調用的時候,因為絕大多數動態庫中的函數可能在程序運行期間一次都不會被使用到。大概思路是,GOT中的跳轉地址默認會指向一段輔助代碼,它也被叫做樁代碼(Stub),在我們第一次調用函數的時候,這段代碼會負責查詢真正函數的跳轉地址,并且去更新GOT表,于是我們再次調用函數的時候,就會直接跳轉到動態庫中真正的函數實現。動態鏈接實際上將鏈接的整個過程,比如符號查詢、地址的重定位從編譯時推遲到了程序的運行時,更關鍵的是,它實現了二進制級別的代碼復用。
從上面的描述可以知道,靜態庫是我們MCU開發者常用的一種,而動態庫常用于Linux、Windows等開發場合
接下來我們創建一個動態鏈接庫。還是先創建一個main.c和math.c.
gcc -shared -fPIC math.c -o libmath.so//-shared 表明這是一個共享對象(共享對象),libmath.so是linux下動態庫的擴展名,windows下的動態庫就是我們熟悉的各種.dll文件,
gcc main.c -lmath -L. -o main我們在編譯主程序的時候,我們需要指定一個-l參數,它告訴編譯器與之前建的libmath.so進行動態鏈接,這里在指定動態庫的時候,我們省略了前綴lib和擴展名.so,最后我們通過-L來指定動態庫所在的路徑,
在運行main時,會提示找不到libmath這個動態庫,這是因為linux默認只會去系統路徑下搜索動態庫
./main: error while loading shared libraries: libmath.so: cannot open shared object file: No such file or directory
解決辦法有
第一種,將動態庫拷貝到系統路徑下,但之后還得刪除這個文件所以不方便
另外一種是使用環境變量,將當前目錄添加到 LD_LIBRARY_PATH 環境變量中,這樣操作系統會先到我們指定的路徑下搜索,找不到的話再去搜索系統路徑。
下圖可以看到它成功調用了動態庫中的add函數并返回了正確的結果,那之前我們提到動態鏈接的一大優勢是允許我們單獨更新動態庫本身。?
這樣我們更新一下動態庫,對主程序不需要做任何修改,直接運行我們就可以看到剛才我們對動態庫的更新?