1. 程式人生 > >讀書筆記--《程式設計師的自我修養》第2章:編譯和連結

讀書筆記--《程式設計師的自我修養》第2章:編譯和連結

一、從原始碼到可執行檔案的過程

分為4個步驟:預處理(prepressing)、編譯(compilation)、彙編(assembly)和連結(linking)。如圖所示
這裡寫圖片描述

1、預編譯
(1)首先,原始碼檔案和相關的標頭檔案,會被預編譯器預編譯為一個.i檔案。
對於C++程式來說,它的原始碼檔案的副檔名可能是.cpp或.cxx,標頭檔案的副檔名可能是.hpp,而預編譯後的副檔名是.ii。
預編譯過程相當於如下命令:

g c c E h e l l o . c
o h e l l o . i
cpp hello.c > hello.i
(2)預編譯過程主要處理那些原始碼中以“#”開始的預編譯指令。比如“#include”,”#define”等。主要規則如下:
這裡寫圖片描述

預編譯後的.i檔案不包含任何巨集定義,因為所有的巨集都已經被展開,並且包含的檔案也已經被插入到.i檔案中。所以當我們不確定巨集定義是否正確或標頭檔案包含是否正確時,可以檢視預編譯後的檔案來確定問題。

(2)編譯
編譯就是把預處理完的檔案進行一系列詞法分析、語法分析、語義分析及優化後生產相應的彙編程式碼檔案。核心部分。編譯過程相當於如下命令: g c c S h e l l o . i o h e l l o . s gcc -S hello.c -o hello.s 都可以得到彙編輸出檔案hello.s。
對於C語言程式碼來說,預編譯和編譯的程式是cc1;對於C++來說,是cc1plus;Objective-C是cc1obj; fortran是f771;Java是jc1。所以實際上gcc這個命令只是這些後臺程式的包裝,它會根據不同的引數要求來呼叫預編譯編譯程式cc1、彙編器as、聯結器ld。

(3)彙編
彙編器是將彙編程式碼轉變成機器可執行的指令,每一個彙編語句幾乎都對應一條機器指令。此彙編過程比較簡單,只需根據彙編指令和機器指令的對照表一一翻譯就可以了。可以調用匯編器as來完成: a s h e l l o . s o h e l l o . o gcc -c hello.s -o hello.o 或 $gcc -c hello.c -o hello.o

(4)連結

二、編譯

編譯過程一般分為6個過程:掃描、語法分析、語義分析、原始碼優化、程式碼生成和目的碼優化。過程如圖所示:
這裡寫圖片描述
以一段C程式碼為例講述這個過程:
array[index] = (index+4)*(2+6)
CompilerExpression.c

1、詞法分析
首先原始碼被輸入掃描器,掃描器對其進行詞法分析。運用一種類似於有限狀態機的演算法可以很輕鬆地將原始碼地字元序列分割成一系列的記號(Token)。
這裡寫圖片描述
這裡寫圖片描述
詞法分析產生的記號一般可以分為:關鍵字、識別符號、字面量(包括數字、字串等)和特殊符號(如加號等號)。在識別記號的同時,掃描器也完成了其他工作,如將識別符號存放到符號表,將數字、字串常量存放到文字表等。

2、語法分析
接下來語法分析器對掃描器產生的記號進行語法分析,從而產生語法樹。整個分析過程採用了上下文無法語法的分析手段。由語法分析器生成的語法樹就是以表示式為節點的樹。上述程式碼產生的語法樹如圖所示:
這裡寫圖片描述

3、語義分析
接下來進行語義分析。語法分析僅僅是完成了對錶達式的語法層面的分析,但是它並不瞭解這個語句是否有意義。編譯器所能分析的語義分為靜態語義和動態語義。
經過語義分析階段以後,整個語法樹的表示式都被標識了型別。如果有些型別需要做隱式轉換,語義分析程式會在語法樹中插入相應的轉換節點。上述語法樹經過語義分析後變為如圖所示形式。
這裡寫圖片描述

4、中間語言生成
現代的編譯器有著很多層次的優化,往往在原始碼級別會有一個優化過程。我們這裡所描述的就是原始碼級優化。在上例中,(2+6)這個表示式可以被優化掉,因為它的值在編譯期就能被確定。經過優化的語法樹如圖所示:
這裡寫圖片描述
我們可以看到(2+6)這個表示式被優化成8。直接在語法樹上作優化比較困難,所以一般是原始碼優化器將整個語法樹轉換成中間程式碼,它是語法樹的順序表示,其實已經很接近目的碼了。它一般跟目標機器和執行環境無關。中間程式碼有很多型別,比較常見的有:三地址碼和P-程式碼。
基本的三地址碼是這樣的:x = y op z ,這裡op操作可以是加減乘除等運算。
上例中的程式碼三地址碼錶示為:
t1 = 2 + 6
t2 = index + 4
t3 = t2 * t1
array[index] = t3

這裡利用了三個臨時變數t1、t2和t3。在三地址碼的基礎上進行優化時,優化程式會將
2+6的值直接計算出來,得到t1=6,然後將後面程式碼中的t1替換成數字6。還可以省去一個臨時變數t3,因為t2可以重複利用,經過優化的程式碼如下所示:
t2 = index + 4
t2 = t2 * 8
array[index] = t2
中間程式碼使得編譯器可以分為前端和後端。前端負責產生機器無關的中間程式碼,後端將中間程式碼轉換成目標機器程式碼。

5、目的碼生成與優化
編輯器後端包括程式碼生成器和目的碼優化器。程式碼生成器將中間程式碼轉換成目標機器程式碼,這個過程十分依賴於目標機器,因為不同的機器有著不同的字長、暫存器、整數資料型別和浮點資料型別等。上例的中間程式碼如下:
這裡寫圖片描述
最後目的碼優化器對上述程式碼進行優化,比如選擇合適的定址方式、使用位移來代替乘法運算、刪除多餘的指令等。上例中,乘法由一條相對複雜的基址比例變址定址的lea指令完成,隨後由一條mov指令完成最後的賦值操作,這條mov指令的定址方式與lea是一樣的。
這裡寫圖片描述
經過掃描、語法分析、語義分析、原始碼優化、程式碼生成和目的碼優化等步驟後,原始碼變成了目的碼,打還是index和array的地址還沒有確定。

三、連結

1、重新計算各個目標的地址的過程叫做重定位

2、組合語言使用符號來標記位置。符號用來表示一個地址,這個地址可能是一段子程式的起始地址,也可以是一個變數的起始地址。

3、人們將程式碼按照功能或性質劃分,分別形成不同的功能模組,不同的模組之間按層次結構或其他結構來組織。在c語言中,最小的單位是變數和函式,若干個變數和函式組成一個模組,存放在一個.c的原始碼檔案裡。然後這些原始碼檔案按照目錄結構來組織。在Java中,每個類是一個基本模組,若干個類模組組成一個包,若干個包組合成一個程式。

4、最常見的C/C++模組之間通訊有兩種方式,一種是模組間的函式呼叫,另一種是模組間的變數訪問。函式訪問和變數訪問都需要知道函式和變數的地址,因此兩種方式可歸為一種方式,那就是模組間符號的引用。

5、模組間依靠符號來通訊類似於拼圖,定義符號的模組多出一塊區域,引用該符號的模組剛好少了那一塊區域,兩者一拼接剛好完美組合。模組間的拼接過程就是連結。

四、模組拼接–靜態連結

連結就是把各個模組之間相互引用的部分都處理好,使得各個模組之間能夠正確地銜接。
連結過程主要包括地址和空間分配、符號決議和重定位
如果我們在程式模組main.c中使用另一個模組func.c中的函式foo(),則我們必須知道foo的地址。但是由於每個模組都是單獨編譯的,因此在編譯main.c時並不知道foo的地址,所以它暫時把這些呼叫foo的指令的目標地址擱置,等最後連結的時候由聯結器將這些指令的目標地址修正。這個地址修正的過程稱為重定位,每個要被修正的地方叫一個重定位入口。重定位所做的就是給程式中每個這樣的絕對地址引用的位置打補丁,使它指向正確的地址。