1. 程式人生 > >深入理解計算機系統(1.1)------Hello World 是如何執行的

深入理解計算機系統(1.1)------Hello World 是如何執行的

上一篇序章我談了談 程式設計師為啥要懂底層計算機結構 ,有人贊同也有人反對,但是這並不影響 LZ 對深入理解計算機系統研究的熱情。這篇部落格以案例驅動的模式,通過跟蹤一個簡單 Hello World 程式的生命週期開始系統的學習,包括它被程式設計師建立,到在系統上執行,輸出簡單的訊息,然後終止。LZ 將沿著這個程式的宣告週期,先簡要的介紹一些逐步出現的關鍵概念、專業術語以及組成部分。後面將會詳細展開。

  

1、計算機系統

  我們知道計算機系統是由硬體和軟體組成的。它們共同工作來執行應用程式。雖然系統的實現方式隨著時間不斷變化,但是系統內在的概念卻沒有改變。所有計算機系統都有相似的硬體和軟體元件,它們執行這相似的功能,我們只有深入瞭解這些元件是如何工作的,以及這些元件是如何影響程式的正確性和效能的,才能寫出高質量的程式碼。

 

2、萬能程式大法----Hello World

#include <stdio.h>
 
int main()
{
    printf("Hello World\n");
    return 0;//c標準規定建議main函式返回值為int
}

這段程式碼不用多說,就是一個C語言的Hello World,程式的執行結果是列印 “Hello World”。

 

3、資訊的表示

我們將上面的 Hello World 程式儲存在一個 hello.c 的檔案中,那麼它是怎麼儲存在檔案中的呢?實際上它是以位元組序列的方式儲存在檔案中。

什麼是位元組?一個位元組由8個位組成,而一個位是由值0和1組成。也就是說 hello.c 源程式是由值0和1組成的位序列。

大部分的現代系統都是用 ASCII 碼構成,這種方式實際上就是用一個唯一的單位元組大小的整數來表示每個字元。下面我們給出 hello.c 程式的 ASCII 碼錶示:

左邊是檔案對應的16進位制程式碼,右邊是我們的源程式,例如:第一個字元“#”的 ASCII 值是0x23。需要特別注意一下:每個文字行都以一個看不見的換行符‘\n’結束的。第2行中有2個連續的0x0D 0x0A ,這是windows中特有的“換行符\r\n” ,在linux中的是“換行符\n”。像hello.c檔案這樣只由 ASCII 碼組成的檔案叫做個“文字檔案”,其他所有檔案都叫“二進位制檔案”。

系統中所有的資訊都是由位+上下文構成。

包括磁碟檔案、儲存器中的程式,儲存器中存放的使用者資料以及網路上傳送的資料都是由一串位表示。而區分不同資料物件的唯一方法就是我們讀到這些物件時的上下文。比如在不同的上下文中,一個同樣的位元組序列可能表示一個整數、浮點數、字串或者機器指令。

作為程式設計師,我們需要了解數字的機器表示方式,因為它們與實際的整數和實數是不同的。它們是對真值的有限近視值,有時候會有意想不到的行為表現。這個後面我們會詳細講解。

 

4、程式的編譯

hello 程式的生命週期是從一個高階 C 語言程式開始的,因為這種形式能被人讀懂。然而,計算機系統是讀不懂高階語言的。為了在系統上執行 hello.c 程式,每條 C 語句都必須要被其他程式轉化為一系列的低階機器語言指令。

一般來說,要將 hello.c 變成一個可執行的目標程式,必須要經過 前處理器、編譯器、彙編器和連結器 的處理。如下:

前處理器、編譯器、彙編器和連結器 一起構成了編譯系統,下面對每個步驟分別進行解析:

①、預處理階段:前處理器 cpp 根據以字元 # 開頭的命令,修改原始的 C 程式,比如 Hello.c 中第一行 #include<studio.h> 命令告訴前處理器讀取系統檔案 stdio.h 的內容,並把它直接插入到程式中。結果就得到另一個 C 程式,通常是以 .i 作為副檔名。

②、編譯階段:編譯器 ccl 將文字檔案 hello.i 翻譯成文字檔案 hello.s,它包含一個組合語言程式,組合語言程式中的每條語句都以一種標準的文字格式確切的描述一條低階機器語言指令。組合語言能為不同高階語言的不同編譯器提供通用的輸出語言。

③、彙編階段:彙編器 as 將hello.s 翻譯成機器語言指令,把這些指令打包成一種叫做可重定位目標程式的格式,並將結果儲存在目標檔案 hello.o 中,hello.o 檔案是一個二進位制檔案,它的位元組編碼是機器預言指令而不是字元。如果我們用文字編輯器開啟 hello.o 檔案,將會是一堆亂碼。

④、連結階段:在 hello.c 程式中,我們看到程式呼叫了 printf 函式,它是每個 C 編譯器都會提供的標準 C 庫中的一個函式。printf 函式存在於一個名為 printf.o 的單獨的預編譯好了的目標檔案中,而這個檔案必須以某種方式合併到我們的 hello.o 程式中。連結器 ld 就是負責處理這種合併,結果就得到一個 hello 檔案,它是一個可執行目標程式,可以被載入到記憶體中,由系統執行。

這裡我做一下驗證,我在 Linux 系統上建立 hello.c 程式,然後依次執行上面的步驟:

預處理:

gcc -E hello.c -o hello.i

然後檢視 hello.i

編譯階段:

gcc -S hello.i

然後檢視 hello.s

上面截圖的是一個彙編程式

5、程式的執行

  經過上面程式的編譯,hello.c 源程式已經被編譯成了可執行目標檔案 hello,並存放在磁碟上,那麼如何執行呢?

  ①、系統的硬體組成

  為了理解執行 hello 程式時發生了什麼,我們先要了解一個典型系統的硬體組織。如下圖:

我們現在不需要對這張圖有很深入的理解,後面會詳細進行介紹。現在先簡單的認識一下下面幾個主要部件:

  一、匯流排:貫穿整個系統的一組電子管道,通常被設計成用來傳送定長的位元組塊,也就是字。字的大小與系統相關,比如在32位作業系統當中,一個字是4個位元組

  二、I/O裝置:輸入/輸出(I/O)裝置是系統與外部世界聯絡通道,上圖有4個I/O裝置。作為使用者輸入的鍵盤和滑鼠,作為使用者輸出的顯示器,以及用於長期儲存資料和程式的磁碟。每一個I/O裝置都通過一個控制器或者介面卡與I/O匯流排相連。控制器是置於I/O裝置本身的或者系統的主印刷電路板(通常稱為主機板)上的晶片組,而介面卡則是一塊插在主機板插槽上的卡。無論如何,它們的功能都是在 I/O 匯流排和 I/O 裝置之間傳遞資訊。

  三、主存:它是計算機中的一個臨時儲存裝置,在處理器執行程式的時候,用來存放程式和程式處理的資料。物理上來說,主存是由一組動態隨機存取儲存器(DRAM)組成的,邏輯上來說,它是一個線性的位元組陣列,每一個位元組都有唯一的地址(即陣列索引)。

  四、處理器:全稱中央處理器(CPU),是解釋(或執行)儲存在主存中指令的引擎。處理器的核心是一個字長的儲存裝置(或暫存器),簡稱程式計數器(PC),在任何時刻,它都會指向主存中的某條機器指令(即含有該條指令的地址)。從系統通電到斷點,處理器一直在不斷的執行程式計數器所指向指令,再更新程式計數器,使其指向下一條指令。處理器所做的操作是圍繞主存、暫存器檔案以及算術/邏輯單元(ALU)進行的,暫存器檔案是一個小的儲存裝置,由一些1字長的暫存器組成,每個暫存器都有唯一的名字。ALU則計算新的資料和地址值。

    CPU 在指令的要求下會做如下操作:

    ①、載入:把一個位元組或者一個字從主存複製到暫存器,以覆蓋暫存器原來的內容

    ②、儲存:把一個位元組或者一個字從暫存器複製到主存的某個位置,以覆蓋這個位置上原來的內容

    ③、操作:把兩個暫存器的內容複製到 ALU,ALU 對這兩個字做算術操作,並把結果存放到一個暫存器中,以覆蓋暫存器原來的內容

    ④、跳轉:從指令本身中抽取一個字,並將這個字複製到程式計數器(PC)中,以覆蓋PC中原來的內容。

  處理器當中提到的是指令集結構的簡單實現,不過實際上現代處理器使用了非常複雜的機制來加速程式的執行。我們可以這樣去區分指令集機構以及微體系結構,指令集結構描述的是每條機器程式碼指令的效果,而微體系結構描述的是處理器實際上是如何實現的,類似於JAVA虛擬機器與JAVA虛擬機器實現的關係。

 

   ②、執行 Hello World 程式

  前面簡單的介紹了系統的硬體組成和操作,那麼接下來介紹我們執行程式時到底發生了什麼。

  想要在 Linux 系統中執行該可執行程式,我們要將它的檔名輸入到稱為外殼(shell)的應用程式中,外殼是一個命令列直譯器,它輸出一個提示符,等待你輸入一個命令,然後執行這個命令。如果該命令列的第一個單詞不是一個內建的外殼命令,那麼外殼就會假設這是一個可執行檔案的名字,它將載入並執行這個檔案。

  初始時,外殼程式執行它的指令,等待我們輸入一個命令。當我們在鍵盤上輸入字串"./hello"後,外殼程式將字元逐一讀入到暫存器中,再把它放入到儲存器中,如下圖:

  PS:為什麼要輸入“./hello”來執行,對於Linux系統有一定了解的人,可能知道這是執行命令的一種方法。

當我們在鍵盤上敲回車鍵的時候,外殼程式知道我們已經結束了命令的輸入。然後外殼執行一系列指令來載入可執行的 hello 檔案,將 hello 目標檔案中的程式碼和資料從磁碟複製到主存。資料包括最終會被輸出的字串“Hello World\n”,一旦目標檔案中的程式碼和資料被載入到主存,處理器就開始執行 hello 程式的 main 程式中的機器語言指令。這些指令將“Hello World\n” 字串中的位元組從主存複製到暫存器檔案,再從暫存器檔案中複製到顯示裝置,最終顯示在螢幕上。

6、 本章總結

   ①、出現的名詞解釋:  

  :"位(bit)"是電子計算機中最小的資料單位。每一位的狀態只能是0或1。

  位元組:8個二進位制位構成1個"位元組(Byte)",它是儲存空間的基本計量單位。1個位元組可以儲存1個英文字母或者半個漢字,換句話說,1個漢字佔據2個位元組的儲存空間。

  :"字"由若干個位元組構成,字的位數叫做字長,不同檔次的機器有不同的字長。例如一臺8位機,它的1個字就等於1個位元組,字長為8位。如果是一臺16位機,那麼,它的1個字就由2個位元組構成,字長為16位。在32位作業系統當中,一個字是4個位元組,字是計算機進行資料處理和運算的單位。

  ASCII:American Standard Code for Information Interchange,美國資訊交換標準程式碼。注意不是ASCⅡ(羅馬數字2),使用指定的7 位或8 位二進位制數組合來表示128 或256 種可能的字元。標準ASCII 碼也叫基礎ASCII碼,使用7 位二進位制數(剩下的1位二進位制為0)來表示所有的大寫和小寫字母,數字0 到9、標點符號, 以及在美式英語中使用的特殊控制字元。

  文字檔案和二進位制檔案

    文字檔案是指以ASCII碼方式(也稱文字方式)儲存的檔案,後面基於 utf-8 編碼的文字檔案,utf-8是能夠向後相容ASCII,即相同的ASCII文字檔案和UTF-8文字檔案完全一致。它是一種典型的順序檔案,其檔案的邏輯結構又屬於流式檔案。

    二進位制檔案:是基於值編碼的檔案,你可以根據具體應用,指定某個值(可以看作是自定義編碼)。

   ②、內容總結

  計算機是由軟體與硬體組成的,而硬體又包括了匯流排、I/O裝置、主存以及處理器,其中資訊是由位以及上下文表示的,而資訊則是從I/O裝置以位的形式通過匯流排進入主存,然後由處理器從主存將資訊取出處理。

         一個程式的執行,是經歷了前處理器、編譯器、彙編器以及連結器的處理之後,才最終成為可執行的檔案。

 

  PS:有人問我《深入理解計算機系統》這本書的PDF文件,這裡給出下載連結:http://pan.baidu.com/s/1boOM3Tl