天天看點

C程式編譯過程及常見選項--靜态庫和動态庫前言一、gcc詳講二、靜态庫和動态庫三、總結

C程式編譯過程及常見選項--靜态庫和動态庫

  • 前言
  • 一、gcc詳講
    • 1.1 編譯過程
    • 1.2 預處理
    • 1.3 編譯(Compilation)
    • 1.4 彙編(Assembly)
    • 1.5 連結(Linking
    • 1.6 ELF可執行檔案格式
    • 1.7 gcc常見編譯選項
  • 二、靜态庫和動态庫
    • 2.1靜态庫
    • 2.2 動态庫
    • 2.3 兩者最大不同點
  • 三、總結

注:本文系湛江市嶺南師範學院物聯網俱樂部原創部分訓練計劃,轉載請保留聲明。

前言

 今天是閉關的第31天,越來越接近秋招了,繼續加油!今天主要給大家帶來gcc編譯器的編譯過程詳講、靜态庫和動态庫的學習。對于大多數程式員而言,大家都知道gcc是什麼,但是如果不接觸到linux平台下的開發,鮮有人真正了解gcc的編譯流程,因為windows+IDE的開發模式簡直是一條龍全套服務,開發者隻需要關系代碼邏輯與功能實作即可,但是,在享受便利的同時,必然也犧牲了一些靈活性。Linux程式員可以根據自己的需要讓gcc在編譯的任何階段結束,以便檢查或使用編譯器在該階段的輸出資訊,或者對最後生成的二進制檔案進行控制,以便通過加入不同數量和種類的調試代碼來為今後的調試做好準備。和其他常用的編譯器一樣,gcc也提供了靈活而強大的代碼優化功能,利用它可以生成執行效率較高的代碼。對于嵌入式ARM Linux開發和各種單片機MCU開發,這些知識也非常重要!

一、gcc詳講

1.1 編譯過程

 Hello world是初學者使用任何一項程式設計語言最基本最簡單的程式。下面是一個C語言版的"Helloworld" ,我們以該程式為例了解一下C程式的編譯過程:

#include <stdio.h> 
#define TIMES 10
int main(int argc, char *argv[])
{ 
 	int i;
 	for(i=0; i<TIMES; i++)
 	{
 		printf("Hello wolrd\n"); 
 	}
 	return 0; 
}
           

 通常我們使用gcc來生成可執行程式,指令為:gcc hello.c,預設生成可執行檔案a.out。其實編譯(包括連結)的指令:gcc hello.c 可分解為如下4個大的步驟:

  預處理(Preprocessing)

  編譯(Compilation)

  彙編(Assembly)

C程式編譯過程及常見選項--靜态庫和動态庫前言一、gcc詳講二、靜态庫和動态庫三、總結

1.2 預處理

預處理是讀取c源程式,對其中的僞指令(以#開頭的指令,也就是宏)和特殊符号
進行“替代”處理;經過此處理,生成一個沒有宏定義、沒有條件編譯指令、沒有特殊
符号的輸出檔案。這個檔案的含義同沒有經過預處理的源檔案是相同的,仍然是C文
件,但内容有所不同。 僞指令主要包括以下三個方面:
 (1)宏定義指令,如#define Name TokenString,#undef以及編譯器内建的一些宏,  
 如__DATE__,__FILE__, __LINE__, __TIME__, __FUNCTION__ 等。 
 (2)條件編譯指令,如#ifdef,#ifndef,#else,#elif,#endif等。 
 (3) 頭檔案包含指令,如#include "FileName"或者#include <FileName>等。
 
  預處理的過程主要處理包括以下過程:
	将所有的#define删除,并且展開所有的宏定義
	處理所有的條件預編譯指令,比如#if #ifdef #elif #else #endif等
	處理#include 預編譯指令,将被包含的檔案插入到該預編譯指令的位置。
	删除所有注釋 “//”和”/* */”.
	添加行号和檔案辨別,以便編譯時産生調試用的行号及編譯錯誤警告行号。
	保留所有的#pragma編譯器指令,因為編譯器需要使用它們

  通常使用以下指令來進行預處理,參數-E表示隻進行預處理:
	 gcc -E hello.c -o hello.i 

  也可以使用以下指令完成預處理過程,其中cpp是預處理器:
	 cpp hello.c > hello.i 
  
  預處理後的結果hello.i還是C語言源代碼,我們可以使用cat或vim指令檢視他的
  代碼。
	 vim hello.i
           

1.3 編譯(Compilation)

編譯程式所要作得工作就是通過詞法分析和文法分析,在确認所有的指令都符合
文法規則之後,将其翻譯成等價的中間代碼表示或彙編代碼。 優化處理是編譯系
統中一項比較艱深的技術。它涉及到的問題不僅同編譯技術本身有關,而且同機器
的硬體環境也有很大的關系。優化一部分是對中間代碼的優化,這種優化不依賴于
具體的計算機。另一種優化則主要針對目标代碼的生成而進行的。對于前一種優化,
主要的工作是删除公共表達式、循環優化(代碼外提、強度削弱、變換循環控制條
件、已知量的合并等)、複寫傳播,以及無用指派的删除,等等。後一種類型的優
化同機器的硬體結構密切相關,最主要的是考慮是如何充分利用機器的各個硬體寄
存器存放的有關變量的值,以減少對于記憶體的通路次數。另外,如何根據機器硬體
執行指令的特點(如流水線、RISC、CISC等)而對指令進行一些調整使目标代碼比
較短,執行的效率比較高,也是一個重要的研究課題。
 使用下面指令進行編譯生成彙編檔案。
	gcc -S hello.i > hello.s
	cat hello.s
或:
	arm-linux-gcc -S hello.i > hello.s
	cat hello.s
 在這裡,我們使用PC的編譯器gcc就會編譯生成x86的彙編,而使用ARM的編譯器則
生成ARM的彙編檔案。同一份C代碼不作任何修改,使用不同的編譯器編譯就生成在
不同機器上運作的程式,這就是C程式的可移植性。我們在PC上編寫程式,在PC上
用ARM的交叉編譯器,生成在ARM平台上的可執行程式的過程叫做交叉編譯。
           

1.4 彙編(Assembly)

彙編過程實際上指把彙編語言代碼翻譯成目标機器指令的過程。對于被翻譯系統
處理的每一個C語言源程式,都将最終經過這一處理而得到相應的目标檔案。目标
檔案中所存放的也就是與源程式等效的目标的機器語言代碼。 目标檔案由段組成。
 
 通常一個目标檔案中至少有兩個段: 
    ①代碼段(文本段):該段中所包含的主要是程式的指令。該段一般是可讀和可執
行的,但一般卻不可寫;
	②資料段:主要存放程式中要用到的各種常量、全局變量、靜态的資料。一般數
據段都是可讀,可寫,可執行的;
	 gcc -c hello.s -o hello.o
           

1.5 連結(Linking

彙程式設計式生成的目标檔案并不能立即就被執行,其中可能還有許多沒有解決的問
題。例如,某個源檔案中的函數可能引用了另一個源檔案中定義的某個符号(如
變量或者函數調用等);在程式中可能調用了某個庫檔案中的函數,等等。所有
的這些問題,都需要經連結程式的處理方能得以解決。 連結程式的主要工作就
是将有關的目标檔案彼此相連接配接,也即将在一個檔案中引用的符号同該符号在另
外一個檔案中的定義連接配接起來,使得所有的這些目标檔案成為一個能夠被操作系
統裝入執行的統一整體,也就是可執行程式。 根據開發人員指定的庫函數的連結
方式的不同,連結處理可分為兩種: 
	1. 靜态連結
	2. 動态連結
 對于可執行檔案中的函數調用,可分别采用動态連結或靜态連結的方法。使用動
态連結能夠使最終的可執行檔案比較短小,并且當共享對象被多個程序使用時能節
約一些記憶體,因為在記憶體中隻需要儲存一份此共享對象的代碼。但并不是使用動态
連結就一定比使用靜态連結要優越。在某些情況下動态連結可能帶來一些性能上損
害。
	gcc hello.o -o hello -static
	du -h hello 
	
顯示(每個人都是不一樣的):856K hello
	
	 file hello
	 
顯示:hello: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), 
statically linked, for GNU/Linux 2.6.24,
BuildID[sha1]=4d58b0d381be9a829a429738361716721e0a130f, not stripped


	
	gcc hello.o -o hello
	du -h hello
	
顯示(每個人都是不一樣的):12K hello

	file hello
	
顯示:hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
dynamically linked (uses shared libs), for GNU/Linux
2.6.24, BuildID[sha1]=3162b0cce098b0ebb49c989d8c5b40530dec09d4,
not stripped
           

1.6 ELF可執行檔案格式

 Linux/Unix的可執行檔案以及動态庫都是以ELF(Executable Linkage Format)存在的。在Linux下,可以使用readelf指令檢視ELF檔案,關于加載過程所需要的資訊都在ELF檔案頭裡面,可以用使用readefl filename -e來檢視EFL檔案所有的頭。我們可以先來檢視下hello.c編譯出來的hello可執行檔案的ELF頭資訊:

指令:readelf hello -e

ELF Header:
 Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
 Class: ELF64
 Data: 2's complement, little endian
 Version: 1 (current)
 OS/ABI: UNIX - System V
 ABI Version: 0
 Type: EXEC (Executable file)
 Machine: Advanced Micro Devices X86-64
 Version: 0x1
 Entry point address: 0x400450
 Start of program headers: 64 (bytes into file)
 Start of section headers: 4504 (bytes into file)
 Flags: 0x0
 Size of this header: 64 (bytes)
 Size of program headers: 56 (bytes)
 Number of program headers: 9
 Size of section headers: 64 (bytes)
 Number of section headers: 30
 Section header string table index: 27
           

指令:readelf -d hello

Dynamic section at offset 0xe68 contains 20 entries:
 Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x4003e0
0x000000000000000d (FINI) 0x400608
0x000000006ffffef5 (GNU_HASH) 0x400298
0x0000000000000005 (STRTAB) 0x400318
0x0000000000000006 (SYMTAB) 0x4002b8
0x000000000000000a (STRSZ) 61 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x601000
0x0000000000000002 (PLTRELSZ) 72 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x400398
0x0000000000000007 (RELA) 0x400380
0x0000000000000008 (RELASZ) 24 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x400360
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x400356
0x0000000000000000 (NULL) 0x0
           

指令: ldd hello

linux-vdso.so.1 => (0x00007fff191be000)
 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fbd3b089000)
 /lib64/ld-linux-x86-64.so.2 (0x000056260f532000)
           

1.7 gcc常見編譯選項

 gcc的編譯選項非常非常多,我們可以使用man手冊檢視,下面是我們經常使用的一些編譯選項:

C程式編譯過程及常見選項--靜态庫和動态庫前言一、gcc詳講二、靜态庫和動态庫三、總結

二、靜态庫和動态庫

 在windows和Linux下都存在着大量的庫,庫是什麼呢?本質上來說,庫時一種可執行代碼的二進制形式,可以被作業系統載入記憶體執行。我們通常将一些通用函數寫成函數庫,是以庫是别人寫好的,現有的,成熟的,可以複用的代碼,你可以使用但要必須得遵守許可協定。在我們現實開發過程中,不可能每一份代碼都從頭編寫,當我們擁有庫時,我們就可以直接将我們所需要的檔案連結到我們的程式中。可以為我們節省大量的時間,提高開發效率。Linux下庫分為兩種,靜态庫和動态庫。

2.1靜态庫

靜态庫檔案名的命名方式是“libxxx.a”,庫名前加”lib”,windows和linux下都是
字尾用”.a”,“xxx”為靜态庫名,windows下的靜态庫名也叫libxxx.a;
  連結時間: 靜态庫的代碼是在編譯過程中被載入程式中。
  連結方式:靜态庫的連結是将整個函數庫的所有資料都整合進了目标代碼。這樣
做優點是在編譯後的執行程式不在需要外部的函數庫支援,因為所使用的函數都已
經被編進去了。缺點是,如果所使用的靜态庫發生更新改變,你的程式必須重新編
譯。
           

2.2 動态庫

動态庫的命名方式與靜态庫類似,字首相同為“lib”,linux下字尾名為
 “.so(shared object)”即libxxx.so;而windows下字尾名為
 “.dll(dynamic link library)”即libxxx.dll;
  連結時間:動态庫在編譯的時候并沒有被編譯進目标代碼,而是當你的
程式執行到相關函數時才調用該函數庫裡的相應函數。這樣做缺點是因為
函數庫并沒有整合程序式,是以程式的運作環境必須提供相應的庫。優點
是動态庫的改變并不影響你的程式,是以動态函數庫更新比較友善。
           

2.3 兩者最大不同點

它們兩個還有很明顯的不同點:當同一個程式分别使用靜态庫,動态庫
兩種方式生成兩個可執行檔案時,靜态連結所生成的檔案所占用的記憶體要
遠遠大于動态連結所生成的檔案。這是因為靜态連結是在編譯時将所有的
函數都編譯進了程式,而動态連結是在程式運作時由作業系統幫忙把動态
庫調入到記憶體空間中使用。另外如果動态庫和靜态庫同時存在時,連結器
優先使用動态庫。
           
C程式編譯過程及常見選項--靜态庫和動态庫前言一、gcc詳講二、靜态庫和動态庫三、總結

重要參考連結:https://www.toutiao.com/i6889283351983686158/?tt_from=mobile_qq&utm_campaign=client_share&timestamp=1627114460

三、總結

 通俗點來講編輯器是指我用它來寫程式的(編輯代碼),而我們寫的代碼語句,電腦是不懂的,我們需要把它轉成電腦能懂的語句,編譯器就是這樣的轉化工具。就是說,我們用編輯器編寫程式,由編譯器編譯後才可以運作!編譯器是将易于編寫、閱讀和維護的進階計算機語言翻譯為計算機能解讀、運作的低級機器語言的程式。

 GCC(GNU Compiler Collection,GNU 編譯器套件),是由 GNU 開發的程式設計語言編譯器。GCC 原本作為GNU作業系統的官方編譯器,現已被大多數類Unix作業系統(如 Linux、BSD、Mac OS X 等)采納為标準的編譯器,GCC 同樣适用于微軟的 Windows。GCC 最初用于編譯 C 語言,随着項目的發展 GCC 已經成為了能夠編譯 C、C++、Java、Ada、fortran、Object C、Object C++、Go 語言的編譯器大家族。

繼續閱讀