從C到匯編:棧是計算機工作的基礎


 
       作者:r1ce
       原創作品轉載請注明出處
      《Linux內核分析》 MOOC課程http://mooc.study.163.com/course/USTC-1000029000
 
       關於計算機是如何工作的,這是一個容易概括卻難以詳解的問題。大家非常清楚的馮諾依曼體系,以存儲程序為最重要的特性,實際上就是CPU像一個大管家一樣,通過種種方式在浩如煙海的內存中,找出需要執行的指令,和需要使用的數據。那么CPU如何區分指令和數據,如何知道確定指令執行的順序呢?
       我們先從上至下來看計算機。普通用戶使用計算機上的軟件,軟件是由程序員編寫的,一般使用高級語言,如Python、C、Java等,這些語言易於人類理解、閱讀和編寫,但是計算機卻不能直接識別。無論是Python還是C,前者需要通過解釋器來執行,后者需要編譯器編譯為可執行文件。計算機最底層的實現是基於電路實現0和1的識別,這也是可執行文件的真貌——一大堆0和1的表示。那么高級語言到0和1之間,看起來好像隔着很大的一條鴻溝,於是匯編語言作為二者的中介,便顯得十分重要了。向上而言,高級語言可以用匯編語言表示;向下而言,每一個匯編語言的指令都可以用二進制0和1表示,從而被計算機CPU識別。理解了匯編語言的操作過程,也就能夠理解計算機究竟是如何工作的。
       匯編語言究竟是什么東西呢?想要理解匯編語言,要先理解計算機的組成。為了簡化,只提CPU和內存。CPU是處理器,內存存放着指令和數據,處理器就像一個管家,從內存中取指令執行,對數據進行出來,並將數據儲存起來。對於CPU來說,每一個程序的執行要解決三個問題:1. 待處理的數據在哪里;2. 如何處理數據;3. 處理好的數據放在哪里。為了解決這三個問題,CPU需要借助一些工具的幫助,這些工具就是各種寄存器。匯編語言實際上就是對這些寄存器進行處理,通俗點說,就是把一大堆數據在寄存器和內存倒騰過來倒騰過去,做一些復制和加加減減的運算。其實學習匯編語言很簡單,只要記住十幾條匯編指令和各種寄存器以及堆棧的用法就可以了。
       在這篇文章中,我們通過對一個簡單的C程序反匯編得到匯編代碼,分析匯編代碼來了解計算機工作的基礎。
       這段C程序是這樣的:
 1 int a(int x)
 2 {
 3       return x + 5;
 4 }
 5 
 6 int b(int x)
 7 {
 8       return a(x);
 9 }
10 
11 int main(void)
12 {
13       return b(5) - 2;
14 }
       可以看到程序中有很多函數的調用和返回。為什么要這樣設置呢?因為程序中的函數調用時計算機工作運行的關鍵,分析函數調用的具體實現能夠幫助理解計算機運行的原理。
       我們將上述代碼寫入main.c文件中。然后使用
gcc -S -o main.s main.c -m32

       命令生成匯編代碼。結果如下圖。后面加-m32是為了讓其按照32位的方式反匯編。

 

       我們只需要看匯編代碼的關鍵部分,可以把點開頭的語句全部刪去,得到如下的匯編指令。

 1 a:
 2 
 3     pushl    %ebp
 4     movl    %esp, %ebp
 5     movl    8(%ebp), %eax
 6     addl    $5, %eax
 7     popl    %ebp
 8     ret
 9 
10 b:
11 
12     pushl    %ebp
13     movl    %esp, %ebp
14     subl    $4, %esp
15     movl    8(%ebp), %eax
16     movl    %eax, (%esp)
17     call    a
18     leave
19     ret
20 
21 main:
22 
23     pushl    %ebp
24     movl    %esp, %ebp
25     subl    $4, %esp
26     movl    $5, (%esp)
27     call    b
28     subl    $2, %eax
29     leave
30     ret
       接下來我們分析C代碼和匯編程序究竟是如何對應起來的,以及匯編語言是如何工作的。
       我們先看C程序,從main函數看起,它返回了一個函數b再進行運算的結果。那么我們來看函數b,它返回的是函數a的結果,而函數a的作用是將傳遞給它的參數x加5。所以對於這個程序,最后得到的數值應該是5+5-2=8。
       再來看匯編代碼,我們還是從main函數看起,一條指令一條指令地分析。第n條表示指令執行的順序,后面列出了代碼的行號和執行的指令。
       第1條:23 pushl %ebp
       一看到push,我們就知道這是在對棧進行操作。ebp是棧頂指針,esp是棧當前位置指針,棧是自上向下生長的,后進先出。先把ebp壓棧,實際上是先將esp-4再將ebp放到棧當前位置。
       第2條:24 movl %esp, %ebp
       將esp的值放到ebp中,也就是說現在ebp的指向改變為esp的指向。
       第3條:25 subl $4, %esp
       將esp-4。
       第4條:26 movl $5, (%esp)
       將5移入esp指向的地址中。
       第5條:27 call b
       調用函數b,這里等於兩個操作,一個是先將現在的eip入棧,此時eip應為subl $2,%eax這條指令的位置,我們記為28。另一個操作是將b函數的地址放入eip,也就是說此時程序要從10開始執行。
        第6條:12 pushl %ebp
       第7條:13 movl %esp, %ebp
       第8條:14 subl $4, %esp
       此時已跳轉到b函數,指令之前已經講過了,與7、8條一起不再贅述。
       第9條:15 movl 8(%ebp), %eax
       movl 8(%ebp), %eax,是將ebp的值+8指向的內容放入eax,實際上就是eax = 5。
       第10條:16 movl %eax, (%esp)
       將eax的內容放入現在esp指向的內容中。
       第11條:17 call a
       調用函數a,與前面的步驟類似。
       第12條:3 pushl %ebp
       第13條:4 movl %esp, %ebp
       第14條:5 movl 8(%ebp), %eax
       是a函數的pushl %ebp,與13、14條一同省略。
       第15條:6 addl $5, %eax
       將eax中的值+5得到10。
       第16條:7 popl %ebp
       將現在esp指向的內容放入ebp,esp+4,所以現在ebp=4。
       第17條:8 ret
       是ret,即popl %eip,也就是現在的eip更改為18,回到函數b,從leave開始執行。
       第18條:18 leave
       leave,表示兩條指令,movl %ebp,%esp和popl %ebp。
       第19條:19 ret
       ret回到main函數,從28處執行。
       第20條:28 subl $2, %eax
       將eax中的內容-2,即8。
       第21條:29 leave
       第22條:30 ret
       如圖所示。從圖中我們可以看到,棧又回到了初始的位置。
      
       至此,匯編代碼就分析完了。
       從上面的過程可以看出,計算機最本質的工作原理,是對存儲的數據進行處理,並把結果保存,然后不斷循環這個處理數據的過程。指令就是對數據進行處理的依據。具體的方法就是借助CPU中的寄存器,以及內存中的棧,依據一個約定的步驟對數據進行操作。計算機其實很簡單,它是一個認死理的家伙,只要確定了每一步要做什么,它就會嚴格地按照步驟把操作完成,絕對不打折扣。因此,相比與人打交道,與計算機打交道可是要輕松多了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM