1. 程式人生 > >計算機的工作原理(基於機器指令的分析)

計算機的工作原理(基於機器指令的分析)

計算機是怎麼樣工作的

實驗環境:IA32體系結構,Ubuntu 12.04作業系統

一 單任務計算機的工作

1.1.巨集觀上的工作原理

1.11馮·諾依曼結構

      要想知道計算機是怎樣工作的,那麼首先對於計算機的結構的瞭解是必不可少,馮·諾依曼結構奠定了現代計算機的基本結構。如圖1所示。



圖1馮·諾依曼結構

1.12基本工作原理

        按照馮·諾依曼儲存程式的原理,計算機在執行程式時須先將要執行的相關程式和資料放入記憶體儲器中,在執行程式時CPU根據當前程式指標暫存器的內容取出指令並執行指令,然後再取出下一條指令並執行,如此迴圈下去直到程式結束指令時才停止執行。那麼我們就可以將計算機的工作過程簡化成如下圖2所示。


圖2 計算機基本工作原理

         程式計數器,在IA32中通常稱PC,linux中暫存器eip儲存將要執行的下一條指令在儲存器中的地址,通過圖2可知,計算機通過這樣不斷的取指令並且去執行。

1.2基於指令的分析

        由上述分析可知,計算機是通過執行機器指令來維持整個計算機的工作,IA32的機器程式碼與原始C程式碼相差很大,一些通常對C語言程式設計師隱藏的處理器狀態是可見的,比如程式計數器、整數暫存器等。下面我就通過分析機器指令來了解計算機的工作過程。

         當我們用高階語言程式設計的時候,機器遮蔽了程式的細節即機器級的實現,GCC C編譯器可以以彙編程式碼的形式產生輸出,彙編程式碼是機器程式碼的文字表示,非常接近於機器程式碼,所以我們可以通過研究彙編程式碼來分析計算機的工作過程。

分析的C程式碼示例如下所示:

int g(int x)
{
return x+3;
}
int f(int x)
{
return g(x);
}
int main(void)
{
return f(8)+1;
}

1.21 GCC的工作過程

         在分析彙編程式碼之前,首先來了解一下GCC的工作過程,從高階C程式檔案到可執行目標檔案的過程如圖3所示。


圖3 GCC工作過程

總共分為四個步驟,分別是:

(1)預處理階段

gcc -E -o example.cpp  example.c

(2)編譯階段

從預處理階段開始:gcc -x cpp-output -S -o example.s  example.cpp

從原始檔開始:gcc -S –o example.s example.c

(3)彙編階段

從編譯後文件開始:gcc -x assembler –c –o example.o  example.s

從原始碼開始:gcc -c –o example.o example.c

(4)連結階段

從彙編後文件開始:gcc -o  example  example.o

從原始碼開始:gcc -o  example example.c

         為實現方面可以直接編寫Makefile同時生成example.cpp、example.s、example.o、example,Makefile以及生成檔案如下所示:


圖4 Makefile


圖5 檔案的生成

1.22彙編程式碼分析

       下面就到了關鍵的地方,就是通過分析彙編程式碼來了解計算機機器指令的執行過程,這裡我們用objdump –d反彙編來生成易讀的彙編程式碼,彙編程式碼如下所示:


圖6 example.c的彙編程式碼

1.222首先介紹一下所用到的暫存器

          eax為32的累加器暫存器,esp為棧頂指標暫存器,ebp為棧基地址指標暫存器。

1.223其次介紹一下用到的彙編命令

(1)push:push就是壓棧操作,首先將棧頂指標減4,然後將內容壓棧,比如push %ebp相當於sub $4 %esp, mov %ebp (%esp)兩條指令。

(2)pop:pop操作與push正好相反,首先彈棧,然後將棧頂指標加4,比如pop %ebp相當於mov (%esp) %ebp, add $4 %esp兩條指令。

(3)call:call指令是一個函式呼叫指令,它會跳轉到其後接的記憶體地址處的函式進行執行。比如這裡的call 80483b4 <g>,就會跳轉到記憶體地址為0x80483b4的函式g去執行。相當於push %eip, mov $0x483b4 %eip,因為eip存放著的是計算機將要執行的下一條指令的地址,所以可以實現跳轉。

(4)leave:leave指令是函式返回是需要執行的指令,它會重置當前的棧頂指標和棧基地指標,相當於mov %ebp %esp,pop %ebp,簡單來說就是函式要返回了,剛才所用到的棧空間需要清理掉,同時要恢復原來的棧基地址和返回地址。

(5)ret:ret指令是函式返回的最後一步,相當於將pop %eip,就是將棧中存放的返回地址放到eip中,返回到之前的地址去執行。

1.224具體分析

首先從main函式開始分析

push %ebp

儲存函式執行前的原來的棧的基地址

mov %esp, %ebp

將當前棧頂指標賦值給ebp,表示現在函式的新的棧基地址

sub $0x4,%esp

將棧頂指標減4,指向可供儲存引數的位置

mov $0x8,(%esp)

因為要呼叫函式f(8),所以將8壓棧將為函式呼叫準備引數

call 80483bf <f>

跳轉到記憶體地址為80483bf處開始執行函式f

add $0x1,%eax

f(8)返回後儲存在eax中,此時將eax內容加一,相當於執行f(8)+1

leave

函式執行結束後清理棧空間,並將old ebp彈棧,記錄原來棧的基地址

ret

執行pop %eip,返回

在main函式中我們會看到呼叫的函式f,函式f的入口地址是0x80483bf,下面我們先分析一下函式f的彙編程式碼

push %ebp

儲存函式執行前的原來的棧的基地址,也就是main函式的棧的基地址

mov %esp, %ebp

將當前棧頂指標賦值給ebp,表示函式f的新的棧基地址

sub $0x4,%esp

將棧頂指標減4,指向棧頂第一個可以儲存的位置

mov 0x8(%ebp),%eax

main函式中已經引數8壓棧,現在可以用0x8(%ebp)來取得引數8,然後將引數8放到暫存器eax中,為函式g準備引數

mov %eax,(%esp)

將引數8壓棧,引數8也是即將呼叫的函式g的引數

call 80483b4 <g>

跳轉到記憶體地址為80483b4處開始執行函式g

leave

f函式執行結束後清理棧空間,並將old ebp(即main函式的棧的基址)彈棧,為返回main函式做準備

ret

執行pop %eip,返回到main函式中呼叫後的地方繼續執行

我們可以看到在函式f的執行過程中我們又呼叫了函式g,函式g的入口地址是0x80483b4,下面來分析一下函式g的執行過程

push %ebp

儲存函式執行前的原來的棧的基地址,也就是f函式的棧的基地址

mov %esp, %ebp

將當前棧頂指標賦值給ebp,表示函式g的新的棧基地址

mov 0x8(%ebp),%eax

在函式f中我們已將用到的引數8壓入到棧中,這裡我們可以通過0x8(%ebp)來獲取引數8,供函式g使用

add $0x3,%eax

這是執行x+3,x就是傳進來的引數8,將計算結果放到eax中,由此我們可以知道返回結果是通過eax在函式間傳遞的

pop %ebp

這裡直接將函式f的基棧指標彈出即可,因為這裡棧頂指標esp根本沒有變化,所以就不用清理棧了

ret

執行pop %eip,返回到f函式中呼叫後的地方繼續執行

        上面通過簡單的分析每條彙編指令來了解了程式的執行過程,下面通過分析棧空間的變化情況來更形象的瞭解程式在機器級的執行流程。


        上面是main函式中的前四條指令執行時相應的棧中的變化,我想通過最初對指令的講解和此時利用棧圖更形象的講解,不難理解這幾條彙編指令的執行過程,這裡的最後一條call 80483bf<f>指令相當於先把main函式的返回地址eip壓棧,然後將80483bf即函式f的執行地址放到eip,這樣就可以跳轉到函式f繼續執行。



        上面是從函式main進入到函式f的執行過程,首先push%ebp和mov %esp, %ebp是儲存main函式的基地址指標和函式f的棧基址指標,接著是sub $0x4,%esp,mov 0x8(%ebp),%eax,從棧(9)中可以看到,0x8(%ebp)就是main函式中壓入的引數8的位置,所以eax中現在存放的是引數8,所以mov %eax,(%esp)其實就是將引數8壓棧,因為下一條指令call 80483b4 <g> 要呼叫函式g,而引數8同樣要作為引數傳遞給函式g,所以此時要壓棧,一般可以用0x8(%ebp)找到第一個引數的位置。

       這樣我們就進入到函式g中去執行,下面看看在g函式中執行的棧的情況。


       這是g函式執行的前三條指令,push%ebp是儲存函式f的棧的基地址,mov %esp, %ebp是重置棧的基地址,即ebp存放的是函式g的基地址。mov 0x8(%ebp),%eax,我們可以看到0x8(%ebp)就是指引數8,因為函式g同樣用到了引數8,所以將引數存放到eax中。


       上面為函式g中最後的三條指令,我們可以知道add $0x3,%eax棧並沒有改變,此時eax中存放的是引數8,這裡其實是執行x+3操作,x即為引數8,返回結果11存放在暫存器eax,pop %ebp是為返回到函式f做準備,在棧(15)中我們可以看到棧頂存放的是ebp(f),即函式f的棧的基址,這條指令執行完成後ebp重新指向函式f的棧的基址。ret操作將(16)中的eip(f)即函式f的返回地址存放到eip暫存器中,返回函式f繼續執行。

       我們知道在函式f中是執行call80483b4 <g>跳轉到函式g的,所以返回函式f是接著call 80483b4 <g>這條指令繼續執行leave和ret操作。


        如上圖所示,棧(18)是指令leave執行後的結果,leave操作首先mov %ebp%esp,這是一個棧的清理過程,因為函式f已經執行完成,然後pop %ebp,我們可知此時棧頂存放的是main函式的棧的基址即ebp(main),所以pop %ebp後ebp存放的是main函式的棧的基址。ret操作就是pop %eip,從棧(18)我們可以看到棧頂存放的是main函式的返回地址eip(main),當執行完ret後我就可以返回到main函式繼續執行。

        main函式是從call80483bf<f>指令跳轉到函式f的,所以返回的是這條指令的下一條指令,即add $0x1,%eax。


        返回到main函式中,首先執行的add$0x1,%eax的操作,因為從函式f返回的結果f(8)存放在eax中,所以這條指令相當於f(8)+1,結果還是存放在暫存器eax中。同樣,leave操作也是分兩步進行的,如棧(22)所示,首先是mov %ebp,%esp,清理main函式的棧空間,然後是pop %ebp,將main函式執行前的old ebp彈棧。至於ret操作,跟前面一樣,就是返回到main函式執行之前的地方繼續執行。

1.3總結

       上面主要通過巨集觀上和基於機器指令的分析來解析了計算機的基本工作原理,巨集觀上主要是基於馮·諾依曼結構來分析計算機是通過不斷的取指令來進行工作的。基於指令的分析,主要是通過example.c程式的彙編程式碼來解析的,這裡主要通過分析每條指令所執行的功能以及相應的棧和暫存器的變化來說明計算機基於機器指令的工作原理,相信通過上面的分析從巨集觀上和底層都會對計算機的工作原理有個清晰的認識。

二 多工計算機的工作

        現在的計算機基本上都是多工計算機,多工計算機的基本原理與單任務計算機基本相同,不同的是多工執行需要CPU有自己的排程原理,可以將CPU時間分成時間片,每個任務會有自己的相應的時間片去執行,這就需要CPU支援在不同的任務間進行切換。對此只進行簡單的介紹。

  

        在多工計算機中,暫存器CS為程式碼段暫存器,我們知道在單任務計算機中是以eip存放下一條指令地址的暫存器,在多工計算機中我們是通過cs:eip來確定將要執行的指令的地址,如上圖所示,如果想要從任務1切換到任務2,那麼首先將要確定程式碼段cs,然後通過cs:eip來決定下一條將要執行的指令。

       多工計算機的執行肯定是需要中斷的支援,至於中斷的具體細節在這裡不加贅述。