C程序如何從源代碼生成指令序列(二進(jìn)制可執(zhí)行文件)
預(yù)處理 -> 編譯 -> 匯編 -> 鏈接 -> 執(zhí)行
預(yù)處理
預(yù)處理 = 文本粘貼
#include <stdio.h>
#define MSG "Hello \
World!\n"
int main() {
printf(MSG /* "hi!\n" */);
#ifdef __riscv
printf("Hello RISC-V!\n");
#endif
return 0;
}
gcc -E a.c
頭文件是如何找到的?
方法: 閱讀工具的日志(查看是否支持verbose, log等選項(xiàng))
gcc -E a.c --verbose > /dev/null
通過man gcc并搜索-I選項(xiàng)可得知頭文件搜索的順序
echo '#warning I am wrong!' > stdio.h
gcc -E a.c --verbose
mkdir aaa bbb
gcc -E a.c -Iaaa -Ibbb --verbose > /dev/null
echo '#warning I am wrong, too!' > bbb/stdio.h
echo '#define printf(...)' >> bbb/stdio.h
gcc -E a.c -Iaaa -Ibbb --verbose
類函數(shù)宏
#define max(a, b) ((a) > (b) ? (a) : (b))
- 好的編程習(xí)慣 -> 總是用括號包圍參數(shù)
- 好的編程習(xí)慣 -> 一個(gè)參數(shù)盡量不要展開多次
define max(a, b) ({ int x = a; int y = b; x > y ? x : y; })
上述代碼使用了GNU C Extension, 跨平臺移植時(shí)需要注意
預(yù)處理的其他工作
- 去掉注釋
- 連接因斷行符(行尾的)而拆分的字符串
- 處理?xiàng)l件編譯 #ifdef/#else/#endif
riscv64-linux-gnu-gcc -E a.c # apt-get install g++-riscv64-linux-gnu
- 字符串化 #
- 標(biāo)識符連接 ##
#define _str(x) #x
#define _concat(a, b) a##b
_concat(pr, intf)(_str(RISC-V));
IOCCC(國際混亂C代碼大賽)
套路: 借助預(yù)處理機(jī)制編寫不可讀代碼
編譯
編譯是一個(gè)比較復(fù)雜的過程
詞法分析 -> 語法分析 -> 語義分析 -> 中間代碼生成 -> 優(yōu)化 -> 目標(biāo)代碼生成
借助合適的工具(clang), 我們來看看每一個(gè)階段都在做什么
- clang功能上等價(jià)于gcc, 但能向我們更好地展示編譯的中間步驟
#include <stdio.h>
int main() { // compute 1 + 2
int x = 1, y = 2;
int z = x + y;
printf("z = %d\n", z);
return 0;
}
詞法分析
clang -fsyntax-only -Xclang -dump-tokens a.c
識別并記錄源文件中的每一個(gè)token
- 標(biāo)識符, 關(guān)鍵字, 常數(shù), 字符串, 運(yùn)算符, 大括號, 分號…
- 還記錄了token的位置(文件名:行號:列號)
C代碼 = 字符串
- 詞法分析器本質(zhì)上是一個(gè)字符串匹配程序
語法分析
clang -fsyntax-only -Xclang -ast-dump a.c
按照C語言的語法將識別出來的token組織成樹狀結(jié)構(gòu)
- AST(Abstract Syntax Tree), 可以反映出源程序的層次結(jié)構(gòu)
- 報(bào)告語法錯(cuò)誤, 例如漏了分號
語義分析
按照C語言的語義確定AST中每個(gè)表達(dá)式的類型
- 相容的類型將根據(jù)C語言標(biāo)準(zhǔn)規(guī)范進(jìn)行類型轉(zhuǎn)換
- 算術(shù)類型轉(zhuǎn)換
- 報(bào)告語義錯(cuò)誤
- 未定義的引用
- 運(yùn)算符的操作數(shù)類型不匹配(如struct + int)
- 函數(shù)調(diào)用參數(shù)的類型和數(shù)量不匹配
…
但大多數(shù)編譯器并沒有嚴(yán)格按階段進(jìn)行詞法分析, 語法分析, 語義分析
-
clang的-ast-dump把語義信息也一起輸出了
- man clang可以得知clang的階段劃分
靜態(tài)程序分析
在不運(yùn)行程序的情況下對其進(jìn)行分析
- 本質(zhì)就是分析AST中的信息
AST(Abstract Syntax Tree)是源代碼的抽象語法結(jié)構(gòu)的樹狀表現(xiàn)形式,也就是源代碼的抽象語法結(jié)構(gòu)的一種樹狀圖表示。
在編譯原理中,AST是源代碼和編譯器之間的一種數(shù)據(jù)結(jié)構(gòu)。編譯器將源代碼轉(zhuǎn)換為AST,然后對AST進(jìn)行一系列的轉(zhuǎn)換,最終生成目標(biāo)代碼。AST的結(jié)構(gòu)和形式是由源代碼的語法決定的,因此AST也被稱為源代碼的抽象語法。
AST可以用于靜態(tài)分析,即在程序運(yùn)行之前對其進(jìn)行分析。通過分析AST,可以了解程序的結(jié)構(gòu)、語法錯(cuò)誤、潛在的邏輯錯(cuò)誤等等。例如,一些代碼質(zhì)量檢查工具、代碼格式化工具、編譯器優(yōu)化等都可以使用AST進(jìn)行分析。
在不運(yùn)行程序的情況下對其進(jìn)行分析,本質(zhì)就是分析AST中的信息。通過解析源代碼生成AST,然后對AST進(jìn)行分析,可以獲取程序的各種信息,如變量使用情況、函數(shù)調(diào)用關(guān)系、代碼覆蓋率等等。這些信息可以用于代碼質(zhì)量評估、性能優(yōu)化、錯(cuò)誤檢測和修復(fù)等等。
可以檢查/分析以下方面
- 語法錯(cuò)誤,
- 代碼風(fēng)格和規(guī)范,
- 潛在的軟件缺陷,
- 安全漏洞,
- 性能問題
#include <stdlib.h>
int main() {
int *p = malloc(sizeof(*p) * 10);
free(p);
*p = 0;
return 0;
}
clang use-after-free.c --analyze -Xanalyzer -analyzer-output=text
中間代碼生成
clang -S -emit-llvm a.c
中間代碼(也稱中間表示, IR) = 編譯器定義的, 面向編譯場景的指令集
define dso_local i32 @main() #0 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
%3 = alloca i32, align 4
%4 = alloca i32, align 4
store i32 0, i32* %1, align 4
store i32 1, i32* %2, align 4
store i32 2, i32* %3, align 4
%5 = load i32, i32* %2, align 4
%6 = load i32, i32* %3, align 4
%7 = add nsw i32 %5, %6
store i32 %7, i32* %4, align 4
%8 = load i32, i32* %4, align 4
%9 = call i32 (i8*, ...) @printf(i8* noundef getelementptr inbounds ([8 x i8], [8 x i8]* @.str, i64 0, i64 0), i32 noundef %8)
ret i32 0
}
將C語言狀態(tài)機(jī)翻譯成IR狀態(tài)機(jī)
- 變量 -> %1, %2, %3, %4, …
- 語句 -> alloca, store, load, add, call, ret, …
中間代碼作為抽象層
為什么不直接翻譯到處理器ISA?
基于抽象層進(jìn)行優(yōu)化很容易
可以支持多種源語言和目標(biāo)語言(硬件指令集)
clang使用的中間代碼叫LLVM IR, gcc的叫GIMPLE
- 我們不需要理解其中的細(xì)節(jié), 研究它是編譯專家的事情
- 知道一些基本概念, 會連蒙帶猜看一看即可(可RTFM了解更多)
優(yōu)化
如果兩個(gè)狀態(tài)機(jī)在某種意義上 “相同”, 就可以用 “簡單”的替代 “復(fù)雜”的
-
“簡單” = 狀態(tài)少(變量少), 激勵事件少(語句少)…
-
最 “復(fù)雜” = 嚴(yán)格按照語句的語義來翻譯(嚴(yán)格的狀態(tài)轉(zhuǎn)移)
-
“相同” = 程序的可觀測行為(C99 5.1.2.3節(jié)第6點(diǎn))的一致性
-
對volatile修飾變量的訪問需要嚴(yán)格執(zhí)行
-
程序結(jié)束時(shí), 寫入文件的數(shù)據(jù)需要與嚴(yán)格執(zhí)行時(shí)一致
-
交互式設(shè)備的輸入輸出(stdio.h)需要與嚴(yán)格執(zhí)行時(shí)一致
這給編譯器優(yōu)化提供了非常廣闊的空間
- 也是因?yàn)樘珡V闊, 以至于編譯器里面有很多bug
- 理論上來說, “判斷任意兩個(gè)程序的可觀測行為是否一致”是不可判定的
- 如果這個(gè)問題可判定, 那么借助它可判定圖靈停機(jī)問題(閱讀材料)
例: 常數(shù)傳播
clang -S -foptimization-record-file=- a.c
clang -S -foptimization-record-file=- a.c -O1
加個(gè)volatile試試
#include <stdio.h>
int main() { // compute 1 + 2
int x = 1, y = 2;
volatile int z = x + y;
printf("z = %d\n", z);
return 0;
}
目標(biāo)代碼生成
clang -S a.c
clang -S a.c --target=riscv32-linux-gnu
gcc -S a.c # 也可以用gcc生成
# apt-get install g++-riscv64-linux-gnu
riscv64-linux-gnu-gcc -march=rv32g -mabi=ilp32 -S a.c
將IR狀態(tài)機(jī)翻譯成處理器ISA狀態(tài)機(jī)
- %1, %2, %3, %4, … -> {R,M}
- alloca, store, load, add, call, ret, … -> ISA的指令
- 同時(shí)進(jìn)行目標(biāo)ISA相關(guān)的優(yōu)化
- 把經(jīng)常使用的變量放到寄存器, 不太常用的變量放到內(nèi)存
- 選擇指令數(shù)量較少的指令序列
- 有很多優(yōu)化的空間, 這里不深入討論
可以通過time report觀察clang嘗試了哪些優(yōu)化工作
clang -S a.c -ftime-report
踏入二進(jìn)制的世界
匯編
gcc -c a.c
riscv64-linux-gnu-gcc -march=rv32g -mabi=ilp32 -c a.c
# alias rv32gcc="riscv64-linux-gnu-gcc -march=rv32g -mabi=ilp32"
根據(jù)指令集手冊, 把匯編代碼(指令的符號化表示)翻譯成二進(jìn)制目標(biāo)文件(指令的編碼表示)
二進(jìn)制文件不能用文本編輯器打開來閱讀了
- 需要binutils(Binary Utilities)或者llvm的工具鏈
objdump -d a.o
riscv64-linux-gnu-objdump -d a.o
# alias rvobjdump="riscv64-linux-gnu-objdump"
llvm-objdump -d a.o # llvm的工具鏈可以自動識別目標(biāo)文件的架構(gòu), 用起來更方便
鏈接
gcc a.c
riscv64-linux-gnu-gcc a.c # apt默認(rèn)不安裝rv32的glibc庫, 無法鏈接到glibc, 這里用rv64演示
合并多個(gè)目標(biāo)文件, 生成可執(zhí)行文件
- 哪里來的多個(gè)目標(biāo)文件呢?
讓我們來看日志!
gcc a.c --verbose
gcc a.c --verbose 2>&1 | tail -n 2 | head -n 1 | tr ' ' '\n' | grep '\.o$'
有很多crtxxx.o的文件
crt = C runtime, C程序的運(yùn)行時(shí)環(huán)境(的一部分)
可以通過objdump確認(rèn)
問題: printf()的代碼在哪里呢?
執(zhí)行
./a.out
# 通過一些配置工作, RISC-V的可執(zhí)行文件也可以在本地執(zhí)行
# apt-get install qemu-user qemu-user-binfmt
# mkdir -p /etc/qemu-binfmt
# ln -s /usr/riscv64-linux-gnu/ /etc/qemu-binfmt/riscv64
file a.out
a.out: ELF 64-bit LSB pie executable, UCB RISC-V, version 1 (SYSV)...
./a.out # 實(shí)際上是在QEMU模擬器中執(zhí)行
把可執(zhí)行文件加載到內(nèi)存, 跳轉(zhuǎn)到程序, 執(zhí)行編譯出的指令序列
Q: 誰來加載?
A: 運(yùn)行時(shí)環(huán)境(宿主操作系統(tǒng)/QEMU)
實(shí)現(xiàn)定義行為和ABI
正確做法: 通過日志觀察工具的行為
- 程序相同, 但編譯選項(xiàng)可能影響程序的解釋 -> 看AST!
clang -fno-color-diagnostics -fsyntax-only -Xclang -ast-dump -w -std=c90 -m32 a.c
RTFM: ABI中定義的基本數(shù)據(jù)類型
- System V ABI for i386 - 32位x86 Linux遵循的ABI:https://math-atlas.sourceforge.net/devel/assembly/abi386-4.pdf
- ABI for RISC-V:https://github.com/riscv-non-isa/riscv-elf-psabi-doc/blob/master/riscv-cc.adoc
ABI,全稱為Application Binary Interface,即應(yīng)用程序二進(jìn)制接口。它定義了應(yīng)用程序與操作系統(tǒng)之間進(jìn)行交互的方式和規(guī)范,確保不同的軟件組件能夠正確地協(xié)同工作。ABI包括了函數(shù)調(diào)用約定、寄存器的使用、參數(shù)傳遞方式、系統(tǒng)調(diào)用接口等內(nèi)容,為軟件開發(fā)者提供了一個(gè)穩(wěn)定和一致的編程接口。
為什么還要學(xué)習(xí)C語言?
“使用語言”和 “學(xué)習(xí)計(jì)算機(jī)”的目的不完全相同
-
使用語言的目標(biāo)是提高效率
- 開發(fā)者友好的語言特性, 更抽象也更安全的功能, 各種開箱即用的庫
- 例 - 數(shù)組越界不再是UB, 而是由運(yùn)行環(huán)境馬上報(bào)錯(cuò)
- 這些功能需要更復(fù)雜的翻譯環(huán)境和運(yùn)行環(huán)境來支撐
- 開發(fā)者友好的語言特性, 更抽象也更安全的功能, 各種開箱即用的庫
作為計(jì)算機(jī)的使用者, 我們應(yīng)該掌握1~2門現(xiàn)代語言提升工作效率
python, bash, go, rust, …
- 學(xué)習(xí)計(jì)算機(jī)的目標(biāo)是理解程序如何在計(jì)算機(jī)上運(yùn)行
- 語言越接近底層硬件, 越有利于我們學(xué)習(xí)其中的細(xì)節(jié)
- 例 - ISA狀態(tài)機(jī)沒有數(shù)組越界的概念
- 作為計(jì)算機(jī)的設(shè)計(jì)者, 我們應(yīng)該掌握C語言, 理解計(jì)算機(jī)的基本原理
- 語言越接近底層硬件, 越有利于我們學(xué)習(xí)其中的細(xì)節(jié)
總結(jié)—從C代碼到指令序列
-
預(yù)處理 -> 編譯 -> 匯編 -> 鏈接 -> 執(zhí)行
-
編譯 = 詞法分析 -> 語法分析 -> 語義分析 -> 中間代碼生成 -> 優(yōu)化 -> 目標(biāo)代碼生成
-
學(xué)會使用工具和日志理解其中的細(xì)節(jié)
-
C語言標(biāo)準(zhǔn)中除了確切的行為, 還包含
- Unspecified Behavior
- Implementation-defined Behavior
- Undefined Behavior
-
通過ABI手冊了解Implementation-defined Behavior的選擇文章來源:http://www.zghlxwxcb.cn/news/detail-818621.html
- 同時(shí)認(rèn)識C語言標(biāo)準(zhǔn), 編譯器, 操作系統(tǒng), 運(yùn)行庫, 處理器之間的協(xié)助
- 程序的運(yùn)行結(jié)果與源代碼和上述因素都有關(guān)系
內(nèi)容來自:一生一芯_余子濠_從C語言到二進(jìn)制程序文章來源地址http://www.zghlxwxcb.cn/news/detail-818621.html
到了這里,關(guān)于【0到1的設(shè)計(jì)之路】從C語言到二進(jìn)制程序的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!