計算機是如何工作的——彙編程式碼分析
譚東旭 原創作品轉載請註明出處 《Linux核心分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000
彙編小知識
在用高階語言如C語言程式設計時,我們被遮蔽了程式的具體的機器實現。相比之下,在用匯編程式碼編寫程式時候,程式設計師必須明確指定程式該如何管理儲存器和用來執行計算的低階指令。我想作為一個嚴謹的程式設計師來說,要想了解程式的執行效率以及更好地提高程式效率,我認為能夠閱讀和理解彙編程式碼仍是一項很重要的技能。
我們知道彙編格式有兩種:AT&T彙編格式與Intel彙編格式。linux GCC採用的是AT&T的彙編格式, 而微軟採用Intel的彙編格式。下面我們就稍微介紹一下他們的一些不同之處:
格式 | 暫存器命名 | 源/目的運算元順序 | 常數/立即數的格式 |
---|---|---|---|
AT&T | %eax | movl %eax, %ebx | movl $_value,%ebx |
Intel | eax | mov ebx, eax | mov eax,_value |
說明 | Intel的不帶百分號 | AT&T目的運算元在後,源運算元在前 | Intel立即數前面不帶$符號 |
還有一點不同的是,在AT&T的格式中, 每個操作符都有一個字元字尾,例如 movl 傳送雙字(32位),movw 傳送字(16位),movb 傳送位元組(8位)。因為在許多機器上, 32位數都稱為長字(long word), 這是沿用以16位字為標準的時代的歷史習慣造成的.
下面我們也給出一些常見的彙編指令的含義:
指令 | 含義 | 說明 |
---|---|---|
movl %eax,%edx | edx=eax | 暫存器定址 |
movl $0x123,%edx | edx=0x123 | 立即定址 |
movl 0x123,%edx | edx=*(int32_t*)0x123 | 直接定址 |
movl (%ebx),%edx | edx=*(int32_t*)ebx | 間接定址 |
movl 4(%ebx),%edx | edx=*(int32_t*)(ebx+4) | 變址定址 |
彙編程式碼的工作過程中堆疊的變化
下面給出示例程式碼:
#include<stdio.h>
int g(int x)
{
return x + 2;
}
int f(int x)
{
return g(x);
}
int main(void)
{
return f(7) + 5;
}
上述是很簡單的一個小程式,我的實驗環境為實驗樓所提供的虛擬機器環境,使用命令gcc –S –o main.s main.c -m32生成彙編程式碼,注意該命令是在實驗樓64位Linux虛擬機器環境下適用,32位Linux環境可能會稍有不同。所得到的彙編程式碼如下:
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 $7, (%esp)
call f
addl $5, %eax
leave
ret
有了彙編程式碼,但是我們不一定能看得懂,我們知道main中呼叫了函式f,f中呼叫了g,但是函式返回時為什麼能返回到正確的位置?其實這個過程中用棧來儲存引數及返回地址,函式返回時再從棧中取出儲存的地址,這樣就不影響主程式的繼續執行。下面我就給出其中一些彙編指令的等價指令,讓我們更能輕易理解這一代段彙編程式碼。
指令 | what it does |
---|---|
pushl %eax | subl $4,%esp ; movl %eax,(%esp) |
popl %eax | movl (%esp),%eax ; addl $4,%esp |
call 0x12345 | pushl %eip ; movl $0x12345,%eip |
ret | popl %eip |
enter | pushl %ebp ; movl %esp,%ebp |
leave | movl %ebp,%esp ; popl %ebp |
好了,有了上面的一些解釋,我們就可以一步一步對程式碼進行分析。首先下面我先看看棧在計算中是如何操作變化的,ebp為棧的基址暫存器,esp為棧頂指標,棧是向低地址方向增長的。
我們回到彙編程式碼,看到在每個彙編程式碼塊的開始都有兩句一樣的彙編指令:
pushl %ebp
movl %esp, %ebp
- 1
- 2
其實這兩句可以理解是儲存上一個函式的棧底指標,然後從新開始一個新的棧。現在從main函式開始我們就以圖的形式來描述彙編在棧中的變化(在這裡我們假設棧的起始地址為0x114)。
當mian函式執行到call指令時候,就跳轉到了f函式的程式碼塊,現在下圖給出了f程式碼塊的棧變化過程。
當f函式執行到call指令時候,就跳轉到了g函式的程式碼塊,下圖給出了g程式碼塊的棧變化過程。
上述g函式程式碼塊已經執行完畢,即f函式call執行完畢,計算機接著出棧16,執行到f函式中call的下一條指令即leave指令,leav指令在上面已經給出解釋。leave指令後ebp指向0x114,esp指向0x10c,接著執行g函式ret,esp執行出棧,回到了main函式的call的下一條指令,即addl $5, %eax,此時eax=eax+5,最終eax=14.最後兩條指令用法和上邊的一樣。esp和ebp回到初態,最後整個程式通過eax返回。
總結
總的來說計算機的執行就是儲存器和cpu之間不停的取指令和執行指令的過程,準確的理解彙編程式碼的含義,對程式設計師來說能更好的深入理解計算機是如何工作的,編寫程式碼過程中才能透過高階語言的現象看到計算機的機器語言的本質。