一,序言
從代碼到能跑的程序,整個過程就像 “把外文翻譯成母語,再組裝成能直接用的東西”,一步步來更清楚:
源代碼(程序員寫的代碼,如C語言文件)↓
預處理(處理#開頭的命令,如#include、#define)↓
編譯(把預處理后的代碼轉成匯編語言)↓
匯編(把匯編語言轉成二進制機器碼,生成目標文件,如main.o)↓
鏈接(合并多個目標文件和庫文件,解決函數/變量地址問題)↓
可執行文件(生成能直接運行的文件,如.exe、ELF格式)↓
加載運行(操作系統把文件加載到內存,開始執行代碼)
二,相關步驟簡介
一、源代碼:程序員寫的 “原始稿”
就是我們用編程語言(比如 C 語言)寫的代碼,比如這個打印 “你好世界” 的小程序:
#include <stdio.h>
int main() {printf("Hello, World!\n");return 0;
}
二、預處理:先 “整理” 代碼
用預處理器(比如 GCC 里的 cpp)處理代碼里帶#的命令,讓代碼更 “干凈”:
1,比如#include <stdio.h>,會直接把 stdio.h 文件里的內容 “復制粘貼” 到代碼里(因為 printf 函數的定義就在這個文件里);
2,要是有#define PI 3.14,會把代碼里所有 “PI” 換成 “3.14”;
3,還能根據#ifdef這類命令,選擇性保留代碼(比如調試時用一段,發布時用另一段)。
處理完后,代碼里就沒有#命令了,只剩純代碼。
三、編譯:翻譯成 “匯編語言”
用編譯器(比如 GCC 里的 cc1)把預處理后的代碼,轉成電腦硬件能理解的 “匯編語言”(相當于 “二進制的半成品”)。
過程中會檢查代碼對不對:比如語法錯了(少個括號)、類型不匹配(整數函數返回空值)都會報錯。
舉個例子,前面的代碼會變成類似這樣的匯編:
main:推棧操作準備"Hello, World!"這個字符串調用printf函數返回0出棧操作
四、匯編:轉成 “二進制指令”
用匯編器(比如 GCC 里的 as)把匯編語言轉成電腦能直接執行的 “二進制機器碼”,生成 “目標文件”(比如 main.o)。
這個文件里存著:
1,代碼段:二進制的指令(比如調用 printf 的操作);
2,數據段:已經初始化的變量(比如int a=10);
3,符號表:記著變量、函數的位置(比如 printf 在哪兒)。
五、鏈接:拼出 “能跑的程序”
用鏈接器(比如 GCC 里的 ld)把多個目標文件(比如自己寫的 main.o,還有系統提供的庫文件)合并成一個 “可執行文件”。
核心是解決 “找不到東西” 的問題:比如代碼里用了 printf,但目標文件里只知道有這個函數,不知道它在哪兒 —— 鏈接器會找到它在標準庫(比如 libc)里的實際位置,把地址填對。
鏈接分兩種:
1,靜態鏈接:直接把庫代碼(比如 printf 的實現)復制到可執行文件里,文件會變大,但能獨立運行;
2,動態鏈接:只記著依賴哪個庫(比如 libc.so),運行時再加載,文件小,但需要系統里有這個庫。
六、可執行文件:最終的 “成品”
生成的文件(比如 Windows 的.exe、Linux 的 ELF 文件)里有:
1,文件頭:告訴系統怎么加載它、從哪兒開始執行;
2,代碼和數據:合并后的二進制指令、變量;
3,動態鏈接信息(如果用了動態鏈接):記著需要哪些庫。
七、運行:雙擊就能跑
雙擊可執行文件后,操作系統會:
1,給它分配內存,建個 “進程”;
2,把文件里的代碼、數據從硬盤讀到內存;
3,如果是動態鏈接,會加載需要的庫;
4,最后跳到入口點(比如 main 函數),開始執行代碼 —— 屏幕上就會顯示 “Hello, World!” 啦。
編譯型 vs 解釋型語言,簡單說:
類型 | 編譯型(比如 C/C++) | 解釋型(比如 Python) |
---|---|---|
執行前 | 先編譯 + 鏈接,生成單獨的可執行文件 | 不用編譯,直接用解釋器一行行讀代碼跑 |
速度 | 快(直接跑機器碼) | 稍慢(每次都要解釋) |
跨平臺 | 不同系統可能要重新編譯(比如 Windows 和 Linux) | 一次寫完,有解釋器就能跑 |