在實際編程過程中诅岩,我們大多是使用高級語言如 Java 語言編程。多數(shù)時候带膜,高級語言因其高度封裝吩谦,使得程序易于編寫,亦是擁有較高的可讀性膝藕,因而廣受歡迎式廷。但由于屏蔽了程序在機械級的具體實現(xiàn)過程,我們如隔層玻璃觀物一般芭挽,無法觸及底層實質滑废。
當然,我們無法直接和機器交流袜爪。不過我們可以通過一種迂回的方式蠕趁,達到交流的目的——讓它們知道我們想要它做什么。那怎么讓那“榆木腦袋”知道我們想要什么呢辛馆?我們可以使用匯編來理解程序的底層邏輯俺陋。
在我們啟動程序的時候,編譯器會產生一個匯編代碼文件,而這個匯編代碼就十分接近于計算機實際過程中執(zhí)行的機器代碼腊状。和機械代碼那種二進制數(shù)字相比诱咏,匯編使用的是更易于讀的字符,方便我們理解底層邏輯寿酌。我們知道它是怎么想的,那它想做什么不就由我們掌控了嘛硕蛹。
有些時候醇疼,高級語言所提供的抽象層會隱藏一些我們想要理解的信息。就像英譯中法焰,閱讀英文原本和譯本的差別是挺大的秧荆,有時候原本里某段很精彩的描述翻譯過來不過寥寥幾筆勾畫,雖然看起來不影響整體埃仪,不過缺失的那部分感覺挺可惜的乙濒。
回到編碼,我們常常望“洋”興嘆卵蛉,怎么就和我們想的不一樣呢颁股?這時候,我們可以通過閱讀匯編代碼傻丝,理解程序其中可做的優(yōu)化甘有,分析代碼中潛在的低效率問題等。而且在用線程包寫并發(fā)程序的時候葡缰,知道用什么存儲器 (storage) 來保存各種變量是很重要的亏掀。這些信息在匯編代碼就一覽無余啦。
閱讀和理解匯編代碼能幫助我們進入另一個層次看待程序泛释。接下來滤愕,讓我們進入小人國的世界。
機械級程序
在類似 C 語言的高級語言中大多提供了一種模型怜校,可以在存儲器中聲明和分配各種數(shù)據(jù)類型的對象间影。但在匯編代碼中,它們只是一個很大的茄茁、按字節(jié)尋址的數(shù)組宇智。像在 C 中,我們所熟知的數(shù)組和結構胰丁,在匯編代碼中是用連續(xù)的字節(jié)表示的随橘。
我們生來沒有什么不同,只是隔了層地址锦庸,最終老死不相往來机蔗。
訪問信息
下圖為寄存器,它用以存儲數(shù)據(jù)和指針。在大多數(shù)情況下萝嘁,前六個寄存器是通用寄存器梆掸,對于它們的使用沒有嚴格限制。最后兩個寄存器 ( %ebp 和 %esp ) 保存著指向程序棧中重要位置的指針牙言, %ebp 指向棧幀開始處酸钦, %esp 指向棧頂,只有根據(jù)棧管理的標準慣例才能修改這兩個寄存器的值咱枉。
大多數(shù)指令有一個或多個操作數(shù)卑硫,指示出執(zhí)行一個操作中要引用的源數(shù)據(jù)值,以及放置結果的位置蚕断。而各種操作數(shù)依各自特性分為三種類型:
立即數(shù) 即常數(shù)值欢伏,依規(guī)定,立即數(shù)的書寫方式是“$”后面跟一個整數(shù)亿乳。
寄存器表示某個寄存器的內容硝拧。對雙字操作來說,可以是八個32位寄存器中的一個葛假,如 %eax障陶。
存儲器引用它會根據(jù)計算出的地址訪問某個存儲器的位置。
棧幀結構
棧用來傳遞過程參數(shù)聊训、存儲返回信息咸这、保存寄存器以供以后恢復只用,以及用于本地存儲魔眨。為單個過程分配的那部分棧稱為棧幀媳维。下圖描繪了棧幀的通用結構。
假設函數(shù) P (調用者) 調用函數(shù) Q (被調用者)遏暴。 Q 的參數(shù)放在 P 的棧幀中侄刽。當 P 調用 Q 時, P 中的返回地址被壓入棧中朋凉,形成 P 的棧幀的末尾州丹,返回地址就是當程序從 Q 返回時應該繼續(xù)執(zhí)行的地方。Q 的棧幀從保存的幀指針的值 (如 %ebp)開始杂彭,后面是保存的其他寄存器的值墓毒。
或許說的有些抽象,你可以將函數(shù)調用想象成俄羅斯套娃亲怠,大的套小的所计,層層遞進。而棧也是如此团秽,棧向下增長主胧,高地址在上叭首,低地址在下, 即棧頂元素在最底部踪栋,而 %esp 一直指向棧頂元素焙格。我們可以通過 pushl 和 popl 指令將數(shù)據(jù)存入棧中和從棧中取出。當然夷都,也可以通過將 %esp 的值減小適當?shù)闹祦矸峙湮粗付ǔ跏贾档臄?shù)據(jù)的空間眷唉。相反,也可以通過增加 %esp 來釋放空間囤官。
因為寄存器是一個資源共享區(qū)冬阳,而我們在調用函數(shù)的時候,為了避免當一個函數(shù)調用另一個函數(shù)時治拿,被調用者覆蓋某個調用者等一下會使用到的寄存器的值摩泪。在這里有一個寄存器使用慣例:
寄存器 %eax笆焰、%edx劫谅、%ecx 被劃分為調用者保存( caller save ) 寄存器。當函數(shù) P 調用 Q 時嚷掠, Q 可以覆蓋這些寄存器捏检,而不會破壞任何 P 所需要的數(shù)據(jù)。另外不皆,寄存器 %ebx贯城、%esi、%edi 被劃分為被調用者保存( callee save ) 寄存器霹娄。即 Q 必須在覆蓋它們之前能犯,將這些寄存器的值保存到棧中,并在返回前恢復它們犬耻。
術語不好記的話踩晶,我們可以想象以下場景:
int P() {
int x = f();;
Q();
return x;
}
函數(shù) P 希望它計算出來的 x 值在調用了 Q 之后任然有效,如果 x 放在一個調用者保存寄存器中枕磁,而 P 必須在調用 Q 之前保存這個值渡蜻,并在 Q 返回后恢復這個值。如果 x 在一個被調用者保存寄存器中计济,Q 想使用這個寄存器茸苇,那么 Q 在使用這個寄存器之前,必須保存這個值沦寂,并在返回前恢復学密。在這兩種情況中,保存就是將寄存器值壓入棧中传藏,而恢復時從棧中彈出到寄存器则果。
接下來幔翰,我們簡單說下匯編指令:
數(shù)據(jù)傳送指令
數(shù)據(jù)傳送指令在底層是最頻繁使用的指令。通常一條簡單的傳送指令能完成許多機器中需要好幾條指令才能完成的操作西壮,而最常用的莫過于傳送雙字的 movl 指令遗增。
movl $0x3051,%eax //將 0x3051 這個值放入寄存器 eax
將一個值從一個存儲器位置考到另一個存儲器位置需要兩條指令——第一條指令將源值加載到寄存器中,第二條將寄存器值寫入目的位置款青。源操作數(shù)指定一個值做修,它可以是立即數(shù),可以存放在寄存器中抡草,也可以放在存儲器中饰及。目的操作數(shù)指定一個位置康震,它可以是寄存器,也可以是存儲器地址屏箍。
加載有效地址
加載有效地址指令 leal 實際上是 movl 指令的變形赴魁。它的第一個操作數(shù)看上去是一個存儲器引用颖御,但該指令并不是從指定的位置讀入數(shù)據(jù)凝颇,而是將有效地址寫入目的操作數(shù) (如寄存器)拧略。
leal -12(%ebp) %eax //將 %ebp 減去12得到的地址辑鲤,放入 eax 寄存器
了解部分匯編代碼含義之后月褥,我們就可以綜合實踐一下了宁赤。接下來,看看程序機械層次是如何運行的决左。
下面是 C 語言編寫的一段小程序:
int demo(){
int x = 10;
int y = 20;
int sum = add(&x, &y);
printf(“the sum is %d\n”,sum);
return sum;
}
int add(int *xp, int *yp){
int x = *xp;
int y = *yp;
return x+y;
}
讓我們將它們轉成匯編代碼來看:
demo:
1 pushl %ebp //將寄存器 ebp 的值壓人棧中
2 movl %esp %ebp //將寄存器 esp 的值放入 ebp
3 subl %24 esp //將 esp 寄存器的值減去24
4 movl $10 -4(%ebp) //將10這個值存放到 ebp 寄存器地址減去4的地方
5 movl $20 -8(%ebp) //將20這個值存放到 ebp 寄存器地址減去8的地方
6 leal -8(%ebp) %eax //將 ebp 減去8得到的地址,放到 eax 寄存器當中
7 movl %eax 4(%esp) //將 eax 的值存放到 esp 寄存器地址增加4的地方
8 leal -4(%ebp) %eax //將 ebp 減去4得到的地址坠狡,放到 eax 寄存器當中
9 movl %eax esp //將 eax 的值存放到 esp 寄存器
10 call add //調用 add 函數(shù)遂跟,將返回值地址壓人棧中
打印結果(略)
如圖幻锁,1-2兩條代碼是將棧幀開始處 ebp 寄存器的地址壓入棧頂,esp 寄存器的地址自動減去4個字節(jié)假消,地址為 800富拗。之后將指向棧頂?shù)?esp 寄存器的地址放入 ebp 寄存器媒峡,ebp 寄存器的地址被 esp 寄存器的地址取代,即地址也為 800酬滤。
第3行代碼是將 esp 寄存器的地址減去24個字節(jié),相當于給棧幀分配一定的自由空間体捏。為什么會減去24個字節(jié)呢糯崎?因為系統(tǒng)的編程指導方針為了嚴格的數(shù)據(jù)對齊所至,即一個函數(shù)使用的椢帜兀空間必須是16個字節(jié)的整數(shù)倍年栓。
4-5行代碼是將兩個立即數(shù) 10 和 20 分別放入棧中薄霜,需要注意的是此時 ebp 寄存器中的地址不會改變纸兔。之后 leal 指令代碼是將 ebp 減去8得到的地址,即792否副,放到 eax 寄存器當中。接著 movl 指令代碼是將 eax 的值备禀,存放到 esp 寄存器地址增加4的地方负甸,即780。這兩段指令就是將變量 x 的值的地址放到棧中痹届,即是指針。8-9段代碼同上队腐,你可以檢驗一下柴淘。
第10行代碼是函數(shù)調用为严,調用 add 函數(shù)第股。執(zhí)行這一行指令的時候应民,會將返回地址寫入棧中,指向棧頂?shù)?esp 寄存器會自動的減去4個字節(jié)涉馅,指向返回地址归园。至此,調用者的函數(shù)棧幀完成稚矿。
接下來庸诱,進入 add 函數(shù)調用。我們會在上面棧幀的下面開辟調用者棧幀的空間晤揣,以便于函數(shù)值的存儲桥爽。
下面是 add 函數(shù)的匯編代碼:
add:
1 pushl %ebp
2 movl %esp %ebp
3 pushl %ebx
4 movl 8(%ebp) %edx
5 movl 12(%ebp) %ecx
6 movl (%edx) %ebx
7 movl (%ecx) %eax
8 add %ebx %eax
9 popl %ebx
10 popl %ebp
11 ret
由于和上面代碼類似,具體解析就此省略碉渡,你可以當作練習聚谁,試著解析代碼。
附:本文大多來自《深入理解計算機系統(tǒng)》一書以及劉欣老師的編程課程內容滞诺。