iT邦幫忙

2024 iThome 鐵人賽

DAY 2
0
Security

Pwn2Noooo! 執行即 Crash 的 PWNer 養成遊戲系列 第 2

[Day2] 基礎知識 - 程式如何被編譯

  • 分享至 

  • xImage
  •  

上篇文章有提到 Pwn 是指利用程式漏洞來入侵並控制程式,最終取得系統(如個人電腦、伺服器、網路設備等)的控制權的過程。為了更好地理解漏洞的成因及其利用方式,在正式開始 Pwn 之前,我們需要先了解程式如何從原始碼變成可執行檔,並最終在系統上執行起來的(本文以 Linux 系統為例)。

本篇文章主要是說明 C 原始碼是如何經歷一系列過程,最後成為一份可執行檔的。
文章架構如下:

  • C 程式碼分析
  • Step 1. 預處理(Preprocessing)
  • Step 2. 編譯(Compilation)
  • Step 3. 組譯(Assembly)
  • Step 4. 連結(Linking)

註:本系列文章中所提及的環境,若無特別說明,皆以 x86-64 架構的 Linux 系統為準。


首先,我們來看一個簡單的 C 程式 hello.c

#include <stdio.h>

int main() {
    if (!printf("Hello, World!\n")) {
        return -1;
    }
    return 0;
}

我們希望這段程式碼能讓電腦在螢幕上輸出 Hello, World!。要達成這個目標,首先需要將人類可讀的程式碼轉換為電腦可理解的機器語言。為此,我們可以使用 GCC(GNU Compiler Collection)這套編譯工具。

通常,我們會使用以下命令來生成執行檔 hello

gcc -o hello hello.c 

在生成執行檔的過程中,會經歷四個主要步驟:

  1. 預處理(Preprocessing)
  2. 編譯(Compilation)
  3. 組譯(Assembly)
  4. 連結(Linking)

在深入探討每個階段之前,我們先來分析這段 C 程式的基本意圖。


C 程式碼分析

在程式中,我們定義了一個 main 函數,這是程式的入口點(Entry point)。在程式完成一些必要的初始化後,main 函數是第一個被呼叫的函數。進入 main 函數後,程式會依次執行其內部的指令。在這裡,我們使用了 printf 函數來輸出 Hello, World!。

在使用函數前,需要先宣告該函數,以告知編譯器函數的名稱、接受的參數類型及其回傳值類型。然而,我們在這段程式碼中並未看到 printf 的宣告,但可以看到 #include <stdio.h> 這一行指令。這表示我們的 C 程式碼包含了標頭檔 stdio.h。標頭檔讓使用者可以重複使用某函數,不需要每次用到某函數時都必須自己實作。

stdio.h 是 C 標準函式庫(C Standard Library)其中一個標頭檔,負責定義所有標準輸入輸出的函數,我們可以在他的原始碼中看到 printf 的宣告

最後,程式若能順利輸出訊息,main 函數會回傳 0,表示成功;若輸出失敗,則回傳 -1。

補充:
函式庫(Library)是一組預先編寫好的函數集合,開發者可以在自己的程式中重複使用這些函數。以 printf 為例,它的實作就是定義在標準 C 函式庫中。函式庫主要分為靜態函式庫(Static Library)和共享函式庫(Shared Library)兩種類型。


Step 1. 預處理(Preprocessing)

正如前文提到的,printf 的宣告包含在 stdio.h 這個標頭檔中。預處理的主要工作是將這些包含的標頭檔展開,即將標頭檔中定義的所有函數宣告和巨集展開到原始碼中,生成一個名為 .i 的中間檔案。此外,預處理還會處理所有以 # 開頭的預處理指令,例如 #define 定義的巨集、#include 包含的其他檔案、#ifdef#endif 的條件編譯等。註解也會在這個階段被移除。


Step 2. 編譯(Compilation)

預處理完成後,GCC 進入編譯階段,將經過預處理的 C 程式碼轉換為組合語言。
image
組合語言長的如上圖所示,每一行組合語言指令通常對應於一條機器指令(Machine Instructions),能直接操作 CPU 的元件(如暫存器、記憶體等)。編譯後的組合語言程式碼通常被儲存為 .s 檔案,準備在下一階段進行組譯。

補充:電腦的運作原理簡單來說就是 CPU 不斷的去讀取一行一行的指令來執行,這些指令即為機器指令(Machine Instructions),它們都是以都是二進位(0 和 1)形式來表示的。


Step 3. 組譯(Assembly)

組合語言雖然比高階語言更接近機器指令,但電腦無法直接執行。電腦只能理解以二進位(0 和 1)形式表示的機器碼(機器語言)。因此,在組譯階段,組譯器(Assembler)會將組合語言程式碼(.s 檔)轉換為機器碼,生成一個名為目標檔案(Object File)的 .o 檔。這份檔案除了有被轉換過後的機器碼還有一個符號表(Symbol Table)。

所謂的符號(Symbol)代表的是程式中函數或變數的名稱,舉例來說現在有隻程式 symbol.c 如下圖左方。注意到我們使用了三個函數 mainfuncprintf,以及兩個變數 ab,列出來的這五個東西就是所謂的符號(Symbol),即下圖右方。

image

其中 B 代表未初始化的全域變數,D 代表已初始化的全域變數,T 代表已經定義過的函數,意思是我們自己實作的函數如 funcmain。而U 則是代表未定義過的函數,這意味著 printf 不是我們自己在程式中定義的函數,而是來自外部函式庫的實作。這類函數稱為外部函數。

而符號表的主要用途是幫助在後續的連結階段中,讓連結器(Linker)能夠將這些外部函數與對應的函式庫連結起來,確保程式可以正常調用外部函數。


Step 4. 連結(Linking)

在連結階段,連結器(Linker)負責將上述未定義的符號與對應的函式庫進行連結。這個過程可以透過兩種方式完成:靜態連結和動態連結。

  • 靜態連結(Static Linking):靜態連結器將所有引用的函數和變數從靜態函式庫中提取並嵌入可執行檔案中,生成的執行檔案不依賴外部函式庫。
  • 動態連結(Dynamic Linking):動態連結則是等程式執行時,由作業系統的動態連結器載入所需的共享函式庫。

最終,連結器生成一個可執行檔案,該檔案可以直接在作業系統上執行。

下一篇文章將會說明執行檔是如何在 Linux 系統上執行起來的。



上一篇
[Day1] 前言與 PWN 概述
下一篇
[Day3] 基礎知識 - 執行檔如何被執行
系列文
Pwn2Noooo! 執行即 Crash 的 PWNer 養成遊戲13
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言