顯示具有 Assembly 標籤的文章。 顯示所有文章
顯示具有 Assembly 標籤的文章。 顯示所有文章

2009年10月18日 星期日

[NASM] BIOS 啟動程序

當電腦的電源開啟後,處理器會進入 reset 狀態,此時:
  1. 所有記憶體的內容會變為 0

  2. CS 的值會被預設為 0FFFF[0]H

  3. IP(Instruction Pointer) 儲存指令的 offset,預設為 0

結合 2 & 3(Code Segment + Offset),可以知道第一個要執行的指令的 memory address 為 0FFFF0H(CS:IP),而這個 memory address 即為 BIOS 的進入點。

BIOS 的常式位於 0FFF0H,他檢查各個 I/O port,並對於存在的設備進行初始化,接著 BIOS 會建立兩個資料區:
  1. Interrupt Vector Table (中斷向量表)
    長度為 1 KB,用來做為當 interrupt(中斷) 發生時,BIOS & OS 用來選擇相對應的 interrupt handler 之用,詳細資料可參考此篇文章。

  2. BIOS 資料區
    起始位址為 40[0]H,大小依據設備的多寡而不同。

接著 BIOS 會檢查系統磁碟是否存在,並從系統磁碟下載啟動程式,啟動程式接著從系統磁碟將系統檔案載入 memory,並將控制權交給 OS。

2008年2月23日 星期六

PC Assembly Language 學習筆記(8) - Arrays

陣列的定義

陣列是一個連續的記憶體空間,每個元素的大小、型態皆相同,在 assembly 中定義陣列的方式很簡單,以下用個範例來說明:
segment .data
;宣告長度為 10 的陣列,每個元素大小為 double word,並分別初始化元素的值為 1, 2, 3, ....., 10
a1 dd 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

;宣告長度為 10 的陣列,每個元素大小為 word,初始化所有元素的值為 0
a2 dw 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

;與上一個宣告相同,但使用 TIMES 關鍵字表示重複指令的次數
a3 times 10 dw 0

;宣告長度為 200 的陣列,每個元素大小為 word,每一個元素的值為 1
a4 times 200 dw 1

segment .bss
;宣告長度為 10 的陣列,每個元素大小為 double word,未初始化
a5 resd 10

;宣告長度為 200 的陣列,每個元素大小為 word,未初始化
a6 resw 200

而若要在在 stack 將陣列定義為 local 變數呢? 假設需要的空間有 1 個 char、2 個 double word、50 個 word 元素的陣列,一共需要 1 * 1 + 2 * 4 + 50 * 2 = 109 個 bytes 的空間,從之前介紹過的程式來看,有兩種方法可以達成,一個是透過 [ESP - 109] 的方式來保留記憶體空間,另一個則是用 ENTER 指令來保留空間,以下為保留記憶體空間後的 stack 圖示:



陣列的存取

在 assembly 中存取陣列的元素值,並非像 C 或其他高階語言一樣,使用中括號([])加索引的方式,而是必須要清楚指定 memory address(以 byte 為單位),以下用幾個範例來說明:
;定義兩個陣列,每個元素的大小分別為 byte 與 word
byteArray db 5, 4, 3, 2, 1
wordArray dw 5, 4, 3, 2, 1

;存取 byte 陣列
mov al, [byteArray] ;AL = byteArray[0]
mov al, [byteArray + 1] ;AL = byteArray[1]
mov [byteArray + 3], al ;byteArray[3] = AL

;存取 word 陣列
mov ax, [wordArray] ;AX = wordArray[0]
mov ax, [wordArray + 2] ;AX = wordArray[1] 注意! 這邊 + 2 bytes 即是 + 1 word
mov [wordArray + 6], ax ;wordArray[3] = AX
mov ax, [wordArray + 1] ;這是錯誤的用法,因為元素大小為 word(2 bytes)
以下介紹如何將陣列的值進行加總:
    mov     ebx, byteArray      ;將 byteArray 的初始位址存到 EBX 中
mov dx, 0 ;總和存於 DX 中
mov ah, 0 ;為了確保 AX = AL,因此要清空 AH
mov ecx, 0 ;loop 的次數以 ECX 中的值為主,每次遞減
lp:
mov al, [ebx] ;AL = *EBX
add dx, ax ;add 指令兩端 operand 的大小必須相同
inc ebx ;移至陣列的下一個元素
loop lp

接著用範例說明陣列的操作方式:
// ==================== array1c.c ====================
#include <stdio.h>

int asm_main(void);
void dump_line(void);

int main() {
int ret_status;
ret_status = asm_main();
return ret_status;
} //end main

void dump_line() {
int ch;
while((ch = getchar()) != EOF && ch != 'n')
;
} //end dump_line


; ==================== array1.asm ====================
%define ARRAY_SIZE 100
%define NEW_LINE 10

segment .data
FirstMsg db "First 10 elements of array", 0
Prompt db "Enter index of element to display: ", 0
SecondMsg db "Element %d is %d", NEW_LINE, 0
ThirdMsg db "Elements 20 through 29 of array", 0
InputFormat db "%d", 0

segment .bss
array resd ARRAY_SIZE

segment .text
extern puts, printf, scanf, dump_line
global asm_main
asm_main:
enter 4, 0 ;預留空間給 local 變數用,長度為 double word [EBP - 4]
push ebx ;保留 EBX 的值,先存入 stack 中
push esi ;保留 ESI 的值,先存入 stack 中

;初始化陣列值為 100, 99, 98, 97, .....
mov ecx, ARRAY_SIZE
mov ebx, array
init_loop:
mov [ebx], ecx
add ebx, 4
loop init_loop

push dword FirstMsg ;印出 FirstMsg
call puts
pop ecx

push dword 10
push dword array
call print_array ;印出陣列中的前十個元素
add esp, 8 ;使用 C function 後必須回復原本 ESP 指向的位址(使用 2 個 stack 空間)

Prompt_loop:
push dword Prompt
call printf
pop ecx ;將 Prompt 的 memory address pop 到 ECX 中(沒意義,只是將其從 stack 中移除)

lea eax, [ebp - 4] ;EAX = local 變數的位址
push eax
push dword InputFormat
call scanf
add esp, 8 ;使用 C function 後必須回復原本 ESP 指向的位址(使用 2 個 stack 空間)
cmp eax, 1 ;EAX = scanf 的 return value
je InputOK

call dump_line
jmp Prompt_loop

InputOK:
mov esi, [ebp - 4]
push dword [array + 4 * esi]
push esi
push dword SecondMsg
call printf
add esp, 12 ;使用 C function 後必須回復原本 ESP 指向的位址(使用 3 個 stack 空間)

push dword ThirdMsg
call puts
pop ecx ;將 ThirdMsg 的 memory address pop 到 ECX 中(沒意義,只是將其從 stack 中移除)

push dword 10
push dword array + 20 * 4
call print_array
add esp, 8 ;呼叫 function 後必須回復原本 ESP 指向的位址(使用 2 個 stack 空間)

pop esi ;還原 ESI 的值
pop ebx ;還原 EBX 的值
mov eax, 0 ;回到 C 程式中
leave
ret

; routine : _print_array
; C 程式
; void print_array(const int *a, int n);
; 參數:
; (1) a - 指向陣列的 pointer [EBP + 8]
; (2) n - 要印出的元素數量 [EBP + 12]
segment .data
OutputFormat db "%-5d %5d", NEW_LINE, 0

segment .text
global print_array
print_array:
enter 0, 0
push esi ;保留 ESI 的值,先存入 stack 中
push ebx ;保留 EBX 的值,先存入 stack 中

xor esi, esi ;ESI = 0
mov ecx, [EBP + 12] ;迴圈次數
mov ebx, [ebp + 8] ;EBX = 陣列位址
print_loop:
push ecx ;printf 可能會改變 ECX 中的值,因此先存入 stack 中作保留

;呼叫 C 語言中的 printf,使用的 stack 中的資料作為參數
;分別為(1)OutputFormat (2)ESI (3)陣列值
push dword [ebx + 4 * esi]
push esi
push dword OutputFormat
call printf
add esp, 12 ;使用 C function 後必須回復原本 ESP 指向的位址(使用 3 個 stack 空間)

inc esi ;index++
pop ecx ;還原 ECX
loop print_loop

pop ebx ;還原 EBX 的值
pop esi ;還原 ESI 的值
leave
ret


Array/String Instructions

在 80x86 的 CPU 中,設計了許多用來處理 array 的指令,這些指令稱為 string instruction,他們利用 index register(ESI、EDI) 做為索引之用,並搭配 FLAGS register 中的 direction flag(DF) 來判斷索引為遞增或遞減,其中有兩個指令可用來設定 direction flag:
  • CLD(clears the direction flag):將 DF 設定為 0,為遞增
  • STD(sets the direction flag):將 DF 設定為 1,為遞減

Reading & writing memory

首先先說明讀寫記憶體內容所使用的指令:

左邊的部分是 read memory,右邊則是 write memory;而從上面這個表中,有三個部分是需要注意的:
  1. 實際資料存放的位置
    LOASx 的部分,是存放在 data segment 中;STOSx 的部分則是存放在 extra segment 中
  2. 所使用到的 index register
    LOASx 使用的是 ESI(source index);STOSx 則是使用 EDI(destination index)
  3. 每次迴圈,index register 所遞增的值
    根據不同的資料長度,分別為 1(byte)、2(word)、4(double word)

此外,還可以注意到所使用的 segment 為 data segment(左邊) 與 extra segment(右邊),以下用一個簡單範例來介紹:
segment .data
ary1 dd 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

segment .bss
ary2 resd 10

segment .text
cld ;將 direction flag 設定為 0,讓 index register 的動作為遞增
mov esi, ary1 ;將 ary1 的 memory address 放到 ESI
mov edi, ary2 ;將 ary2 的 memory address 放到 EDI
mov ecx, 10 ;為了執行迴圈 10 次

lp:
;將每個在 ary1 中的元素複製到 ary2 中
lodsd ;load single double word
stosd ;store single double word
loop lp
而上述的效果,還可以用 MOVSx 指令來達成,而 MOVSx 指令的使用可以參考下表:

上述的 mov 所處理的資料大小是不同的,因此使用上就必須注意一下囉! 在上一個範例中,LODSD 與 STOSD 的結合,其實用 MOVSD 就可以達到相同效果囉!!!


REP 指令


講完 loop 的寫法後,一定要提到 REP 指令,其實這個指令就是 repeat 的意思,也是以 ECX 中的值作為重複次數的依據,而且可以用很簡單的語法就可以完成,以上面程式的最後三行為例(lp label 後的三行),可以改成以下這一行:
rep movsd ;MOVSD 的效果可以參考上表
以下用一段範例程式碼將陣列所有元素初始化為 0:
segment .bss
ary resd 10

segment .text
cld ;讓 index register 為遞增
mov edi, ary
mov ecx, 10
xor eax, eax ;下一個指令會用到 EAX
rep stosd ;[EDI] = EAX (使用 extra segment,EDI += 4)

Comparison 相關指令

接著這邊介紹的是用來進行比對的指令,這些指令用來比較 register 與 memory 的值,以下用一張圖來介紹有哪些指令可用:

以上這些指令,跟 CMP 相同,也是會修改 FLAGS register 中的值做為程式判斷的依據,以下用一段簡單的程式碼來說明使用方式:
segment .bss
ary resd 100

segment .text
cld ;index register 為遞增
mov edi, ary
mov ecx, 100
mov eax, 12 ;為了找尋陣列中有無元素值為 12

lp:
scasd ;比對 EAX 與 [ES:EDI] 的值
je found ;有尋找到,跳至 found label
loop lp
;=====================
;找不到的時候,所要執行的程式碼放這
;=====================

found:
sub edi, 4 ;確定目前 EDI 所儲存的為指向 12 元素的 pointer
;=====================
;找到以後,所要執行的程式碼放這
;=====================

REPx 指令

REPx 指令與 REP 是類似的,都是作為 repeat 指令之用,以下先用張圖來說明有哪些相關指令:

而 REPx 通常用來與 comparison 指令進行搭配,而若是有比對相同,程式並不會因此停止執行,index register 會持續遞增,而 ECX 則是會持續遞減,直到條件達到(Z flag=1/0 或 ECX=0)為止。

以下用一段比對 memory block 的程式來說明 REPx 的使用方式:
segment .text
cld
mov esi, block1 ;第一個 memory block 的 address
mov edi, block2 ;第二個 memory block 的 address
mov ecx, size
repe cmpsb ;持續的比對每一個 byte,直到 Z=1 或是 ECX=0
je equal

;=====================
;比對不同,所要執行的程式碼置於此處
;=====================

jmp onward ;必須跳離,不然會執行到 equal Label 去

equal:
;=====================
;比對相同,所要執行的程式碼置於此處
;=====================

onward:

範例說明

最後用一個較為完整的範例來說明這些指令的使用:

memory.asm
global  asm_copy, asm_find, asm_strlen, asm_strcpy

segment .text

; function: asm_copy
; 用途:複製 memory block
; C functin 定義 :
; void asm_copy(void *dest, const void *src, unsigned sz);
; 參數說明 :
; (1) dest - 指向目的地位址的 pointer
; (2) sec - 指向來源端位址的 pointer
; (3) sz - 要複製的 byte 數量
%define dest [ebp + 8]
%define src [ebp + 12]
%define sz [ebp + 16]

asm_copy:
enter 0, 0
push esi ;保留 ESI 與 EDI 的值(先存入 stack)
push edi

mov esi, src
mov edi, dest
mov ecx, sz

cld ;指定 index register 為遞增 (direction flag = 0)
rep movsb ;[ES:EDI] = [DS:ESI] (ESI += 4 、 EDI += 4)

pop edi ;還原 ESI 與 EDI 的值
pop esi
leave
ret


; function: asm_find
; 用途:在記憶體中尋找指定的 byte
; C functin 定義 :
; void *asm_find(const void *src, char target, unsigned sz);
; 參數說明 :
; (1) src - 指向來源端位址的 pointer
; (2) target - 所要尋找的 byte 值
; (3) sz - 所要尋找的 memory block 大小
; 回傳值說明 :
; 若找到指定的值,回傳指定該值的 pointer;若無,回傳 null
;
; 注意:尋找的是 byte,但因為使用到 stack,因此要把他當作 double word 來處理(使用 lower 8 bits)
%define src [ebp + 8]
%define target [ebp + 12]
%define sz [ebp + 16]

asm_find:
enter 0, 0
push edi

mov eax, target
mov edi, src
mov ecx, sz

cld
repne scasb ; scan memory block 的值,直到 ECX=0 或是 [ES:EDI]=AL

je found_it
mov eax, 0 ;未發現,回傳 null pointer
jmp short quit

found_it:
mov eax, edi ;由 C 呼叫 assembly function 時,回傳值必須存在 EAX 中(因為長度為 double word)
dec eax ;讓 pointer 指向正確的位址

quit:
pop edi
leave
ret


; function: asm_strlen
; 用途:回傳字串的長度
; C functin 定義 :
; unsigned asm_find(const char *);
; 參數說明 :
; (1) src - 指向字串的 pointer
; 回傳值說明 :
; 字串長度(會存入 EAX 中)
%define src [ebp + 8]

asm_strlen:
enter 0, 0
push edi

mov edi, src ;將 string pointer 放到 EDI 中
mov ecx, 0FFFFFFFFh ;設定 ECX 為最大
xor al, al ;AL=0

cld
repnz scasb ;尋找字串結尾(\0)

mov eax, 0FFFFFFFEh
sub eax, ecx ;length = 0FFFFFFFEh - ecx

pop edi
leave
ret


; function: asm_strcpy
; 用途:複製字串
; C functin 定義 :
; void *asm_strcpy(char *dest, const char *src);
; 參數說明 :
; (1) dest - 指向複製來源字串的 pointer
; (2) src - 指向複製目的地字串的 pointer
%define desc [ebp + 8]
%define src [ebp + 12]

asm_strcpy:
enter 0, 0
push esi
push edi

mov edi, dest
mov esi, src

cld
cpy_loop:
lodsb ;將字元讀取到 AL 中,並遞增 ESI 的值
stosb ;將字元從 AL 中讀取出來,並遞增 EDI 的值
or al, al ;設定 condition flags
jnz cpy_loop ;尚未遇到結束字元(\0),繼續執行

pop edi
pop esi
leave
ret
memex.c
#include <stdio.h>

#define STR_SIZE 30

void asm_copy(void *, const void *, unsigned);
void * asm_find(const void *, char target, unsigned);
unsigned asm_strlen(const char *);
void asm_strcpy(char *, const char *);

int main(void) {
char st1[STR_SIZE] = "test string";
char st2[STR_SIZE];
char *st;
char ch;

asm_copy(st2, st1, STR_SIZE); //呼叫 assembly function
printf("%s\n", st2);

printf("Enter a char: ");
scanf("%c%*[^\n]", &ch);
st = asm_find(st2, ch, STR_SIZE); //呼叫 assembly function
if(st)
printf("Found it: %s\n", st);
else
printf("Not Found\n");

st1[0] = 0;
printf("Enter string: ");
scanf("%s", st1);
printf("len = %u\n", asm_strlen(st1)); //呼叫 assembly function

asm_strcpy(st2, st1); //呼叫 assembly function
printf("%s\n", st2);

return 0;
} //end main

編譯、執行指令
shell> nasm -f elf memory.asm ; gcc -o memex memex.c memory.o ; ./memex

2008年2月18日 星期一

PC Assembly Language 學習筆記(7) - Subprograms

Indirect Addressing

indirect address 的用途是在於讓 register 以類似 pointer 的方式來運作,而使用方式上必須用中括號([])包住 register 名稱,以下舉例說明:
mov     ax, [myData]    ;指定以類似 pointer 方式處理
mov ebx, myData ;EBX = &myData
mov ax, [ebx] ;AX = *EBX


The Stack

現在幾乎大多數 CPU 中都已經支援 stack 了,其特色在於 LIFO(Last In First Out),因此在程式中常被用來作為資料暫存以及呼叫 subprogram 之用,

以下分別舉兩個例子說明:

資料暫存

在資料暫存的部分,必須透過 PUSH、POP 兩個指令以及 ESP(stack pointer) register 的搭配;其中 PUSH 會將 ESP 中的位址 - 4,並將內容塞入 [ESP] 的位址;而 POP 則會將 ESP 中的位址 + 4,並將內容塞到指定的地方去;以下用範例來說明:
;假設 EPS 一開始儲存的 memory address 為 0x1000
push dword 1 ;將 1 存於 0x0FFC 的位置,ESP = 0x0FFC(0x1000 - 0x0004)
push dword 2 ;將 2 存於 0x0FF8 的位置,ESP = 0x0FF8(0x0FFC - 0x0004)
push dword 3 ;將 3 存於 0x0FF4 的位置,ESP = 0x0FF4(0x0FF8 - 0x0004)
pop eax ;EAX = 3,ESP = 0x0FF8(0x0FF4 + 0x0004)
pop ebx ;EBX = 2,ESP = 0x0FFC(0x0FF8 + 0x0004)
pop ecx ;ECX = 1,ESP = 0x1000(0x0FFC + 0x0004)
【註】PUSH 的資料長度必須是 double word

呼叫 subprogram

若要透過 stack 來呼叫 subprogram,則必須使用 CALL 與 RET 兩個指令,其中 CALL 為 unconditional jump,會直接跳到所指定的 Label,並將原本他所在的 memory address push 到儲存下一個執行指令位址的 stack 中;而 RET 則是將 stack 中的 memory address pop 出來,並跳回該 memory address。以下用個範例說明:
mov     ebx, input1
;uncondictional jump,跳至 get_int 所在位址,並將原本的 memory address push 到儲存下一個執行指令的 stack 中
call get_int

mov ebx, input2
;uncondictional jump,跳至 get_int 所在位址,並將原本的 memory address push 到儲存下一個執行指令的 stack 中
call get_int

get_int:
call read_int
mov [ebx], eax
ret ;將原本呼叫 subprogram 時所在的 memory address 從 stack pop 出來,以返回呼叫點
透過此種方式,呼叫 subprogram 就更為方便了!


Calling Conventions

在其他高階程式語言中,常常會在呼叫 subprogram 時帶入多個參數,這在底層是如何處理的呢? 答案就是利用 stack。

由於參數資料會在 subprogram 中被使用一次甚至數次,因此這些資料不適合放置於 register 中,因此一般會放在 stack 中;而且會在 CALL 指令發生前先 push 進入 stack 中,因此 return address 也會比參數先從 stack 中 pop 出來。以下用一張圖來說明:


但如果在 subprogram 中又有資料要存入 stack 中呢? 在 stack 中的情形會變成如下:

如此一來,參數的位置就從 [ESP+4] 的位置跑到 [ESP+8] 了,若是在 subprogram 中要使用到 stack,就必須注意參數被移到那個位置去,其實蠻麻煩的。

因此,在 80386 以後的 CPU,提供了 EBP(base pointer) register 來解決這個問題,而 EBP 所儲存的內容,則是對應到 return address 所存放的位置(因此一開始 EBP 的內容要設定與 ESP 相同)。

以下用個簡單範例說明 EBP 在 subprogram 中的使用方式:
subprogram_label:
push ebp ;將 EBP 的內容先存到 stack 中
mov ebp, esp ;EBP = ESP
;subprogram 的實作內容置於此處
pop ebp ;還原 EBP 的值
ret
透過以上這一段程式的使用,ESP 與 EBP 的關係會變成如下圖:

之後若要取得參數的內容,不管 stack 中已經塞了多少 subprogram 的資料,只要用 [EBP+8] 就可以取得參數的內容囉!

以下用個範例來進行以上說明的總結:
%include "asm_io.inc"

segment .data
sum dd 0 ;定義記憶體空間,長度為 double word

segment .bss
input resd 1

;
; pseudo-code algorithm
; i = 1;
; sum = 0;
; while(get_int(i, &input), input != 0) {
; sum += input;
; i++;
; }
; print_sum(sum);
segment .text
global asm_main
asm_main:
enter 0, 0
pusha

mov edx, 1 ;將 EDX 視為變數 i
while_loop:
push edx
push dword input
call get_int
add esp, 8 ;將 i 與 &input 從 stack 中清除

;判斷輸入的值是否為 0
mov eax, [input]
cmp eax, 0
je end_while

add [sum], eax ;sum += input

inc edx ;i++
jmp while_loop ;繼續執行下一個迴圈

end_while:
push dword [sum] ;將 [sum] 存入 stack 中
call print_sum
pop ecx ;將 [sum] 從 stack 中清除

popa
leave
ret


;subprogram: get_int
;參數(根據順序放入 stack 中)
; 1、輸入的值(位於 [EBP + 12])
; 2、輸入資料所存放的位址(位於 [EBP + 8])
;[EBP] 儲存原本 EBP 中的內容
;[EBP + 4] 儲存 return address
;[EBP + 8] 之後開始儲存參數的相關內容,因為有兩個參數,因此 ESP 要存取第一個參數就必須指向 [EBP + 12]
;注意:
; 在 EAX 與 EBX 中的資料將會被清除
;
segment .data
prompt db ") Enter an integer number (0 to quit): ", 0

segment .text
get_int:
push ebp ;將 EBP 內容暫時存於 stack 中
mov ebp, esp

mov eax, [ebp + 12] ;上面的程式會將 EDX 的值塞到 [EBP + 12] 所儲存的 memory address 中
call print_int ;印出輸入第幾個數字

mov eax, prompt
call print_string

call read_int
mov ebx, [ebp + 8] ;上面的程式會將 input 所在的 memory address 塞到 [EBP + 8],因此 EBP 儲存的為 memory address 而不是整數
mov [ebx], eax ;以類似 pointer 方式將 EAX 的值存到 EBX 所儲存的 memory address 上

pop ebp ;回復 EBP 中的值
ret


;subprogram: print_sum
; 印出總和
;參數:
; (1)所要輸出的總和(位於 [EBP + 8])
;注意:
; 在 EAX 中的資料將會被清除
;
segment .data
result db "The sum is ", 0

segment .text
print_sum:
push ebp
mov ebp, esp

mov eax, result
call print_string

mov eax, [ebp + 8]
call print_int
call print_nl

pop ebp
ret

Local variables on the stack


在 subprogram 中,難免會使用到 local 變數,一般都是利用 stack 來處理 local 變數(例如:C 語言),local 變數儲存的地方在原本 EBP 儲存位置之後,以下用一張圖來說明:

(其中 sum 即為 local 變數)

若要取得第一個 local 變數,可透過 [ESP - 4](此時 ESP 指到 EBP 的地方,也就是 +4 的位置),而在 subprogram 中使用 local 變數的方法,可以使用類似以下的程式碼:
subprogram_label:
push ebp
mov ebp, esp
sub esp, LOCAL_BYTES ;在此處根據需求保留給 local 變數的空間
;subprogram 的實作內容置於此處
mov esp, ebp
pop ebp
ret
以下用一個較為完整的範例來說明:
; (C 語言程式碼)
; void calc_sum(int n, int *sump) {
; int i, sum = 0;
;
; for(i = 1 ; i <= n ; i++)
; sum += i;
;
; *sump = sum;
; } //end calc_sum
calc_sum:
push ebp
mov ebp, esp
sub esp, 4 ;保留儲存 local 變數的 memory address 的空間

mov dword [ebp - 4], 0 ;sum = 0;
mov ebx, 1 ;假設 EBX = i

for_loop:
cmp ebx, [ebp + 8] ;i <= n
jnle end_for ;jump if not(less & equal)

add [ebp - 4], ebx ;sum += i
inc ebx
jmp short for_loop

end_for:
mov ebx, [ebp + 12] ;ebx = sump
mov eax, [ebp - 4] ;eax = sum
mov [ebx], eax ;*sump = sum

mov esp, ebp
pop ebp
ret


Multi-Module Programs

所謂的 multi-module program 是由多個 object file 所組合而成的,而之前下一堆指令所產生的執行檔,就是屬於 multi-module program。

而當程式大一點時,總是希望可以將其進行模組化的設計,將龐大的程式拆開成為較小容易閱讀及 debug 的多個檔案,在 assembly 中,可以透過 extern 以及 global 兩個關鍵字。

external

以下先用一段範例程式說明:
extern  read_int, print_int, print_string
extern read_char, print_char, print_nl
extern sub_dump_regs, sub_dump_mem, sub_dump_math, sub_dump_stack
透過 extern 的宣告,用來告知 assembler 以上這些 subprogram 的定義位於外部的檔案中,不在這個檔案中。

不過在編譯的階段必須將包含定義這些 subprogram 的檔案一併加入編譯,否則會產生編譯時的錯誤。

global

global 在之前的每一支程式都會使用到,用意是讓其他的 module 可以直接呼叫使用 global 關鍵字宣告的 module。

使用範例說明

了解兩個關鍵字的使用方式後,接著以下介紹其使用方式:
; ==================== main4.asm ====================
%include "asm_io.inc"

segment .data
sum dd 0

segment .bss
input resd 1

segment .text
global asm_main ;宣告為 global,才可以由 C 程式中直接呼叫使用
extern get_int, print_sum ;宣告為 extern,表示這兩個 subprogram 的定義存在於其他檔案中

asm_main:
enter 0, 0
pusha

mov edx, 1
while_loop:
push edx
push dword input
call get_int
add esp, 8 ;清除兩個參數的內容

mov eax, [input]
cmp eax, 0
je end_while

add [sum], eax ;sum += [input]

inc edx
jmp short while_loop

end_while:
push dword [sum]
call print_sum ;印出在 stack 中的 sum
pop ecx

popa
leave
ret


; ==================== sub4.asm ====================
%include "asm_io.inc"

segment .data
prompt db ") Enter an integer number (0 to quit): ", 0

segment .text
global get_int, print_sum

get_int:
enter 0, 0

;saved EBP(EBP) + return address(EBP + 4) + &input(EBP + 8) + i(EBP + 12)
mov eax, [ebp + 12] ;第一個參數(i)
call print_int

mov eax, prompt
call print_string

call read_int
mov ebx, [ebp + 8] ;取得資料所要儲存的 memory address
mov [ebx], eax ;將資料存於指定的 memory address

leave
ret


segment .data
result db "The sum is ", 0

segment .text
print_sum:
enter 0, 0

mov eax, result
call print_string

;saved EBP(EBP) + return address(EBP + 4) + sum(EBP + 8)
mov eax, [ebp + 8]
call print_int
call print_nl

leave
ret
編譯與執行可用下列指令:
shell> nasm -f elf main4.asm ; nasm -f elf sub4.asm ; gcc -o main4 driver.c main4.o sub4.o asm_io.o; ./main4


Interfacing Assembly with C

之前有提過,由於目前的 compiler 越來越完善,因此編譯出來的 machine code 效能不見得會輸給直接寫 assembly,因此現在已經很少所有程式都使用 assembly 開發了,大多都是在 performance tunning 時,或是需要直接控制到硬體時,才會撰寫 assembly。

因此在使用時,通常都是 C 與 assembly 混合使用,不過有些事情是必須了解的,例如:
  1. C 語言中在 subroutine 中通常都會維護 EBX、ESI、EBP、CS、DS、SS、ES .... 等 register 的值,不會去更動他(但不代表不能更動,只是必須要回復,通常用 stack 來儲存這些 register 中的值)
  2. C 語言中 global 或 static 的 function 或變數名稱前都會加上底線(_),這跟 assembly 中加上底線是不同的意思
  3. 在參數的傳遞上,C call convention 規定 function 中的參數是以反向的順序塞入 stack 中;因此,第一個參數位於 [EBP + 8],第二個參數位於 [EBP + 12] (原本第一個參數位於 [EBP + 12],第二個參數位於 [EBP + 8])
  4. 回傳值的部分大多都是透過將值存於 register 的方式來達成,可能存於 EAX、EDX:EAX、或是 ST0 中
  5. 程式中可以指定不同的 call convention,一般的 compiler 都會支援多種 calling convention

以下用個範例說明 C 與 assembly 的搭配:
// ==================== main5.c ====================
#include <stdio.h>

void calc_sum(int, int *) __attribute__((cdecl));

int main(void) {
int n, sum;

printf("Sum integer up to: ");
scanf("%d", &n);
calc_sum(n, &sum);
printf("Sum is %dn", sum);

return 0;
} //end main


;==================== sub5.asm ====================
%include "asm_io.inc"

; subroutine: calc_sum
; 目的: 計算從 1~n 的加總
; 參數:(因為 C calling convention 的關係,因此參數以反向的順序存入 stack 中)
; (1)加總的上限(即為n) [EBP + 8]
; (2)指向儲存加總資料位址的 pointer [EBP + 12]
; pseudo C code:
; void calc_sum(int n, int *sump) {
; int i, sum = 0
; for(i = 1 ; i <= n ; i++)
; sum += i;
; *sump = sum;
; } //end calc_sum

segment .text
global calc_sum

;local variable:
; sum => [ebp - 4]
calc_sum:
enter 4, 0 ;在 stack 中預留空間給 sum 使用
push ebx

mov dword [ebp - 4], 0
dump_stack 1, 2, 4
mov ecx, 1

for_loop:
cmp ecx, [ebp + 8] ;i <= n ?
jnle end_for

add [ebp - 4], ecx
inc ecx
jmp short for_loop

end_for:
mov ebx, [ebp + 12] ;EBX = sump (儲存加總結果的 pointer)
mov eax, [ebp - 4] ;EAX = sum (加總結果)
mov [ebx], eax ;使用 [pointer] 指定該 memory address 儲存的資料為 sum

pop ebx
leave
ret

然而,從 C 可以呼叫 assembly code,當然也可以在 assembly 中使用 C library 囉! 這個部分下個章節會繼續討論囉!!

2008年2月15日 星期五

PC Assembly Language 學習筆記(6) - Bit Operations

Shift Operations

bit shift 分為三種,分別是 logical shift(用於 unsigned integer)、arithmetic shift(用於 signed integer) 以及 rotate shift

Logical Shift 所用的指令為 SHR(向右位移) 與 SHL(向左位移),位移的最後一個 bit 會存於 carry flag 中。

Arithmetic Shift 所用的指令為 SAL 與 SAR,專門用來處理 signed integer。

【註】Logical Shift 與 Arithmetic Shift 常用於乘二與除二的計算上,速度比 MUL 和 DIV 快上許多!

Rotate Shift 則是將所有 bit 的集合視為一個環狀佇列進行位移,所用的指令為 ROL、ROR、RCL(carry flag 也算佇列中一員)、RCR(carry flag 也算佇列中一員)。

三種 bit shift 的語法都相同,將要位移的資料搬到 register 後,使用類似下方的指令:
# 將 AX register 中的資料向右位移 1 的 bit
shr ax, 1


Boolean Bitwise Operations

最常用的 boolean operation 有 AND、OR、NOT、XOR 四種,在 assembly 中指令也是相同的名稱,使用方式就是將要處理的資料搬入 register 中,再跟另外一個數值或 register 進行 boolean operation。

以下舉個範例:
mov ax, 0x123H or ax, 0E831H
但有幾點必須要注意,NOT 使用後可取得該數值的 1 補數,且不會改變 EFLAGS register 中的值。

另外,AND、OR、XOR 的用途通常在於:
  • AND:關閉某幾個 bit
  • OR:打開某幾個 bit
  • XOR:取得指定 bit 的補數(特殊用法:同樣數值進行 XOR 結果為 0)


Avoiding Conditional Branches

一般在程式中會有許多條件判斷,而 CPU 中有特定的單元可以負責預測程式的走向(不一定每次會準確),若是太多條件判斷,CPU 誤判的機會就越高,執行效率也就越差。

因此,在 assembly 的撰寫中可以避免 conditional branch 的情況發生,可以用 SETxx 指令,以下用個範例來說明:
;檔案: max.asm
;描述:求兩數之間較大的數

%include "asm_io.inc"

segment .data
message1 db "Enter a number: ", 0
message2 db "Enter another number: ", 0
message3 db "The larger number is ", 0

segment .bss
input1 resd 1 ;第一個輸入的數字

segment .text
global asm_main
asm_main:
enter 0, 0
pusha

mov eax, message1 ;取得使用者第一個輸入的整數值
call print_string
call read_int
mov [input1], eax

mov eax, message2 ;取得使用者第二個輸入的整數值
call print_string
call read_int

;一連串為了取得兩數之間的較大值所必須要作的處理
xor ebx, ebx ;EBX=0
cmp eax, [input1] ;比較的結果會影響後續程式的進行
setg bl ;EBX = (input2 > input1) ? 1 : 0 (set 1 if greater)
neg ebx ;EBX = (input2 > input1) ? -1(0xFFFFFFFF) : 0
mov ecx, ebx ;ECX = (input2 > input1) ? -1(0xFFFFFFFF) : 0
and ecx, eax ;ECX = (input2 > input1) ? [intpu2] : 0
not ebx ;EBX = (input2 > input1) ? 0 : -1(0xFFFFFFFF)
and ebx, [input1] ;EBX = (input2 > input1) ? 0 : [input1]
or ecx, ebx ;ECX = (input2 > input1) ? input2 : input1

mov eax, message3
call print_string
mov eax, ecx
call print_int
call print_nl

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


Big and Little Endian Representations

假設要表示 double word - 0x12345678,兩種表示法分別如下:
  • Big Endian:0x 12 34 56 78
  • Little Endian:0x 78 56 34 12

通常會需要注意到 big/little endian 問題是在寫 TCP/IP 相關程式的時候,在 C 語言中,提供了 htonl() 與 ntohl() 兩個 function 作為 network byte order 與 host byte order 之間的轉換。

而在 486 的 CPU 中提供了兩個指令用來可以反轉 32-bit register 與 16-bit register 的 byte order 之用,以下為使用範例:
#用於 32-bit register
bswap edx

#用於 16-bit register(將 AH 與 AL 對調)
xchg ah, al