Mã nguồn của một chương trình được ghi vào đĩa và thực thi bởi CPU dưới dạng nhị phân. Nói cách khác, mã nguồn chính là một dãy liên tục các bit 1 và bit 0. Để làm dãy số này trở nên dễ hiểu hơn, chúng ta gom nhóm 8 bit thành 1 byte và biểu diễn bằng một con số thập lục phân (hexadecimal). Như vậy, các chỉ thị được thực thi bởi CPU chẳng qua là một dãy các số hex. Trong dãy các số hex đó thì gồm có opcode (mã hoạt động) và operand (toán hạng). Operand có thể là các thanh ghi hoặc địa chỉ vùng nhớ dùng để thực thi mã lệnh.
Opcodes
Chính là các con số tương ứng với các chỉ thị được thực thi bởi CPU. Khi disassemble một chương trình thì disassembler sẽ dịch các opcode thành các chỉ thị hợp ngữ mà con người có thể đọc được. Ví dụ, chỉ thị di chuyển giá trị 0x5f
vào thanh ghi eax
có dạng như sau:
Khi nhìn vào disassebler, chúng ta sẽ thấy dạng hex của chỉ thị:
Trong đó:
- Giá trị
040000
là địa chỉ của chỉ thị. - Có hai toán hạng là:
b8
tương ứng với chỉ thịmov eax
.5f 00 00 00
là giá trị0x5f
. Do đây là little endian nên0x5f
được ghi xuống bộ nhớ là5f 00 00 00
.
Tương tự đối với các opcode khác: mỗi opcode sẽ tương ứng với một chỉ thị hợp ngữ.
Types of Operands
Có ba loại toán hạng trong hợp ngữ:
- Immediate operand: có thể xem như là các constant, chẳng hạn giá trị
0x5f
ở trên. - Register: có thể được dùng như là các toán hạng, chẳng hạn thanh ghi
eax
ở trên. - Memory operand: được ký hiệu bằng cặp ngoặc vuông để tham chiếu đến địa chỉ vùng nhớ. Ví dụ,
[memory_address]
được xem như là một toán hạng và giá trị của nó sẽ là giá trị tại vùng nhớmemory_address
.
General Instructions
Các chỉ thị sẽ cho biết CPU cần phải làm gì. Các chỉ thị thường sử dụng các toán hạng để tính toán kết quả rồi lưu vào các thanh ghi hoặc bộ nhớ.
The MOV Instruction
Chỉ thị mov
cho phép di chuyển giá trị (bản chất là sao chép). Cú pháp:
Một số ví dụ:
Gán giá trị 0x5f
cho thanh ghi eax
:
Sao chép giá trị trong thanh ghi eax
đến thanh ghi ebx
:
Sao chép giá trị ở vùng nhớ 0x5fc53e
đến thanh ghi eax
:
Nếu thanh ghi nằm bên trong cặp ngoặc vuông:
Thì có nghĩa là: giá trị lưu trong ebx
là một địa chỉ vùng nhớ và giá trị tại địa chỉ đó sẽ được sao chép qua cho thanh ghi eax
.
Chúng ta cũng có thể thực hiện các phép toán với giá trị vùng nhớ ở bên trong cặp ngoặc vuông:
Một ví dụ khác, câu lệnh sau sẽ sao chép giá trị của thanh ghi ebx
đến địa chỉ vùng nhớ được lưu ở trong thanh ghi eax
:
Câu lệnh trên sao chép giá trị tại vùng nhớ có địa chỉ ebp+4
(4 có đơn vị là byte) qua cho thanh ghi eax
.
Attention
Không thể di chuyển giá trị giữa hai vùng nhớ, chẳng hạn như:
The LEA Instruction
Chỉ thị lea
là viết tắt của “load effective address” và có cú pháp như sau:
Chỉ thị này sẽ sao chép địa chỉ vùng nhớ thay vì dữ liệu.
Ví dụ, trong câu lệnh bên dưới, địa chỉ vùng nhớ ebp+4
sẽ được sao chép đến eax
thay vì giá trị tại địa chỉ này:
Một ví dụ khác:
Giả sử ebx = 0x00000045
và ecx = 0x00000006
thì giá trị của eax
sẽ là 0x0000004B
ở dạng địa chỉ.
The NOP Instruction
Chỉ thị nop
là viết tắt của “no operation”. Chỉ thị này giúp trao đổi giá trị eax
cho chính nó và khiến cho chương trình chạy đến chỉ thị tiếp theo mà không làm thay đổi bất cứ thứ gì. Nó thường được dùng để tiêu thụ chu kỳ CPU trong khi chờ một thao tác hoặc các tiến trình khác.
Cú pháp:
Chỉ thị nop
thường được dùng bởi malware nhằm chuyển hướng luồng thực thi của chương trình vào shellcode của nó. Vị trí chính xác của luồng thực thi bị chuyển hướng thường không xác định được nên người tạo mã độc thường sử dụng nhiều chỉ thị nop
nhằm đảm bảo việc thực thi của shellcode không bắt đầu ở giữa.
Shift Instructions
Có hai loại chỉ thị dịch bit là dịch phải và dịch trái.
Cú pháp:
Với count
là số bit cần dịch chuyển.
Các bit mới được thêm vào sẽ là bit 0. Ví dụ, nếu ta có giá trị 00000010
trong thanh ghi eax
và ta dịch trái thì giá trị sẽ trở thành 00000100
.
Trong trường hợp có một bit 1 đi ra khỏi bên ngoài phạm vi một byte thì carry flag sẽ có giá trị là 1. Ví dụ, nếu ta dịch phải giá trị 00000101
trong thanh ghi eax
thì thanh ghi sẽ có giá trị là 0000010
và carry flag sẽ có giá trị là 1. Tương tự, nếu ta dịch trái giá trị 10000010
thì thanh ghi sẽ có giá trị là 00000100
và carry flag sẽ có giá trị là 1.
Các thao tác dịch bit dùng để thực hiện phép tính nhân và chia cho 2 hoặc lũy thừa 2 () với n
là số lần dịch trái.
Rotate Instructions
Tương tự với phép dịch bit. Tuy nhiên, các bit đi ra khỏi phạm vi một byte thì sẽ quay trở lại ở đầu bên kia của byte thay vì được lưu trong carry flag.
Cú pháp:
Ví dụ, khi ta xoay giá trị 10101010
sang phải 1 bit thì nó sẽ trở thành 01010101
.
Flags
Bảng các flag phổ biến có trong thanh ghi EFLAGS:
Flag | Abbreviation | Explanation |
---|---|---|
Carry | CF | Set when a carry-out or borrow is required from the most significant bit in an arithmetic operation. Also used for bit-wise shifting operations. |
Parity | PF | Set if the least significant byte of the result contains an even number of 1 bits. |
Auxiliary | AF | Set if a carry-out or borrow is required from bit 3 to bit 4 in an arithmetic operation (BCD arithmetic). |
Zero | ZF | Set if the result of the operation is zero. |
Sign | SF | Set if the result of the operation is negative (i.e., the most significant bit is 1). |
Overflow | OF | Set if there’s a signed arithmetic overflow (e.g., adding two positive numbers and getting a negative result or vice versa). |
Direction | DF | Determines the direction for string processing instructions. If DF=0, the string is processed forward; if DF=1, the string is processed backward. |
Interrupt Enable | IF | If set (1), it enables maskable hardware interrupts. If cleared (0), interrupts are disabled. |
Các flag thường được sử dụng trong các bước nhảy có điều kiện (conditional jumps). Ví dụ, chúng ta thường nhảy đến địa chỉ nào đó nếu có một flag cụ thể được set.
Arithmetic and Logical Instructions
Addition and Subtraction Instructions
Cú pháp của phép cộng:
Giá trị của value
sẽ được cộng vào destination
và kết quả sẽ được lưu ở trong destination
.
Phép trừ tương tự:
Giá trị của destination
sẽ được trừ cho value
và kết quả sẽ được lưu ở trong destination
.
Trong hai phép toán trên thì value
có thể là giá trị hằng số hoặc thanh ghi. Với phép trừ thì ZF sẽ được set nếu kết quả của phép trừ là 0 và CF sẽ được set nếu destination
nhỏ hơn value
.
Multiplication and Division Instructions
Phép nhân và chia sẽ sử dụng thanh ghi eax
và edx
.
Chỉ thị nhân có cú pháp như sau:
Chỉ thị trên sẽ nhân value
cho giá trị trong thanh ghi eax
và lưu kết quả ở trong edx:eax
như là một giá trị 64-bit. Lý do cần dùng hai thanh ghi là vì kết quả của phép nhân hai giá trị 32-bit thường có kết quả lớn hơn 32-bit. Phần 32-bit thấp được lưu ở trong thanh ghi eax
còn phần 32 bit cao được lưu ở trong thanh ghi edx
.
Giá trị của value
có thể là thanh ghi hoặc hằng số.
Với phép chia thì ta dùng chỉ thị như sau:
Trong trường hợp phép chia thì ngược lại: nó sẽ chia giá trị nằm trong thanh ghi edx:eax
cho value
rồi lưu phần thương số ở trong eax
và phần số dư ở trong edx
.
Increment and Decrement Instructions
Các chỉ thị này giúp tăng giá trị của thanh ghi lên 1 giá trị hoặc giảm giá trị của thanh ghi xuống 1 giá trị. Cú pháp:
AND Instruction
Chỉ thị and
giúp thực hiện phép toán AND trên các bit. Ví dụ:
Trong ví dụ trên:
- Giá trị
0x7c
quy đổi thành01111100
ở dạng nhị phân. - Giả sử địa chỉ
al
đang lưu giá trị0xfc
(tương ứng với11111100
).
Trong trường hợp này, kết quả của chỉ thị trên sẽ là 01111100
. Kết quả này sẽ được lưu vào địa chỉ al
.
OR Instruction
Thực hiện phép toán OR trên các bit. Ví dụ:
Giả sử địa chỉ al
lưu giá trị 0xfc
thì kết quả của chỉ thị trên sẽ là 11111100
.
NOT Instruction
Nó sẽ lật tất cả các bit. Ví dụ:
Giả sử al
lưu giá trị 11110000
thì kết quả sẽ là 00001111
.
XOR Instruction
Thực hiện phép toán XOR trên các bit. Cú pháp:
Nếu al
có giá trị là 0xfc
(11111100
) thì kết quả của ví dụ trên sẽ là 10000000
. Đặc biệt, nếu giá trị của al
cũng là 0x7c
(01111100
) thì kết quả sẽ là 0x00
. Do đó, chỉ thị xor
thường được dùng để clear giá trị của thanh ghi vì nó tối ưu hơn chỉ thị mov
.
Conditionals
The TEST Instruction
Chỉ thị test
thực hiện phép toán AND trên bit và set giá trị của zero flag và parity flag là 1 nếu kết quả là 0 (không lưu kết quả vào destination như chỉ thị and
).
Chúng ta thường kiểm tra một toán hạng có là NULL hay không bằng cách sử dụng chỉ thị test
cho chính nó. Lý do là vì việc dùng chỉ thị test
là vì nó tiết kiệm byte hơn việc so sánh với số 0.
The CMP Instruction
Chỉ thị cmp
so sánh hai toán hạng và set zero flag hoặc carry flag. Cú pháp:
Chỉ thị này hoạt động tương tự như chỉ thị sub
. Điểm khác biệt duy nhất là các toán hạng không bị thay đổi giá trị. Có ba trường hợp:
- Zero flag được set là 1 nếu hai toán hạng bằng nhau (parity flag cũng bằng 1 do kết quả là 0).
- Nếu
source
lớn hơndestination
thì carry flag và sign flag sẽ được set. - Trong trường hợp
destination
lớn hơnsource
thì zero flag và carry flag đều có giá trị là 0.
Branching
Khi không có các câu lệnh rẽ nhánh thì instruction pointer sẽ thực thi chỉ thị này đến chỉ thị khác tùy theo thứ tự chúng được đặt vào bộ nhớ.
Các câu lệnh rẽ nhánh sẽ thay đổi giá trị của instruction pointer và làm thay đổi luồng thực thi của chương trình.
The JMP Instruction
Chỉ thị jmp
khiến cho luồng điều khiển nhảy đến một vị trí cụ thể. Cú pháp:
Về bản chất, câu lệnh trên sẽ chuyển giá trị của location
vào instruction pointer.
Conditional Jumps
Trong hợp ngữ không có câu lệnh if
mà chỉ có các conditional jump. Một vài các conditional jump phổ biến:
Instruction | Explanation |
---|---|
jz | Jump if the ZF is set (ZF=1). |
jnz | Jump if the ZF is not set (ZF=0). |
je | Jump if equal. |
jne | Jump if not equal. |
jg | Jump if the destination is greater than the source operand. Performs signed comparison and is often used after a CMP instruction. |
jl | Jump if the destination is lesser than the source operand. Performs signed comparison and is often used after a CMP instruction. |
jge | Jump if greater than or equal to. Jumps if the destination operand is greater than or equal to the source operand. Similar to the above instructions. |
jle | Jump if lesser than or equal to. Jumps if the destination operand is lesser than or equal to the source operand. Similar to the above instructions. |
ja | Jump if above. Similar to jg, but performs an unsigned comparison. |
jb | Jump if below. Similar to jl, but performs an unsigned comparison. |
jae | Jump if above or equal to. Similar to the above instructions. |
jbe | Jump if below or equal to. Similar to the above instructions. |
Important
Trừ hai chỉ thị đầu (
jz
vàjnz
) thì các chỉ thị còn lại đều được sử dụng sau một chỉ thịcmp
.
Stack and Function Calls
The PUSH Instruction
Chỉ thị push
có cú pháp như sau:
Câu lệnh trên sẽ đẩy source
vào stack. Ví dụ, câu lệnh sau sẽ đẩy giá trị của thanh ghi eax
vào stack:
Về bản chất, địa chỉ vùng nhớ của giá trị được đẩy vào stack sẽ được stack pointer trỏ đến.
Các chỉ thị sau sẽ đẩy các general purpose register vào stack:
pusha
(push all words): đẩy tất cả các 16-bit general purpose register (AX, BX, CX, DX, SI, DI, SP, BP) vào stack.pushad
(push all double words): đẩy tất cả các 32-bit general purpose register (EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP) vào stack.
Info
Khi gặp các chỉ thị đẩy các general purpose register thì ta biết rằng có ai đó đang chèn các chỉ thị hợp ngữ một cách thủ công nhằm lưu lại giá trị của các thanh ghi.
The POP Instruction
Chỉ thị pop
sẽ lấy ra giá trị ở đầu stack rồi lưu vào destination
. Cú pháp:
Sau đó, stack pointer sẽ được thay đổi để trỏ đến đầu stack.
Các chỉ thị sau sẽ lấy ra tất cả các general purpose register từ stack:
popa
(pop all words): lấy ra tất cả các 16-bit general purpose register.popad
(pop all double words): lấy ra tất cả các 32-bit general purpose register.
Sau khi hai chỉ thị trên được thực thi thì thanh ghi SP hoặc ESP sẽ được điều chỉnh để trỏ đến vị trí đầu stack mới.
The CALL Instruction
Chỉ thị call
dùng để gọi hàm trong hợp ngữ có cú pháp như sau:
Các đối số sẽ được đặt vào stack hoặc các thanh ghi tùy vào quy ước gọi hàm.