C編譯器入門~想懂低階系統從自幹編譯器開始~
  • 譯者序
  • 前言
    • 符號與規範
    • 本書的開發環境
    • 關於作者
    • 結束前言之前
  • 機械語言與組譯器
    • CPU 與記憶體
    • 什麼是組譯器
    • C程式和所對應的組合語言
      • 簡單的範例
      • 包含呼叫函式的範例
    • 本章小結
  • 創造計算機等級的語言
    • 第1步:創造能編譯1個整數的語言
    • 第2步:製作可以算加減法的編譯器
    • 第3步:加入標記解析器(tokenizer)
    • 第4步:改良錯誤訊息
    • 文法的記法與遞迴下降分析法
      • 將文法結構表示為樹(tree)
      • 以生成規則定義文法
      • 以 BNF 描述生成規則
      • 簡單的生成規則
      • 以生成規則描述運算子的優先順序
      • 包含遞迴的生成規則
      • 遞迴下降語法分析
    • 堆疊機
      • 堆疊機的概念
      • 編譯成堆疊機指令
      • 以x86-64實作堆疊機的方法
    • 第5步:製作可進行四則運算的編譯器
    • 第6步:單項加與單項減
    • 第7步:比較運算子
      • 修改標記解析器
      • 新的文法
      • 產生組合語言指令
  • 分離編譯與連結
    • 分離編譯
      • 分離編譯與其必要性
      • 標頭檔的必要性與其內容
      • 連結錯誤
      • 全域變數的宣告與定義
    • 第8步:分割檔案與修改 Makefile
      • 分割檔案
      • 修改 Makefile
  • 函式與區域變數
    • 第9步:1個字的區域變數
      • 堆疊上的變數空間
      • 修改標記解析器
      • 修改分析器
      • 左邊值與右邊值
      • 從任意的記憶體位址取得其值
      • 修改指令產生器
      • 修改主函式
    • 第10步:複數文字的區域變數
    • 第11步:return
    • 1973年的C編譯器
Powered by GitBook
On this page

Was this helpful?

  1. 分離編譯與連結
  2. 第8步:分割檔案與修改 Makefile

修改 Makefile

已經把程式分成數個檔案了,接著,也該來跟著改 Makefile 了。底下的 Makefile,會把現在所在資料夾的所有 .c 檔全部編譯並連結起來,做出 9cc 這個可執行檔。假設專案的標頭檔只有 9cc.h 這1個檔案,而這個標頭檔會被所有的 .c 檔所引入。

Makefile
CFLAGS=-std=c11 -g -static
SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)

9cc: $(OBJS)
        $(CC) -o 9cc $(OBJS) $(LDFLAGS)

$(OBJS): 9cc.h

test: 9cc
        ./test.sh

clean:
        rm -f 9cc *.o *~ tmp*

.PHONY: test clean

注意 Makefile 的縮排必須要用 tab 而不能用空白。

make是非常強力的工具,精通不是必要的,但是熟悉到可以讀懂上面的 Makefile 的程度的話,在很多地方都能派上用場。我們在此針對上面的 Makefile 進行說明。

Makefile 中,1條規則是由以冒號分隔的行,和以 tab 縮排的0行以上的指令所構成。冒號前的名字稱為「目標」(target)。冒號後面0個以上的檔案名稱被稱為相依檔案。

執行make foo,make就會試著做出 foo 的檔案。如果所指定的目標檔案已經存在的話,如果目標檔案比相依檔案舊的話,make就會重新執行該條目標的規則。因此,就可以只在程式碼有變更的時候才進行重編。

.PHONY代表不是真正的目標的特殊名字。make test或make clean並不是為了做出 test 或 clean 的檔案而執行的,但make並不知道這件事,如果剛好有 test 或 clean 的檔案在的話,make test或make clean就不會做任何事了。只要在在.PHONY裡指定像這樣的假目標,就不會真的想要做出該檔案,告訴make說:不論是否存在該目標的檔案都要執行該目標的規則。

CFLAGS或SRCS、OBJS為變數。

CFLAGS是make內嵌的規則中會讀取的變數,是用來寫要給C編譯器下的指令參數。此處我們給出以下幾個 flags:

  • -std=c11: 告訴編譯器我們的程式碼是用C最新的標準C11寫的

  • -g: 輸出除錯資訊

  • -static: 靜態連結

SRCS的右側使用的wildcard是make所提供的函式,會把和其引數符合的檔名展開。$(wildcard *.c)在現階段,會展開為main.c parse.c codegen.c container.c。

OBJS的右側使用變數的替換規則,生成把SRCS中的.c換成.o的值。SRCS為main.c parse.c codegen.c container.c,所以OBJS會是main.o parse.o codegen.o container.o。

有這些認識之後,我們來追看看執行make 9cc會發生什麼事。make會想要生成引數所指定的目標,所以生出 9cc 這個檔案就是指令的最終目標(如果沒有引數就會是最初的目標,所以在此處不指定9cc也沒關係)。make會確認相依性,看有沒有少檔案,或是檔案太舊都會執行指令編出檔案。

9cc相依的檔案為當下資料夾內 .c 檔所對應的 .o 檔。如果上次make執行後有留下 .o 檔,並且其時間戳記(timestamp)比 .c 檔新,make就不會多此一舉重新執行一樣的指令。只有在 .o 不存在,或是 .c 檔比較新的時候,編譯器才會執行產生 .o 檔。

$(OBJS): 9cc.h這條規則表示所有的 .o 檔案都相依於 9cc.h。所以如果 9cc.h 有變更,所有的 .o 檔都會被重編。

小知識:用編譯器製作後門

有種把系統裡的編譯器換成惡意的編譯器,用其編譯出來的程式會被擅自埋進惡意程式碼的攻擊方式。

舉例來說,螢幕保護程式本身是寫成以全螢幕顯示,直到輸入正確格式的密碼才會結束的程式。我們來想想要怎麼破解它。

假定攻擊者有編譯器和螢幕保護程式的原始程式碼。於是攻擊者就可以改造編譯器,判斷輸入的檔案和螢幕保護程式是否一致,當一致的時候就追加上特別的操作。

螢幕保護程式中,應該可以找到檢查輸入的密碼的程式碼。我們來想像一下,當編譯含有這段程式碼的檔案的時候,編譯器會輸出會擅自讓攻擊者的密碼通過的程式。如此,使用該編譯器編譯出來的螢幕保護程式,不只知道密碼的合法使用者,連攻擊者也可以成功解鎖。

像這樣的攻擊,再怎麼檢查螢幕保護程式的原始程式碼也檢查不出後門。因為後門並不是在那邊,而是藏在編譯器的二進制檔案裡。

這類攻擊不僅限於編譯器。例如針對 OS 做修改,我們可以想像,可以只在特定使用者執行螢幕保護程式的時候,默默執行別的惡意的螢幕保護程式的 OS。為了妨礙檢查,修改objdump或除錯器、OS 的檔案 IO,以達到隱藏後門的精巧架構也是可以想像的。

總而言之,軟體這種東西,除非使用可信賴的程式來製作和執行,都無法真正被相信。

Previous分割檔案Next函式與區域變數

Last updated 5 years ago

Was this helpful?