1. 程式人生 > >RISC-V嵌入式開發準備篇1:編譯過程簡介

RISC-V嵌入式開發準備篇1:編譯過程簡介

原文出處:https://mp.weixin.qq.com/s/-syKN0DibKGGPCllaeNqMg

隨著國內第一本RISC-V中文書籍《手把手教你設計CPU——RISC-V處理器篇》 正式上市,越來越多的愛好者開始使用開源的蜂鳥E203 RISC-V處理核,很多初學者留言詢問有關RISC-V工具鏈使用的問題,因此本公眾號將開始陸續發表若干篇有關RISC-V軟體工具鏈使用的文章,包括:

本文為RISC-V嵌入式開發準備篇1:編譯過程簡介。本文的目的是對編譯過程進行簡單的科普與回顧,為後續詳細介紹“RISC-V GCC工具鏈”和“RISC-V組合語言程式設計”打下基礎。

注:本文力求通俗易懂,主要面向初學者,對編譯過程有所瞭解的讀者可以忽略此文。

1 本文概述

本文將介紹如何將高層的C/C++語言編寫的程式轉換成為處理器能夠執行的二進位制程式碼的過程,該過程即一般編譯原理書籍所介紹的過程,包括四個步驟:

  • 預處理(Preprocessing)
  • 編譯(Compilation)
  • 彙編(Assembly)
  • 連結(Linking)

本文限於篇幅,將不會對各個步驟的原理進行詳解,將僅僅結合Linux自帶的GCC工具鏈對其過程進行簡述。感興趣的讀者可以自行查閱其他資料深入學習編譯原理的相關知識。

注意:

  • 本文為了簡化描述與便於初學者理解,將在Linux作業系統平臺上編譯一個Hello World程式並在此Linux平臺上執行作為示例。而嵌入式開發所使用的交叉編譯的使用方法與本文所述的編譯過程有所差異,本公眾號將在後續發文《嵌入式開發的特點介紹》中對嵌入式系統編譯進行更多介紹。
  • 本文使用的是Linux自帶的GCC工具鏈作為演示,而未涉及到如何使用RISC-V GCC工具鏈,本公眾號將在後續發文《RISC-V GCC工具鏈的介紹》中對RISC-V GCC工具鏈進行更多介紹。

2 GCC工具鏈介紹

通常所說的GCC是GUN Compiler Collection的簡稱,是Linux系統上常用的編譯工具。GCC實質上不是一個單獨的程式,而是多個程式的集合,因此通常稱為GCC工具鏈。工具鏈軟體包括GCC、C執行庫、Binutils、GDB等。

  • GCC
    • GCC(GNU C Compiler)是編譯工具。本文所要介紹的將C/C++語言編寫的程式轉換成為處理器能夠執行的二進位制程式碼的過程即由編譯器完成。有關編譯過程的更多介紹請參見後文。
    • GCC既支援本地編譯(即在一個平臺上編譯該平臺執行的程式),也支援交叉編譯(即在一個平臺上編譯供另一個平臺執行的程式)。

本文為了簡化描述與便於初學者理解,將在Linux作業系統平臺上編譯一個Hello World程式並在此Linux平臺上執行作為示例,即為一種本地編譯的開發方式。

交叉編譯多用於嵌入式系統的開發,有關交叉編譯,本公眾號將在後續發文《嵌入式開發的特點介紹》中對嵌入式系統交叉編譯進行更多介紹。

  • C執行庫
    • 由於C執行庫的相關背景知識較多,請參見後文對其單獨進行介紹。
  • Binutils
    • 由於Binutils的相關資訊較多,請參見後文對其單獨進行介紹。
  • GDB
    • GDB(GNU Project Debugger)是除錯工具,可以用於對程式進行除錯。

2.2 Binutils

一組二進位制程式處理工具,包括:addr2line、ar、objcopy、objdump、as、ld、ldd、readelf、size等。這一組工具是開發和除錯不可缺少的工具,分別簡介如下:

  • addr2line:用來將程式地址轉換成其所對應的程式原始檔及所對應的程式碼行,也可以得到所對應的函式。該工具將幫助偵錯程式在除錯的過程中定位對應的原始碼位置。
  • as:主要用於彙編,有關彙編的詳細介紹請參見後文。
  • ld:主要用於連結,有關連結的詳細介紹請參見後文。
  • ar:主要用於建立靜態庫。為了便於初學者理解,在此介紹動態庫與靜態庫的概念:
    • 如果要將多個.o目標檔案生成一個庫檔案,則存在兩種型別的庫,一種是靜態庫,另一種是動態庫。
    • 在windows中靜態庫是以 .lib 為字尾的檔案,共享庫是以 .dll 為字尾的檔案。在linux中靜態庫是以.a為字尾的檔案,共享庫是以.so為字尾的檔案。
    • 靜態庫和動態庫的不同點在於程式碼被載入的時刻不同。靜態庫的程式碼在編譯過程中已經被載入可執行程式,因此體積較大。共享庫的程式碼是在可執行程式執行時才載入記憶體的,在編譯過程中僅簡單的引用,因此程式碼體積較小。在Linux系統中,可以用ldd命令檢視一個可執行程式依賴的共享庫。
    • 如果一個系統中存在多個需要同時執行的程式且這些程式之間存在共享庫,那麼採用動態庫的形式將更節省記憶體。但是對於嵌入式系統,大多數情況下都是整個軟體就是一個可執行程式且不支援動態載入的方式,即以靜態庫為主。
  • ldd:可以用於檢視一個可執行程式依賴的共享庫。
  • objcopy:將一種物件檔案翻譯成另一種格式,譬如將.bin轉換成.elf、或者將.elf轉換成.bin等。
  • objdump:主要的作用是反彙編。有關反彙編的詳細介紹,請參見後文。
  • readelf:顯示有關ELF檔案的資訊,請參見後文瞭解更多資訊。
  • size:列出可執行檔案每個部分的尺寸和總尺寸,程式碼段、資料段、總大小等,請參見後文瞭解使用size的具體使用例項。
  • Binutils的每個工具的功能均很強大,本節限於篇幅無法詳細介紹其功能,讀者可以自行查閱資料瞭解其詳情。Binutils還有其他工具,在此不一一贅述,感興趣的讀者可以自行查閱其他資料學習。

2.3 C執行庫

為了解釋C執行庫,需要先回憶一下C語言標準。C語言標準主要由兩部分組成:一部分描述C的語法,另一部分描述C標準庫。C標準庫定義了一組標準標頭檔案,每個標頭檔案中包含一些相關的函式、變數、型別宣告和巨集定義,譬如常見的printf函式便是一個C標準庫函式,其原型定義在stdio標頭檔案中。

C語言標準僅僅定義了C標準庫函式原型,並沒有提供實現。因此,C語言編譯器通常需要一個C執行時庫(C Run Time Libray,CRT)的支援。C執行時庫又常簡稱為C執行庫。與C語言類似,C++也定義了自己的標準,同時提供相關支援庫,稱為C++執行時庫。

如上所述,要在一個平臺上支援C語言,不僅要實現C編譯器,還要實現C標準庫,這樣的實現才能完全支援C標準。glibc(GNU C Library)是Linux下面C標準庫的實現,其要點如下:

  • glibc本身是GNU旗下的C標準庫,後來逐漸成為了Linux的標準C庫。glibc 的主體分佈在Linux系統的/lib與/usr/lib目錄中,包括 libc 標準 C 函式庫、libm數學函式庫等等,都以.so做結尾。
    • 注意:Linux系統下面的標準C庫不僅有這一個,如uclibc、klibc、以及Linux libc,但是glibc使用最為廣泛。而在嵌入式系統中使用較多的C執行庫為Newlib,有關Newlib的詳細介紹將在本公眾號後續發文《嵌入式開發的特點介紹》中進行。
  • Linux系統通常將libc庫作為作業系統的一部分,它被視為作業系統與使用者程式的介面。譬如:glibc不僅實現標準C語言中的函式,還封裝了作業系統提供的系統服務,即系統呼叫的封裝。
    • 通常情況,每個特定的系統呼叫對應了至少一個glibc 封裝的庫函式,如系統提供的開啟檔案系統呼叫sys_open對應的是glibc中的open函式;其次,glibc 一個單獨的API可能呼叫多個系統呼叫,如glibc提供的 printf 函式就會呼叫如 sys_open、sys_mmap、sys_write、sys_close等系統呼叫;另外,多個 glibc API也可能對應同一個系統呼叫,如glibc下實現的malloc、free 等函式用來分配和釋放記憶體,都利用了核心的sys_brk的系統呼叫。
  • 對於C++語言,常用的C++標準庫為libstdc++。注意:通常libstdc++與GCC捆綁在一起的,即安裝gcc的時候會把libstdc++裝上。而glibc並沒有和GCC捆綁於一起,這是因為glibc需要與作業系統核心打交道,因此其與具體的作業系統平臺緊密耦合。而libstdc++雖然提供了c++程式的標準庫,但其並不與核心打交道。對於系統級別的事件,libstdc++會與glibc互動,從而和核心通訊。

2.4 GCC命令列選項

GCC有著豐富的命令列選項支援各種不同的功能,本文由於篇幅有限,無法一一贅述,請讀者自行查閱相關資料學習。

對於RISC-V的GCC工具鏈而言,還有其特有的編譯選項,本公眾號將在後續發文《RISC-V GCC工具鏈的介紹》中介紹RISC-V GCC工具鏈的更多詳情。

3 準備工作

3.1 Linux安裝

由於GCC工具鏈主要是在Linux環境中進行使用,因此本文也將以Linux系統作為工作環境。

對於Linux的安裝,準備好自己的電腦環境。如果是個人電腦,推薦如下配置:

  • 使用VMware虛擬機器在個人電腦上安裝虛擬的Linux作業系統。
  • 由於Linux作業系統的版本眾多,推薦使用Ubuntu 16.04版本的Linux作業系統。
    有關如何安裝VMware以及Ubuntu作業系統本文不做介紹,有關Linux的基本使用本文也不做介紹,請讀者自行查閱資料學習。

3.2 準備Hello World程式

為了能夠演示編譯的整個過程,本節先準備一個C語言編寫的簡單Hello程式作為示例,其原始碼如下所示:

// C語言編寫的Hello World程式原始碼hello.c

#include <stdio.h> //由於printf函式是一個標準的C語言庫函式,其函式原型定義在標準的
                     // C語言stdio標頭檔案中。stdio 是指 “standard input & output”
                  //(標準輸入輸出)的縮寫。所以,原始碼中如用到標準輸入輸出函式時,
                  // 就要包含此標頭檔案

//此程式很簡單,僅僅列印一個Hello World的字串。
int main(void)
{
  printf("Hello World! \n");
  return 0;
}

4 編譯過程

4.1 預處理

預處理的過程主要包括以下過程:

  • 將所有的#define刪除,並且展開所有的巨集定義,並且處理所有的條件預編譯指令,比如#if #ifdef #elif #else #endif等。
  • 處理#include預編譯指令,將被包含的檔案插入到該預編譯指令的位置。
  • 刪除所有註釋“//”和“/* */”。
  • 新增行號和檔案標識,以便編譯時產生除錯用的行號及編譯錯誤警告行號。
  • 保留所有的#pragma編譯器指令,後續編譯過程需要使用它們。
    使用gcc進行預處理的命令如下:
$ gcc -E hello.c -o hello.i // 將原始檔hello.c檔案預處理生成hello.i
                        // GCC的選項-E使GCC在進行完預處理後即停止

hello.i檔案可以作為普通文字檔案開啟進行檢視,其程式碼片段如下所示:

// hello.i程式碼片段

extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 942 "/usr/include/stdio.h" 3 4

# 2 "hello.c" 2


# 3 "hello.c"
int
main(void)
{
  printf("Hello World!" "\n");
  return 0;
}

4.2 編譯

編譯過程就是對預處理完的檔案進行一系列的詞法分析,語法分析,語義分析及優化後生成相應的彙編程式碼。

使用gcc進行編譯的命令如下:

$ gcc -S hello.i -o hello.s // 將預處理生成的hello.i檔案編譯生成彙編程式hello.s
                        // GCC的選項-S使GCC在執行完編譯後停止,生成彙編程式

上述命令生成的彙編程式hello.s的程式碼片段如下所示,其全部為彙編程式碼。

// hello.s程式碼片段

main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $.LC0, %edi
    call    puts
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc

4.3 彙編

彙編過程呼叫對彙編程式碼進行處理,生成處理器能識別的指令,儲存在後綴為.o的目標檔案中。由於每一個彙編語句幾乎都對應一條處理器指令,因此,彙編相對於編譯過程比較簡單,通過呼叫Binutils中的彙編器as根據彙編指令和處理器指令的對照表一一翻譯即可。

當程式由多個原始碼檔案構成時,每個檔案都要先完成彙編工作,生成.o目標檔案後,才能進入下一步的連結工作。注意:目標檔案已經是最終程式的某一部分了,但是在連結之前還不能執行。

使用gcc進行彙編的命令如下:

$ gcc -c hello.s -o hello.o // 將編譯生成的hello.s檔案彙編生成目標檔案hello.o
                        // GCC的選項-c使GCC在執行完彙編後停止,生成目標檔案
//或者直接呼叫as進行彙編
$ as -c hello.s -o hello.o //使用Binutils中的as將hello.s檔案彙編生成目標檔案

注意:hello.o目標檔案為ELF(Executable and Linkable Format)格式的可重定向檔案,不能以普通文字形式的檢視(vim文字編輯器開啟看到的是亂碼)。有關ELF檔案的更多介紹,請參見後文。

4.4 連結

經過彙編以後的目標檔案還不能直接執行,為了變成能夠被載入的可執行檔案,檔案中必須包含固定格式的資訊頭,還必須與系統提供的啟動程式碼連結起來才能正常執行,這些工作都是由連結器來完成的。

GCC可以通過呼叫Binutils中的連結器ld來連結程式執行需要的所有目標檔案,以及所依賴的其它庫檔案,最後生成一個ELF格式可執行檔案。

如果直接呼叫Binutils中的ld進行連結,命令如下,則會報出錯誤:

//直接呼叫ld試圖將hello.o檔案連結成為最終的可執行檔案hello
$ ld hello.o –o hello
ld: warning: cannot find entry symbol _start; defaulting to 00000000004000b0
hello.o: In function `main':
hello.c:(.text+0xa): undefined reference to `puts'

之所以直接用ld進行連結會報錯是因為僅僅依靠一個hello.o目標檔案還無法連結成為一個完整的可執行檔案,需要明確的指明其需要的各種依賴庫和載入程式以及連結指令碼,此過程在嵌入式軟體開發時是必不可少的。而在Linux系統中,可以直接使用gcc命令執行編譯直至連結的過程,gcc會自動將所需的依賴庫以及載入程式連結在一起成為Linux系統可以載入的ELF格式可執行檔案。使用gcc進行編譯直至連結的命令如下:

$ gcc hello.c -o hello  // 將hello.c檔案編譯彙編連結生成可執行檔案hello
                        // GCC沒有新增選項,則使GCC一步到位地執行到連結後停
// 止,生成最終的可執行檔案

$ ./hello                 //成功執行該檔案,在終端上會列印Hello World!字串 Hello World!

注意:hello可執行檔案為ELF(Executable and Linkable Format)格式的可執行檔案,不能以普通文字形式的檢視(vim文字編輯器開啟看到的是亂碼)。

在前文介紹了動態庫與靜態庫的差別,與之對應的,連結也分為靜態連結和動態連結,其要點如下:

  • 靜態連結是指在編譯階段直接把靜態庫加入到可執行檔案中去,這樣可執行檔案會比較大。連結器將函式的程式碼從其所在地(不同的目標檔案或靜態連結庫中)拷貝到最終的可執行程式中。為建立可執行檔案,連結器必須要完成的主要任務是:符號解析(把目標檔案中符號的定義和引用聯絡起來)和重定位(把符號定義和記憶體地址對應起來然後修改所有對符號的引用)。

  • 而動態連結則是指連結階段僅僅只加入一些描述資訊,而程式執行時再從系統中把相應動態庫載入到記憶體中去。

    • 在Linux系統中,gcc編譯連結時的動態庫搜尋路徑的順序通常為:首先從gcc命令的引數-L指定的路徑尋找;再從環境變數LIBRARY_PATH指定的路徑定址;再從預設路徑/lib、/usr/lib、/usr/local/lib尋找。
    • 在Linux系統中,執行二進位制檔案時的動態庫搜尋路徑的順序通常為:首先搜尋編譯目的碼時指定的動態庫搜尋路徑;再從環境變數LD_LIBRARY_PATH指定的路徑定址;再從配置檔案/etc/ld.so.conf中指定的動態庫搜尋路徑;再從預設路徑/lib、/usr/lib尋找。
    • 在Linux系統中,可以用ldd命令檢視一個可執行程式依賴的共享庫。
  • 由於連結動態庫和靜態庫的路徑可能有重合,所以如果在路徑中有同名的靜態庫檔案和動態庫檔案,比如libtest.a和libtest.so,gcc連結時預設優先選擇動態庫,會連結libtest.so,如果要讓gcc選擇連結libtest.a則可以指定gcc選項-static,該選項會強制使用靜態庫進行連結。以本節的Hello World為例:

    • 如果使用命令“gcc hello.c -o hello”則會使用動態庫進行連結,生成的ELF可執行檔案的大小(使用Binutils的size命令檢視)和連結的動態庫(使用Binutils的ldd命令檢視)如下所示:
$ gcc hello.c -o hello
$ size hello  //使用size檢視大小
   text    data     bss     dec     hex filename
   1183     552       8    1743     6cf     hello
$ ldd hello //可以看出該可執行檔案連結了很多其他動態庫,主要是Linux的glibc動態庫
        linux-vdso.so.1 =>  (0x00007fffefd7c000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fadcdd82000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fadce14c000)
  • 如果使用命令“gcc -static hello.c -o hello”則會使用靜態庫進行連結,生成的ELF可執行檔案的大小(使用Binutils的size命令檢視)和連結的動態庫(使用Binutils的ldd命令檢視)如下所示:
$ gcc -static hello.c -o hello
$ size hello //使用size檢視大小
     text    data     bss     dec     hex filename
 823726    7284    6360  837370   cc6fa     hello //可以看出text的程式碼尺寸變得極大
$ ldd hello
       not a dynamic executable //說明沒有連結動態庫

連結器連結後生成的最終檔案為ELF格式可執行檔案,一個ELF可執行檔案通常被連結為不同的段,常見的段譬如.text、.data、.rodata、.bss等段。有關ELF檔案和常見段的更多介紹,請參見後文。

4.5 一步到位的編譯

從功能上分,預處理、編譯、彙編、連結是四個不同的階段,但GCC的實際操作上,它可以把這四個步驟合併為一個步驟來執行。如下例所示:

$ gcc –o test first.c second.c third.c
      //該命令將同時編譯三個原始檔,即first.c、second.c和 third.c,然後將它們連結成
//一個可執行檔案,名為test。

注意:

  • 一個程式無論有一個原始檔還是多個原始檔,所有被編譯和連結的原始檔中必須有且僅有一個main函式。
  • 但如果僅僅是把原始檔編譯成目標檔案,因為不會進行連結,所以main函式不是必需的。

5 分析ELF檔案

5.1 ELF檔案介紹

在介紹ELF檔案之前,首先將其與另一種常見的二進位制檔案格式bin進行對比:

  • binary檔案,其中只有機器碼。

  • elf檔案除了含有機器碼之外還有其它資訊,如:段載入地址,執行入口地址,資料段等。
    ELF全稱Executable and Linkable Format,可執行連結格式。ELF檔案格式主要三種:

  • 可重定向(Relocatable)檔案:

    • 檔案儲存著程式碼和適當的資料,用來和其他的目標檔案一起來建立一個可執行檔案或者是一個共享目標檔案。
  • 可執行(Executable)檔案:

    • 檔案儲存著一個用來執行的程式(例如bash,gcc等)。
  • 共享(Shared)目標檔案(Linux下字尾為.so的檔案):

    • 即所謂共享庫。

5.2 ELF檔案的段

ELF檔案格式如圖1中所示,位於ELF Header和Section Header Table之間的都是段(Section)。一個典型的ELF檔案包含下面幾個段:

  • .text:已編譯程式的指令程式碼段。
  • .rodata:ro代表read only,即只讀資料(譬如常數const)。
  • .data:已初始化的C程式全域性變數和靜態區域性變數。
    • 注意:C程式普通區域性變數在執行時被儲存在堆疊中,既不出現在.data段中,也不出現在.bss段中。此外,如果變數被初始化值為0,也可能會放到bss段。
  • .bss:未初始化的C程式全域性變數和靜態區域性變數。
    • 注意:目標檔案格式區分初始化和未初始化變數是為了空間效率,在ELF檔案中.bss段不佔據實際的儲存器空間,它僅僅是一個佔位符。
  • .debug:除錯符號表,偵錯程式用此段的資訊幫助除錯。
  • 上述僅講解了最常見的節,ELF檔案還包含很多其他型別的節,本文在此不做贅述,請感興趣的讀者自行查閱其他資料瞭解學習。

在這裡插入圖片描述

圖1 ELF格式

5.3 檢視ELF檔案

可以使用Binutils中readelf來檢視ELF檔案的資訊,可以通過readelf --help來檢視readelf的選項:

$ readelf --help
Usage: readelf <option(s)> elf-file(s)
 Display information about the contents of ELF format files
 Options are:
  -a --all               Equivalent to: -h -l -S -s -r -d -V -A -I
  -h --file-header       Display the ELF file header
  -l --program-headers   Display the program headers
     --segments          An alias for --program-headers
  -S --section-headers   Display the sections' header

以本文Hello World示例,使用readelf -S檢視其各個section的資訊如下:

$ readelf -S hello
There are 31 section headers, starting at offset 0x19d8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
……
  [11] .init             PROGBITS         00000000004003c8  000003c8
       000000000000001a  0000000000000000  AX       0     0     4
……
  [14] .text             PROGBITS         0000000000400430  00000430
       0000000000000182  0000000000000000  AX       0     0     16
  [15] .fini             PROGBITS         00000000004005b4  000005b4
……

5.4 反彙編

由於ELF檔案無法被當做普通文字檔案開啟,如果希望直接檢視一個ELF檔案包含的指令和資料,需要使用反彙編的方法。反彙編是用於除錯和定位處理器問題時最常用的手段。
可以使用Binutils中objdump來對ELF檔案進行反彙編,可以通過objdump --help來檢視其選項:

$ objdump --help
Usage: objdump <option(s)> <file(s)>
 Display information from object <file(s)>.
 At least one of the following switches must be given:
……
  -D, --disassemble-all    Display assembler contents of all sections
  -S, --source             Intermix source code with disassembly
……

以本文Hello World示例,使用objdump -D對其進行反彙編如下:

$ objdump -D hello
……
0000000000400526 <main>:  // main標籤的PC地址
//PC地址:指令編碼                  指令的彙編格式
  400526:    55                          push   %rbp 
  400527:    48 89 e5                mov    %rsp,%rbp
  40052a:    bf c4 05 40 00          mov    $0x4005c4,%edi
  40052f:    e8 cc fe ff ff          callq  400400 <[email protected]>
  400534:    b8 00 00 00 00          mov    $0x0,%eax
  400539:    5d                      pop    %rbp
  40053a:    c3                          retq   
  40053b:    0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
……

使用objdump -S將其反彙編並且將其C語言原始碼混合顯示出來:

$ gcc -o hello -g hello.c //要加上-g選項
$ objdump -S hello
……
0000000000400526 <main>:
#include <stdio.h>

int
main(void)
{
  400526:    55                          push   %rbp
  400527:    48 89 e5                mov    %rsp,%rbp
  printf("Hello World!" "\n");
  40052a:    bf c4 05 40 00          mov    $0x4005c4,%edi
  40052f:    e8 cc fe ff ff          callq  400400 <[email protected]>
  return 0;
  400534:    b8 00 00 00 00          mov    $0x0,%eax
}
  400539:    5d                          pop    %rbp
  40053a:    c3                          retq   
  40053b:    0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
……

6 嵌入式系統編譯的特殊性

為了易於讀者理解,本文以一個Hello World程式為例講解了在Linux環境中的編譯過程以幫助初學者入門,但是瞭解這些基礎背景知識對於嵌入式開發還遠遠不夠。
對於嵌入式開發,嵌入式系統的編譯有其特殊性,譬如:

  • 嵌入式系統需要使用交叉編譯與遠端除錯的方法進行開發。
  • 需要自己定義載入程式。
  • 需要注意減少程式碼尺寸。
  • 需要移植printf從而使得嵌入式系統也能夠列印輸入。
  • 使用Newlib作為C執行庫。
  • 每個特定的嵌入式系統都需要配套的板級支援包。
    為了易於讀者理解,本文使用的是Linux自帶的GCC工具鏈,其並不能反映嵌入式開發的特點。本公眾號將在後續發文《嵌入式開發的特點介紹》《RISC-V GCC工具鏈的介紹》中介紹“嵌入式開發特點”和“RISC-V GCC工具鏈”的更多詳情。

7 總結

編譯原理是一門博大精深的學科,雖然大多數的使用者只是將編譯器作為一門工具使用而無需關注其內部原理,但是適當的瞭解編譯的過程對於開發大有裨益,尤其是對於嵌入式軟體開發而言,更需要了解編譯與連結的基本過程。

本文為了簡化描述與便於初學者理解,僅僅以在Linux作業系統平臺上使用其自帶的GCC編譯一個Hello World程式作為示例。本文雖面向的是RISC-V嵌入式開發,其使用的RISC-V工具鏈交叉編譯使用方法與本文所述的編譯過程有所差異,但是其原理和使用方法大致相同,因此也可以作為初學者的學習參考。

更多資訊

感興趣的讀者可以通過下面二維碼關注公眾號“矽農亞歷山大”,瞭解Verilog、IC設計、CPU、RISC-V和人工智慧AI相關的更多設計技巧和經驗分享,注意:由於乾貨太多,請自備茶水。

在這裡插入圖片描述