cygwin编译生成hello world_RISC-V 入门 Part4: 编译、链接、加载
我們介紹了 RISC-V 的指令,你可以當作介紹了匯編語言。但是,我們現在知道的是:
- RV32I 的格式都是 32bit 的
- 以上內容可以以 beq 等格式讓讀者可讀,但是機器執行的還是那6種格式的代碼
我們也了解了 RISC-V 的 calling convention 和 ABI, 這一節介紹程序的編譯、鏈接、加載。基礎知識可以閱讀 CSAPP 第七章和 https://zhuanlan.zhihu.com/p/125163040 我之前寫的垃圾。不過今天我寫的會細一些。
編譯
編譯由 .c 轉為匯編語言,形式是 .s, 這個我們之前都用過了
? riscv64-unknown-elf-gcc -march=rv32imac -mabi=ilp32 -S這樣體驗一把就行。Compiler 需要走:
- Lexer: 語法分析,把目標轉成 token. 實際上可以借用 Lex 工具,而 Flex 是 Lex 的一個實現。
- Parser: 將內容變為 AST
- Semantic Analysis and Optimization: 檢查 AST, 然后做一些優化
- Code generation: 做寄存器 allocation, 代碼生成
實際上,我們生成 -S 也需要指定編譯選項,能指定 -O。這里我們可以得到可靠的 RISC-V 的代碼。它和我們的程序是邏輯上等價的,當然可能要進行一定的優化。
注意,在 RISC-V 中,編譯是會產生便于理解的偽指令的。
匯編
將匯編語言生成 ELF 的 object file, object file 屬于 machine language 了。
ELF 文件包括:
- ELF Header, 以一個 16byte 的序列開始,描述系統 word 大小、字節順序等
- .text text segment, 編譯程序的機器代碼
- .data 已初始化的 global/static C variable, 即源代碼的 static 部分。
- Local 是在 stack 中的
- 未初始化的、被初始化為 0 的,在 .bss 中。它不占據實際空間,有點類似 gcc 的 __weak__ .
- .symtab ,符號表,存放定義、引用的函數、全局變量和不可被 reference 的 static 變量
- .debug 調試符號表,包含原始文件; .line 同樣,包含行號和 .text 的映射。只有 -g 編譯才會產生
- .strtab 字符串表,包含定義的 string 和 section 的名字。
ELF 具體信息可以看:
那么我們還要注意,有的 directions, 即匯編指示符,會被丟給鏈接器,但不產生什么代碼
同時,在鏈接的時候,會完成偽指令的替換,把它們全部替換成具體的指令(這里可以表示 RV32I 這樣的)。
我們來看看 hello world:
#include <stdio.h> int main() {printf("Hello, %s", "world");return 0; }在 RISC-V Book 上,它生成了如下的匯編:
其中圖 3.6 中用到的指示符有:
- .text:進入代碼段。
- .align2:后續代碼按22字節對齊。
- .globl main:聲明全局符號“main”。
- .section .rodata:進入只讀數據段
- .balign4:數據段按4字節對齊。
- .string “Hello, %s!n”:創建空字符結尾的字符串。
- .string “world”:創建空字符結尾的字符串。
tail call optimization
fn:return foo(x)這個時候,正常行為應該是:把 a0 設置 x, 然后返回調用后的 a0, 即:
- 然后用 j 或者 tail 直接調用 foo(y) ,這玩意會做個保存,把本函數的 ra, sp 保存,這樣跳轉的話就可以直接跳轉到 fn 的調用者。
在 RISC-V 偽指令中,有一條 tail, 會被解釋成
auipc x6, offset[32:12] jalr x0, x6, offset[11:0]用 tail 和 j 來完成上述的 jump,而不再調用前再去設 sp/ra
這個行為讓我有些頭暈,我在網上找到了這篇 blog: https://www.sifive.com/blog/all-aboard-part-3-linker-relaxation-in-riscv-toolchain
上面的鏈接倒是介紹的比較清楚。
生成機器代碼
我們描述過 ELF 格式了,我們有下列幾個問題
壓縮指令
RV32C 支持壓縮指令:
- 每條短指令長度為 16bits
- 必須和 32bits 指令一一對應
- 只對匯編器和連接器可見,并且是否以短指令取代對應的寬指令由 它們決定。編譯器編寫者和匯編語言程序員可以幸福地忽略 RV32C 指令及其格式,他們能 感知到的則是最后的程序大小小于大多數其它 ISA 的程序。
RV32C 如上
那么,考慮壓縮指令,會有下列問題:
So the presence of the 16b instructions doesn't need to be known to anybody but the assembler and the RISC-V processor itself!Forward Reference
即假設 L1 --> L2, 但 L1 在 L2 之前,那么這暗示編譯器需要一張局部的符號表,并掃描不止一個 pass,來完成這個操作。
jal/la static 加載
jal 會跳轉一個 imm, 而 static 變量加載中,可能對應的符號來自另一個文件定義的內存中。
Tables
為了解決上述問題,有了 symbol table 和 relocation table:
symbol table 展示可能被其他文件用到的本文件符號,例如 function call 的 label, 和 .data 中可以被外部訪問的符號。
Relocation Table 展示 jal 和 la 中需要重定位的地址。
鏈接
講 ELF 的 objective code 轉化為可執行文件,這一過程被稱為 linking, 這一過程有邏輯上如下的流程:
- 從 .o 文件把 text segment 合在一起
- 拿到 data segment, 拼接到一起
- resolve reference, 解決掉跨文件的符號、依賴問題,用絕對的地址填充
實際上,beq bne jal 這類 PC-relevant 的指令不會被 relocate, 而用 name/label 相關的和 static 的會 relocate.
加載
通常 OS 會加載、運行程序:
它需要:
全流程
#include <stdio.h> int main(int argc, char* argv[]) {int i;int sum = 0;for (i = 0; i <= 100; i++) sum += i * i;printf("The sum of sq from 0 .. 100 is %dn", sum); }編譯后生成:
assembly 的時候處理偽指令:
assembly 的時候生成 symbol table 和 relocation table:
以上的信息在鏈接的時候一起使用。
動態鏈接庫和靜態鏈接庫
對于靜態庫而言,它是可執行文件的一部分,庫更新了,運行中的程序需要重新編譯。這是編譯時鏈接的。
在 Linux 下,提供了 .a 文件,用于處理,單個文件即使沒有用到所有部分,也需要全部加載。
在動態鏈接庫中,允許編譯時、運行時鏈接。
共享庫有 .so 文件,引用庫的用戶可以共享這些數據,而在內存中,共享庫的.text 可以共享內存,被多個進程使用:
CSAPP 中,指導可以在 dlfcn.h 中使用該功能。
此外,為了共享,需要處理位置無關代碼(Position-Independent Code, PIC)。這需要 -fPIC 選項。
它在編譯時成功設置一個便宜量,并在運行時不改變這個便宜量,讓代碼能夠運行便宜量上的 .text
而 PLT 條目類似懶惰加載,作為鏈接的表來完成工作:
總結
以上是生活随笔為你收集整理的cygwin编译生成hello world_RISC-V 入门 Part4: 编译、链接、加载的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: cfile清空文件内容_编译-链接-加载
- 下一篇: spring boot web项目_Sp