一、簡單的彙編程式 以下麵這段簡單的彙編代碼為例 (註意是globl不是global;movl(MOVL)不是mov1(MOV一)) 將這段程式保存為demo.s,然後用彙編器as把彙編程式中的助記符翻譯成機器指令(彙編指令與機器指令是對應的)生成目標文件demo.o。然後用鏈接器ld把目標文件de ...
一、簡單的彙編程式
以下麵這段簡單的彙編代碼為例
.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的通用寄存器有eax、ebx、ecx、edx、edi、esi。這些寄存器在大多數指令中是可以任意使用的。但有些指令限制只能用其中某些寄存器做某種用途,例如除法指令idivl規定被除數在eax寄存器中,edx寄存器必須是0,而除數可以是任何寄存器中。計算結果的商數保存在eax寄存器中(覆蓋被除數),餘數保存在edx寄存器。
x86的特殊寄存器有ebp、esp、eip、eflags。eip是程式計數器。eflags保存計算過程中產生的標誌位,包括進位、溢出、零、負數四個標誌位,在x86的文檔中這幾個標誌位分別稱為CF、OF、ZF、SF。ebp和esp用於維護函數調用的棧幀。
三、第二個彙編程式
求一組數最大值的彙編程式:
.section .data data_items: .long 3,10,9,29,5,19,9,36,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_item(, %edi,4), %eax cmpl %ebx, %eax jle start_loop movl %eax, %ebx jmp start_loop loop_exit: mov $1, %eax int $0x80
彙編鏈接執行:
這個程式在一組數中找到一個最大的數,並把它作為程式的退出狀態。這段數在.data段給出:
data_items: .long 3,10,9,29,5,19,9,36,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