對于任何一個學習過C語言的來說,“HelloWorld”程序都不會陌生。因為它應該是你打開新世界的看到的第一束光。至今我還記得第一次敲出這個程序的時候激動了好久。但是你們知道短短的幾行代碼,是怎么讓程序運行起來的么?
// hello.c#include <stdio.h>int main(int argc, char *argv[]) { printf('Hello World!\n'); return 0;}
程序是如何運行起來的?很多人可能會說,不就是五個步驟:預處理(Prepressing),編譯(Compilation),匯編(Assembly)和鏈接(Linking),裝載(Loading)么?是這樣的。但是你知道每一步背后都做過一些什么嗎?如果你能回答上以下的問題,我想這個文章就沒有必要看下去了。
在main()函數調用之前,程序做過一些什么?
編譯出來的可執(zhí)行文件里面有什么,在內存中是什么樣子的,是怎么來組織的?
靜態(tài)鏈接、動態(tài)鏈接,有什么區(qū)別?
不同的編譯器(Micrsoft VC/VS, GCC)和不同的硬件平臺(X86,SPARC,MIPS,ARM),以及不同的操作系統(tǒng)(Windows,Linux,Unix,Solaris),最終編譯出來的結果一樣么?
ELF文件,PE文件,COFF文件,是什么?
如果你發(fā)現對其中的一些問題,不是很了解的話,甚至沒有想過這些問題的時候,而你有向了解一下,那么就可以,跟著我的步伐一步倆步,往下看啦。這個文章是為你準備的。需要聲明的是,本文主要針對gcc編譯器,也就是針對C和C ,不一定適用于其他語言的編譯。下圖為總覽。
GCC編譯過程
預處理的過程,其實,主要是處理那些源代碼中以#
開始的預編譯指令。比如#include
,#define
等,處理的規(guī)則如下:
將所有的#define
刪除,并且展開所有的宏定義
處理所有的條件預編譯指令,比如#if
, #ifdef
, #elif
, #else
, #endif
等
處理#include
預編譯指令,將被包含的文件插入到該預編譯指令的位置。在這個插入的過程中,是遞歸進行的,也就是說被包含的文件,可能還包含其他文件。
刪除所有注釋 //
和/**/
.
添加行號和文件標識,以便編譯時產生調試用的行號及編譯錯誤警告行號。
保留所有的#pragma
編譯器指令,因為編譯器需要使用它們。
對于第一步預編譯的過程,可以通過以下方式完成:
gcc -E hello.c -o hello.i
或者
cpp hello.c > hello.i
編譯過程可分為6步:詞法分析、語法分析、語義分析、源代碼優(yōu)化、代碼生成、目標代碼優(yōu)化。對應與下圖的每一步。下面我們以一個具體的表達式進行分析:
array[index] = (index 4)*(2 6);
Compilation
詞法分析:掃描器(Scanner)將源代的字符序列分割成一系列的記號(Token)。
記號 | 類型 |
---|---|
array | 標記符 |
[ | 左方括號 |
index | 標記符 |
] | 右標記符 |
= | 賦值 |
( | 左圓括號 |
index | 標記符 |
加號 | |
4 | 數字 |
) | 右圓括號 |
* | 乘號 |
( | 左圓括號 |
2 | 數字 |
加號 | |
6 | 數字 |
) | 右圓括號 |
注:lex工具,可實現按照用戶描述的詞法規(guī)則將輸入的字符串分割為一個一個記號。
語法分析:語法分析器將記號(Token)產生語法樹(Syntax Tree)。
Syntax Tree
注:yacc工具(yacc: Yet Another Compiler Compiler)可實現語法分析,根據用戶給定的語法規(guī)則對輸入的記號序列進行解析,從而構建一個語法樹,所以它也被稱為“編譯器編譯器(Compiler Compiler)”。
語義分析:編譯器所分析的語義是靜態(tài)語義,所謂靜態(tài)語義就是指在編譯期可以確定的語義,通常包括聲明,和類型的匹配,類型的轉換。
Commented Syntax Tree
注:與之對于的為動態(tài)語義分析,只有在運行期才能確定的語義。
源代碼優(yōu)化:源代碼優(yōu)化器(Source Code Optimizer),在源碼級別進行優(yōu)化,例如(2 6)
這個表達式,其值在編譯期就可以確定。優(yōu)化后的語法樹。
Paste_Image.png
但是直接作用于語法樹比較困難,所以源代碼優(yōu)化器往往將整個語法數轉化為中間代碼(Intermediate Code)。
注:中間代碼是與目標機器和運行環(huán)境無關的。中間代碼使得編譯器被分為前端和后端。編譯器前端(1-4步)負責產生機器無關的中間代碼;編譯器后端(5-6步)將中間代碼轉化為目標機器代碼。
目標代碼生成:代碼生成器(Code Generator)。
目標代碼優(yōu)化:目標代碼優(yōu)化器(Target Code Optimizer)。
最后的倆個步驟十分依賴與目標機器,因為不同的機器有不同的字長,寄存器,整數數據類型和浮點數據類型等。
匯編器是將匯編代碼轉變成機器可以執(zhí)行的命令,每一個匯編語句幾乎都對應一條機器指令。匯編相對于編譯過程比較簡單,所以根據匯編指令和機器指令的對照表一一翻譯即可。匯編過程可以通過以下方式完成。
as hello.s -o hello.o
或者
gcc -c hello.s -o hello.o
把一個程序分割為多個模塊,然后通過某種方式組合形成一個單一的程序,這就是鏈接。而模塊間如何組合的問題,歸根到底,就是模塊如何進行通信的倆個問題:(1) 模塊間的函數調用,(2) 模塊間的變量訪問。但無論是那一個問題,其本質是獲取一個地址,函數運行的地址、或者變量存放的地址。
如果熟悉匯編的,應該會知道hello.o
文件,既目標文件,是以分段的形式組織在一起的。其簡單來說,把程序運行的地址劃分為了一段一段的片段,有的片段是用來存放代碼,叫代碼段,這樣,可以給這個段加個只讀的權限,防止程序被修改;有的片段用來存放數據,叫數據段,數據經常修改,所以可讀寫;有的片段用來存放標識符的名字,比如某個變量 ,某個函數,叫符號表;等等。由于有這么多段,所以為了方便管理,所以又引入了一個段,叫段表,方便查找每個段的位置。
當文件之間相互需要鏈接的時候,就把相同的段合并,然后把函數,變量地址修改到正確的地址上 。這就是靜態(tài)鏈接,如下圖。
靜態(tài)鏈接
但是這里有倆個問題:
對于計算機的內存和磁盤的空間浪費比較嚴重
想想一下,現在一個靜態(tài)庫,至少都是1MB以上。但是假如有1000個或者更多的程序在鏈接的時候,都靜態(tài)鏈接了它,那么當這些程序運行起來的時候,內存中就會存在1000 相同的副本,還是一模一樣的。這樣,至少1GB空間就浪費了。
程序的更新,部署,和發(fā)布會帶來很多麻煩
比如一個程序Program
所使用的Lib.o
是使用的第三方廠商提供的,那么當該廠商更新了Lib.o
(比如修復了一個bug,或者優(yōu)化了性能),那么Program
的廠商就必須要拿到最新版的Lib.o
,然后與Program.o
鏈接。將新的Program
發(fā)給用戶。這樣,一旦程序任何位置有一個小小的改動,都會導致重新下載整個程序。
我們的想法很簡單,就是當第一個例子在運行時,在內存中只有一個副本;第二個例子在發(fā)生時,只需要下載更新后的lib,然后鏈接,就好了。那么其實,這就是動態(tài)鏈接的基本思想了:把鏈接這個過程推遲到運行的時候在進行。在運行的時候動態(tài)的選擇加載各種程序模塊,這個優(yōu)點,就是后來被人們用來制作程序的插件(Plug-in)。
這里,我們不得不介紹一個東西,叫做動態(tài)鏈接器。它會在程序運行的時候,把程序中所有未定義的符號(比如調了動態(tài)庫的一個函數,或者訪問了一個變量)綁定到動態(tài)鏈接庫中。簡單的來說就是把程序中函數的地址改正到動態(tài)庫,之后動態(tài)鏈接器會把控制權交給程序,然后程序執(zhí)行。
這種在裝載時修正地址,經常被稱為裝載時重定位(Load Time Relocation)。而靜態(tài)鏈接時修正,則被稱為鏈接時重定位(Link Time Relocation)。
可能有的人,就要問了,多個程序應用一個庫不會有問題么?變量沖突?是這樣的。動態(tài)鏈接文件,把那些需要修改的部分分離了出來,與數據放在了一起,這樣指令部分就可以保持不變,而數據部分可以在每個進程中擁有一個副本,這種方案就是目前被稱為地址無關代碼(PIC,Position-independent Code)的技術。
通過上面,我們了解到了動態(tài)鏈接,靜態(tài)鏈接。一組相應目標文件的集合,我們稱它為庫。因而也就有了靜態(tài)鏈接庫,動態(tài)鏈接庫。
靜態(tài)鏈接庫:在Linux
平臺上,常以.a
或者.o
為拓展名的文件,我們最常用的C語言靜態(tài)庫,就位于/usr/lib/libc.a
;而在Windows
平臺上,常以.lib
為拓展名的文件,比如Visual C 附帶的多個版本C/C 運行庫,在VC安裝的目錄下的`lib`目錄。
動態(tài)鏈接庫:在Linux
平臺上,動態(tài)鏈接文件為稱為動態(tài)共享對象(DSO,Dynamic Shared Objects),簡稱共享對象。他們一般常以.so
為拓展名的文件;而在Windows
平臺上,動態(tài)鏈接文件被稱為動態(tài)鏈接庫(DLL,Dynamical Linking Library),通常就是我們常見的.dll
為拓展名的文件。
介紹裝載就不得不介紹三種文件格式了:ELF,PE,COFF?,F在PC平臺上流行的可執(zhí)行文件格式(Executable),無論是Windows
下的PE(Portable Executable)文件,還是Linux
下的ELF(Executable Linkable Format)文件,都是COFF(Common file format)文件格式的變種。可執(zhí)行文件例如,Windows
下的*.exe
,Linux
下的/bin/bash
。其實目標文件,內部結構上來說和可執(zhí)行文件的結構幾乎是一樣的,所以一般跟可執(zhí)行文件格式一起用一種格式進行存儲。
下面以ELF文件為例子,介紹。
每一個ELF文件,都會有一個ELF文件頭,里面會記錄很多關于這個程序相關信息,通過它確定段表,進而確定各個段。總的來說,裝載做了以下三件事情:
創(chuàng)建虛擬地址空間
讀取可執(zhí)行文件頭,并且建立虛擬空間與可執(zhí)行文件的映射關系
將CPU的指令寄存器設置為運行庫的初始函數(初始函數不止一個,第一個啟動函數為:_start
),初始了main()
函數的環(huán)境,然后指向可執(zhí)行文件的入口
以上就是最近幾天看完《程序員的自我修養(yǎng)》一些感悟吧。
└(^o^)┘;
本文轉自:簡書
微信號:IdeaofSE
聯系客服