交叉編譯工具鏈構建原理

汪淼焱發表於2024-03-14

交叉編譯工具鏈構建原理

這是與弗朗西斯科·圖爾科(Francesco Turco)討論的結果

弗朗西斯科為初學者提供了一個很好的教程(死鏈,Wayback機器沒有存檔版本),以及一個示例,從x86_64 Debian主機為ARM目標構建工具鏈的分步過程。

謝謝弗朗西斯科發起這個活動!

我想要一個交叉編譯器!你說的這個工具鏈是什麼?

交叉編譯器實際上是不同工具的集合,這些工具被設定為緊密地協同工作。這些工具以某種級聯方式連結在一起,其中一個工具的輸出成為另一個工具的輸入,最終產生在機器上執行的實際二進位制程式碼。因此,我們稱這種安排為“工具鏈”。當工具鏈要為執行它的機器之外的機器生成程式碼時,這被稱為交叉工具鏈。

那麼,工具鏈中的元件是什麼?

在工具鏈中起作用的元件首先是編譯器本身。編譯器將原始碼(用C、C++等語言)轉換成彙編程式碼。選擇的編譯器是GNU編譯器集合,眾所周知的gcc

彙編程式解釋彙編程式碼以生成目的碼。這是由二進位制實用程式完成的,如GNU binutils。

一旦生成了不同的目的碼檔案,它們就會聚集在一起形成最終的可執行二進位制檔案。這稱為連結,透過使用連結器來實現。GNU binutils還附帶了一個連結器。

到目前為止,我們得到了一個完整的工具鏈,能夠將原始碼轉化為實際的可執行程式碼。根據目標上執行的作業系統,我們還需要C庫。C庫提供了一個執行基本任務的標準抽象層(例如分配記憶體、在終端上列印輸出、管理檔案訪問……)。有許多C庫,每個都針對不同的系統。對於Linux桌面,有glibceglibc甚至uClibc,對於嵌入式Linux,您可以選擇eglibcuClibc,而對於沒有作業系統的系統,您可以使用newlibdietlibc,甚至根本不使用。還有一些其他的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

依次構建元件:

  1. binutils
  2. 核心通道1編譯器
  3. 核心標頭檔案
  4. c庫標頭檔案和啟動檔案
  5. 核心通道2編譯器
  6. 完整的C庫
  7. 最終編譯器

是啊!:-)但是我們結束了嗎?

事實上,不是的,仍然有缺失的依賴項。就工具本身而言,我們不需要任何其他東西。

但是gcc有一些先決條件。它依靠一些外部庫來執行一些重要的任務(比如處理常數中的複數……)。構建這些庫有幾種選擇。首先,人們可能會認為依靠Linux發行版來提供這些庫。唉,直到最近,它們才被廣泛使用。因此,如果發行版不是太新的話,我們很有可能必須構建這些庫(我們將在下面進行構建)。受影響的庫包括:

  • GNU多精度算術庫——GMP(GNU Multiple Precision Arithmetic Library);
  • 具有正確舍入的多精度浮點計算的C庫——MPFR(Multiple-Precision Floating-point-computations with correct Rounding);
  • 複數算術的C語言庫——MPC。

這些庫的依賴關係如下:

  1. MPC需要GMP和MPFR
  2. MPFR需要GMP
  3. GMP沒有先決條件

因此,構建順序變為:

  1. GMP
  2. MPFR
  3. MPC
  4. binutils
  5. 核心通道1編譯器
  6. 核心標頭檔案
  7. C庫標頭檔案和啟動檔案
  8. 核心通道2編譯器
  9. 完整的C庫
  10. 最終編譯器

是啊!或者更多?

這足以構建一個功能工具鏈。所以如果你現在已經受夠了,你可以到此為止。或者如果你很好奇,你可以繼續閱讀。

gcc還可以利用其他一些外部庫。這些額外的可選庫用於啟用gcc中的高階功能,如迴圈最佳化(GRAPHITE)鏈路時間最佳化(LTO, Link Time Optimisation)。如果要使用這些庫,您需要另外三個庫:

要啟用GRAPHITE,根據GCC版本,可能需要以下一項或多項:

  1. PPL, Parma多面體庫;
  2. ISL, 整數集庫;
  3. CLooG/PPL, 使用PPL後端的Chunky迴圈生成器;
  4. CLooG, 使用ISL後端的Chunky迴圈生成器。

要啟用LTO:-ELF物件檔案訪問庫,libelf

這些庫的依賴關係如下:

  1. PPL要求GMP
  2. CLooG/PPL需要GMP和PPL或ISL之一;
  3. ISL沒有先決條件;
  4. libelf沒有先決條件。

列表現在看起來像這樣:

  1. GMP
  2. MPFR
  3. MPC
  4. CLooG/PPL(如果需要)
  5. ISL(如果需要)
  6. libelf(如果需要)
  7. binutils
  8. 核心通道1編譯器
  9. 核心標頭檔案
  10. C庫標頭檔案和啟動檔案
  11. 核心通道2編譯器
  12. 完整的C庫
  13. 最終編譯器

這個列表現在已經完成了!哇哦!或者是?

但是為什麼crosstool-NG的步驟更多呢?

從理論的角度來看,已經制定的十三個步驟是必要的步驟。然而在現實中,還是有一些小的不同。crosstool-NG中的額外步驟有三個不同的原因。

第一,GNU binutils不支援某些型別的輸出。用binutils生成平面二進位制檔案是不可能的,所以我們必須使用另一個新增了這種支援的元件: elf2flt。elf2flt還需要zlib壓縮庫-如果我們正在構建加拿大的或跨本地的工具鏈,我們可能無法使用主機的zlib。

第二,工具鏈的本地化需要一些主機作業系統上的附加庫: gettextlibiconv

第三,crosstool-NG還可以構建一些額外的除錯實用程式在目標上執行。這是我們構建的地方,例如,cross-gdbgdbserver和原生gdb(最後兩個在目標上執行,第一個在工具鏈所在的機器上執行)。其他工具(straceltraceDUMAdmalloc)與工具鏈完全無關,但在開發時非常有用,因此作為好東西包含在內(它們很容易構建,所以沒問題;更復雜的東西不值得包含在crosstool-NG中)。

相關文章