對於不折不扣的彙編新手來說,第一部分中出現的很多概念可能不是很明白,於是我決定寫更多有價值的文章。所以,讓我們開始《我的彙編學習之路》的第二部分的學習。
術語和概念
當我寫了第一篇之後,我從不同的讀者那獲得很多反饋,第一篇中有些部分不明白,這就是本文以及接下來幾篇從一些術語的描述開始的原因。
暫存器(Register):暫存器是處理器內小容量的儲存結構,處理器的主要功能是資料處理,處理器可以從記憶體中獲得資料,但這是一種低速的操作,這就是為什麼處理器為什麼要有自己資料儲存結構,稱為“暫存器”。
L小端(Little-endian):我們可以假設記憶體是一個大的陣列,它包含一個位元組一個位元組的數。每個地址儲存了記憶體“陣列”中的一個元素,每個元素一個位元組。舉例來說,我們有 4 位元組數:AA 56 AB FF
,在小端模式下最低位存放在低地址上:
1 2 3 4 |
0 FF 1 AB 2 56 3 AA |
這裡,0、1、2、3 是記憶體地址。
大端(Big-endian):大端儲存資料與小端相反。所有上面的位元組序在大端模式下是:
1 2 3 4 |
0 AA 1 56 2 AB 3 FF |
系統呼叫(Syscall):系統呼叫是使用者程式要求作業系統為其完成某些工作的一種方式。你可以在這裡找到系統呼叫表。
棧(Stack):處理器中暫存器的個數非常有限。所以棧是一塊連續的記憶體空間,可以通過特殊暫存器如 RSP、SS、RIP 等來定址。在接下來的文章我會專門深入介紹棧。
段(Section): 每個彙編程式都是由段來組成的,有以下的段:
- data —— 用來宣告初始化的資料或常量
- bss —— 用來宣告未初始化的變數
- text —— 用來存放程式碼
通用暫存器(General-purpose register): 有 16 個通用暫存器:rax、rbx、rcx、rdx、rbp、rsp、rsi、rdi、r8、r9、r10、r11、r12、r13、r14、r15。
當然這不是與組合語言有關的全部的術語和概念,如果在接下來的文章中遇到奇怪的不熟悉的詞彙,我們再來解釋這些詞的意思。
資料型別
基本的資料型別有:位元組(bytes)、字(words)、雙字(doublewords)、四字(duadwords)以及雙四字(double dualwords),它們的長度分別為:8位、2個位元組、4個位元組、8個位元組、16個位元組(128位)。
現在我們只使用整數,所以只看看它的表示。整型有兩種型別:無符號和有符號。無符號整型是一個位元組、字、雙字、四字表示的無符號二進位制數,它們能表示的範圍分別為:0~255、0~65,535、0~2^32-1、0~2^64-1。有符號整型是一個位元組、字、雙字、四字表示的有符號的二進位制數。符號位在負數的時候是置位的,在正數和0的時候是清零的。整數能表示的範圍是:1個位元組 -128~127,1個字 -32,768~32,767,1個雙字 -2^31~2^31-1,1個四字 -2^63~2^63-1。
段
正如我上面提到的,每個彙編程式都是由段來組成的,它包含資料段、程式碼段、bss 段。我們先來看看資料段,這是主要用來定義初始化的常量。例如:
1 2 3 4 |
section .data num1: equ 100 num2: equ 50 msg: db "Sum is correct", 10 |
好了,這兒差不多清楚了,三個常量名字分別為 num1、num2 和 msg,值分別是 100、50 和 “Sum is correct”,10 。但是 db 、equ 又是什麼呢?實際上,NASM 支援大量偽指令:
- DB、DW、DD、DQ、DT、DO、DY 和 DZ —— 用來定義初始化資料的。例如:
1 2 3 4 5 |
;; Initialize 4 bytes 1h, 2h, 3h, 4h db 0x01,0x02,0x03,0x04 ;; Initialize word to 0x12 0x34 dw 0x1234 |
- RESB、RESW、RESD、RESQ、REST、RESO、RESY、RESZ —— 用來定義非初始化變數
- INCBIN —— 包含外部二進位制檔案
- EQU —— 定義常量,例如:
1 2 |
;; now one is 1 one equ 1 |
- TIMES —— 重複指令或資料(下一篇文章中描述)
算術操作
下面是算術操作指令的簡單列表:
- ADD —— 整數加
- SUB —— 減
- MUL —— 無符號乘
- IMUL —— 有符號乘
- DIV —— 無符號除
- IDIV —— 有符號除
- INC —— 自增
- DEC —— 自減
- NEG —— 取反
本文會用到一些,其它的在接下來的文章有所覆蓋。
控制流
通常程式語言使用 if
、case
、goto
等等來改變程式的執行順序,當然彙編也可以。這裡我們提及到一些。有一個專門用來比較兩個數大小的 cmp
指令,它被用來接著條件判斷指令來決定是否跳轉。例如:
1 2 |
;; compare rax with 50 cmp rax, 50 |
cmp
指令僅僅比較兩個數,但是對它們的值沒有影響,也不會根據比較的結果執行任何東西。為了在比較之後執行操作,有條件跳轉指令,可以是下面的一個:
- JE —— 如果相等
- JZ —— 如果為零
- JNE —— 如果不相等
- JNZ —— 如果不為零
- JG —— 如果第一個運算元比第二個大
- JGE —— 如果第一個運算元比第二個大或者相等
- JA —— 與 JG 指令相同,只不過比較的是無符號數
- JAE —— 與 JGE 指令相同,只不過比較的是無符號數
例如如果我們想寫 C 語言中類似於 if/else 的語句:
1 2 3 4 5 |
if (rax != 50) { exit(); } else { right(); } |
在彙編中是這樣的:
1 2 3 4 5 |
;; compare rax with 50 cmp rax, 50 ;; perform .exit if rax is not equal 50 jne .exit jmp .right |
也有一種無條件跳轉的指令語法:
1 |
JMP LABEL |
例如:
1 2 3 4 5 6 7 8 9 10 |
_start: ;; .... ;; do something and jump to .exit label ;; .... jmp .exit .exit: mov rax, 60 mov rdi, 0 syscall |
這裡 _start
標籤後有一些程式碼,這些程式碼會被執行到,彙編最後控制轉向到 .exit
標籤處,該標籤後的程式碼開始執行。
通常無條件跳轉用在迴圈中,例如我們有 label
標籤,它後面有一些程式碼,程式碼執行完之後進行條件判斷,如果條件不成立將跳到該段程式碼的起始處。迴圈將在後面文章中介紹。
示例
我們看個簡單的例子:兩個數相加,得到它們的和,然後與預定義的一個數進行比較,如果相等輸出一些東西到螢幕上;如果不等退出。下面是例子的原始碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
;initialised data section section .data ; Define constants num1: equ 100 num2: equ 50 ; initialize message msg: db "Sum is correctn" section .text global _start ;; entry point _start: ; set num1's value to rax mov rax, num1 ; set num2's value to rbx mov rbx, num2 ; get sum of rax and rbx, and store it's value in rax add rax, rbx ; compare rax and 150 cmp rax, 150 ; go to .exit label if rax and 150 are not equal jne .exit ; go to .rightSum label if rax and 150 are equal jmp .rightSum ; Print message that sum is correct .rightSum: ;; write syscall mov rax, 1 ;; file descritor, standard output mov rdi, 1 ;; message address mov rsi, msg ;; length of message mov rdx, 15 ;; call write syscall syscall ; exit from program jmp .exit ; exit procedure .exit: ; exit syscall mov rax, 60 ; exit code mov rdi, 0 ; call exit syscall syscall |
我們過一下這段程式碼。首先在資料段定義了三個數:num1、num2 和值為 “Sum is correctn” 的 msg。現在看到第 14 行,這是程式的入口的地方。我們將 num1 和 num2 的值放到通用暫存器 rax 和 rbx 中,使用 add 指令相加,在 add 指令執行完之後,rax 和 rbx 相加之和儲存到 rax 中,即現在 num1 和 num2 的和存放在 rax 暫存器中。
好了,我們讓 num1 是 100,num2 是 50,之和是 150,用 cmp 指令比較。在比較完 rax 和 150 之後,檢查比較的結果,如果 rax 和 150 不等,我們跳轉到 .exit
處,如果相等,跳到 .rightSum
標籤處。
接著有兩個標籤:.exit
和 .rightSum
。首先將 rax 設定為 60,這是 exit 系統呼叫號,以及將 rdi 設為 0,這是退出碼。然後,.rightSUm
相當簡單,只是列印出 Sum is corretn
,如果你不能理解怎麼工作的,看看第一篇文章。
總結
這是 《我的彙編學習之路》 系列文章的第二篇,如果你有任何問題或建議,給我留言。