1. 程式人生 > >C語言的本質(29)——C語言與彙編之暫存器和定址方式

C語言的本質(29)——C語言與彙編之暫存器和定址方式

x86的通用暫存器有eax、ebx、ecx、edx、edi、esi。這些暫存器在大多數指令中是可以任意選用的,比如movl指令可以把一個立即數傳送到eax中,也可傳送到ebx中。但也有一些指令規定只能用其中某些暫存器做某種用途,例如除法指令idivl要求被除數在eax暫存器中,edx暫存器必須是0,而除數可以在任意暫存器中,計算結果的商數儲存在eax暫存器中,而原來的被除數被覆蓋掉,餘數儲存在edx暫存器中。也就是說,通用暫存器對於某些指令而言不是通用的。

x86的特殊暫存器有ebp、esp、eip、eflags。eip是程式計數器,eflags儲存著計算過程中產生的標誌位,包括進位、溢位、零、負數四個標誌位,在x86的文件中這幾個標誌位分別稱為CF、OF、ZF、SF。ebp和esp用於維護函式呼叫的棧幀。

下面我們通過一個求一組數的最大值的彙編程式來體會:

#max.asm
.section .data
data_items:             #資料項,即陣列元素
.long3,67,34,222,45,75,54,34,44,33,22,11,66,0
 
.section .text
.globl _start
_start:
         movl$0, %edi                     # 把當前位置0存入%edi暫存器
         movldata_items(,%edi,4), %eax     #從資料的第一個整數開始處理
         movl%eax, %ebx             # 因為是資料的第一個數字,所以當前%eax的值是最大的
 
start_loop:               # 迴圈開始
         cmpl$0, %eax        # 檢查是否已經把所有的數字遍歷了
         jeloop_exit
         incl%edi                    # 處理下一個數字
         movldata_items(,%edi,4), %eax
         cmpl%ebx, %eax    # 比較數值
         jlestart_loop # 如果新的數字不是最大的值,就跳轉回迴圈的開始
         movl%eax, %ebx    #如果是最大值,那麼就把這個數字取代原來的最大值 move the value as the largest
         jmpstart_loop        # 繼續迴圈
 
loop_exit:
         movl$1, %eax
         int$0x80

彙編、連結、執行:

可以看到最大數為222。

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

data_items:
.long3,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 "Helloworld",聲明瞭11個數,取值為相應字元的ASCII碼。注意,和C語言不同,這樣宣告的字串末尾是沒有'\0'字元的,如果需要以'\0'結尾可以宣告為.ascii "Hello world\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
 movldata_items(,%edi,4), %eax

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

 cmpl%ebx, %eax
 jlestart_loop

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

更新了最大值ebx然後跳轉到迴圈開頭比較下一個數。jmp是一個無條件跳轉指令,什麼條件也不判斷,直接跳轉。loop_exit標號後面的指令用_exit系統呼叫退出程式。

通過這個例子我們瞭解到,訪問記憶體時在指令中可以用多種方式表示記憶體地址,比如可以用陣列基地址、元素長度和下標三個量來表示,增加了定址的靈活性。下面介紹x86常用的幾種定址方式(Addressing Mode)。記憶體定址在指令中可以表示成如下的通用格式:

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。

常見的定址方式:

1、直接定址(Direct Addressing Mode)。只使用ADDRESS_OR_OFFSET定址,例如movl ADDRESS, %eax把ADDRESS地址處的32位數傳送到eax暫存器。

2、變址定址(Indexed Addressing Mode) 。上一節的movldata_items(,%edi,4), %eax就屬於這種定址方式,用於訪問陣列元素比較方便。

3、間接定址(Indirect Addressing Mode)。只使用BASE_OR_OFFSET定址,例如movl(%eax), %ebx,把eax暫存器的值看作地址,把這個地址處的32位數傳送到ebx暫存器。注意和movl %eax, %ebx區分開。

4、基址定址(Base Pointer Addressing Mode)。只使用ADDRESS_OR_OFFSET和BASE_OR_OFFSET定址,例如movl4(%eax), %ebx,用於訪問結構體成員比較方便,例如一個結構體的基地址儲存在eax暫存器中,其中一個成員在結構體內的偏移量是4位元組,要把這個成員讀上來就可以用這條指令。

5、立即數定址(Immediate Mode)。就是指令中有一個運算元是立即數,例如movl $12, %eax中的$12,這其實跟定址沒什麼關係,但也算作一種定址方式。

6、暫存器定址(Register Addressing Mode)。就是指令中有一個運算元是暫存器,例如movl$12, %eax中的%eax,這跟記憶體定址沒什麼關係,但也算作一種定址方式。在彙編程式中暫存器用助記符來表示,在機器指令中則要用幾個Bit表示暫存器的編號,這幾個Bit也可以看作暫存器的地址,但是和記憶體地址不在一個地址空間。