文中講述的原理是推理和探討 , 和現實中的實現不必定徹底相同 。html
編譯原理 給人的感受好像一直都比較晦澀, 其實 編譯原理 沒那麼難啦 。函數
編譯原理 分 3 步 :spa
1 語法分析 (文法分析)操作系統
2 生成 目標代碼htm
3 連接 (Link)blog
關於 語法分析, 能夠參考我以前寫的一個項目 《SelectDataTable》 http://www.javashuo.com/article/p-qbebfnqj-hd.html , 能夠解析簡單的 Sql 語句, 用 Sql 查詢 DataTable 裏的資料 。遞歸
生成目標代碼 的 部分, 仍是能夠參考 SelectDataTable …… 哈哈哈, SelectDataTable 裏並無生成 目標代碼, SelectDataTable 解析 Sql 的結果是 一個 表達式樹, 經過 遞歸執行 表達式樹, 來獲得 Sql 的 執行結果 。接口
對於 生成目標代碼, 能夠 遞歸執行 表達式樹 來 產生 目標代碼 。進程
固然, 要產生 具體 的 目標代碼, 須要對 硬件 操做系統 彙編 熟悉 。開發
可是, 若是咱們從 廣義的角度 來 定義 編譯原理 的 話, 也許不須要了解 硬件 操做系統 彙編 也能夠實現一個 編譯器 哦 ~ ~ !
好比, 若是 目標代碼 是 .Net IL , 那麼, 只要 瞭解 .Net 平臺就能夠了 。
又如, 若是 目標代碼 是 C#, …… 那麼, 只要瞭解 C# 就能夠了 。 ^^
事實上, 我提議過 用 C 語言做爲 中間語言 來 開發一個 泛語言(跨語言)的 編譯器 。 參考 《我發起了一個用 C 語言做爲中間語言的編譯器項目 VMBC》 http://www.javashuo.com/article/p-djxdwein-hp.html 。 VMBC 是受到 LLVM 的啓發而產生的想法 。
用 C 語言做爲中間語言 來 開發編譯器, 意味着 能夠用 C 語言 做爲 目標代碼 語言 。
對於 第 3 部分, 連接, 傳統的教科書是這樣說的 「目標代碼還須要和外部的一些 庫 連接 ……」, 我一直都搞不懂這個 外部的一些庫 是什麼, 不過就在前不久終於明白了 。 外部的一些 庫 包括 3 類 :
1 操做系統原語(或者叫 系統調用)
2 基礎庫 (就像 .Net 裏的 System.XXX, 固然 操做系統 提供的 基礎庫 可能比較原始)
3 第三方 庫, 這個就很容易理解了, 程序引用的 DLL 什麼的
連接, 本質上就是 填入調用方法的 入口地址, 或者 按格式 留下空位, 在運行時根據動態加載的 庫 (好比 DLL), 填入 調用方法 的 入口地址 。
前者 是 靜態連接, 後者是 動態連接 。固然 靜態連接 還分爲 2 種狀況, 一種只是 填入 入口地址, 一般這些 入口地址 是 固定的, 好比 操做系統 的 底層 API, 一種是把 庫 的代碼也包含進入到 目標代碼 裏, 做爲 可執行程序 的 一部分 。
實際上 一般講 的 靜態連接 是指將 庫 的代碼 包含到 目標代碼 裏。
上面說的, 在 編譯時就填入 庫 的 調用方法 的 入口地址 。
等, 咱們先 捋 一下, 否則有點亂 。
實際上咱們說了 3 種狀況 :
1 在 編譯 時 將 調用方法 的 入口地址 填入 目標代碼,
2 在 編譯 時 將 庫 的 代碼 包括進入到 目標代碼 裏
3 在 編譯 時 按 規則 生成 調用 庫 方法 的 代碼, 固然 也 預留 了 可填入 庫 方法 入口地址 的 空位空間, 在 運行時 告知 操做系統 要 連接 的 庫, 操做系統 返回 庫 的 入口地址, 程序 將 入口地址 填入 預留 的 空位 中, 今後 能夠 經過 編譯時 生成的 調用 庫 方法 的 代碼 調用 庫方法 。
這裏說的 庫 的 入口地址 是一個 籠統的 概念, 前面咱們提的是 調用方法 的 入口地址, 這裏又變成了 庫 的入口地址 。
調用方法 的 入口地址 = 庫的入口地址 + 方法在庫裏的偏移量
方法在庫裏的偏移量 是在編譯時能夠肯定的, 根據 庫 的 元數據文件 能夠知道 方法 在 庫裏的偏移量 。 庫 的 元數據文件 能夠是 DLL 自己, 也能夠是 DLL 之外的一些文件, 只包含 接口 的 元數據 文件, Win 32 下我記得有好幾種 非 DLL 的 文件 能夠做爲 接口元數據 文件, 擴展名我記不得了 。 這些 接口 元數據 文件 就相似 C / C++ 語言 裏的 頭文件(Head File), 只包含 接口 的 定義, 不包含具體的實現代碼 。
對於 狀況 1, 這種狀況 實際中可能並不太可能使用 。 這種方法 不像 狀況 2 同樣 將 庫 的 代碼 包括 到 目標代碼 裏, 也不像 狀況 3 同樣在運行時才由 操做系統 告知 庫 的 地址, 實際上, 狀況 1 比較像是 早期 的 作法, 透着 原始 樸素 實驗室 的 氣質 。
要使用 狀況 1 的方式, 一般比較適用的是 操做系統原語 和 基礎庫, 但即使如此, 在 操做系統 的 不一樣版本 和 世代 之間 要 保持兼容性 也不容易 。
所謂世代, 好比 Win7 , Win8 , Win10 , 這樣是 3 個世代, 版本的話, 好比 Win7.1 , Win7.2 , Win7.3 這樣是不一樣的版本 。
要在 不一樣版本 和 世代 之間都保持 操做系統 內核庫 的 庫 地址不變, 這個可能比較勉強 。 在 實驗室 的 時代 卻是應該能夠 。
因此, 如今實際在用的, 應該是 狀況 2 和 狀況 3, 這 2 種 方式 也就是如今所說的 「靜態連接」 和 「動態連接」 。
咱們來做一個假設, 系統調用 分爲 2 種, 一種是 跨進程的, 另外一種是 不跨進程的 。 跨進程的 就是要 切換到 系統進程, 不跨進程的 不須要切換到系統進程, 至關因而調用了一個 函數 。
對於 跨進程 的 狀況, 須要設置一個系統中斷, 經過 中斷 來切換到 系統進程, 不跨進程的, 就是調用一個 庫函數 。
但實際上, 對於 跨進程的狀況, 也能夠經過調用 庫函數 的 方式 來完成, 在 庫函數 裏由 庫函數 來 設置 系統中斷 。
這樣的話, 問題就歸結到 調用 庫函數 了 。
接下來, 庫函數 應如何調用呢 ?
操做系統 應該 制定一個 規則, 讓 程序 和 庫 遵照, 就是 調用函數 的 規則 。
函數 如何 調用 ?
函數 就是 堆棧(Stack) 。 假設 CPU 有 3 個 寄存器 A B C , 那麼, 用 A 來保存 棧頂, B 來保存 本次函數調用 在 棧 裏的 開始地址(也能夠叫 基址), 那麼, 就能夠開始 函數調用 了 。
個人理解是, 棧底 是 棧 固定的一端, 棧頂 是 棧 活動的一端, 能夠 壓入(Push) 和 彈出(Pop) 數據 的 一端 。
假設 棧底 的 地址 是 100,
當 第一個函數調用 開始時, 向 棧 裏 壓入 參數 和 局部變量, 假設這些 壓入 的數據 佔用了 10 個 字節, 那麼, 此時, 棧頂(地址) 是 110, 本次調用 的 基址 是 100 + 0 = 100 。 直觀的來看, 第一個函數調用 佔用 的 棧空間 是 100 - 109 這段 地址空間 。
當 第二個函數調用 開始時, 一樣向 棧 裏 壓入 參數 和 局部變量, 假設壓入的 數據 佔用了 20 個 字節, 那麼, 此時, 棧頂 是 130, 本次調用 的 基址 是 上次調用 的 棧頂, 即 第一次調用 的 棧頂 110 。 直觀的來看, 第二個函數調用 佔用 的 棧空間 是 110 - 129 這段 地址空間 。
以此遞推 。
若是 棧頂 超過了 堆棧 的 最大 Size, 就會 拋出 「StackOverflow」 的 異常 。
這就是 函數 的 調用方法, 也是 程序 和 庫 要 共同遵照 的 規則 。 程序 和 庫 共同遵照 了 這個 規則, 程序 就能夠 調用 庫, 庫 也能夠調用 其它 庫 。
但 要在 操做系統 和 各類語言 的 編譯器 之間 都 遵照這個 規則, 可能 不太現實 。 好比 操做系統 和 各類語言 的 編譯器 編譯 函數調用 的 時候 都 處理爲 寄存器 A 存 棧頂, 寄存器 B 存 調用基址, 這個 太死板, 實際中 很難統一 。
因此, 咱們還有 方案二 。 ^^
方案二 其實 是 方案一 的 擴展版 。
就是 在 調用 庫 的時候, 把 當前函數 的 棧頂 和 調用基址 傳給 庫, 實際上 也是 做爲 參數 傳給 庫, 這樣能夠和 其它參數 同樣, 在 堆棧 裏 保存起來 。 接下來 的 執行 就交給 庫, 庫 一樣 將 參數 和 局部變量 壓入 棧, 同時將 新的 棧頂 和 調用基址 存入 寄存器, 庫 能夠按照本身的 規則 來將 棧頂 和 調用基址 存入 寄存器, 好比 能夠 用 寄存器 C 來 保存 棧頂, 寄存器 D 來 保存 調用基址 。 當 庫函數 調用完成, 返回 主程序 時, 將 做爲參數 保存在 堆棧 裏的 主程序函數 的 棧頂 和 調用基址 返回給 主程序, 主程序 將 棧頂 和 調用基址 按本身的 規則 保存回 寄存器, 好比 寄存器 A 存 棧頂, 寄存器 B 存 調用基址, 而後就能夠 繼續 執行後面的代碼了 。
因此, 現代操做系統 的系統調用, 大概 都是 基於 動態連接庫, 而 動態連接庫 的 調用過程, 就是 上述 的 過程 。
固然, 也可能使用 靜態連接, 上述過程 對 靜態連接 也適用 。
同時, 除了 系統庫 之外, 上述過程 對 第三方 庫 的 調用 也適用 。
因此, 上述過程, 也是 編譯器 要處理的 連接 的 過程 。