和很多程式員一樣,編譯器到目前為止對我還存在一些讓我迷惑的地方。比如編譯器與作業系統的關係,編譯器與CPU的關係,動態連結器從哪尋找共用庫等。讓人困惑的原因有幾,第一是編譯器的功能角色特殊,編譯器是產生程式的程式;第二是編譯過程變得越來越複雜[注],一支編譯器支援多種程式語言、支援共用庫、編譯最佳化,編譯與連結可分開等;第三,作業系統的介入。本文試著從第三點——作業系統介入編譯過程後對編譯器的影響,看看作業系統與編譯器有什麼關係。
註:編譯過程變複雜源於電腦應用變複雜。例如應用程式項目越來越龐大,功能越來越多;為了管理大項目,拆分來源程式檔案為多個;為了提高程式的效能,目標程式檔案的連結期被延遲到運行前;為了程式的靈活安裝和升級,引入各種指令碼工具,如make,configure(shell)。
編譯器
我們看看編譯器的一種傳統定義:
編譯器是一支將抽象度較高的程式設計語言程式(也稱來源程式)【轉化】成抽象度較低的程式設計語言程式(也稱目標程式)的【程式】[注]。抽象度的兩端分別是機器語義和人理解語義。【處理器體系】和【程式設計語言】是一支編譯器的根本屬性。
註:以下僅使用【程式】一語,“器”是一種形象的比喻,不夠嚴謹;而軟體(software)是產品性的程式,最好只用在商業語境中。
以上對編譯器的定義描述在【現代意義下】對全面認識編譯器是不夠的,因為它沒有涉及作業系統,沒有涉及現代的複雜的程式構建過程。有一定開發經驗的程式員都知道,程式的“編譯過程”包括編譯、連結(靜態連結與動態連結)、調試,還可能包括組態配置和安裝兩步。“編譯”一詞已經不能很好描述這個過程。整個過程可稱為【程式構建】,而編譯只是第一步,在這一點上,編譯器在傳統意義上與現代意義上產生了不同。由於本文試圖討論編譯器與作業系統的關係,為了避免產生歧義,本文的【編譯器】包括編譯和靜態連結兩個部分,動態連結部分有點特殊,後面會提到它的角色。
下面我們給出有關【作業系統與編譯器關係】的三個問題,並試圖回答它們:
·第一,編譯器與作業系統的關係是什嗎?
·第二,編譯器對作業系統有依賴嗎?
·第三,編譯器與CPU的關係又是什嗎?
·第四,作業系統對C標準庫與C編譯器的關係有什麼影響?
為了更好的進行下一步討論,我先給出現代編譯器比較完整的定義,並由定義引出問題:
現代編譯器是一支將某抽象度較高的程式設計語言程式【轉化】為運行在【某軟硬體系下】的抽象度較低的程式設計語言程式的【程式】。所謂硬體體系是指處理器體系,軟體體系指作業系統體系。
要回答前三個問題,我們得釐清現代編譯器定義中的【屬概念】——程式,並對作業系統有更深一層認識。
程式
程式的分類是多種多樣的,比如常見的兩分法是【系統程式】和【應用程式】。這是一種粗粒度的按【計算任務】不同的分法。我們看程式的定義:
程式(program)是完成特定【計算任務】的【指令】序列,指令由相應的【圖靈機】讀取並操作。
由以上對程式的定義可知,還可根據——程式【指令的性質】和讀取程式的【圖靈機】性質——兩個標準再進一步對程式分類。比如,按指令序列是否連續可以分為獨立程式和共用程式(使用了共用庫);按指令的抽象度可分為進階語言程式和低級語言程式。按【圖靈機】的體系可分為X86程式和ARM程式,16位程式和32位程式等。
進階語言程式是不是【程式】?如果是,它的【圖靈機】是什嗎?
我們一般理解下的【程式】是指二進位的可執行檔,那麼進階語言的來源程式是不是程式?從指令序列的定義看,【進階語言的來源程式】是程式,因為【進階語言的來源程式】與【二進位的可執行檔】一樣,也是指令序列,只不過【進階語言的來源程式】的【圖靈機】不是CPU,也不是編譯器或解譯器,而是程式員。【進階語言的來源程式】的功能更多體現在程式員間的相互學習和交流。
除了以上基本分類外,現代的程式還會受為其提供虛擬運行環境的作業系統影響,可以根據作業系統的體系屬性對程式再分類,例如win32程式,linux程式。
作業系統
作業系統是什麼類程式?
作業系統是一類比較獨立的系統程式,作業系統有支援各種【圖靈機】的體系類型,比如16位DOS,32位Windows,X86的BSD,ARM的 Linux等。而系統程式一般是指一支為應用程式直接提供半成品(為應用程式提供執行的虛擬環境)和協調多個應用程式並行啟動並執行程式。所謂半成品是指,系統程式的一部分(指令序列)也是應用程式的一部分(指令序列),但這部分程式不專屬任何應用程式,它是共用的。例如各種新硬體的驅動程式、C標準庫函數、POSIX庫函數等。而作一個協調程式,作業系統表現出與一般應用程式的程式性,如獨立調度的線程,只是它們運行在權力更高的狀態下。協調程式如線程發送器。
非作業系統程式與作業系統的關係
這裡的作業系統泛指像Linux這樣的現代32位作業系統,而【非作業系統程式】運行在作業系統之上,對作業系統存在可能的依賴的程式。
其實只要是運行在某作業系統之上的程式都會烙上該作業系統的印,對作業系統有依賴,包括編譯器。不過這些程式對作業系統的依賴程度和依賴的內容確實有很多區別。例如一支最簡單的【Hello world程式】都會對【作業系統的C庫】產生依賴,如果去掉【Hello world程式】的輸入輸出功能,只作加減或邏輯運算,【Hello world程式】依然會對作業系統有少量依賴,因為【Hello world程式】由運行在該【作業系統上的編譯器】編譯的,有特定的目標檔案格式,並由該【作業系統的載入程式】載入記憶體運行[注]。這種只【在形式上】對OS存在依賴的“無用”程式可謂是最獨立於OS的程式。在此基礎之上,其它程式都對OS有不同程度的依賴,依賴表現在對OS內的各種程式庫的依賴,比如C標準庫,POSIX系統庫,線程庫、網路程式庫和其它基於這些基礎庫的第三方應用程式碼程式庫。
註:由此可見編譯器與引導程式、SHELL程式一樣,是現代作業系統的基本部分。
問題初步解決
編譯器與作業系統的關係
有了以上的對程式以及作業系統本質的一定瞭解後,我們知道編譯器與作業系統有一定親緣性。但這種親緣性的一些表現會讓人迷惑。例如Linux發行版可以不安裝有編譯器的,只有開發工作站才需要編譯器。而所有Linux發行版的應用程式都可能使用了共用庫,需要動態連結這些系統共用庫。由此可見,應該分開【開發期】與【運行期】來看待編譯器與作業系統的關係。在開發期,編譯器運行作業系統之上,屬於【非作業系統程式】,對作業系統有依賴;在運行期,編譯器的子部分——動態連結程式和載入程式屬於作業系統有機部分。
由以可得編譯器與作業系統的關係有:
·第一,編譯器的編譯部分和靜態連結部分是運行在作業系統上的系統程式;
·第二,編譯器的動態連結部分與作業系統的親緣性更強,所以完全可把動態連結部分獨立出來[FIXME:動態連結程式與作業系統具體關係未知];
·第三,編譯器的編譯輸出格式是作業系統相關的。
由此可見,編譯器是作業系統相關的,編譯器也是作業系統的功能很重要組成部分,但編譯器沒有被整合入作業系統核心內,所以編譯器不算是作業系統的有機組成部分。
編譯器對作業系統的依賴
由上面可得,編譯器是運行作業系統之上【非作業系統程式】,對作業系統有依賴。編譯器是一支【計算集中】更大的程式,它相對於應用程式對OS依賴會少一些,依賴有:
·形式依賴(由另一支同軟硬體系的編譯器[行話本地編譯器],編譯得到或不同軟硬體系的編譯器[行話交叉編譯器],交叉編譯得到)
·C庫依賴,讀取進階語言源碼程式檔案,寫入低級語言的目標檔案
編譯器與CPU的關係
這個問題在編譯器的定義裡已經有答案了,一支編譯器只編譯產生一種機器碼。我們說編譯器的【作業系統相關性】是後天進化得到的,而編譯器的【處理器相關性】是天生的。
作業系統對C標準庫與C編譯器的關係的影響
作業系統對C庫沒有什麼影響,C庫是一種通用程式碼程式庫。是給使用者編程提供的介面,程式員只需要和這些介面打交道就可以了,而不需要知道具體怎麼實現的。c庫中的有些功能是c庫代碼本身實現的,也有一些是利用作業系統實現的。