x86匯編程序基礎(AT&T語法)


 

 

 

ins.luhannews.cn

一、簡單的匯編程序

 以下面這段簡單的匯編代碼為例

.section .data
.section .text
.globl _start
_start:
movl $1, %eax
movl $4, %ebx
int $0x80

(注意是globl不是global;movl(MOVL)不是mov1(MOV一))

 

將這段程序保存為demo.s,然后用匯編器as把匯編程序中的助記符翻譯成機器指令(匯編指令與機器指令是對應的)生成目標文件demo.o。然后用鏈接器ld把目標文件demo.o鏈接成可執行文件demo(雖然只有一個目標文件但是也需要經過鏈接才能成為可執行文件因為鏈接器要修改目標文件中的一些信息)。這個程序只做了一件事就是退出,退出狀態為4。shell中可以echo $?得到上一條命令的退出狀態。

 

【解釋】:匯編程序中以"."開頭的名稱不是指令的助記符,不會被翻譯成機器指令,而是給匯編器一些特殊的指示,稱為匯編指示或偽操作。

.section .data
.section .text

.section指示把代碼划分成若干個段(section),程序被操作系統加載時,每個段被加載到不同的地址,具有不同的讀寫執行權限。

.data段保存程序的數據是可讀寫的,C程序的全局變量也屬於.data段。上邊的程序沒定義數據所以.data是空的。

.text段保存代碼,是只讀和可執行的,后面那些指令都屬於這個.text段。

.globl  _start

_start是一個符號(Symbol),符號在匯編程序中代表一個地址,可以用在指令中,匯編程序經過匯編器的處理后所有的符號都被替換成它所代表的地址值。在C中我們可以通過變量名訪問一個變量,其實就是讀寫某個地址的內存單元,我們通過函數名調用一個函數其實就是調轉到該函數的第一條指令所在的地址,所以變量名和函數名都是符號,本質上是代表內存地址的。

.globl指示告訴匯編器_start這個符號要被鏈接器用到,所以要在目標文件的符號表中給它特殊標記。_start就像C程序的main函數一樣特殊是整個程序的入口,鏈接器在鏈接時會查找目標文件中的_start符號代表的地址,把它設置為整個程序的入口地址,所以每個匯編程序都要提供一個_start符號並且用.globl聲明。如果一個符號沒有用.globl指示聲明這個符號就不會被鏈接器用到。

_start:

_start在這里就像C語言的語句標號一樣。匯編器在處理匯編程序時會計算每個數據對象和每條指令的地址,當匯編器看到這樣一個標號時,就把它下面一條指令的地址作為_start這個符號所代表的地址。而_start這個符號又比較特殊事整個程序的入口地址,所以下一條指令movl $1, %eax就成了程序中第一條被執行的指令。

movl $1, %eax

這是一條數據傳送指令,CPU內部產生一個數字1, 然后傳送到eax寄存器中。mov后邊的l表示long,說明是32位的傳送指令。CPU內部產生的數稱為立即數,在匯編程序中立即數前面加"$"寄存器前面加"%",以便跟符號名區分開。

movl $4, %ebx

與上條指令類似,生成一個立即數4,傳送到ebx寄存器中。

int $0x80

前兩條指令都是為這條指令做准備的,執行這條指令時:

  1. int指令稱為軟中斷指令,可以用這條指令故意產生一個異常。異常的處理與中斷類似,CPU從用戶模式切換到特權模式,然后跳轉到內核代碼中執行異常處理程序。

  2. int指令中的立即數0x80是一個參數,在異常處理程序中根據這個參數決定如何處理,在linux內核中,int $0x80這種異常稱系統調用(System Call)。內核提供了許多系統服務供用戶程序使用,但這些系統服務不能像庫函數(比如printf)那樣調用,因為在執行用戶程序時CPU處於用戶模式不能直接調用內核函數,所以需要通過系統調用切換CPU模式,通過異常處理程序進入內核,用戶程序只能通過寄存器傳幾個參數,之后就要按內核設計好的代碼路線走,而不能由用戶程序隨心所欲想調那個內核函數,這樣保證了系統服務被安全的調用,在調用結束后CPU再切換回用戶模式,繼續執行int指令后面的指令,在用戶程序看來就像函數的調用和返回一樣。

  3. eax和ebx寄存器的值是傳遞給系統調用的兩個參數,eax的值是系統調用號,1表示_exit系統調用,ebx的值則是傳給_exit系統調用的參數,也就是退出狀態。_exit這個系統調用會終止掉當前進程,而不會返回它繼續執行。不同的系統調用需要的參數個數也不同,有的會需要ebx、ecx、edx三個寄存器的值做參數,大多數系統調用完成之后是會返回用戶程序繼續執行的,_exit系統調用特殊。

 

x86匯編的兩種語法:intel語法和AT&T語法
x86匯編一直存在兩種不同的語法,在intel的官方文檔中使
用intel語法,Windows也使用intel語法,而UNIX平台的匯編器一
直使用AT&T語法,所以本書使用AT&T語法。 mov %edx,%eax 這條
指令如果用intel語法來寫,就是 mov eax,edx ,寄存器名不加 % 號,
並且源操作數和目標操作數的位置互換。本書不詳細討論這兩種
語法之間的區別,讀者可以參考[AssemblyHOWTO]。
介紹x86匯編的書很多,UNIX平台的書都采用AT&T語法,例
如[GroudUp],其它書一般采用intel語法,例如[x86Assembly]。

 

二、x86的寄存器

  x86的通用寄存器eaxebxecxedxediesi。這些寄存器在大多數指令中是可以任意使用的。但有些指令限制只能用其中某些寄存器做某種用途,例如除法指令idivl規定被除數在eax寄存器中,edx寄存器必須是0,而除數可以是任何寄存器中。計算結果的商數保存在eax寄存器中(覆蓋被除數),余數保存在edx寄存器。

  x86的特殊寄存器ebpespeipeflags。eip是程序計數器。eflags保存計算過程中產生的標志位,包括進位、溢出、零、負數四個標志位,在x86的文檔中這幾個標志位分別稱為CF、OF、ZF、SF。ebp和esp用於維護函數調用的棧幀。

  esp為棧指針,用於指向棧的棧頂(下一個壓入棧的活動記錄的頂部),而ebp為幀指針,指向當前活動記錄的底部。每個函數的每次調用,都有它自己獨立的一個棧幀,這個棧幀中維持着所需要的各種信息。寄存器ebp指向當前的棧幀的底部(高地址),寄存器esp指向當前的棧幀的頂部(低地址)。

  注意:ebp指向當前位於系統棧最上邊一個棧幀的底部,而不是系統棧的底部。嚴格說來,“棧幀底部”和“棧底”是不同的概念;esp所指的棧幀頂部和系統棧的頂部是同一個位置。

 

三、第二個匯編程序

求一組數最大值的匯編程序:

.section .data
data_items:
.long 3,67,34,222,45,75,54,34,44,33,22,11,66,0
.section .text
.globl _start
_start:
movl $0, %edi
movl data_items(,%edi,4), %eax
movl %eax, %ebx
start_loop:
cmpl $0, %eax
je loop_exit
incl %edi
movl data_items(, %edi,4), %eax
cmpl %ebx, %eax
jle start_loop
movl %eax, %ebx
jmp start_loop
loop_exit:
mov $1, %eax
int $0x80

匯編鏈接執行,然后echo $?會看到輸出222。

 

這個程序在一組數中找到一個最大的數,並把它作為程序的退出狀態。這段數在.data段給出:

data_items:
.long 3,67,34,222,45,75,54,34,44,33,22,11,66,0

 .long指示聲明一組數,每個數32位,相當於C數組。數組開頭有個標號data_items,匯編器會把數組的首地址作為data_items符號所代表的地址,data_items類似於C中的數組名。data_items這個標號沒有.globl聲明是因為它只在這個匯編程序內部使用,鏈接器不需要知道這個名字的存在。除了.long之外常用的聲明:

  • .byte,也是聲明一組數,每個數8位
  • .ascii,例: .ascii "Hello World",聲明了11個數,取值為相應字符的ASCII碼。和C語言不同的是這樣聲明的字符串末尾是沒有'\0'字符的。

data_items數組的最后一個數是0,我們在一個循環中依次比較每個數,碰到0的時候就終止循環。在這個循環中:

  • edi寄存器保存數組中的當前位置,每次比較完一個數就把edi的值加1,指向數組中的下一個數。
  • ebx寄存器保存到目前為止找打的最大值,如果發現有更大的數就更新ebx的值。
  • eax寄存器保存當前要比較的數,每次更新edi之后,就把下一個數讀到eax中。
_start:
movl $0, %edi

初始化edi,指向數組的第0個元素。

 

movl data_items(,%edi,4), %eax

這條指令把數組的第0個元素傳送到eax寄存器中。data_items是數組的首地址,edi的值是數組的下標,4表示數組的每個元素占4字節,那么數組中第edi個元素的地址應該是data_items+edi*4。從這個地址讀數據,寫成指令就是上面那樣。

 

movl %eax, %ebx

ebx的初始值也是數組的第0個元素。

 

下面進入一個循環,在循環的開頭用標號start_loop表示,循環的末尾之后用標號loop_exit表示。

start_loop:
cmpl $0, %eax
je loop_exit

比較eax的值是不是0,如果是0就說明到了數組末尾了,就要跳出循環。cmpl指令將兩個操作數相減,但計算結果並不保存,只是根據計算結果改變eflags寄存器中的標志位。如果兩個操作數相等,則計算結果為0,eflags中的ZF位置1。je是一個條件跳轉指令,它檢查eflags中的ZF位,ZF位為1則發生跳轉,ZF位為0則不跳轉繼續執行下一條指令。(條件跳轉指令和比較指令是配合使用的)je的e就表示equal

 

incl %edi
movl data_items(,%edi,4), %eax

將edi的值加1,把數組中的下一個數組傳送到eax寄存器中。

 

cmpl %ebx, %eax
jle start_loop

把當前數組元素eax和目前為止找到的最大值ebx做比較,如果前者小於等於后者,則最大值沒有變,跳轉到循環開頭比較下一個數,否則繼續執行下一條指令。jle也是一個條件跳轉指令,le表示less than or equal

 

movl %eax, %ebx
jmp start_loop

更新了最大值ebx然后跳轉到循環開頭繼續比較下一個數。jmp是一個無條件跳轉指令,什么條件也不判斷直接跳轉。loop_exit標號后面的指令用_exit系統調用來退出程序。

 

四、尋址方式

訪問內存時在指令中可以用多種方式表示內存地址。內存尋址在指令中可以表示成如下的通用格式:

ADDRESS_OR_OFFSET(%BASE_OR_OFFSET,%INDEX,MULTIPLIER)

它所表示的地址可以這樣計算出來:

FINAL ADDRESS = ADDRESS_OR_OFFSET + BASE_OR_OFFSET + MULTIPLIER * INDEX

其中ADDRESS_OR_OFFSET和MULTIPLIER必須是常數,BASE_OR_OFFSET和INDEX必須是寄存器。在有些尋址方式中會省略這4項中的某些項,相當於這些項是0。

  • 直接尋址:只使用ADDRESS_OR_OFFSET尋址,例如movl ADDRESS, %eax把ADDRESS地址處的32位數傳送到eax寄存器。
  • 變址尋址:movl data_items(,%edi,4), %eax就屬於這種方式,用於訪問數組很方便
  • 間接尋址:只使用BASE_OR_OFFSET尋址,例如movl (%eax), %ebx,把eax寄存器的值看作地址,把這個地址處的32位數傳送到ebx寄存器。
  • 基址尋址:只使用ADDRESS_OR_OFFSET和BASE_OR_OFFSET尋址,例如movl 4(%eax), %ebx,用於訪問結構體成員比較方便,例如一個結構體的基地址保存在eax寄存器中,其中一個成員在結構體內偏移量是4字節,要把這個成員讀上來就可以用這條指令。
  • 立即數尋址:就是指令中有一個操作數是立即數,例:movl $3, %eax。
  • 寄存器尋址:就是指令中有一個操作數是寄存器。在匯編程序中寄存器用助記符來表示,在機器指令中則要用幾個Bit表示寄存器的編號,這幾個Bit與可以看做寄存器的地址,但是和內存地址不在一個地址空間。

 

關於匯編程序的Hello World可以參看我的另一篇文章:http://www.cnblogs.com/orlion/p/5316519.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM