2008年2月8日 星期五

PC Assembly Language 學習筆記(4) - Working with Integer

整數表示法

在電腦系統中,根據有無正負號,分為 unsigned 與 signed;在 unsigned 的部分就不需太多討論了,因為就很直覺的進行換算即可,而 signed 的部分,在電腦系統中是使用 2 補數的方式來表示。

2 補數的換算很簡單,只要先換成 1 補數(0 1 對調),再加上 1 即為 2 補數;使用 2 補數除了有表示範圍較大之外,CPU 也較容易處理;而在 assembly 中若是要計算 2 補數,可以用 NEG 這個指令,可以直接將指定的運算子轉換為 2 補數(運算元可以是 register 或是 memory address)。

另外,對 CPU 來說,並不知道要資料的長度為何(完全不知道所處理的是 byte、word、還是 double word),因此程式設計師若要指定處理資料的大小,就必須仰賴所使用的指令


signed extension

在 assembly 中,所有的資料都會有其大小,根據程式需求改變資料程度是很平常的事情,以下給的例子:
;AX = 0x134(長度為 16 bits)
;AX = 308
mov ax, 0134H
;將 AX register 中的 lower 8 bits 放入 CL register 中
;CL = 0x34(長度為 8 bits)
;CL = 52
mov cl, al
從上面例子可看出,由於資料大小改變了,因此其值也改變了。

另外,若要加大資料的長度可就不是這麼簡單了,舉個例子來說,假設有一個值為 0xFF,要從原本的 8 bits 轉為 16 bits,若是 unsigned integer 倒也是簡單,但若是 signed integer,那可就不是隨便前面補 0 就可以完成的了!

在 80386 系列的 CPU,提供了多個指令可以進行擴充資料長度這一類的工作,以下用範例來說明:
;以下指令僅用於 unsigned integer
;destination 的長度必須大於 source
movzx eax, ax ;複製 AX 的內容到 EAX (16 bits -> 32 bits)
movzx eax, al ;複製 AL 的內容到 EAX (8 bits -> 32 bits)
movzx ax, al ;複製 AL 的內容到 AX (8 bits -> 16 bits)
movzx ebx, ax ;複製 AX 的內容到 RBX (16 bits -> 32 bits)
如註解上說明,movzx 指令僅能用於 unsigned integer,而若是要擴充 signed integer 的長度,在 8086 下可使用以下指令:(此時僅有 16-bit register)
  • CBW(Convert Byte to Word)
    將 AL register 中的資料加以擴充轉換後,存入 AX register 中
  • CWD(Convert Word to Double word)
    將 AX register 中的資料加以擴充轉換後,分成兩組存入 DX 與 AX register 中,變成 DX:AX (32 bits)

若在 80386 以後,則提供了以下指令:
  • CWDE(Convert Word to Double word extended)
    將 AX register 中的資料加以擴充轉換後,存入 EAX register 中
  • CDQ(Convert Double word to Quad word)
    將 EAX register 中的資料加以擴充轉換後,分成兩組存入 EDX 與 EAX register 中,變成 EDX:EAX (64 bits)

此外還多提供了 MOVSX 的指令,用法語 MOVZX 相同,但可以用來搬移 signed integer。


與 C 語言的對照

上面說了一堆 unsigned/signed,在 C 語言中又是如何呢? 以下用程式來說明:

在 C 語言中,char 為 1 byte,而 int 為 4 bytes;以上程式轉為 assembly 應該是怎麼作呢?

unsigned char -> integer,使用 MOVZX (此指令用於 unsigned integer)

signed char -> integer:使用 MOVSX (此指令用於 signed integer)


然而有沒有指定 unsigned 或是 signed 真的很重要嗎? 以下用另外一段程式來說明:

仔細看 while 執行的條件,必須判斷 char 並非 EOF(-1) 才會繼續執行。

而 char 要與 integer 作比較,就必需要經過 extend 的動作;然而,若是遇到 EOF 字元為 0xFF,在 unsigned/signed 兩種不同情況下,會擴充為:
  • unsigned => 0x000000FF
  • signed => 0xFFFFFFFF (因為 signed char 0xFF 為 -1)

若是 unsigned 的情況下,原本的 EOF char 會被轉為 255 而非 -1,如此一來結果就不對了! 所以,C 語言中預設變數都是 signed 原因即是如此。


2 補數的四則運算

加法(addition)、減法(subtraction)

2 補數有一個很大的優點,就是不論 unsigned 或是 signed,進行加法(ADD)或減法(SUB)的方式都是相同的。

但在進行加減法運算時,會影響到 EFLAGS register 中的兩個 bit,分別是 overflow 以及 carry,以下說明一下相關情況:
  1. 針對 signed 數值運算,若 destination 空間不夠存放計算出來的結果,則 overflow=1,反之則為 0
  2. 若計算中產生借位或溢位的情況,則 carry=1,反之為 0

以下用一個算式說明 carry 的發生:


乘法(multiply)

為了處理 unsigned 以及 signed 的問題,乘法的部分有兩種指令,分別是 MUL(用於 unsigned) 與 IMUL(用於 signed)。

MUL 的用法如下:
mul source
結果會有三種,分別是:(source 也可以是相同大小的 memory 位址)
  1. source 為 AL(8 bits) 中的值,則結果會存在 AX(16 bits) register 中
  2. source 為 AX(16 bits) 中的值,則結果會存在 DX:AX(32 bits) 中
  3. 若 source 為 EAX(32 bits),則結果會存在 EDX:EAX(64 bits) 中。

而 IMUL 就是負責處理 signed 的部分,IMUL 的用法有以下幾種:
imul source1
imul dest, source1
imul dest, source1, source2
這樣一來使用組合就有很多種了,以下用一張圖來說明:



除法(devide)

除法與乘法是差不多的,針對 unsigned 與 signed 也都有兩種不同的指令,分別是 DIV(用於 unsigned) 與 IDIV(用於 signed)。

而除法的部分就牽涉到商數(quotient)和餘數(remainder)的部分,不過最重要的則是除數(source),以下說明一下除法的規則:
  1. source(除數) 為 8 bits,則要將被除數存放在 AX 中,最後 quotient 存於 AL 中,remainder 存於 AH 中
  2. source(除數) 為 16 bits,則要將被除數分成兩個部分存放在 DX:AX 中,最後 quotient 存於 AX 中,remainder 存於 DX 中
  3. source(除數) 為 32 bits,則要將被除數分成兩個部分存放在 EDX:EAX 中,最後 quotient 存於 EAX 中,remainder 存於 EDX 中

而 IDIV 的用法跟 DIV 是相同的,不像 IMUL 有另外不同的用法。


四則運算說明完後,當然要來個範例囉! 以下有個簡單的範例程式:
;檔案: math.asm
;描述:四則運算練習

%include "asm_io.inc"

;初始化資料
segment .data
prompt db "Enter a number: ", 0
square_msg db "Square of input is ", 0
cube_msg db "Cube of input is ", 0
cube25_msg db "Cube of input times 25 is ", 0
quot_msg db "Quotient of cube/100 is ", 0
rem_msg db "Remainder of cube/100 is ", 0
neg_msg db "The negation of the remainder is ", 0

;未初始化資料
segment .bss
input resd 1

segment .text
global asm_main
asm_main:
enter 0, 0 ;程式開始
pusha

;顯示提示訊息
mov eax, prompt
call print_string

;讀取使用者輸入的整數
call read_int
mov [input], eax

imul eax ;edx:eax = eax * eax
mov ebx, eax ;將相乘後的結果暫時存放於 EBX 中

;顯示[intput]^2的結果
mov eax, square_msg
call print_string
mov eax, ebx
call print_int
call print_nl

;顯示[intput]^3的結果
mov ebx, eax
imul ebx, [input] ;ebx *= [input]
mov eax, cube_msg
call print_string
mov eax, ebx
call print_int
call print_nl

;顯示[intput]^3 * 25的結果
imul ecx, ebx, 25
mov eax, cube25_msg
call print_string
mov eax, ecx
call print_int
call print_nl


mov eax, ebx
cdq ;將 EAX(double word) 轉為 EDX:EAX(quad word)
mov ecx, 100
idiv ecx ;eax/ecx => [intpu]^3 / 100 (商數存在 EAX,餘數存在 EDX)
mov ecx, eax ;暫時將商數放在 ECX
mov eax, quot_msg
call print_string
mov eax, ecx
call print_int ;印出商數
call print_nl
mov eax, rem_msg
call print_string
mov eax, edx ;印出餘數
call print_int
call print_nl

;轉換為負數(2補數)
neg edx
mov eax, neg_msg
call print_string
mov eax, edx
call print_int
call print_nl

popa
mov eax, 0 ;回到 C 程式
leave
ret

; 結果如下:
; Enter a number: 9
; Square of input is 81
; Cube of input is 729
; Cube of input times 25 is 18225
; Quotient of cube/100 is 7
; Remainder of cube/100 is 29
; The negation of the remainder is -29


更大數值的運算處理

雖然機會不多,可是並不表示不可能會有兩個 64 bits 數值的相加或相減,此時要怎麼作呢? 答案是使用 ADCSBB 兩個指令。

以上那兩個指令都是使用 carry flag 來完成的,以下是他們的計算公式:
# ADC
operand1 = operand1 + carry flag + operand2

# SBB
operand1 = operand1 - carry flag - operand2

因此,假設有兩個 64 bits 的數值(EDX:EAX 與 EBX:ECX)要進行相加及相減的動作,可以用以下方式處理:
;相加
add eax, ecx ;lower 32 bits 相加
adc edx, ebx ;upper 32 bits 相加 + carry flag + 之前的計算結果

;相減
sub eax, ecx ;lower 32 bits 相減
sbb edx, ebx ;upper 32 bits 相減並根據 carry flag 決定是否借位

1 則留言:

  1. 大大您這邊寫錯囉^^

    ;將 AX register 中的 lower 8 bits 放入 CL register 中
    ;CL = 0x34(長度為 8 bits)
    ;CL = 52

    "mov cl, al" =>這邊錯了

    應改成mov cl, ax (16bits轉成8bits)

    回覆刪除