編譯原理 (預處理>編譯>彙編>鏈接)

 一般高級語言程序編譯的過程:預處理、編譯、彙編、鏈接。gcc在後臺實際上也經歷了這幾個過程,我們可以通過-v參數查看它的編譯細節,如果想看某個具體的編譯過程,則可以分別使用-E,-S,-c和 -O,對應的後臺工具則分別爲cpp,cc1,as,ld。下面我們將逐步分析這幾個過程以及相關的內容,諸如語法檢查、代碼調試、彙編語言等。

 

1、預處理

 

預處理是C語言程序從源代碼變成可執行程序的第一步,主要是C語言編譯器對各種預處理命令進行處理,包括頭文件的包含、宏定義的擴展、條件編譯的選擇等。打印出預處理之後的結果:gcc -E hello.c 或者 cpp hello.c這樣我們就可以看到源代碼中的各種預處理命令是如何被解釋的,從而方便理解和查錯。

 

gcc調用了cpp(雖然我們通過gcc-v僅看到cc1)cppThe C Preprocessor,主要用來預處理宏定義、文件包含、條件編譯等。下面介紹它的一個比較重要的選項-D。在命令行定義宏:gcc Dmacro=1 hello.c 或者 cpp Dmacro=1 hello.c等同於在文件的開頭定義宏,即#define maco,但是在命令行定義更靈活。例如,在源代碼中有這些語句:

 

#ifdef DEBUG

printf("this code is for debuggingn");

#endif

 

 

2、編譯

 

編譯之前,C語言編譯器會進行詞法分析、語法分析(-fsyntax-only),接着會把源代碼翻譯成中間語言,即彙編語言。如果想看到這個中間結果,可以用-S選項。

編譯程序工作時,先分析,後綜合,從而得到目標程序。所謂分析,是指詞法分析和語法分析;所謂綜合是指代碼優化,存儲分配和代碼生成。爲了完成這些分析綜合任務,編譯程序採用對源程序進行多次掃描的辦法,每次掃描集中完成一項或幾項任務,也有一項任務分散到幾次掃描去完成的。下面舉一個四遍掃描的例子:第一遍掃描做詞法分析;第二遍掃描做語法分析;第三遍掃描做代碼優化和存儲分配;第四遍掃描做代碼生成。 

值得一提的是,大多數的編譯程序直接產生機器語言的目標代碼,形成可執行的目標文件,但也有的編譯程序則先產生彙編語言一級的符號代碼文件,然後再調用匯編程序進行翻譯加工處理,最後產生可執行的機器語言目標文件。 

語法檢查之後是翻譯動作,gcc提供了一個優化選項-O,以便根據不同的運行平臺和用戶要求產生經過優化的彙編代碼。例如,

$ gcc -o hello hello.c             #採用默認選項,不優化
$ gcc -O2 -o hello2 hello.c        #優化等次是2
$ gcc -Os -o hellos hello.c        #優化目標代碼的大小

$ time ./hello         #查看代碼運行時間
hello, world

根據上面的簡單演示,可以看出gcc有很多不同的優化選項,主要看用戶的需求了,目標代碼的大小和效率之間貌似存在一個糾纏,需要開發人員自己權衡。

 

下面我們通過-S選項來看看編譯出來的中間結果,彙編語言,還是以之前那個hello.c爲例。

複製代碼
$ gcc -S hello.c                 #默認輸出是hello.s,可自己指定

$ cat hello.s

cat hello.s

        .file   "hello.c"

        .section        .rodata

.LC0:

        .string "hello, world"

        .text

.globl main

        .type   main, @function

main:

        leal    4(%esp), %ecx

        andl    $-16, %esp

        pushl   -4(%ecx)

        pushl   %ebp

        movl    %esp, %ebp

        pushl   %ecx

        subl    $4, %esp

        movl    $.LC0, (%esp)

        call    puts

        movl    $0, %eax

        addl    $4, %esp

        popl    %ecx

        popl    %ebp

        leal    -4(%ecx), %esp

        ret

        .size   main, .-main

        .ident  "GCC: (GNU) 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)"

        .section        .note.GNU-stack,"",@progbits
複製代碼

 

 

intel的彙編語法不太一樣,這裏用的是AT&T語法格式。這裏需要補充的是,在寫C語言代碼時,如果能夠對編譯器比較熟悉(工作原理和一些細節)的話,可能會很有幫助。包括這裏的優化選項(有些優化選項可能在彙編時採用)和可能的優化措施。

 

3、彙編

 

把作爲中間結果的彙編代碼翻譯成了機器代碼,即目標代碼,不過它還不可以運行。如果要產生這一中間結果,可用gcc-c選項,當然,也可通過as命令_彙編_彙編語言源文件來產生。

 

$ file hello.s

hello.s: ASCII assembler program text

 

$ gcc -c hello.s             #gcc把彙編語言編譯成目標代碼

$ file hello.o                 #file命令可以用來查看文件的類型

hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

 

$as -o hello.o hello.s     #as把彙編語言編譯成目標代碼

$ file hello.o

hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

 

gccas默認產生的目標代碼都是ELF格式的,因此這裏主要討論ELF格式的目標代碼。目標代碼不再是普通的文本格式,無法直接通過文本編輯器瀏覽,需要一些專門的工具。

 

binutils(GNU Binary Utilities)的很多工具都採用這個庫來操作目標文件,這類工具有objdump, objcopy, nm, strip等,不過另外一款非常優秀的分析工具readelf並不是基於這個庫,所以你也應該可以直接用elf.h頭文件中定義的相關結構來操作ELF文件。

 

ELF文件的結構:

1. ELF Header (ELF文件頭)說明了文件的類型,大小,運行平臺,節區數目等。

2. Porgram Headers Table (程序頭表,實際上叫段表好一些,用於描述可執行文件和可共享庫)

Section 1

Section 2   

...

3. Section Headers Table(節區頭部表,用於鏈接可重定位文件成可執行文件或共享庫)

 

可以分別通過 readelf文件的-h-l-S參數查看ELF文件頭(ELF Header)、程序頭部表(Program Headers Table,段表)和節區表(Section Headers Table)

 

下面通過這幾段代碼來演示通過readelf -h參數查看ELF的不同類型。期間將演示如何創建動態連接庫(即可共享文件)、靜態連接庫,並比較它們的異同。

 

$ gcc -c myprintf.c test.c          #編譯產生兩個目標文件myprintf.otest.o,它們都是可重定位文件(REL)

$ readelf -h test.o | grep Type   

  Type:                              REL (Relocatable file)

$ readelf -h myprintf.o | grep Type

  Type:                              REL (Relocatable file)

$ gcc -o test myprintf.o test.o     #根據目標代碼連接產生可執行文件,這裏的文件類型是可執行的(EXEC)

$ readelf -h test | grep Type

  Type:                              EXEC (Executable file)

$ ar rcsv libmyprintf.a myprintf.o  #ar命令創建一個靜態連接庫

$ readelf -h libmyprintf.a | grep Type  #因此,使用靜態連接庫和可重定位文件一樣,它們之間唯一不同是前者可以是多個可重定位文件的集合

  Type:                              REL (Relocatable file)

$ gcc -o test test.o -llib -L./      #可以直接連接進去,也可以使用-l參數,-L指定庫的搜索路徑

$ gcc -Wall myprintf.o -shared -Wl,-soname,libmyprintf.so.0 -o libmyprintf.so.0.0                   #編譯產生動態鏈接庫,並支持majorminor版本號,動態鏈接庫類型爲DYN

$ ln -sf libmyprintf.so.0.0 libmyprintf.so.0

$ ln -sf libmyprintf.so.0 libmyprintf.so

$ readelf -h libmyprintf.so | grep Type

  Type:                              DYN (Shared object file)

$ gcc -o test test.o -llib -L./      #編譯時和靜態連接庫類似,但是執行時需要指定動態連接庫的搜索路徑

$ LD_LIBRARY_PATH=./ ./test          #LD_LIBRARY_PATH爲動態鏈接庫的搜索路徑

$ gcc -static -o test test.o -llib -L./  #在不指定static時會優先使用動態鏈接庫,指定時則阻止使用動態連接庫這個時候會把所有靜態連接庫文件加入到可執行文件中.

 

可重定位文件本身不可以運行,僅僅是作爲可執行文件、靜態連接庫(也是可重定位文件)、動態連接庫的 組件

 

下面來看看ELF文件的主體內容,節區(Section)ELF文件具有很大的靈活性,它通過文件頭組織整個文件的總體結構,通過節區表 (Section Headers Table)和程序頭(Program Headers Table或者叫段表)來分別描述可重定位文件和可執行文件。在可重定位文件中,節區表描述的就是各種節區本身;而在可執行文件中,程序頭描述的是由各個節區組成的段(Segment),以便程序運行時動態裝載器知道如何對它們進行內存映像,從而方便程序加載和運行。

 

可以通過readelf-S參數查看ELF的節區。先來看看可重定位文件的節區信息,通過節區表來查看:

$ gcc -c myprintf.c             #默認編譯好myprintf.c,將產生一個可重定位的文件myprintf.o

 

$ readelf -S myprintf.o         #通過查看myprintf.o的節區表查看節區信息

There are 11 section headers, starting at offset 0xc0:

Section Headers:

  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al

  [ 0]                   NULL            00000000 000000 000000 00      0   0  0

  [ 1] .text             PROGBITS        00000000 000034 000018 00  AX  0   0  4

  [ 2] .rel.text         REL             00000000 000334 000010 08      9   1  4

  [ 3] .data             PROGBITS        00000000 00004c 000000 00  WA  0   0  4

  [ 4] .bss              NOBITS          00000000 00004c 000000 00  WA  0   0  4

  [ 5] .rodata           PROGBITS        00000000 00004c 00000e 00   A  0   0  1

  [ 6] .comment          PROGBITS        00000000 00005a 000012 00      0   0  1

  [ 7] .note.GNU-stack   PROGBITS        00000000 00006c 000000 00      0   0  1

  [ 8] .shstrtab         STRTAB          00000000 00006c 000051 00      0   0  1

  [ 9] .symtab           SYMTAB          00000000 000278 0000a0 10     10   8  4

  [10] .strtab           STRTAB          00000000 000318 00001a 00      0   0  1

Key to Flags:

  W (write), A (alloc), X (execute), M (merge), S (strings)

  I (info), L (link order), G (group), x (unknown)

  O (extra OS processing required) o (OS specific), p (processor specific)

 

$ objdump -d -j .text myprintf.o    #這裏是程序指令部分,用objdump-d選項可以看到反編譯的結果,-j指定需要查看的節區

myprintf.o:     file format elf32-i386

Disassembly of section .text:

複製代碼
00000000 :

   0:   55                      push   %ebp

   1:   89 e5                   mov    %esp,%ebp

   3:   83 ec 08                sub    $0x8,%esp

   6:   83 ec 0c                sub    $0xc,%esp

   9:   68 00 00 00 00          push   $0x0

   e:   e8 fc ff ff ff          call   f

  13:   83 c4 10                add    $0x10,%esp

  16:   c9                      leave

  17:   c3                      ret

 
複製代碼

 

$ readelf -r myprintf.o              #-r選項可以看到有關重定位的信息,這裏有兩部分需要重定位

Relocetion section '.rel.text' at offset 0x334 contains 2 entries:

 Offset     Info    Type            Sym.Value  Sym. Name

0000000a  00000501 R_386_32          00000000   .rodata

0000000f  00000902 R_386_PC32        00000000   puts

 

$ readelf -x .rodata myprintf.o      #.rodata節區包含只讀數據,即我們要打印的hello, world!.

Hex dump of section '.rodata':

  0x00000000 68656c6c 6f2c2077 6f726c64 2100     hello, world!.

 

$ readelf -x .data myprintf.o         #沒有這個節區,.data應該包含一些初始化的數據

Section '.data' has no data to dump.

 

$ readelf -x .bss myprintf.o          #也沒有這個節區,.bss應該包含一些未初始化的數據,程序默認初始爲0

Section '.bss' has no data to dump.

 

$ readelf -x .comment myprintf.o      #是一些註釋,可以看到是是GCC的版本信息

Hex dump of section '.comment':

  0x00000000 00474343 3a202847 4e552920 342e312e .GCC: (GNU) 4.1.

  0x00000010 3200                                2.

 

$ readelf -x .note.GNU-stack myprintf.o #這個也沒有內容

Section '.note.GNU-stack' has no data to dump.

 

$ readelf -x .shstrtab myprintf.o      #包括所有節區的名字

Hex dump of section '.shstrtab':

  0x00000000 002e7379 6d746162 002e7374 72746162 ..symtab..strtab

  0x00000010 002e7368 73747274 6162002e 72656c2e ..shstrtab..rel.

  0x00000020 74657874 002e6461 7461002e 62737300 text..data..bss.

  0x00000030 2e726f64 61746100 2e636f6d 6d656e74 .rodata..comment

  0x00000040 002e6e6f 74652e47 4e552d73 7461636b ..note.GNU-stack

  0x00000050 00                                  .

 

$ readelf x .symtab myprintf.o        #符號表,包括所有用到的相關符號信息,如函數名、變量名

Symbol table '.symtab' contains 10 entries:

   Num:    Value  Size Type    Bind   Vis      Ndx Name

     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND

     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS myprintf.c

     2: 00000000     0 SECTION LOCAL  DEFAULT    1

     3: 00000000     0 SECTION LOCAL  DEFAULT    3

     4: 00000000     0 SECTION LOCAL  DEFAULT    4

     5: 00000000     0 SECTION LOCAL  DEFAULT    5

     6: 00000000     0 SECTION LOCAL  DEFAULT    7

     7: 00000000     0 SECTION LOCAL  DEFAULT    6

     8: 00000000    24 FUNC    GLOBAL DEFAULT    1 myprintf

     9: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND puts

 

$ readelf -x .strtab myprintf.o    #字符串表,用到的字符串,包括文件名、函數名、變量名等。

Hex dump of section '.strtab':

  0x00000000 006d7970 72696e74 662e6300 6d797072 .myprintf.c.mypr

  0x00000010 696e7466 00707574 7300              intf.puts.

 

從上表可以看出,對於可重定位文件,會包含這些基本節區.text, .rel.text, .data, .bss, .rodata, .comment, .note.GNU-stack, .shstrtab, .symtab.strtab

 

看一看myprintf.c產生的彙編代碼。

$ gcc -S myprintf.c

複製代碼
$ cat myprintf.s

        .file   "myprintf.c"

        .section        .rodata

.LC0:

        .string "hello, world!"

        .text

.globl myprintf

        .type   myprintf, @function

myprintf:

        pushl   %ebp

        movl    %esp, %ebp

        subl    $8, %esp

        subl    $12, %esp

        pushl   $.LC0

        call    puts

        addl    $16, %esp

        leave

        ret

        .size   myprintf, .-myprintf

        .ident  "GCC: (GNU) 4.1.2"

        .section        .note.GNU-stack,"",@progbits

 
複製代碼

 

4、鏈接

 

鏈接是處理可重定位文件,把它們的各種符號引用和符號定義轉換爲可執行文件中的合適信息(一般是虛擬內存地址)的過程。鏈接又分爲靜態鏈接和動態鏈接,前者是程序開發階段程序員用ld(gcc實際上在後臺調用了ld)靜態鏈接器手動鏈接的過程,而動態鏈接則是程序運行期間系統調用動態鏈接器(ld-linux.so)自動鏈接的過程。比如,如果鏈接到可執行文件中的是靜態連接庫libmyprintf.a,那麼.rodata節區在鏈接後需要被重定位到一個絕對的虛擬內存地址,以便程序運行時能夠正確訪問該節區中的字符串信息。而對於puts,因爲它是動態連接庫libc.so中定義的函數,所以會在程序運行時通過動態符號鏈接找出puts函數在內存中的地址,以便程序調用該函數。

 

靜態鏈接過程主要是把可重定位文件依次讀入,分析各個文件的文件頭,進而依次讀入各個文件的節區,並計算各個節區的虛擬內存位置,對一些需要重定位的符號進行處理,設定它們的虛擬內存地址等,並最終產生一個可執行文件或者是動態鏈接庫。這個鏈接過程是通過ld來完成的,ld在鏈接時使用了一個鏈接腳本(linker scripq),該鏈接腳本處理鏈接的具體細節。這裏主要介紹可重定位文件中的節區(節區表描述的)和可執行文件中段(程序頭描述的)的對應關係以及gcc編譯時採用的一些默認鏈接選項。

 

下面先來看看可執行文件的節區信息,通過程序頭(段表)來查看:

=======================================================================

$ readelf -S test.o                        #爲了比較,先把test.o的節區表也列出
There are 10 section headers, starting at offset 0xb4:
Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000034 000024 00  AX  0   0  4
  [ 2] .rel.text         REL             00000000 0002ec 000008 08      8   1  4
  [ 3] .data             PROGBITS        00000000 000058 000000 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 000058 000000 00  WA  0   0  4
  [ 5] .comment          PROGBITS        00000000 000058 000012 00      0   0  1
  [ 6] .note.GNU-stack   PROGBITS        00000000 00006a 000000 00      0   0  1
  [ 7] .shstrtab         STRTAB          00000000 00006a 000049 00      0   0  1
  [ 8] .symtab           SYMTAB          00000000 000244 000090 10      9   7  4
  [ 9] .strtab           STRTAB          00000000 0002d4 000016 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

=======================================================================

$ gcc -o test test.o libmyprintf.o
$ readelf -l test        #我們發現,testtest.o,libmyprintf.o相比,多了很多節區,如.interp.init
Elf file type is EXEC (Executable file)
Entry point 0x80482b0
There are 7 program headers, starting at offset 52
Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
  INTERP         0x000114 0x08048114 0x08048114 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x0047c 0x0047c R E 0x1000
  LOAD           0x00047c 0x0804947c 0x0804947c 0x00104 0x00108 RW  0x1000
  DYNAMIC        0x000490 0x08049490 0x08049490 0x000c8 0x000c8 RW  0x4
  NOTE           0x000128 0x08048128 0x08048128 0x00020 0x00020 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame 
   03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag 
   06     

上表給出了可執行文件的如下幾個段(segment)

PHDR: 給出了程序表自身的大小和位置,不能出現一次以上。
INTERP: 因爲程序中調用了puts(在動態鏈接庫中定義),使用了動態連接庫,因此需要動態裝載器/鏈接器(ld-linux.so)
LOAD: 包括程序的指令,.text等節區都映射在該段,只讀(R)
LOAD: 包括程序的數據,.data, .bss等節區都映射在該段,可讀寫(RW)
DYNAMIC: 動態鏈接相關的信息,比如包含有引用的動態連接庫名字等信息
NOTE: 給出一些附加信息的位置和大小
GNU_STACK: 這裏爲空,應該是和GNU相關的一些信息

這裏的段可能包括之前的一個或者多個節區,也就是說經過鏈接之後原來的節區被重排了,並映射到了不同的段,這些段將告訴系統應該如何把它加載到內存中。這些新的節區來自哪裏?它們的作用是什麼呢?先來通過gcc-v參數看看它的後臺鏈接過程。

=======================================================================

$ gcc -v -o test test.o myprintf.o    #把可重定位文件鏈接成可執行文件
Reading specs from /usr/lib/gcc/i486-slackware-linux/4.1.2/specs
Target: i486-slackware-linux
Configured with: ../gcc-4.1.2/configure --prefix=/usr --enable-shared --enable-languages=ada,c,c++,fortran,java,objc --enable-threads=posix --enable-__cxa_atexit --disable-checking --with-gnu-ld --verbose --with-arch=i486 --target=i486-slackware-linux --host=i486-slackware-linux
Thread model: posix
gcc version 4.1.2
 /usr/libexec/gcc/i486-slackware-linux/4.1.2/collect2 --eh-frame-hdr -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/gcc/i486-slackware-linux/4.1.2/http://www.cnblogs.com/../crt1.o /usr/lib/gcc/i486-slackware-linux/4.1.2/http://www.cnblogs.com/../crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/lib/gcc/i486-slackware-linux/4.1.2/http://www.cnblogs.com/http://www.cnblogs.com/i486-slackware-linux/lib -L/usr/lib/gcc/i486-slackware-linux/4.1.2/http://www.cnblogs.com/.. test.o myprintf.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/gcc/i486-slackware-linux/4.1.2/http://www.cnblogs.com/../crtn.o

發佈了43 篇原創文章 · 獲贊 6 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章