1. 程式人生 > >第一次作業 圖解彙編程式碼以及分析計算機是如何工作的

第一次作業 圖解彙編程式碼以及分析計算機是如何工作的

 朱毅   原創作品轉載請註明出處 

 《Linux核心分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000 

C程式程式碼如下:

int g(int x)
{
  return x + 2;
}

int f(int x)
{
  return g(x);
}

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

在ubuntu中使用如下命令得到彙編檔案
gcc –S –o main.s main.c -m32

彙編檔案如下:

g:
	pushl	%ebp
	movl	%esp, %ebp
	movl	8(%ebp), %eax
	addl	$2, %eax
	popl	%ebp
	ret
f:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$4, %esp
	movl	8(%ebp), %eax
	movl	%eax, (%esp)
	call	g
	leave
	ret
main:
	pushl	%ebp			
	movl	%esp, %ebp		
	subl	$4, %esp
	movl	$3, (%esp)
	call	f
	addl	$1, %eax
	leave
	ret

C程式從main函式開始執行,我們也從彙編程式碼中的main過程開始分析。先假設整個棧空間是空的,並假設棧從地址0x1000開始向地地址擴充套件,即esp = 0x1000,此處ebp為何值並不影響整個彙編程式碼的分析,但為了方便和簡單,我們同樣假設ebp = 0x1000。

在main過程中,首先是儲存ebp的值,然後將esp的值賦值給ebp,此時esp = ebp = 0x0FFC,然後esp -= 4,並且將立即數3存入esp所指的棧空間中(這就是C程式碼中的引數傳遞,C中所有的實參和區域性變數都是儲存在棧空間中的),最後利用call指令呼叫f過程,call指令會先將eip的值壓棧,eip永遠指向當前正在執行的指令的下一條指令的首地址,所以此時壓入的eip的值應該是addl $1, %eax這條指令的地址,在本例中,我們將該地址成為"main address"來表示是返回到main過程的地址,此時整個棧空間的狀態如下圖所示


此時的ebp = 0x0FFC,而esp = 0x0FF4,我們需要格外注意下ebp的值,因為我們需要依靠ebp的值來維護棧空間。現在程式已經跳轉到f過程中進行執行,f的執行過程與main過程類似。首先仍然是將ebp的值壓棧,並將esp的值賦給ebp,然後esp的值再減去4,此時的esp = 0x0FEC,而ebp = 0x0FF0。我們現在需要取得main過程傳遞過來的引數,也就是立即數3,由於我們是將C程式碼彙編為32位的彙編程式碼的,因此棧空間中的每個棧單元所佔的記憶體空間為4個位元組(16位彙編程式碼中是2個位元組),因此movl8(%ebp), %eax就可以取得立即數3,並將3儲存在esp所指向的棧空間,這也是f需要向g傳遞的引數。再call g,此時的棧空間如下圖所示


現在進入g過程,同樣是ebp壓棧,ebp = esp,然後從棧中取得f傳遞過程的引數,此時,esp = ebp= 0x0FE4。(注意在g中我們沒有對esp減4,稍後會解釋為什麼)。現在eax中已經取得了f傳遞過來的引數3,然後再執行eax += 2,此時的eax = 5,棧空間如下圖所示:


此時,我們g函式的功能已經完成了,我們對照c程式碼可以發現,現在需要執行的是返回操作,在彙編程式碼中,返回值預設是存放在eax中的。現在eax中存放的就是我們需要返回的值5。popl%ebp後,ebp = 0xFF0,ret指令會從桟中彈出一個值並用該值修改eip的值,也就是說ret執行之時,從棧中彈出的剛好是f address,也即f過程中call g指令的後一條指令的地址。此時程式又處於f過程中,esp與ebp的值分別為:ebp = 0x0FF0,esp = 0x0FEC。注意此時esp指向的棧空間中存放的是3,就是f傳遞給g的引數,被調函式執行完之後,為了傳遞實參而開闢的棧空間需要撤銷掉,此處使用的就是leave指令,leave指令完成的功能是esp = ebp,pop ebp,ebp = 0x0FFC, esp = 0x0FF4,此時的棧空間如下


然後就是ret指令,與前面的型別,會從棧中彈出main address,並且跳轉到addl$1, %eax處執行,此時eax = 6,最後是leave和ret指令,最終整個棧空間又清空。

也許第一次看到這些彙編程式碼會感覺頭暈,可是其實每個過程的程式碼都是有一部分相同的,就是每個子過程的開始的幾行程式碼

pushl%ebp
movl%esp, %ebp

將ebp的值壓棧自然是因為後面需要用到ebp暫存器,所以先將該暫存器中的值儲存在棧中,在這裡我們ebp最主要的作用是為了維護幫助主調函式維護因傳遞引數而開闢的棧空間的。上面已經說了,在C語言中實參與區域性變數都是儲存在棧中的,現在我們只考慮實參,那麼就會有一個問題,為了傳遞實參而開闢的棧空間最後由誰來回收?此處我們是由主調函式來回收的,也就是誰開闢誰回收。所以我們需要記錄下沒有傳遞引數之前的esp的值,此處的方法是movl%esp, %ebp,即ebp用來儲存沒有傳遞實參之前的esp的值。然後就是sub指令,將esp減去4來儲存實參,接著呼叫被調函式,在被調函式中也做類似的事情(例如f),g稍有不同,它也儲存了esp的值,但因為它不需要呼叫其他的過程,所以沒有減去esp的值,所以最後還是將ebp的值出棧(這也是為什麼只有g中沒有leave指令)。最後實參所使用的棧空間的回收就是使用leave指令完成的,上面已經解釋了,所以不再贅述。到此,整個彙編程式碼分析完成。

關於計算機是如何工作的,我們首先需要介紹一個概念,就是馮諾依曼體系結構,此處不再解釋,請自行百度。其中比較重要的一點是,資料與指令不加區分混合儲存在同一個儲存器中,cpu會從記憶體中取出指令或資料來並進行相關的操作。


Cpu就是這樣不定的取指令,分析指令,執行指令的。