天天看點

帶你讀《C指針原理揭秘:基于底層實作機制》之三:AT&T彙編概述第3章

點選檢視第一章 點選檢視第二章

第3章

AT&T彙編概述

3.1 AT&T彙編基礎

3.1.1 IA-32指令

當計算機處理應用程式,運作其中的二進制指令碼時,資料指針将訓示處理器如何在記憶體的資料區域尋找要處理的資料,這塊區域稱為堆棧;指令碼放在另外的指令區,并通過指令指針機制管理目前運作中的指令,當處理器完成一個指令碼的處理之後,指令指針将指向下一條指令碼。

IA-32指令碼(Intel、AMD的CPU使用的指令碼)由二進制碼組成,格式如圖3-1所示。

帶你讀《C指針原理揭秘:基于底層實作機制》之三:AT&T彙編概述第3章

其中,指令字首(Instruction Prefixes)可包含1到4個修改操作碼行為的1位元組字首,它們分别是鎖定字首和重複字首、段覆寫字首和分支提示字首、操作數長度覆寫字首、位址長度覆寫字首等。操作碼(Opcode)定義了處理器執行的功能;修飾符包括ModR/M(尋址方式說明符)、SIB(比例–索引–基址)、Displacement(移位),定義執行的功能中涉及存取的具體寄存器和記憶體位置;資料元素(Immediate)是完成功能所需要使用的資料,這些資料既可以是直接的資料值,也可以是資料在記憶體中的位址。

當資料元素被放入到堆棧中時,資料指針在記憶體中“向下”(位址減少)移動,而當資料從堆棧中讀取時,資料指針則在記憶體中“向上”(位址增加)移動;指令指針可讓處理器了解哪些指令碼已經處理過了,接下來需要處理的指令碼是哪條。資料指針的移動過程如圖3-2中的左圖所示,指令指針的移動如圖3-2中的右圖所示。

帶你讀《C指針原理揭秘:基于底層實作機制》之三:AT&T彙編概述第3章

3.1.2 彙編的作用

C語言看似簡單,但要想真正駕馭它難度卻很大,需要非常小心和謹慎。C語言非常接近底層硬體,極具靈活性,在實際工作中,C程式員往往需要比Java程式員掌控更多的底層細節,他們通常要親自控制記憶體的配置設定與回收、控制接口通信、優化檔案系統和網絡協定、調用作業系統核心函數,等等,這對C代碼的品質提出了較高的要求,代碼品質越高,編譯後運作的效率就越高,代碼品質太低,将會使運作效率也變得低下,可能還不及完成同樣功能的Java程式,諸如記憶體洩漏等問題甚至會造成作業系統的漏洞和崩潰。

彙編語言(Assembly language)是一種用于電子計算機、微處理器、單片機或其他可程式設計器件的低級語言,采用助記符代表特定低級機器語言的操作,特定的彙編語言和特定的機器語言指令集是一一對應的,是以,相對C等進階語言而言,彙編語言的移植性較差,一種彙編語言通常專用于某種計算機系統結構,而且不可以在不同的系統平台之間移植。使用彙編語言編寫的源代碼,需要使用相應的彙程式設計式将它們轉換成可執行的機器代碼,這一過程被稱為彙編過程。彙編語言的優勢在于:速度快,可以對硬體底層直接進行操作,這對諸如圖形處理、高性能運算、底層接口操作等關鍵應用是非常重要的。

彙編語言與C語言是相輔相成的,對編譯後的C程式進行反彙編,剖析生成的彙編代碼,能夠更好地了解編譯過程、指針原理、記憶體配置設定、代碼優化等關鍵問題,進而提高C代碼的品質;此外,彙編速度快,可以直接對硬體進行操作,這對諸如圖形處理等關鍵應用是非常重要的,可以将彙編語言直接嵌入C代碼中,運用彙編這一底層語言優化程式的性能。

3.1.3 AT&T彙編語言的特點

彙編語言允許程式員友善地建立指令碼程式,但不是用那些二進制編碼的格式,而是使用助記符,助記符使用不同的單詞表示不同的指令碼。有了助記符,程式員可以用英語來編寫在目标機器上執行的指令碼,而不用記憶那些無趣的二進制編碼。

絕大多數程式員以前隻接觸過DOS/Windows下的彙編語言,這些彙編代碼都是Intel風格的,在Linux系統中,更多采用的還是AT&T彙編語言,是以,本書将以AT&T彙編語言為例進行講解。

AT&T彙編文法主要包含如下特點。

1)程式源檔案一般以“.s”作為字尾檔案名,以“#”開頭表示注釋。

2)寄存器名以“%”作為字首。例如,下面的代碼表示将eax寄存器的内容複制到ebx中:

movl %eax,%ebx

3)立即操作數以“$”字首表示。例如,下面的代碼表示将1複制到eax記憶體位址中(eax用括号包圍,表示操作數的記憶體位置,而不是操作數本身):

movl $1, (%eax)

4)目标操作數在源操作數的右邊。例如,下面的代碼表示将寄存器eax的内容複制到ebx中:

5)操作數的字長由操作符的最後一個字母決定,字尾“b”、“w”、“l”分别表示操作數為位元組(byte,8 比特)、字(word,16 比特)和長字(long,32比特)。

下面以複制指令mov為例進行說明。

movl對32位進行操作,下面的代碼表示将eax寄存器32位的内容複制到ebx中:

movl %eax, %ebx

movw對16位進行操作,下面的代碼表示将ax寄存器的内容複制到bx中:

movw %ax, %bx

movb對8位進行操作,下面的代碼表示将al寄存器的内容複制到bl中:

movb %al, %bl

下面再來看一下入棧指令push(如下面的代碼所示,“#”後的注釋是對本行代碼的說明):

pushl %ecx # 32位ecx的内容入棧

pushw %cx # 16位ecx的内容入棧

pushl $180 # 180作為一個32位整數入棧

pushl data # data變量内容入棧,長度為32位

pushl $data # 這個操作很特别,在變量前面加上"$"表示擷取變量的位址,這裡是将data變量的位址入棧

6)遠端轉移指令和遠端子調用指令的操作碼分别為ljump和lcall。

3.1.4 第一個AT&T彙編

學習彙編最有效的方法就是動手實踐,下面就來開始編寫第一個彙程式設計式吧!本程式需要完成的功能是:将66與20相加,相加的結果(88)是字母“B”的ASCII碼,将“B”與後面跟随的換行符(換行符的ASCII碼為10)一起輸出到螢幕上。不同的作業系統中,彙編代碼會稍有不同,下面将分别以FreeBSD與Ubuntu系統為例進行講解。

1. FreeBSD系統

FreeBSD系統可通過Vim或ee編輯檔案3-1.s,輸入如程式3-1所示的代碼:

帶你讀《C指針原理揭秘:基于底層實作機制》之三:AT&T彙編概述第3章

與C程式需要編譯才能運作一樣,彙程式設計式不能直接執行,也需要先進行彙編,并且要在連結後才能執行,具體過程如下。

首先,進行彙編:

%as -o 3-1.o 3-1.s

然後,連結:

%ld -o 3-1 3-1.o

最後,運作測試:

% ./3-1

B

下面對程式3-1進行剖析,初步熟悉一下AT&T彙編。

1)AT&T彙編代碼通過.section聲明不同的段,程式3-1聲明了兩個段,它們分别是.section .data段和.section .text段。.section .data段為資料段,用于存放可供彙程式設計式讀寫的資料;.section .text段為代碼段,用于存放彙程式設計式代碼,程式在運作時會為這兩個段配置設定相應大小的記憶體,當程式結束時,這些記憶體會被自動釋放。

2)程式3-1中的資料段中存放了1個位元組(byte)大小的數字66和同樣大小的換行符(數字10表示換行符的ASCII碼),代碼片斷如下:

.section .data

output:

.byte 46

.byte 10

上述彙編代碼完成的功能相當于下面這條C語句:

unsigned char output[2]={60,10};

3)程式3-1的代碼段的開頭處有這樣一條語句“globl _start”,這條語句标注了程式的起始點(相當于C語言的main函數)。globl标記用于訓示外部程式可通路的程式标簽,_start标簽是ld連結器進行連結時預設程式的起始點,它們組合在一起的含義是:當彙程式設計式運作時,指令指針将指向_start标簽處(即代碼段的開頭,第一條彙編代碼處),從_start标簽指向的彙編代碼開始運作。注意,“_start”是預設的名字,如果要使用其他名字,則需要在連結時使用“-e”選項指定起始點名稱。

4)緊接着_start标簽的程式可分為如下兩塊。

第一塊是顯示字元“B”及換行符,代碼片斷如下:

movl $output,%edx

addl $20,(%edx)

pushl $2 # 參數三:字元串長度,包括換行符共2個字元

pushl $output # 參數二:要顯示的字元串

pushl $1 # 參數一:檔案描述符(stdout)

movl $4, %eax # 系統調用号(sys_write)

pushl %eax

int $0x80 # 調用核心功能顯示字元及回車

上述代碼片斷的前2行将output标記指向的數字66所在的記憶體位址送入edx寄存器,然後調用addl指令完成66+20的運算,addl指令的目标操作數為(%edx),而不是%edx,用括号包圍表示目标操作數在%edx指向的記憶體位址中。餘下幾行則通過将參數依次入棧,然後使用UNIX核心的系統調用從核心通路控制台顯示,最後一行int $0x80表示使用int指令碼,生成具有0x80值的軟體中斷,要求核心執行的具體操作由eax寄存器決定,這個核心函數省去了将每個輸出字元親自送到顯示器的I/O位址的過程。該代碼片斷相當于執行以下C語句:

output[0]+=20;

printf("%s",output);

第二塊是退出程式,代碼片斷如下:

pushl $0 # 參數一:退出代碼

movl $1,%eax # 系統調用号(sys_exit)

int $0x80 # 調用核心功能

上述代碼片斷以退出代碼0作為參數入棧,并以系統調用号1來調用核心,進而正常退出程式,這段代碼相當于執行以下C語句:

return 0;

以上兩塊代碼均使用了int $0x80軟中斷語句來通路核心,軟中斷指令通常要運作一個切換CPU至核心态(Ring 0)的子例程,這個過程用于實作系統調用,它觸發了核心事件,實作了宏觀的異步執行,與硬中斷類似,也與信号有些類似。這意味着,當執行int $0x80時,通過軟中斷觸發核心事件,進而調用核心函數。調用函數通常需要傳入參數,核心函數也不例外,棧就是使用者程式與核心函數的交換空間。

與Ubuntu不同的是,FreeBSD 核心預設使用 C 語言的調用規範,因為作為一個類Unix作業系統,它遵守Unix規範,該規範允許任何語言所寫的程式通路核心,也就是說FreeBSD通路核心的方式是先将參數壓入棧中,然後再執行 int $0x80調用核心中斷,執行核心函數。程式3-1也是這樣做的:它首先通過push指令,将調用參數将壓入棧中,然後通過int $0x80指令觸發軟體中斷,執行核心函數,最後核心函數從棧中将參數取出,執行完畢後,由核心态傳回使用者态。

2. Ubuntu 系統

Ubuntu等Linux系統與FreeBSD等類UNIX系統在調用核心函數時略有不同。Linux核心在傳遞參數的時候,使用了與MS-DOS/Windows相同的系統調用規範,例如,在 UNIX 的規範中,代表核心函數的數字存放在eax中,而在Linux中,調用參數并未壓入棧中,而是存放在ebx,ecx,edx,esi,edi,ebp等寄存器中。是以在Ubuntu下,程式3-1需要稍做修改,修改後的代碼如程式3-2所示:

帶你讀《C指針原理揭秘:基于底層實作機制》之三:AT&T彙編概述第3章
帶你讀《C指針原理揭秘:基于底層實作機制》之三:AT&T彙編概述第3章

彙編并運作程式3-2:

$as -o 3-2.o 3-2.s

$ld -o 3-2 3-2.o

$ ./3-2

程式3-2與程式3-1的結構類似。首先,在可讀寫資料段(.section .data)中存放66以及10(換行符的ASCII碼);然後,在程式段(.section .text)的起始處,使用“.global _start ”聲明入口程式名;最後,在_start程式中,先後使用兩次“int $0x80”語句調用核心函數,顯示字元串後退出程式。但有一個“陷阱”,程式段的前2行看似完成了66+edx的操作,但卻忽略了一點,%edx表示一個操作數,而不是操作數的記憶體位置。是以輸出的是66對應的“B”,而不是ASCII碼86對應的“√”。

程式3-2與程式3-1的主要差別在于:程式3-2并沒有像程式3-1那樣将調用參數推入棧中,而是将參數放在edx、ecx、ebx、eax寄存器中,然後調用核心功能輸出字元串。

從以下代碼片斷可以看出:字元串長度2被放置在edx寄存器中,字元串的首位址放置在ecx寄存器中,輸出裝置stdout的描述符放置在ebx寄存器中,系統調用号4放置在eax寄存器中,最後一行調用了核心功能函數。

movl $2, %edx # 參數三:字元串長度

movl $output, %ecx # 參數二:要顯示的字元串

movl $1, %ebx # 參數一:檔案描述符(stdout)

movl $4, %eax # 系統調用号(sys_write)

int $0x80 # 調用核心功能

3.2 程式運作機制

C程式運作機制與Python、Lua等腳本語言的運作機制不同,腳本語言由解釋程式讀取後運作,由解釋程式負責運作腳本語言的指令,而不是由CPU直接運作腳本語言的指令。雖然某些腳本語言解釋器具有JIT(just-in-time compiler)功能,可将腳本語言轉換成能被處理器直接執行的指令,但是,轉化的過程實質上也是一個編譯的過程,這個編譯過程仍然需要編譯器的幫忙,是以,從某種角度上來說,此類腳本語言解釋器可稱為“腳本語言編譯器”。而C語言則不同,它屬于編譯型語言,當然,彙編語言也是可編譯運作的,但C語言相比彙編語言而言更簡潔,在完成同樣任務的情況下,C程式的編碼量要少很多,這對彙編語言程式員來說也許是一種解脫。

C語言将生成機器語言的工作托付給編譯器執行,機器語言是計算機能夠直接解讀、運作的語言,C語言編譯器将源程式作為輸入,翻譯成目智語言機器的二進制執行檔案,在Linux平台下,GCC是使用最多的編譯器,GCC原名為GNU C 語言編譯器(GNU C Compiler),經過後期的不斷改進,目前GCC可用于編譯C、C++、Fortran、Pascal、Objective-C、Java、Ada等,此外,GCC還能編譯彙編語言。Unix平台預設的編譯器是cc,使用方式與GCC類似。

C語言編譯生成的二進制可執行檔案通常分為應用程式和庫檔案兩種,其中,應用程式可以直接執行,庫檔案是多個目标檔案的組合,通常來說不能直接執行,但其提供了多個功能的調用接口。在編譯C語言時,連結進應用程式的稱為靜态庫;在系統運作時,調用應用程式的稱為動态庫。

GCC等 C語言編譯器簡化了C程式員的工作,讓他們能夠将大部分精力放在處理程式與算法邏輯上來,但美中不足的是:C語言編譯生成的二進制程式比彙編器生成的程式要大,包含的指令也更多,是以程式執行效率要比彙編語言低,雖然GCC編譯器擁有優化編譯的功能,可提高生成機器代碼的執行效率,但是其仍然無法與彙編代碼彙編生成後的應用程式相比,是以,在執行效率要求很高的場合,仍然需要全部使用彙編語言編寫或将彙編代碼嵌入到C語言中。

3.3 小結

彙編語言與C語言是相輔相成的,彙編語言能夠幫助C程式員提高代碼品質,更好地參與數十萬行以上C代碼的複雜項目的開發;同時,C語言代碼中可以内嵌彙編語言,将程式中的關鍵部分用彙編語言來實作,進而進一步提高效率。本章首先簡要介紹了IA-32指令構造、AT&T彙編的作用與文法特點,然後以輸出單個字元為例,講解了AT&T彙編的編寫、彙編以及連結過程,最後解說了程式的運作機制。

繼續閱讀