Linux 程序編譯過程的來龍去脈

來自公衆號:人人都是極客

作者:佈道師 Peter

大家肯定都知道計算機程序設計語言通常分爲機器語言、彙編語言和高級語言三類。高級語言需要通過翻譯成機器語言才能執行,而翻譯的方式分爲兩種,一種是編譯型,另一種是解釋型,因此我們基本上將高級語言分爲兩大類,一種是編譯型語言,例如 C,C++,Java,另一種是解釋型語言,例如 Python、Ruby、MATLAB 、JavaScript。

本文將介紹如何將高層的 C/C++ 語言編寫的程序轉換成爲處理器能夠執行的二進制代碼的過程,包括四個步驟:

GCC 工具鏈介紹

通常所說的 GCC 是 GUN Compiler Collection 的簡稱,是 Linux 系統上常用的編譯工具。GCC 工具鏈軟件包括 GCC、Binutils、C 運行庫等。

GCC

GCC(GNU C Compiler)是編譯工具。本文所要介紹的將 C/C++ 語言編寫的程序轉換成爲處理器能夠執行的二進制代碼的過程即由編譯器完成。

Binutils

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

C 運行庫

C 語言標準主要由兩部分組成:一部分描述 C 的語法,另一部分描述 C 標準庫。C 標準庫定義了一組標準頭文件,每個頭文件中包含一些相關的函數、變量、類型聲明和宏定義,譬如常見的 printf 函數便是一個 C 標準庫函數,其原型定義在 stdio 頭文件中。

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

準備工作

由於 GCC 工具鏈主要是在 Linux 環境中進行使用,因此本文也將以 Linux 系統作爲工作環境。爲了能夠演示編譯的整個過程,本節先準備一個 C 語言編寫的簡單 Hello 程序作爲示例,其源代碼如下所示:

#include <stdio.h> 

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

編譯過程

  1. 預處理

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

$ 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;
}
  1. 編譯

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

使用 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
  1. 彙編

彙編過程調用對彙編代碼進行處理,生成處理器能識別的指令,保存在後綴爲. 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)格式的可重定向文件。

  1. 鏈接

鏈接也分爲靜態鏈接和動態鏈接,其要點如下:

由於鏈接動態庫和靜態庫的路徑可能有重合,所以如果在路徑中有同名的靜態庫文件和動態庫文件,比如 libtest.a 和 libtest.so,gcc 鏈接時默認優先選擇動態庫,會鏈接 libtest.so,如果要讓 gcc 選擇鏈接 libtest.a 則可以指定 gcc 選項 - static,該選項會強制使用靜態庫進行鏈接。以 Hello World 爲例:

鏈接器鏈接後生成的最終文件爲 ELF 格式可執行文件,一個 ELF 可執行文件通常被鏈接爲不同的段,常見的段譬如. text、.data、.rodata、.bss 等段。

分析 ELF 文件

1.ELF 文件的段

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

可以使用 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
……
  1. 反彙編 ELF

由於 ELF 文件無法被當做普通文本文件打開,如果希望直接查看一個 ELF 文件包含的指令和數據,需要使用反彙編的方法。

使用 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 <puts@plt>
  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 <puts@plt>
  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)
……
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Jp9s9j6e2sA4rvevOQEzOQ