編譯、連結學習筆記(一)簡述編譯連結過程

炫目蕭蕭發表於2017-08-17

一直很希望清楚的瞭解C語言是如何從編寫程式碼到編譯、連結成可執行檔案,最後執行程式碼的整個過程。今天開始學習《程式設計師的自我修養》,並從讀書的過程中做一些總結與思考,也希望從中可以將晦澀難懂的概念以我自己的理解以簡單的語言總結出來。
書中所用到的例子都是以pc為例子,我也試著從mac與ios的角度試著以類比探究他們三者的區別與相同之處。

原始碼的編譯過程

原始碼從文字,經過編譯器的處理最終生成可執行檔案的過程中一共經歷了四個步驟,分別是預處理(prepressing)編譯(compliation)彙編(assembly),和連結(linking)
下圖是四個步驟以及對應的生成產物。

這裡寫圖片描述

步驟1 預編譯

預編譯是整個編譯過程最開始的工作,它的工作是做些程式碼的替換工作,過程中主要處理檔案中以#開始的預編譯指令,例如引用其他檔案#include 、#define巨集替換、#pragma等。

gcc編譯器中預編譯命令

gcc中使用-E選項進行編譯可以輸出預編譯後的結果,結果輸出在字尾為i的檔案中。

gcc -E hello.c -o hello.i

主要的規則有以下幾個
1. #define ,刪除#define並展開所有定義,並做巨集文字替換
2.#include,將宣告的檔案插入到指令的位置,而且插入的過程是遞迴進行的,也就是說會將所有使用到的檔案遞迴引用
3.處理#if、#ifdef 、#else 、#endif等條件預編譯指令
4.保留#pragma編譯器命令
5.新增行號和檔名標識,比如 #2 “hello.c” 2

下面舉一個簡單的例子來說明預編譯的輸出結果。

例子是一個非常簡單的c的名為hello.c的原始檔,例子中使用到了上面所說的幾種預編譯命令,#include、#define、#pargma。

如引用stdio.h標頭檔案,宣告巨集TEST_MARCO_1,使用條件編譯命令修改TEST_MARCO_2巨集的值,使用#pragma message在編譯時控制檯輸出log。

#include <stdio.h>

#define TEST_MARCO_1 1
#pragma message("訊息文字")

#ifdef TEST_MARCO_1
    #define TEST_MARCO_2 2
#elif
    #define TEST_MARCO_2 3
#endif

int main()
{
    int a = TEST_MARCO_1;
    int b = TEST_MARCO_2;
    printf("helloworld\n");
    return 1;
}

編譯後控制檯會因為#pargma message的原因輸出一段文字
這裡寫圖片描述

先看編譯結果,因編譯後的程式碼非常的冗長,此處只顯示一些輸出檔案一頭一尾的部分。

# 1 "hello.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 330 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "hello.c" 2
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/stdio.h" 1 3 4
# 64 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/stdio.h" 3 4
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/sys/cdefs.h" 1 3 4
# 587 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/sys/cdefs.h" 3 4
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/sys/_symbol_aliasing.h" 1 3 4

....


# 2 "hello.c" 2
# 12 "hello.c"
int main()
{
 int a = 1;
 int b = 2;
 printf("helloworld\n");
 return 1;
}

可以看到經過預編譯後,#include<stdio.h>將標頭檔案的絕對值路徑編譯了進來,並且也將其引用的其他路徑也遞迴的編譯了進來(如sys/cdefs.h、sys/_symbol_aliasing.h等)
而且原始檔的#if、#elseif等條件編譯命令也被去除了,int a = TEST_MARCO_1也被最終解析成了int a = 1

步驟2 編譯

編譯的過程是把預處理完的檔案進行一系列的詞法分析、語法分析、語義分析、中間程式碼生成,目的碼生成以及優化,最終生成彙編程式碼檔案。

gcc編譯器中編譯

gcc中使用-S選項進行編譯,結果輸出在字尾為s的檔案中。

gcc -S hello.i -o hello.s

編譯的過程比較多也比較複雜,從比較概況的總結這個過程,用比較通俗的話便是,從無語義至有語義,從與機器無關至於機器有關,將原始碼從簡單的字串最終編譯成計算機語義的彙編程式碼。
而整個編譯過程分成編譯前端與後端,前端負責生產與機器無關的中間程式碼,後端負責生成與機器有關的目的碼。

下面是編譯過程的具體流程:
這裡寫圖片描述

詞法分析

原始碼是由一個一個的字元組成,編譯的第一步是將其中的字元序列使用掃描器(scanner)分割成一系列單詞或符號,在編譯器中稱為記號(token)。
詞法分析產生的記號一般可以分成“關鍵字”,“識別符號”,“字面量(數字,字串等)”,“特殊符號(+,-,=)”。
在計算機語言中,我們說的語法的不同,在編譯系統中最直接的便是詞法分析的方法不同導致的。

語法分析

從詞法分析過程中得到的token序列,僅僅是簡單的單詞序列,並不能表達意義。語法分析這一過程,通過語法分析器(grammar parser)採用上下文無關語法分析手段,產生語法樹。這個樹是以表示式為節點的樹。

語義分析

從語法分析過程中得到的語法樹,僅僅是完成了語法層面的分析,無法瞭解這個語句是否真正有意義,是否合法。語義分析過程便是對錶達式中的變數與型別進行判斷,分析其是否語義不匹配。

中間程式碼生成

將語義分析的步驟中得到的標識後的語法樹(commended syntax tree)通過原始碼級優化器(source code optimizer)做優化,生成中間程式碼

目的碼生成與優化

從上步驟中拿到的中間程式碼是與機器無關的,通過此步驟中程式碼生成器(code generator)生成與機器相關的目的碼。

自此編譯階段的程式碼便生成了與機器相關的彙編程式碼。

步驟3 彙編

彙編的過程是將彙編程式碼轉換成可以執行的機器碼,每一條彙編語句幾乎都對應一條機器指令。
gcc中使用-c選項彙編或使用匯編器as命令執行

as hello.s -o hello.o 
gcc -c hello.s -o hello.o

步驟4 連結

連結過程是將多個目標檔案連結起來得到最終可執行檔案的過程。

小TPS

  1. 若想知道系統庫標頭檔案的具體路徑,可以使用預編譯命令,因預編譯後會展示完整的檔案路徑
  2. 使用#pargma message指令可以在編譯資訊輸出視窗中輸出相應的資訊,使用這個命令可以輸出一些編譯時重要的過程。

相關文章