陣列是一個連續的記憶體空間,每個元素的大小、型態皆相同,在 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;而從上面這個表中,有三個部分是需要注意的:
- 實際資料存放的位置
LOASx 的部分,是存放在 data segment 中;STOSx 的部分則是存放在 extra segment 中 - 所使用到的 index register
LOASx 使用的是 ESI(source index);STOSx 則是使用 EDI(destination index) - 每次迴圈,index register 所遞增的值
根據不同的資料長度,分別為 1(byte)、2(word)、4(double word)
此外,還可以注意到所使用的 segment 為 data segment(左邊) 與 extra segment(右邊),以下用一個簡單範例來介紹:
而上述的效果,還可以用 MOVSx 指令來達成,而 MOVSx 指令的使用可以參考下表: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
上述的 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
memex.cglobal 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
#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
大哥 我在stackoverfow 看了1個禮拜 不如你給的10篇文章 我跪了 太感謝了
回覆刪除