通過彙編一個簡單的C程序,分析彙編代碼理解計算機是如何工作的
計算機的工作方式:
現代計算機的基本體系結構都是採用馮諾依曼結構,馮諾依曼的設計思想最重要之處是"存儲程序"的這個概念。計算機的工作過程,就是執行程序的過程。首先編寫需要執行的程序,然後通過輸入設備送到存儲器保存起來,即程序存儲。根據馮諾依曼的設計,計算機應能自動執行程序,而執行程序又歸結爲逐條執行指令。執行一條指令又可分爲以下4個基本操作:
取出指令:從存儲器某個地址中取出要執行的指令送到CPU內部的指令寄存器暫存。
分析指令:把保存在指令寄存器中的指令送到指令譯碼器,譯出該指令對應的微操作。
執行指令:根據指令譯碼,向各個部件發出相應控制信號,完成指令規定的各種操作。
爲執行下一條指令作好準備,即取出下一條指令地址。
接下來通過一個簡單的c程序來分析一下,程序的執行過程
這裏是一個非常簡單的c程序,源代碼如下:
輸入:gcc S o main.s main.c m32 來生成彙編代碼
整潔一下彙編代碼以後,查看彙編代碼:
我在我的虛擬機中的ubuntu系統與實驗樓ubuntu系統的彙編代碼有點不一樣
下面通過gdb單步執行來分析棧上寄存器的情況:
首先我們從main函數開始。(前兩條語句在gdb執行時設置不了斷點,但是執行函數的語句都有這2條,放到其他函數來說明):
在main函數上先設置一個斷點,然後運行:
此時查看寄存器的值:
他們的值:esp和ebp都是0xbffff568,eip是0x8048409(正好是下一條要執行的指令的地址)
接下來繼續執行:
把2壓到棧上
此時,esp的減4了,而ebp不變,eip繼續指向下一條指令
下一條要執行call指令,這裏再對函數f設置一個斷點,繼續執行:
此時,程序跳到函數f中去了
call f
調用函數 f,其實這條指令等價於
pushl %eip
movl $f, %eip
eip的值被保存在esp-4的位置上,保存eip的目的是函數調用返回時能夠繼續執行call f下面的語句:
此時,esp的值爲0xbffff560,ebp都是0xbffff568
跳轉到函數f後,前兩條語句和 main 函數相同,都是保存堆棧狀態,這裏詳細來說明一下:
先把ebp的值保存咋esp-4的位置上
再把esp的值賦給sbp,此時esp和ebp的值都爲0xbffff55c
然後繼續執行,把ebp+8的內容即2這個值壓棧:
此時esp繼續-4
查看寄存器的值
寄存器的值:esp的值爲0xbffff558,ebp的值爲0xbffff55c,
接下來要跳轉到函數g了,因此再對函數g設置一個斷點,然後繼續執行:
觀察寄存器的值:
同理:寄存器eip的值繼續被保存了在esp-4的位置上,以便能夠返回到函數f
進入g函數老的ebp的值也被保存了,新的esp和ebp相同
此時,esp和ebp的值都是0xbffff550
接下來繼續執行,把ebp+8的值給eax
查看寄存器,此時esp和ebp的值都是0xbffff550,eax的值是2
繼續執行:
把3和eax的值相加結果再保存到eax中
查看寄存器
寄存器eax的值變成5了,esp和ebp的值都是0xbffff550
然後繼續執行:
把esp指向的值給ebp,查看寄存器
寄存器的值:esp的值爲0xbffff554,ebp的值0xbffff55c,eax還是5
然後繼續執行:
指令ret相當於指令popl %eip
esp的值爲0xbffff558,ebp的值0xbffff55c,eax還是5
這樣又返回到函數f繼續執行:
繼續執行,然後查看寄存器:
Esp和ebp的值都是0xbffff55c,eax還是5
繼續執行,leave,這條指令相當於下面兩條指令:
movl %ebp, %esp
popl %ebp
查看寄存器
esp的值爲0xbffff560,ebp的值0xbffff568,eax還是5
繼續執行ret,彈出保存的eip的值,返回到main函數執行:
查看寄存器的值,esp的值爲0xbffff564,ebp的值0xbffff568,eax還是5
連續執行2步,繼續執行
此時eax的值變成了6,esp和ebp的值0xbffff568
然後繼續執行2步,main函數就返回了
總結
通過分析對應的彙編代碼和觀察運行棧的變化,加深了對程序執行過程的瞭解,也明白了計算機的工作方式:
根據eip 指指令執行,同時eip自增;
如果執行的是跳轉語句時,先把eip壓棧,然後將需要跳轉的目的地址賦給 eip,實現跳轉;
若執行函數調用時,將 eip 壓棧,同時將ebp壓棧,然後將相應函數地址賦給 eip;
若爲其他指令,則繼續從 eip 指向的地址取指令執行。