編譯器是什麼?程式變成機器碼的過程

約 10 分鐘 · 更新:2026年10月23日

編譯器是一種軟體,會在執行之前,把人寫的程式一次全部翻譯成CPU能執行的0和1指令(機器碼)。翻譯途中,它也會找出寫錯的地方,或把程式改成用更少步驟就能得到相同結果的形式(最佳化)。CPU只能執行機器碼,所以這樣的翻譯角色不可或缺。

這篇文章可以了解

  • 人寫的程式,為什麼CPU不能直接執行
  • 一行程式變成機器碼的各個階段(切成詞→整理成樹→檢查→最佳化→轉成指令→連結)
  • 編譯器、直譯器、JIT的差異,以及Java、JavaScript、Python的情況
  • 編譯器找得到的錯誤,和找不到的錯誤

前提知識:不需要。如果知道CPU是「一個一個執行以0和1寫成之簡單指令的零件」,會比較容易讀。

生活中的例子

在電腦上安裝App時,下載的檔案內容,多半是已經翻譯成機器碼的程式。同一款遊戲,電腦版和手機版發布的檔案有時是分開的。寫程式的人在聊天時,常會說「建置成功了」「出現編譯錯誤」。

在這些情境背後運作的,就是編譯器。本文以一行程式為例,依序追蹤編譯器的工作。

為什麼需要「翻譯」

CPU能直接執行的,只有用0和1排列寫成的指令。這種形式的程式稱為「機器碼」。一個指令是「讀入數字」「相加」「比較」這類非常小的工作。詳細說明請見「CPU是什麼?」。

機器碼對人來說很難讀,寫起來也很辛苦。所以人會用接近英文單字或數學式的寫法來寫程式。這種寫法的規則稱為「程式語言」,例如C、Java、JavaScript、Python等。

人寫的程式文字稱為「原始碼」。原始碼只是一串文字,對CPU來說沒有意義。因此,需要一個把它轉成機器碼的翻譯角色。

在執行之前,把整個程式一次轉成機器碼的軟體,稱為「編譯器」。機器碼指令的種類和表示方式,每種CPU都不一樣。因此,編譯器會先決定要轉成哪種CPU用的機器碼,再進行翻譯。電腦版和手機版發布的檔案不同,原因之一就在這裡。

一行程式變成機器碼的過程

舉例來說,我們用下面這一行,把時間 h 換算成秒數 s。「*」是乘法,「;」是表示一句結束的符號。

s = 60 * 60 * h;

這裡為了簡化說明,以接近C語言的語言編譯器為例,把過程分成7個階段。實際的編譯器,階段的分法和名稱會有些不同。

一行程式經過7個階段,變成可執行檔的過程 原始碼 s = 60 * 60 * h; ① 切分成詞 s = 60 * 60 * h ; ② 檢查文法,整理成樹 把 (60 × 60) × h 存進 s ③ 檢查意義 h 是準備好的數嗎? ④ 最佳化 60 × 60 → 3600 ⑤ 轉成指令序列 讀取 h 乘以 3600 寫入 s ⑥ 轉成0和1 機器碼(0和1的序列) ⑦ 與其他部分連結 完成可執行檔
①~⑤是編譯器的工作,⑥是組譯器的工作,⑦是連結器的工作。⑤的指令是用中文寫出的示意。省略的部分:實際指令的寫法(依CPU種類而不同)、也有用一個軟體完成⑤和⑥的編譯器、途中使用的中間形式。

① 切分成詞(詞法分析)

從編譯器的角度看,原始碼一開始只是一串字元。所以首先要把它切成一個個有意義的單位。例如「60」是一個數字,「*」是一個符號。這個階段稱為詞法分析。範例這一行去掉空白後,會切成8個詞。

② 檢查文法,整理成樹狀(語法分析)

接著,要檢查詞的排列是否符合語言的文法。例如像「60 * * h」這樣符號連在一起,就會回報文法錯誤。這個階段稱為語法分析。

如果符合文法,就把計算的組成整理成樹狀。乘法的規則是從左往右算,所以先算「60 × 60」,再把結果「× h」。最上面的「=」,意思是「把右邊計算的結果存進 s」。這棵樹稱為語法樹,正式名稱是「抽象語法樹」(AST)。

把語法樹中「60 × 60」的部分,透過最佳化替換成3600 最佳化之前 最佳化之後 = s × × h 60 60 = s × 3600 h
左邊是「s = 60 * 60 * h;」的語法樹。從樹下方的材料依序計算,最後由最上面的「=」存進 s。虛線框起來的部分,在執行前答案就已確定,所以像右邊一樣替換成3600。省略的部分:實際的樹裡記錄的值的種類等資訊。

③ 檢查意義

即使文法正確,意思也可能說不通。例如,如果哪裡都沒有準備名為 h 的值的容器(變數),就無法計算。編譯器會檢查用到的名稱是否都已經準備好。

值是數字還是文字,這種值的種類稱為「型別」。編譯器也會檢查有沒有型別不合的計算,例如把數字和「abc」這種文字串相乘。

④ 改成步驟更少的形式(最佳化)

編譯器會在不改變結果的範圍內修改程式,讓它跑得更快,或變得更小。這稱為「最佳化」。

範例中的「60 × 60」,在執行前就知道答案是3600。所以編譯器把樹的這個部分替換成3600。這種最佳化稱為常數摺疊。

⑤ 轉成CPU的指令序列(程式碼產生)

接著把最佳化後的結果,轉成CPU的指令序列。範例這一行,會變成類似「讀入 h 的值」「乘以3600」「把結果寫入 s 的位置」的指令。實際指令的數量和形式,依CPU種類而不同。

這個階段的輸出,常常是用英文單字縮寫成的簡短符號,來寫機器碼的指令。這種寫法稱為「組合語言」。

⑥ 轉成0和1(組譯器)

把組合語言的指令,替換成CPU能讀的0和1序列的軟體,就是組譯器。平常只要下一個編譯器的指令,就會自動接著進行⑥和⑦。

⑦ 連結各部分(連結器)

大型程式會分成很多檔案來寫。另外,像在畫面上顯示文字這類常用的處理,會使用事先準備好的零件集。這種零件集稱為「函式庫」。

把翻譯好的各部分接起來,整合成一個可執行檔的軟體,就是連結器。它會一一填上「這個處理位在那個部分的這個位置」的對應關係。函式庫可以直接放進執行檔,也可以在執行時才載入。兩者的差異,整理在「靜態連結與動態連結的差異」的表格裡。

執行是作業系統的工作

完成的執行檔,是存放在儲存裝置裡的檔案。在電腦和手機上,程式要先放進記憶體才能執行。點一下圖示,作業系統就會把檔案內容讀進記憶體。它也會準備好執行時要用的函式庫,再讓CPU從第一個指令開始執行。負責這種讀入與準備的作業系統功能,稱為載入器。作業系統的工作,請見「作業系統(OS)是什麼?」。

最佳化為什麼重要

如果只執行一次,少一個乘法也感覺不出差別。但很多程式裡,都有把同一段重複執行數百萬次的地方。如果範例這一行重複執行1億次,光是常數摺疊,就能少掉1億次乘法。

編譯器還會做許多其他的最佳化。刪掉結果沒有用到的計算。把迴圈裡每次都相同的計算,移到迴圈外面。把這類技巧疊加幾十種,讓整體變快。

另一方面,最佳化會讓翻譯花更多時間,也讓「一行一行停下來檢查程式」的工作變得比較困難。因此,C語言等廣泛使用的編譯器GCC,在沒有指定時幾乎不做最佳化。製作正式版時,一般會用「-O2」這類選項開啟最佳化。

編譯器、直譯器、JIT的差異

也有不把原始碼轉成機器碼檔案,就能讓它運作的方法。一邊讀程式、一邊當場逐一執行的軟體,稱為「直譯器」。優點是不必等翻譯,寫好就能馬上試。代價是同一段即使執行很多次,每次都要重新解讀,所以容易比較慢。

結合兩者優點的方式,就是JIT編譯。JIT是「Just In Time」(在需要的那一刻)的縮寫,所以也稱為即時編譯。常見的做法是,一開始像直譯器一樣邊讀邊執行,同時找出經常執行的部分。然後只把這些部分,在執行中轉成機器碼。

編譯器、直譯器、JIT轉成機器碼的時機不同 編譯器 全部轉成機器碼 執行機器碼 執行之前 直譯器 一邊逐一解讀,一邊執行 JIT 邊讀邊執行 只把常用的部分以機器碼執行 時間
橫向是時間的流向。編譯器在執行前就翻譯完畢。JIT從執行途中開始,只把常用的部分轉成機器碼。省略的部分:事先轉成「位元組碼」這種中間形式的階段,以及把JIT轉換過的部分還原的情形。

要注意的是,「編譯型」「直譯型」不是語言的性質,而是執行它的軟體的性質。實際上,許多語言都組合運用了這些方式。

  • Java:開發者會事先把原始碼編譯成稱為「位元組碼」的形式再發布。位元組碼不是特定CPU的機器碼,而是給Java專用的虛擬機器的指令。執行時,Java的執行軟體會解讀並執行它,並用JIT把常用的部分轉成機器碼。
  • JavaScript:Chrome等使用的V8這套軟體,會先把原始碼轉成位元組碼,再用直譯器執行。經常執行的部分,則由進行最佳化的編譯器轉成機器碼。
  • Python:標準的CPython這套軟體,也會先把原始碼編譯成位元組碼,再用直譯器執行。2024年的Python 3.13加入了實驗性的JIT,但預設是停用的。

三種方式的比較,整理在「編譯器、直譯器、JIT 的差異」的表格裡。

編譯器找得到的錯誤,找不到的錯誤

編譯器會在執行前找出很多錯誤,例如文法錯誤、使用沒有準備的名稱、型別不合的計算等。有些編譯器,也會對不算錯誤但可疑的寫法發出提醒(警告)。

另一方面,有些錯誤不實際執行就不會知道。例如,用輸入的數字做除法的程式,輸入0就無法計算。要讀的檔案不存在之類的問題,也要到執行時才會發現。這種在執行中發生的錯誤,稱為「執行時錯誤」。

更麻煩的是,程式能跑完不會停,但結果和作者的意圖不同的錯誤。本來想寫「加」卻寫成「減」,編譯器也只會照文法翻譯。這種錯誤,只能靠實際執行並確認結果的「測試」來找出。

哪些錯誤能事先找到,也因語言而不同。例如在Python中,許多型別不合的計算,要到執行那一行時才會出錯。

編譯器的歷史

直到1950年代初期,程式都是由人用機器碼,或接近機器碼的形式來寫。1951~1952年,葛麗絲·霍普(Grace Hopper)為UNIVAC I這台計算機,開發了名為「A-0」的系統。只要用編號指定事先準備好的計算零件,它就會把這些零件接起來,做成機器碼的程式。

A-0在當時就被稱為「編譯器」。不過,它的功能和今天所說的編譯器不同。電機電子領域的學會IEEE,把A-0選為技術史上的里程碑,並在2024年設置了紀念銘牌。其中的說明,也把A-0描述為「作為連結器與載入器運作的一組規格」。

一般認為,為今日編譯器的分工方式與技術奠定基礎的,是IBM的約翰·巴科斯(John Backus)等人開發的FORTRAN。它把接近數學式寫法的原始碼,翻譯成IBM 704這台計算機的機器碼。這項計畫始於1954年夏天,並在1957年4月發送到所有使用704的地方。

如果編譯器產生的機器碼,比人手寫的慢很多,就不會有人用。因此,FORTRAN團隊以接近手寫的速度為目標,在最佳化上投入心力。1982年寫成的回顧論文,評價這個目標達成的程度令人驚訝。

硬體也有類似「編譯器」的東西

在半導體晶片的電路設計中,也用到類似的想法。現在的大型電路,一般不是畫圖,而是用類似文章的形式來寫。用來寫電路的語言稱為硬體描述語言,例如Verilog和VHDL。

把寫好的電路行為,自動轉換成「且」「或」這類基本邏輯電路組合的工作,稱為邏輯合成。讀入、檢查、最佳化、輸出的流程,和編譯器很像。

最大的差別是,輸出的不是「依序執行的指令序列」,而是「同時運作的電路連接」。做好的電路,可以寫進事後能改寫的晶片,也可以做成專用晶片來製造。前者在「FPGA是什麼?」中說明。

常見的誤解

誤解:用編譯器轉成機器碼的語言,一定比較快

實際上,是否編譯,只是決定速度的因素之一。如果計算的步驟本身繞了遠路,再怎麼最佳化也還是慢。JIT能觀察執行中的情況來最佳化,所以長時間執行的程式,有時速度能接近事先翻譯好的程式。另外,Python常用的計算零件集NumPy,核心的計算是用編譯好的C程式碼進行的。比較同樣的處理時,編譯過的往往比較快,所以容易讓人以為「一定比較快」。

誤解:編譯器會理解寫程式的人想做什麼

實際上,編譯器只是照既定的規則翻譯,並不知道作者的意圖。像「這裡少了 ;」這樣的錯誤訊息看起來很貼心,是因為它在回報不合規則的地方。最佳化也只在依語言規則、結果不變的範圍內進行。在C語言中,如果用了規則沒有規定結果的寫法,可能產生和預期不同的機器碼。這種情況稱為未定義行為。編譯成功,不代表程式就是正確的。

誤解:Python不編譯,而是一行一行直接執行

實際上,標準的CPython在執行前,會把整個檔案轉成位元組碼,再用直譯器執行。所以檔案裡只要有一處文法錯誤,連第1行都不會執行,就會出錯。不過,它預設不會轉成機器碼,所以說它是「用直譯器執行的語言」也沒有錯。如果以為「編譯」只是指轉成機器碼,就會產生這種誤解。

從數字來看

項目數字/時期備註
A-01951~1952年葛麗絲·霍普。UNIVAC I用
FORTRAN計畫開始1954年夏天IBM,約翰·巴科斯的團隊
FORTRAN編譯器發布1957年4月發送到所有IBM 704的設置地點
FORTRAN的開發工作量約18人年相當於1個人做18年的工作量。1957年的論文
GCC的預設設定-O0幾乎不做最佳化。重視速度時用 -O2 等
Python的實驗性JIT2024年(3.13)3.13中預設停用

從FORTRAN算起已將近70年,翻譯分成各階段的想法,到今天仍沒有太大改變。增加的是最佳化的種類,以及像JIT這樣在執行中翻譯的方式。

進一步學習

接著閱讀

系列「電腦的原理」的文章

  1. 電腦怎麼用0和1計算?二進位與加法電路
  2. CPU是什麼?它在電腦和手機裡做些什麼
  3. AI為什麼要用GPU?同時處理大量乘法的原理
  4. 作業系統(OS)是什麼?它在電腦和手機裡做些什麼
  5. CPU快取記憶體是什麼?讓CPU不必苦等記憶體的原理
  6. NPU是什麼?手機和電腦裡的AI專用電路原理
  7. FPGA是什麼?做好之後還能改寫電路的晶片原理
  8. ASIC是什麼?特定應用積體電路與CPU、FPGA的差異
  9. 編譯器是什麼?程式變成機器碼的過程
  10. 邏輯閘是什麼?用AND、OR、NOT計算的原理
  11. RISC-V是什麼?人人都能使用的開放指令集
  12. 演算法是什麼?靠步驟的巧思讓計算變快的原理
  13. 資料庫是什麼?表格、SQL與索引的運作原理
  14. Git是什麼?版本控制的原理與GitHub的差異

這個領域的題目: 編譯器

參考資料

入門解說一覽