第二章讀書讀書筆記----靜態鏈接

        最近搞了一個Linux愛好者的微信訂閱號,以Linux平臺爲主,定期也會分享一些網友的博文,和技術經驗。因爲網絡文章,不和博主溝通,沒法隨意轉發分享,如果您方便讓您的經驗一起分享,請您留下博客地址~~~以下是微信的訂閱號,歡迎關注和討論: 


        對一個不善寫作的人來說,寫一篇博客着實痛苦,但還是逼着自己去寫,讓所學的東西加深。本篇博客將對第二章內容進行回顧,理解,當然還要加上實踐!

從C語言到可執行程序

        依稀記得第一次見到同學在TC編譯器上運行一個“HelloWorld”的程序時的心情。不是驚歎,而是疑惑。寫的代碼比打印出來的“Hello World”字符串還要多很多,爲啥不直接寫在NotePad中顯示出來?第一個問題好傻,是吧,各位看官笑笑就行。接下來我又問同學,“爲什麼你所寫的代碼能夠運行後顯示出來Hello World呢?”,那時候其實我對於“代碼”,“運行”這些概念都不是很清楚。這個問題也就是本章內容所闡述的,我們一起來看看吧?

#include<stdio.h>
int main ()
{
         printf(“Hello World!”);
         return 0;
}

        上面是一段C語言的HelloWorld程序,C代碼經過編譯器編譯之後就可以運行在操作系統之上了。這其中主要經歷了4個階段:預處理(Prepressing),編譯(Compilation),彙編(Assembly)鏈接(Linking),如下圖所示。


預編譯

        預編譯過程主要處理源代碼文件中以”#”開始的預編譯指令,主要規則如下:

  • 將”#define”刪除,並展開所有的宏定義;
  • 處理所有條件預編譯指令,比如”#if”, “#ifdef”, “#elif”, “else”, 和”#endif”;
  • 處理”#include”預編譯指令,將包含的文件插入到預編譯指令的位置;
  • 刪除所有註釋”//”和”/* */”;
  • 添加行號和文件名標識,以便於編譯時編譯器產生調試用的行號信息及用於編譯時產生編譯錯誤或警告時能夠顯示行號;
  • 保留所有”#pragma”編譯器指令,因爲編譯器需要使用他們。

        接下來,我們就來動手看一看,到底被預處理後的文件是什麼個樣子,首先我們寫”hello.h”和”hello.c”:

//hello.h
#defineMACRO_TEST 5
 
/*
Function"add"
This function isused for add operation for two int type values.
*/
int add(intiVal1, int iVal2)
{
   return iVal1+iVal2;
}

//hello.c
#include<stdio.h>
#include"hello.h"
 
#ifdefMACRO_TEST
int iPlatform =1;
#else
int iPlatform =2;
#endif
 
#pragma pack(1)
 
int iVal =MACRO_TEST;
 
int main()
{
   //Just printf the values
   printf("iPlatform: %d, iVal %d, add%d\n", iPlatform, iVal, add(iPlatform, iVal));
   return 0;
}
 
然後在使用gcc對源碼進行預編譯處理:
[root@localhosttest]# gcc -E hello.c -o hello.i
在預處理後,可以看到結果如下所示(只取了部分):
# 2"hello.c" 2
# 1"hello.h" 1
 
int add(intiVal1, int iVal2)
{
   return iVal1+iVal2;
}
# 3"hello.c" 2
int iPlatform =1;
 
#pragma pack(1)
 
int iVal = 5;
 
int main()
{
 
   printf("iPlatform: %d, iVal %d, add%d\n", iPlatform, iVal, add(iPlatform, iVal));
   return 0;
}

         預處理的結果和之前所說的六條理論相一致,在自己開發過程中,用的最多的應該是去看宏擴展後的情況。有網友可能看到了[ # 3 "hello.c" 2], 其中3表示第三行,”hello.c”表示所在文件,”2”是跟在其後的flag,這裏表示在”include”某個文件之後,再次回到這個文件,具體其他的falg,可以參考PreprocessorOutput

編譯

         很多時候,我們認爲編譯時指從源代碼到可執行文件的過程,這裏我們編譯指:把預處理文件進行一系列詞法分析語法分析語義分析優化後生成相應的彙編代碼文件。

         使用gcc的命令進行反彙編:

 [root@localhosttest]# gcc -S hello.c -o hello.s

         反彙編的結果如下(只取了部分),彙編和我一樣不怎麼好的同學,也能明白個大概吧:

         main:
.LFB3:
       pushq   %rbp
.LCFI2:
       movq    %rsp, %rbp
.LCFI3:
       movl    iVal(%rip), %esi
       movl    iPlatform(%rip), %edi
       call    add
       movl    %eax, %ecx
       movl    iVal(%rip), %edx
       movl    iPlatform(%rip), %esi
       movl    $.LC0, %edi
       movl    $0, %eax
       call    printf
       movl    $0, %eax
       leave
       ret

        下面我們將根據如下代碼,所經歷的詞法分析語法分析語義分析優化的過程進行分析:

         array[index]= (index + 4) * (2 + 6)

 詞法分析

         學過編譯原理課的都知道,這一步只需要通過一個有限狀態機(Finite State Machine)來實現對代碼的掃描,上面的代碼將會被分成一個個的符號:”array”, “[“, “index”, “]”, “=”, “(“, “index”, “+”, “4”, “*”, “(“, “2”,“+”, “6”, “)” 。

         在詞法分析的過程中,如果遇到非法的字符,則進行報錯,退出編譯過程。

 語法分析

         語法分析器採用了上下文無關語法(Context-free Grammar),這個是偏理論的東西,在這邊我們暫且不做深入研究,可以看看相關的書籍。簡單來說,由語法分析器生成的語法樹就是以表達式爲節點的數。以上的代碼的語法樹如下圖所示:


         在語法分析階段如果出現表達式不合法,比如括號不匹配、表達式缺少操作符等,編譯器會報告語法分析階段的錯誤。

 語義分析

         語法分析中只是將表達式已語法樹的形式組織起來,但並沒有對其進行含義的分析,即語義的分析,比如”*”表示兩個變量或者值相乘,並且變量必須要能夠進行乘法運算。下圖爲標示了語義的語法樹:


我們在編譯階段最經常碰到的就是語義問題,比如類型不匹配。

 中間語言生成

         之前提到過代碼優化,代碼優化可分爲兩部分,一部分是編譯器前端產生機器無關的中間代碼,並進行優化;另一部分是,編譯器後端將中間代碼轉換爲目標機器代碼,並進行優化。這一部分主要講中間語言生成和優化。

         因爲在語法樹中直接對其進行優化比較困難(我想是樹結構的操作吧?),所以往往先將語法樹轉換爲中間代碼。比較常見的中間代碼類型有:三地址碼(Three-addressCode)P-代碼(P-Code). 採用三地址碼描述之前的表達式如下:

t1 = 2 + 6
t2 = index + 4
t3 = t2 * t1
array[index] = t3

         優化後,即變成了:

t2 = index +4
t2 = t2 * 8
array[index] = t2

目標代碼生成和優化

         中間代碼,將根據不同的硬件平臺,產生目標機器的能夠執行的代碼。注意這裏的是能夠執行的目標機器的代碼,是進行彙編(Assembly)之後的結果,不過放在本章一起講解一下。

         之前代碼最終可能生成如下指令,這裏使用x86彙編語言來描述:

movl index, %ecx          ; value of index to ecx
addl $4, %ecx             ; ecx = ecx + 4
mull $8, %ecx             ; ecx = ecx * 8
movl index, %eax          ; value of index to eax
movl %ecx, array(,eax,4)    ; array[index] = ecx

         8即爲2的三次方,所以乘法可以採用移位的方式進行,則優化可以採用基指比例變址尋址(Base Index Scale Addressing)的lea指令來完成,優化後如下所示:

movl index, %ecx
leal 32(,%edx,8), %eax
movl %eax, arrray(,%edx,4)

         可能對AT&T彙編不熟悉的朋友,對上面的”array(,eax,4)”和”arrray(,%edx,4)”不是很理解。AT&T彙編形式”base(offset, index, i)”即爲”base+offset+index*i”.

 彙編

         這裏的彙編是指將編譯過程中產生的彙編代碼,轉換爲目標機器可以執行的指令。這個一個步驟比較簡單,就將彙編代碼對應機器指令一條一條的翻譯。

         產生的hello.s可以用如下命令生成目標文件(ObjectFile)hello.o:

[root@localhost test]# gcc -c hello.s -ohello.o
         在上一章節中已經提到了目標代碼生成後的優化。

 鏈接

         一個工程中一般由多個源文件構成,每一個源文件編譯器處理過程中都會產生目標文件(C/C++中爲.o文件)。這些目標文件以及靜態庫,動態庫需要通過鏈接來完成(使用gcc或者ld命令去完成,其實gcc也是調用了ld^_^)。

         鏈接過程主要包括了地址和空間分配(Address and Storage Allocation)符號決議(Symbol Resolution)重定位(Relocation)等步驟。這些在後續的章節中會有詳細的描述。

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章