程序員的自我修養——鏈接、裝載與庫 筆記(一)

程序員的自我修養

  悄咪咪的說一句,這篇文章可能需要對計算機有過系統的學習,不然看着可能一臉懵。如果有疑問的話,當然,很可能是我太菜了,寫的不好,歡迎大家評論區留言指教!此筆記只是剛剛開始,後續我會接着寫後面的筆記。


  《程序員的自我修養——鏈接、裝載與庫》,初次聽到這本書是因爲學長學姐的推薦,其實在剛接觸C語言的時候,就很好奇一個程序最終是怎麼跑起來的,一個簡單的輸出“Hello Word!”中間到底經歷了什麼樣的過程?

  奈何那時對計算機的瞭解剛剛入門,計算機的體系架構混亂,(計算機的五大部件,一開始我自信的告訴別人是:顯卡、CPU、內存條、硬盤、電源)真想給當時的自己一巴掌~~~~,實在無法理解書中的精華。大三第一學期結束,已經學完操作系統和計算機組成原理這兩門課,有了基礎後,準備詳細的閱讀這本書,我會陸陸續續的把我的筆記和所感記錄在這。

//hello.c文件
#include <stdio.h>
 int main(void)
 {
  printf("hello world!\n");
  return 0;
 }

1. 被隱藏了的過程

在這裏插入圖片描述

預編譯(也可以叫預處理) 展開所有的宏定義(就是#define)、處理所有的預編譯指令(如:“#if”、“#ifdef”、“#elif”、“#else”、“endif”等)、遞歸包含頭文件並將其中邏輯插入需要的地方、刪除所有註釋、添加行號和文件名標識(便於編譯器產生調試用的行號信息)、保留所有#pragma指令

gcc -E hello.c -o hello.i

輸出的hello.i文件中存放着hello.c經預處理之後的代碼

編譯:編譯過程就是把預處理完的文件進行一系列的詞法分析、語法分析、語義分析及優化後生成相應的彙編文件。可以使編程者不必過多考慮與機器有關的細節。

gcc -S hello.c -o hello.s

可以得到彙編的輸出文件hello.s

彙編:將彙編代碼轉換成機器可以執行的命令(一連串的二進制數,你看不懂,但機器看得懂)。hello.o是目標文件。 調用匯編器as來完成

as hello.s -o hello.o

得到可以執行的機器指令目標文件——hello.o

鏈接:把一大堆用到的文件拼接到一起,通過符號表解析和重定位等最終輸出可加載、可執行的目標文件。

gcc hello.o -o hello

得到可執行程序hello.exe,在命令行窗口運行hello.exe即可出現“hello world!”




2.編譯器扮演了一個什麼樣的角色?

  提到編譯器,我就想到了編輯器和IDE,三者的區別是很大的,不瞭解的人經常鬧出笑話,在這裏做一點普及。

編輯器,提供非常方便易用的開發環境,提供很好的界面。你可以用它們來編寫代碼,查看源文件和文檔等,簡化你的工作。例如VS code、Nodepad++、Atom等。

編譯器,大多數情況下,編譯是將更高級的語言(C、C++等)編譯成低級語言(彙編語言、機器語言)。比如gcc。

集成開發環境(IDE,Integrated Development Environment )是用於提供程序開發環境的應用程序,一般包括代碼編輯器、編譯器、調試器和圖形用戶界面工具。集成了代碼編寫功能、分析功能、編譯功能、調試功能等一體化的開發軟件服務套。常見的有Dev-c、VS、Eclipse等IDE。

  有一句話很經典: “計算機科學領域內的任何問題都可以通過增加一個間接的中間層來解決”,這句話會貫穿你學習計算機的始終,很有意義,值得慢慢品味。

   編譯器簡單來說就是將高級語言翻譯成機器語言的一個工具。使用機器指令或者彙編語言寫的程序依賴於特定機器,一個爲某種CPU編寫的程序在另一塊CPU下完全無法運行,需重新編寫,這是令人無法接受的。所以人們期望能夠採用類似於自然語言來描述一個程序,但自然語言不夠精確,所以類似於數學定義的編程語言誕生了:C、C++、JAVA、Python、PHP等等。


2.1 編譯的過程

在這裏插入圖片描述
編譯過程一般可以分爲五步:

  • 詞法分析:源代碼程序被輸入掃描器,掃描器運用一種類似於有限狀態機的算法可以很輕鬆的將源代碼的字符分割成一系列記號。產生的記號一般可以分爲:關鍵字、標識符、字面量(包含數字、字符串等)和特殊符號(如加號、等號)等等。
    eg: array[index]=(index+4)*(2+6)
記號 類型
array 標識符
[ 左方括號
index 標識符
] 右方括號
= 賦值
左圓括號
+ 加號
4 數字
右圓括號
* 乘號
  • 語法分析:語法分析器將對由掃描器產生的記號進行語法分析,從而產生語法樹。簡單的講,由語法分析器生成的語法樹就是以表達式爲節點的樹。在語法分析的同時,很多運算符號的優先級和函數也被確定下來。比如乘法表達式的優先級比加法高,而圓括號的優先級比乘法高,等等。另外有些符號具有多重含義,比如'*'既有乘號的意思,也有指針的意思,所以語法分析階段必須對這些內容進行區分。如果出現表達式不合法,比如各種括號不匹配、表達式中缺少操作符等,編譯器就會報告語法分析階段的錯誤。(這就是讓人難過的語法報錯,與我們幾乎天天見面。只要你經歷過編程,你一定見過他~~,雖然我極其極其討厭他,可他對我卻不離不棄,生死相依/++/)
    eg:id1:=id2+id3*10 生成語法樹:在這裏插入圖片描述

  • 語義分析:由語義分析器來完成。語法分析僅僅是完成了對表達式的語法層面的分析,但他並不瞭解這個語句是否有真正意義。比如C語言裏兩個指針相乘是沒有意義的,這一個語句在語法上是合法的。又比如說特別讓人頭疼的內存泄漏、指針泄露、野指針等等。編譯器所能分析的語義是靜態語義,所謂靜態語義是指在編譯期可以確定地語義,與之對應的是動態語義,就是只有在運行期才能確定的語義。經過語義分析階段之後,整個語法樹的表達式都被標識了類型,如果有些類型需要隱式轉換,語義分析程序會在語法樹中插入相應的轉換節點。(如果說上邊那玩意語法分析報錯還可以很容易找到哪出錯了,語義分析這如果警告,那你自求多福吧,慢慢找~~)

  • 中間語言的生成:現代編譯器有着很多層次的優化,往往在源代碼級別會有一個優化過程。我們這裏描述的源碼級優化器在不同編譯器中可能會有不同定義。例如語句:array[index]=(index+4)*(2+6),2+6將被優化成8。源代碼優化器往往將整個語法樹轉換成中間代碼,他是語法樹的順序表示,其實他已經非常接近目標代碼了。但它一般跟目標機器和運行環境是無關的,比如不包含數據尺寸、變量地址、和寄存器名字等。中間代碼有很多種類型,常見的有三地址碼和P-代碼。比如,上式的三地址中間代碼是:

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

  中間代碼使得編譯器可以被分爲前端和後端。編譯器前端負責產生於機器無關的中間代碼,編譯器後端將中間代碼轉換爲目標機器代碼。這樣對於一個跨平臺的編譯器而言,他們可以針對不同的平臺使用同一個前端和針對不同機器平臺的數個後端。

  • 目標代碼生成與優化:源代碼優化器產生中間代碼標誌着下面這些過程都屬於編譯器後端。編譯器後端主要包括代碼生成器目標代碼優化器。代碼生成器將中間代碼轉換成目標機器代碼,這個過程十分依賴於目標機器,因爲不同的機器有不同的字長、寄存器、整數數據類型等。
movl index, %ecx;
addl $4, %ecx;
mull $8, %ecx;
movl index, %ecx;
movl %ecx, array(,eax,4);

最後目標代碼優化器對上述的目標代碼進行優化,比如選擇合適的尋址方式、使用位移來代替乘法運算、刪除多餘的指令等。

movl     index, %edc
leal     32(,%edx,8),%eax
movl     %eax,array(,%edx,4)

 現代編譯器有着異常複雜的結構,這是因爲現代高級編程語言本身也很複雜,比如C++語言的定義就極爲複雜。另外現代計算機的CPU也相當複雜,爲了支持CPU的特性,編譯器的機器指令也變得十分複雜。比如著名的GCC編譯器就支持幾乎所有的CPU平臺,這也導致了編譯器的指令生成過程十分複雜。

2.2鏈接器

 經過上述步驟,源代碼終於被編譯成目標代碼。但還有一個問題,那就是index和array的地址問題。index和array的地址從哪得到呢?如果index和array定義在跟上邊源代碼同一個編譯單元裏,那麼編譯器可以爲他們分配空間。那如果是定義在其他程序模塊呢?這裏引出一個問題,很重要!目標代碼中有變量定義在其他模塊該怎麼辦?事實上,定義在其他模塊的全局變量和函數在最終運行時的絕對地址都要在最終鏈接的時候才能確定。所以,現代編譯器可以將一個源代碼文件編譯成一個未鏈接的目標文件,然後由鏈接器最終將這些個目標文件鏈接起來形成可執行文件。 事實上,鏈接器的年齡比編譯器年齡要長。

  • 程序並不是一成不變的,修改後的地址變化問題非常繁瑣。爲了簡化編程,人們開始使用符號來代指位置,其地址在使用過程中動態插入到需要的位置。重新計算各個目標位置的過程即爲重定位
  • 運行一個程序所需要的代碼量可能非常龐大,而且很大一部分代碼可重用性很高,而且模塊之間耦合度很低。於是人們便將代碼分割成了很多部分,使用時再將各個部分拼接起來,這個過程就是鏈接,這些部分在不同的語言中有不同的形式,比如引用、包、或者庫。
  • 可想而知,鏈接過程中必定存在很多內部或外部的函數或變量,所以這個過程包括了很多諸如地址空間分配、符號決議、重定位等步驟。
  • 鏈接的主要內容就是把各個模塊之間相互引用的部分都處理好,使得各個模塊之間能夠正確的銜接。從原理上講,他的工作無非就是把一些指令對其他符號地址的引用加以修正。每個模塊的源代碼文件(.c)文件經過編譯器編譯成目標文件(.o或.obj),目標文件和庫一起鏈接形成最終可執行文件。

3.小結

  首先回顧了從源程序代碼到最終可執行文件的4個步驟:預編譯、編譯、彙編、鏈接。IDE集成開發環境和編譯器默認命令通常將這些步驟合成一步,使得我們通常很少關注這些步驟。
  還回顧了4個步驟中的主要步驟,編譯步驟。介紹了編譯器將C程序源代碼轉變成彙編代碼的若干個步驟。最後介紹了鏈接的歷史及一些基本概念:重定位、符號、目標文件、庫、運行庫的概念。



這是本寶初次寫博客,如有不當之處,還請各位前輩多多指教,謝謝(✿◠‿◠)!

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