程序編譯與鏈接的原理
『壹』 編譯原理及步驟有哪些呢
編譯原理及步驟主要包括以下幾點:
詞法分析:
- 步驟描述:源代碼被讀取並轉換為抽象語法樹之前的第一個步驟。
- 核心任務:將源代碼中的字元序列轉換為標記序列,這些標記代表了源代碼中的詞法單元。
語法分析:
- 步驟描述:通過解析抽象語法樹,確保源代碼遵循語言的語法規則。
- 核心任務:檢查源代碼的結構是否符合語言的語法定義,構建出符合語法規則的語法樹。
語義分析:
- 步驟描述:在語法分析之後進行,對語法樹進行進一步的檢查和處理。
- 核心任務:使用子表達式的類型標注語法樹,檢查賦值、函數調用等操作是否具有語義上的意義,如類型匹配等。
優化:
- 步驟描述:對語法樹進行修改,以提升程序的執行效率。
- 核心任務:通過各種優化技術對語法樹進行優化,以提高代碼的性能,但不改變程序的執行結果。
目標代碼生成:
- 步驟描述:將優化後的語法樹轉換為目標代碼,如機器語言或位元組碼。
- 核心任務:生成可在目標機器上執行的代碼,包括指令選擇、寄存器分配、指令調度等過程。此外,還包括將生成的目標代碼與庫文件鏈接,以創建可執行文件或庫。
綜上所述,編譯原理涉及從詞法分析到目標代碼生成的多個步驟,每個步驟都有其特定的任務和目標,共同確保源代碼能夠被正確、高效地轉換為可執行代碼。
『貳』 編譯器概述
編譯器概述
編譯器是一種重要的系統軟體,其核心功能是將高級語言編寫的程序轉換成等價的低級語言(目標語言)編寫的程序。以下是對編譯器的詳細概述:
一、編譯器的基本概念
編譯器負責將高級語言(如C/C++、Java、Python等)編寫的源代碼轉換成低級語言(如匯編、位元組碼或機器碼)的程序。這種轉換過程確保了高級語言的抽象性和可讀性能夠被轉換成計算機能夠直接執行或解釋執行的代碼。
- 高級語言:包括C/C++、Java、Python等,這些語言提供了豐富的語法和語義,使得程序員能夠用更抽象的方式表達計算過程。
- 低級語言:
匯編:一種低級的、人類編程語言,使用助記符和符號來表示計算機的指令集。
機器碼:計算機能夠直接在硬體上執行的二進制代碼,由0和1組成,代表著特定的CPU指令含義。
位元組碼:一種中間代碼表示形式,通常不直接在硬體上執行,而是在虛擬機或解釋器上運行。
從廣義上看,編譯器也可以被定義為將一種語言構造的程序翻譯為等價的另一種語言構造的程序,例如將Java語言程序翻譯成C語言程序,或將C語言程序翻譯成Rust語言程序。
二、編譯器的歷史發展
- 1946年:世界上第一台計算機誕生,隨之第一代編程語言誕生,即二進制語言。
- 1946~1950年:第二代編程語言(匯編語言)誕生,隨之誕生了匯編器,從廣義上來說,這也算是一種編譯器。
- 1956~1957年:第三代編程語言(高級語言)誕生,1957年第一個商用編譯器「Fortran編譯器」誕生。
- 20世紀60年代:誕生了許多新的編程語言,如Pascal、Cobol、Algol、PL/I等,編譯器得到了迅速的發展。
- 20世紀70年代:Dennis Ritchie開發了C編程語言,隨之第一個C語言編譯器發布,這個編譯器將C代碼編譯成PDP-11計算機上的機器代碼。
- 20世紀80年代:編譯器技術進一步演進,包括了對編譯器優化的研究,以提高程序的性能。
- 從20世紀80年代末到21世紀:眾多現代高級編程語言出現,如C++、Java、Python、Ruby等,編譯器再一次得到了迅猛的發展,出現了類似如JIT等編譯技術。
- 現代編譯器:隨著硬體和編程語言的不斷發展,編譯器技術也不斷演進,以支持多核處理器、並行計算、GPU編程等新興技術。近幾年,隨著深度學習、機器學習的發展,AI編譯器也得到了快速的發展。
三、編譯器的基本結構
編譯器的基本結構可以分為前端、中端和後端。
前端:主要負責理解程序的語法形式和內容,即程序的語法和語義。前端會檢查源程序代碼是否符合正確的語法和語義,如果檢查通過,它會給該程序建立一個中間表示代碼。前端一般可分為詞法分析、語法分析、語義分析和中間代碼生成四個階段。
中端:現代編譯器引入的結構,主要使用合適的優化來提高代碼質量,並將優化後的程序輸出給後端進行進一步編譯。這里的「提高」可以有多種含義,通常指執行速度更快、運行時耗費資源較少、佔用的內存空間較小等。中端優化並不是越多越好,可能會出現互斥的情況,如優化執行速度有可能會導致佔用的內存空間變大,所以優化需要根據具體目標進行優化。
引入中端的目的主要有兩個:一是前端和後端解耦,即多個語言的前端都可以生成公共的中端代碼,而中端代碼又可以進一步生成不同目標機器的代碼;二是實現通用優化,在中端上可設計並實現與語言以及目標機器無關的程序優化演算法,對程序的性能、規模或其它指標進行通用優化。
後端:主要任務是讀入程序中間表示形式,並生成目標機器的指令代碼。後端一般可分為指令選擇、指令調度、寄存器分配以及代碼發射四個階段。
四、編譯器框架
現代主流開源的編譯器框架主要有GCC和LLVM。
GCC:經過多年的開發和測試,GCC已經成為一個成熟且穩定的編譯器。它支持多種編程語言,如C、C++等,並在各種平台上得到了廣泛的驗證和使用。GCC的架構相對一體化,代碼之間的耦合度較高,因此擴展難度相對較大。
LLVM:LLVM是一個高度模塊化的編譯器框架,成為了許多編程語言的首選編譯器基礎設施。與GCC相比,LLVM的架構更加靈活,擴展難度相對較小。然而,LLVM的文檔更新速度相對較慢,在某些方面相對於GCC的成熟度較低。
以下是GCC和LLVM的架構圖:
在編譯器框架中,除了編譯器本身外,還包括預處理器、匯編器、鏈接器和載入器等組件。這些組件共同協作,完成從源代碼到可執行程序的整個編譯過程。
- 預處理器:負責將源程序聚合在一起,並處理頭文件包含和宏等工作。
- 匯編器:將編譯器生成的匯編語言程序進行處理,生成可重定位的機器代碼。
- 鏈接器:將多個可重定位的機器代碼文件以及庫文件鏈接到一起,形成真正能在機器上運行的二進制代碼。
- 載入器:負責將可執行程序從存儲介質載入到計算機的內存中,以便執行程序。
五、總結
編譯器是計算機系統中不可或缺的重要組件,它負責將高級語言編寫的程序轉換成計算機能夠直接執行或解釋執行的代碼。隨著硬體和編程語言的不斷發展,編譯器技術也在不斷演進,以支持更多的特性和優化。了解編譯器的基本概念、歷史發展、基本結構和編譯器框架,有助於我們更好地理解計算機系統的運作原理,並優化我們的程序代碼。
『叄』 linux configure、 make、 make install編譯原理
Linux中configure、make、make install編譯原理
在Linux系統中,從源代碼編譯和安裝軟體是一個常見的任務。這個過程中,configure、make和make install是三個關鍵的命令。以下是它們各自的作用和背後的原理:
1. configure
作用:在編譯軟體之前,configure腳本用於檢測系統的各種特性,如操作系統、編譯器、庫文件等,以確保軟體可以在當前系統上成功編譯和運行。
原理:configure腳本通常由autoconf工具生成,它包含了大量的檢查項和條件語句。當執行./configure命令時,腳本會運行這些檢查項,並設置相應的編譯參數和變數。這些參數和變數會被保存到Makefile文件中,供後續的make命令使用。
配置參數:
- --build=BUILD:指定執行代碼編譯的主機系統,一般默認為當前系統。
- --host=HOST:指定軟體運行的系統平台,用於交叉編譯時指定目標系統的工具鏈前綴。
- --target=TARGET:僅在建立交叉編譯環境時用到,指定新編譯的編譯器將運行在哪個系統上。
- CC:指定C編譯器命令,如交叉編譯器。
- --prefix=PREFIX:指定軟體安裝後的目錄。
2. make
作用:make是一個自動化構建工具,用於根據Makefile文件中定義的規則來編譯和鏈接源代碼,生成可執行文件或庫文件。
原理:Makefile文件描述了構建軟體所需的所有步驟和依賴關系。當執行make命令時,它會讀取Makefile文件,並根據其中的規則來執行相應的命令(如編譯、鏈接等)。這些規則可以包括編譯器的選項、鏈接器的選項、需要包含的源文件等。
執行機制:make命令在執行時,會檢查源文件的時間戳和目標文件的時間戳。如果源文件比目標文件新(或者目標文件不存在),那麼make就會重新編譯該源文件;否則,make就會跳過該步驟,以節省時間。
3. make install
作用:make install命令用於將編譯生成的可執行文件、庫文件、配置文件等復制到指定的安裝目錄(通常由configure命令的--prefix選項指定)。
原理:當執行make install命令時,make會讀取Makefile文件中定義的安裝規則,並將編譯生成的文件復制到指定的目錄。這些規則可以包括文件的復制、許可權的設置、鏈接的創建等。
注意事項:在執行make install之前,通常需要先執行make命令來確保源代碼已經被成功編譯。
總結:
- ./configure的作用是檢測系統配置,生成Makefile文件,以便可以用make和make install來編譯和安裝程序。
- ./configure是源代碼安裝的第一步,主要的作用是對即將安裝的軟體進行配置,檢查當前的環境是否滿足要安裝軟體的依賴關系。
- make根據Makefile中的規則編譯源代碼。
- make install將編譯生成的文件復制到指定的安裝目錄。
這三個命令共同構成了Linux系統中從源代碼編譯和安裝軟體的標准流程。通過合理使用這些命令和配置參數,用戶可以在不同的系統平台上靈活地編譯和安裝所需的軟體。
『肆』 編譯原理基本概念
編譯原理的基本概念包括以下幾點:
編譯原理的定義:編譯原理涉及計算機程序設計中的重要概念,主要關注一種語言到另一種語言的轉換過程,這個轉換由專門的計算機程序——編譯器來完成。
編譯器的核心任務:接收源代碼並轉換為目標程序,即機器可讀的形式。
源程序與目標程序:
- 源程序:原始的編程語句,是程序員使用的語言形式,包含復雜的邏輯和功能,但無法直接被計算機理解。
- 目標程序:編譯器將源程序轉換後的形式,更為機器可讀,可以直接在特定的計算機硬體上執行。
編譯過程:
- 輸入:源程序被輸入到編譯器中。
- 處理:編譯器通過一系列復雜的處理步驟,包括詞法分析、語法分析、語義分析和代碼生成等。
- 輸出:生成的目標程序可以直接在特定的計算機硬體上執行。
編譯原理的重要性:
- 確保高級語言的代碼能夠高效、准確地被計算機執行。
- 使程序員可以更專注於問題的邏輯設計,而非底層的指令操作。
編譯原理課程的內容:
- 包括語言和文法、詞法分析、語法分析、語法制導翻譯、中間代碼生成、存儲管理、代碼優化和目標代碼生成等。
編譯原理課程的意義:
- 雖然只有少數人從事編譯方面的工作,但編譯原理課程在理論、技術、方法上都對學生提供了系統而有效的訓練,有利於提高軟體人員的素質和能力。
『伍』 編譯原理
C語言編譯過程詳解
C語言的編譯鏈接過程是要把我們編寫的一個C程序(源代碼)轉換成可以在硬體上運行的程序(可執行代碼),需要進行編譯和鏈接。編譯就是把文本形式源代碼翻譯為機器語言形式的目標文件的過程。鏈接是把目標文件、操作系統的啟動代碼和用到的庫文件進行組織形成最終生成可執行代碼的過程。過程圖解如下:
從圖上可以看到,整個代碼的編譯過程分為編譯和鏈接兩個過程,編譯對應圖中的大括弧括起的部分,其餘則為鏈接過程。
一、編譯過程
編譯過程又可以分成兩個階段:編譯和匯編。
1、編譯
編譯是讀取源程序(字元流),對之進行詞法和語法的分析,將高級語言指令轉換為功能等效的匯編代碼,源文件的編譯過程包含兩個主要階段:
第一個階段是預處理階段,在正式的編譯階段之前進行。預處理階段將根據已放置在文件中的預處理指令來修改源文件的內容。如#include指令就是一個預處理指令,它把頭文件的內容添加到.cpp文件中。這個在編譯之前修改源文件的方式提供了很大的靈活性,以適應不同的計算機和操作系統環境的限制。一個環境需要的代碼跟另一個環境所需的代碼可能有所不同,因為可用的硬體或操作系統是不同的。在許多情況下,可以把用於不同環境的代碼放在同一個文件中,再在預處理階段修改代碼,使之適應當前的環境。
主要是以下幾方面的處理:
(1)宏定義指令,如 #define a b。
對於這種偽指令,預編譯所要做的是將程序中的所有a用b替換,但作為字元串常量的 a則不被替換。還有 #undef,則將取消對某個宏的定義,使以後該串的出現不再被替換。
(2)條件編譯指令,如#ifdef,#ifndef,#else,#elif,#endif等。
這些偽指令的引入使得程序員可以通過定義不同的宏來決定編譯程序對哪些代碼進行處理。預編譯程序將根據有關的文件,將那些不必要的代碼過濾掉
(3) 頭文件包含指令,如#include "FileName"或者#include <FileName>等。
在頭文件中一般用偽指令#define定義了大量的宏(最常見的是字元常量),同時包含有各種外部符號的聲明。採用頭文件的目的主要是為了使某些定義可以供多個不同的C源程序使用。因為在需要用到這些定義的C源程序中,只需加上一條#include語句即可,而不必再在此文件中將這些定義重復一遍。預編譯程序將把頭文件中的定義統統都加入到它所產生的輸出文件中,以供編譯程序對之進行處理。包含到C源程序中的頭文件可以是系統提供的,這些頭文件一般被放在/usr/include目錄下。在程序中#include它們要使用尖括弧(<>)。另外開發人員也可以定義自己的頭文件,這些文件一般與C源程序放在同一目錄下,此時在#include中要用雙引號("")。
(4)特殊符號,預編譯程序可以識別一些特殊的符號。
例如在源程序中出現的LINE標識將被解釋為當前行號(十進制數),FILE則被解釋為當前被編譯的C源程序的名稱。預編譯程序對於在源程序中出現的這些串將用合適的值進行替換。
預編譯程序所完成的基本上是對源程序的「替代」工作。經過此種替代,生成一個沒有宏定義、沒有條件編譯指令、沒有特殊符號的輸出文件。這個文件的含義同沒有經過預處理的源文件是相同的,但內容有所不同。下一步,此輸出文件將作為編譯程序的輸出而被翻譯成為機器指令。
第二個階段編譯、優化階段。經過預編譯得到的輸出文件中,只有常量;如數字、字元串、變數的定義,以及C語言的關鍵字,如main,if,else,for,while,{,}, +,-,*,\等等。
編譯程序所要作得工作就是通過詞法分析和語法分析,在確認所有的指令都符合語法規則之後,將其翻譯成等價的中間代碼表示或匯編代碼。
優化處理是編譯系統中一項比較艱深的技術。它涉及到的問題不僅同編譯技術本身有關,而且同機器的硬體環境也有很大的關系。優化一部分是對中間代碼的優化。這種優化不依賴於具體的計算機。另一種優化則主要針對目標代碼的生成而進行的。
對於前一種優化,主要的工作是刪除公共表達式、循環優化(代碼外提、強度削弱、變換循環控制條件、已知量的合並等)、復寫傳播,以及無用賦值的刪除,等等。
後一種類型的優化同機器的硬體結構密切相關,最主要的是考慮是如何充分利用機器的各個硬體寄存器存放的有關變數的值,以減少對於內存的訪問次數。另外,如何根據機器硬體執行指令的特點(如流水線、RISC、CISC、VLIW等)而對指令進行一些調整使目標代碼比較短,執行的效率比較高,也是一個重要的研究課題。
2、匯編
匯編實際上指把匯編語言代碼翻譯成目標機器指令的過程。對於被翻譯系統處理的每一個C語言源程序,都將最終經過這一處理而得到相應的目標文件。目標文件中所存放的也就是與源程序等效的目標的機器語言代碼。目標文件由段組成。通常一個目標文件中至少有兩個段:
代碼段:該段中所包含的主要是程序的指令。該段一般是可讀和可執行的,但一般卻不可寫。
數據段:主要存放程序中要用到的各種全局變數或靜態的數據。一般數據段都是可讀,可寫,可執行的。
UNIX環境下主要有三種類型的目標文件:
(1)可重定位文件
其中包含有適合於其它目標文件鏈接來創建一個可執行的或者共享的目標文件的代碼和數據。
(2)共享的目標文件
這種文件存放了適合於在兩種上下文里鏈接的代碼和數據。
第一種是鏈接程序可把它與其它可重定位文件及共享的目標文件一起處理來創建另一個 目標文件;
第二種是動態鏈接程序將它與另一個可執行文件及其它的共享目標文件結合到一起,創建一個進程映象。
(3)可執行文件
它包含了一個可以被操作系統創建一個進程來執行之的文件。匯編程序生成的實際上是第一種類型的目標文件。對於後兩種還需要其他的一些處理方能得到,這個就是鏈接程序的工作了。
二、鏈接過程
由匯編程序生成的目標文件並不能立即就被執行,其中可能還有許多沒有解決的問題。
例如,某個源文件中的函數可能引用了另一個源文件中定義的某個符號(如變數或者函數調用等);在程序中可能調用了某個庫文件中的函數,等等。所有的這些問題,都需要經鏈接程序的處理方能得以解決。
鏈接程序的主要工作就是將有關的目標文件彼此相連接,也即將在一個文件中引用的符號同該符號在另外一個文件中的定義連接起來,使得所有的這些目標文件成為一個能夠被操作系統裝入執行的統一整體。
根據開發人員指定的同庫函數的鏈接方式的不同,鏈接處理可分為兩種:
(1)靜態鏈接
在這種鏈接方式下,函數的代碼將從其所在地靜態鏈接庫中被拷貝到最終的可執行程序中。這樣該程序在被執行時這些代碼將被裝入到該進程的虛擬地址空間中。靜態鏈接庫實際上是一個目標文件的集合,其中的每個文件含有庫中的一個或者一組相關函數的代碼。
(2) 動態鏈接
在此種方式下,函數的代碼被放到稱作是動態鏈接庫或共享對象的某個目標文件中。鏈接程序此時所作的只是在最終的可執行程序中記錄下共享對象的名字以及其它少量的登記信息。在此可執行文件被執行時,動態鏈接庫的全部內容將被映射到運行時相應進程的虛地址空間。動態鏈接程序將根據可執行程序中記錄的信息找到相應的函數代碼。
對於可執行文件中的函數調用,可分別採用動態鏈接或靜態鏈接的方法。使用動態鏈接能夠使最終的可執行文件比較短小,並且當共享對象被多個進程使用時能節約一些內存,因為在內存中只需要保存一份此共享對象的代碼。但並不是使用動態鏈接就一定比使用靜態鏈接要優越。在某些情況下動態鏈接可能帶來一些性能上損害。
我們在linux使用的gcc編譯器便是把以上的幾個過程進行捆綁,使用戶只使用一次命令就把編譯工作完成,這的確方便了編譯工作,但對於初學者了解編譯過程就很不利了,下圖便是gcc代理的編譯過程:
從上圖可以看到:
預編譯
將.c 文件轉化成 .i文件
使用的gcc命令是:gcc –E
對應於預處理命令cpp
編譯
將.c/.h文件轉換成.s文件
使用的gcc命令是:gcc –S
對應於編譯命令 cc –S
匯編
將.s 文件轉化成 .o文件
使用的gcc 命令是:gcc –c
對應於匯編命令是 as
鏈接
將.o文件轉化成可執行程序
使用的gcc 命令是: gcc
對應於鏈接命令是 ld
總結起來編譯過程就上面的四個過程:預編譯、編譯、匯編、鏈接。了解這四個過程中所做的工作,對我們理解頭文件、庫等的工作過程是有幫助的,而且清楚的了解編譯鏈接過程還對我們在編程時定位錯誤,以及編程時盡量調動編譯器的檢測錯誤會有很大的幫助的。
