交叉編譯工具鏈構建原理
這是與弗朗西斯科·圖爾科(Francesco Turco)討論的結果。
弗朗西斯科為初學者提供了一個很好的教程(死鏈,Wayback機器沒有存檔版本),以及一個示例,從x86_64 Debian主機為ARM目標構建工具鏈的分步過程。
謝謝弗朗西斯科發起這個活動!
我想要一個交叉編譯器!你說的這個工具鏈是什麼?
交叉編譯器實際上是不同工具的集合,這些工具被設定為緊密地協同工作。這些工具以某種級聯方式連結在一起,其中一個工具的輸出成為另一個工具的輸入,最終產生在機器上執行的實際二進位制程式碼。因此,我們稱這種安排為“工具鏈”。當工具鏈要為執行它的機器之外的機器生成程式碼時,這被稱為交叉工具鏈。
那麼,工具鏈中的元件是什麼?
在工具鏈中起作用的元件首先是編譯器本身。編譯器將原始碼(用C、C++等語言)轉換成彙編程式碼。選擇的編譯器是GNU編譯器集合,眾所周知的gcc
。
彙編程式解釋彙編程式碼以生成目的碼。這是由二進位制實用程式完成的,如GNU binutils。
一旦生成了不同的目的碼檔案,它們就會聚集在一起形成最終的可執行二進位制檔案。這稱為連結,透過使用連結器來實現。GNU binutils還附帶了一個連結器。
到目前為止,我們得到了一個完整的工具鏈,能夠將原始碼轉化為實際的可執行程式碼。根據目標上執行的作業系統,我們還需要C庫。C庫提供了一個執行基本任務的標準抽象層(例如分配記憶體、在終端上列印輸出、管理檔案訪問……)。有許多C庫,每個都針對不同的系統。對於Linux桌面,有glibc
或eglibc
甚至uClibc
,對於嵌入式Linux,您可以選擇eglibc
或uClibc
,而對於沒有作業系統的系統,您可以使用newlib
、dietlibc
,甚至根本不使用
。還有一些其他的C庫,但它們沒有被廣泛使用,並且/或者是針對非常特殊的需求(例如,klibc
是C庫的一個非常小的子集,旨在構建受約束的初始ramdisks)。
在Linux下,C庫需要知道核心的API,以決定存在什麼特性,如果需要的話,為缺失的特性包含什麼模擬。該API由核心標頭檔案提供。注意:這是特定於Linux的(可能還有極少數其他作業系統),其他作業系統上的C庫不需要核心標頭檔案。
現在,所有這些元件是如何連線在一起的?
到目前為止,已經涵蓋了所有主要元件,但它們需要按照特定的順序構建。從我們最終要使用的編譯器開始,我們可以看到依賴關係
是什麼。我們稱該編譯器為最終編譯器
。
- 最終的編譯器需要C庫來知道如何使用它,但是:
- 構建C庫需要編譯器
A需要B,B需要A .這是典型的先有雞還是先有蛋的問題……這可以透過構建一個精簡的編譯器來解決
,該編譯器不需要C庫,但能夠構建C庫
。我們稱之為引導
、初始
或核心編譯器
。這是新的依賴列表:
- 最終的編譯器需要C庫來知道如何使用它
- 構建C庫需要核心編譯器,但是:
- 核心編譯器需要C庫標頭檔案和啟動檔案來知道如何使用C庫
B需要C,C需要B .又是先有雞還是先有蛋。為了解決這個問題,我們需要構建一個只安裝標頭檔案和啟動檔案的C庫
。啟動檔案
(也稱為C執行時
或CRT
)是gcc需要在NPTL系統上啟用執行緒本地儲存(TLS)的極少數檔案
。所以現在我們有:
- 最終的編譯器需要C庫來知道如何使用它
- 構建C庫需要一個核心編譯器
- 核心編譯器需要C庫標頭檔案和啟動檔案來了解如何使用C庫,但是:
- 構建啟動檔案需要編譯器
天啊… C需要D,D又需要C。因此我們需要構建一個更簡單的編譯器,它不需要標頭檔案,但需要啟動檔案。這個編譯器也是一個引導、初始或核心編譯器。為了區分兩個核心編譯器,我們稱之為一個核心通道1(core pass 1
),而前者為一個核心通道2(core pass 2
)。依賴性列表變成:
- 最終的編譯器需要C庫來知道如何使用它
- 構建C庫需要編譯器
- 核心通道2編譯器需要C庫標頭檔案和啟動檔案來了解如何使用C庫
- 構建啟動檔案需要編譯器
- 我們需要一個核心通道1編譯器
正如我們前面所說的,C庫也需要核心標頭檔案
。對核心頭沒有要求,所以在這種情況下故事結束了:
- 最終的編譯器需要C庫來知道如何使用它
- 構建C庫需要一個核心編譯器
- 核心通道2編譯器需要C庫標頭檔案和啟動檔案來了解如何使用C庫
- 構建啟動檔案需要編譯器和核心標頭檔案
- 我們需要一個核心通道1編譯器
我們需要增加一些新的要求。當我們為目標編譯程式碼時,我們需要彙編程式和連結程式。當然,這樣的程式碼是從C庫構建的,所以我們需要在C庫啟動檔案之前構建binutils,以及完整的C庫本身。此外,gcc中的一些程式碼也將轉而在目標上執行。幸運的是,對binutils沒有要求。因此,我們的依賴鏈如下:
- 最終的編譯器需要C庫來知道如何使用它,還需要binutils
- 構建C庫需要核心通道2編譯器和binutils
- 核心通道2編譯器需要C庫標頭檔案和啟動檔案,以瞭解如何使用C庫和binutils
- 構建啟動檔案需要編譯器、核心標頭檔案和binutils
- 核心通道1編譯器需要binutils
依次構建元件:
- binutils
- 核心通道1編譯器
- 核心標頭檔案
- c庫標頭檔案和啟動檔案
- 核心通道2編譯器
- 完整的C庫
- 最終編譯器
是啊!:-)但是我們結束了嗎?
事實上,不是的,仍然有缺失的依賴項。就工具本身而言,我們不需要任何其他東西。
但是gcc有一些先決條件。它依靠一些外部庫來執行一些重要的任務(比如處理常數中的複數……)。構建這些庫有幾種選擇。首先,人們可能會認為依靠Linux發行版來提供這些庫。唉,直到最近,它們才被廣泛使用。因此,如果發行版不是太新的話,我們很有可能必須構建這些庫(我們將在下面進行構建)。受影響的庫包括:
- GNU多精度算術庫——GMP(GNU Multiple Precision Arithmetic Library);
- 具有正確舍入的多精度浮點計算的C庫——MPFR(Multiple-Precision Floating-point-computations with correct Rounding);
- 複數算術的C語言庫——MPC。
這些庫的依賴關係如下:
- MPC需要GMP和MPFR
- MPFR需要GMP
- GMP沒有先決條件
因此,構建順序變為:
- GMP
- MPFR
- MPC
- binutils
- 核心通道1編譯器
- 核心標頭檔案
- C庫標頭檔案和啟動檔案
- 核心通道2編譯器
- 完整的C庫
- 最終編譯器
是啊!或者更多?
這足以構建一個功能工具鏈。所以如果你現在已經受夠了,你可以到此為止。或者如果你很好奇,你可以繼續閱讀。
gcc還可以利用其他一些外部庫。這些額外的可選庫
用於啟用gcc中的高階功能
,如迴圈最佳化(GRAPHITE)
和鏈路時間最佳化(LTO, Link Time Optimisation)
。如果要使用這些庫,您需要另外三個庫:
要啟用GRAPHITE,根據GCC版本,可能需要以下一項或多項:
- PPL, Parma多面體庫;
- ISL, 整數集庫;
- CLooG/PPL, 使用PPL後端的Chunky迴圈生成器;
- CLooG, 使用ISL後端的Chunky迴圈生成器。
要啟用LTO:-ELF物件檔案訪問庫,libelf
這些庫的依賴關係如下:
- PPL要求GMP
- CLooG/PPL需要GMP和PPL或ISL之一;
- ISL沒有先決條件;
- libelf沒有先決條件。
列表現在看起來像這樣:
- GMP
- MPFR
- MPC
- CLooG/PPL(如果需要)
- ISL(如果需要)
- libelf(如果需要)
- binutils
- 核心通道1編譯器
- 核心標頭檔案
- C庫標頭檔案和啟動檔案
- 核心通道2編譯器
- 完整的C庫
- 最終編譯器
這個列表現在已經完成了!哇哦!或者是?
但是為什麼crosstool-NG的步驟更多呢?
從理論的角度來看,已經制定的十三個步驟是必要的步驟。然而在現實中,還是有一些小的不同。crosstool-NG中的額外步驟有三個不同的原因。
第一,GNU binutils不支援某些型別的輸出。用binutils生成平面二進位制檔案是不可能的,所以我們必須使用另一個新增了這種支援的元件: elf2flt
。elf2flt還需要zlib
壓縮庫-如果我們正在構建加拿大的或跨本地的工具鏈,我們可能無法使用主機的zlib。
第二,工具鏈的本地化需要一些主機作業系統上的附加庫: gettext
和 libiconv
。
第三,crosstool-NG還可以構建一些額外的除錯實用程式在目標上執行。這是我們構建的地方,例如,cross-gdb
、gdbserver
和原生gdb
(最後兩個在目標上執行,第一個在工具鏈所在的機器上執行)。其他工具(strace
、ltrace
、DUMA
和dmalloc
)與工具鏈完全無關,但在開發時非常有用,因此作為好東西包含在內(它們很容易構建,所以沒問題;更復雜的東西不值得包含在crosstool-NG中)。