# Important x86/x64 Assembly

## 1. Các phương pháp biểu diễn số có dấu

Phương pháp "bù hai" là pp phổ biến nhất trong máy tính:

<figure><img src="/files/lXRRT93JAhKYGLBiZIjw" alt=""><figcaption></figcaption></figure>

Để đơn giản, đây là những điều cần biết:

* Các số có thể có dấu hoặc không có dấu
* Các kiểu số có dấu trong C/C++:
  * `int64_t` (0x8000000000000000..0x7FFFFFFFFFFFFFFF)
  * `int` (0x80000000..0x7FFFFFFF)
  * `char` (0x80..0x7F)
  * `ssize_t`
* Không dấu:
  * `uint64_t` (0..0xFFFFFFFFFFFFFFFF)
  * `unsigned int` (0..0xFFFFFFFF)
  * `unsigned char` (0..0xFF)
  * `size_t`
* Các kiểu có dấu có dấu ở bit quan trọng nhất: **1** có nghĩa là "**âm"**, **0** có nghĩa là "**dương".**
* Việc chuyển đổi sang các kiểu dữ liệu lớn hơn rất đơn giản.
* Phép lấy số đối rất đơn giản: chỉ cần đảo ngược tất cả các bit và cộng thêm 1.

## 2. XOR

<figure><img src="/files/YaFuRaDB8Y7ihsc446a5" alt=""><figcaption></figcaption></figure>

XOR rất hữu ích ở đây vì: `cipher_text = plain_text ⊕ key` và sau đó: `(plain_text ⊕ key) ⊕ key = plain_text`.

## 3. Endianness (thứ tự byte)

Endianness là một cách biểu diễn các giá trị trong bộ nhớ.

**Big-endian (Thứ tự byte lớn)**

Giá trị 0x12345678 được biểu diễn trong bộ nhớ như sau:

<figure><img src="/files/CtQcIRtkNHEIeBw4q7tv" alt=""><figcaption><p>Các CPU Big-endian bao gồm Motorola 68k, IBM POWER</p></figcaption></figure>

**Little-endian (Thứ tự byte nhỏ)**

Giá trị 0x12345678 được biểu diễn trong bộ nhớ như sau:

<figure><img src="/files/Ncse0pUgEAw8UbwQRW9X" alt=""><figcaption><p>Các CPU Little-endian bao gồm Intel x86</p></figcaption></figure>

## 4. Memory

Có 3 loại bộ nhớ chính:

* **Bộ nhớ toàn cục (Global memory)** hay còn gọi là "cấp phát bộ nhớ tĩnh". Không cần cấp phát một cách rõ ràng, việc cấp phát được thực hiện đơn giản bằng cách khai báo các biến/mảng trên toàn cục. Đây là các biến toàn cục, nằm trong phân đoạn dữ liệu (data segment) hoặc hằng số (constant segment). Chúng có sẵn trên toàn cục (do đó, được coi là một anti-pattern - mô hình chống thiết kế). Không thuận tiện cho các bộ đệm/mảng, vì chúng phải có kích thước cố định. Lỗi tràn bộ đệm (buffer overflow) xảy ra ở đây thường ghi đè lên các biến hoặc bộ đệm nằm cạnh chúng trong bộ nhớ.
* **Ngăn xếp (Stack)** hay còn gọi là "cấp phát trên ngăn xếp". Việc cấp phát được thực hiện đơn giản bằng cách khai báo các biến/mảng cục bộ trong hàm. Đây thường là các biến cục bộ cho hàm. Đôi khi, các biến cục bộ này cũng có sẵn cho các hàm con (các hàm được gọi, nếu hàm gọi truyền một con trỏ đến một biến cho hàm được gọi để thực thi). Việc cấp phát và giải phóng rất nhanh, chỉ cần SP (Stack Pointer - con trỏ ngăn xếp) được dịch chuyển. Nhưng chúng cũng không thuận tiện cho các bộ đệm/mảng, vì kích thước bộ đệm phải cố định, trừ khi sử dụng `alloca()` (hoặc một mảng có độ dài thay đổi).
* **Heap (Vùng nhớ động)** hay còn gọi là "cấp phát bộ nhớ động". Việc cấp phát/giải phóng được thực hiện bằng cách gọi `malloc()/free()` hoặc `new/delete` trong C++. Đây là phương pháp thuận tiện nhất: kích thước khối có thể được đặt tại thời gian chạy. Có thể thay đổi kích thước (sử dụng `realloc()`), nhưng có thể chậm. Đây là cách cấp phát bộ nhớ chậm nhất: bộ cấp phát bộ nhớ phải hỗ trợ và cập nhật tất cả các cấu trúc điều khiển trong khi cấp phát và giải phóng. Lỗi tràn bộ đệm thường ghi đè lên các cấu trúc này. Việc cấp phát heap cũng là nguồn gốc của các vấn đề rò rỉ bộ nhớ: mỗi khối bộ nhớ phải được giải phóng một cách rõ ràng, nhưng người ta có thể quên nó hoặc thực hiện không chính xác. Một vấn đề khác là "sử dụng sau khi giải phóng" - sử dụng một khối bộ nhớ sau khi `free()` đã được gọi trên nó, điều này rất nguy hiểm.&#x20;

## 5. Các lệnh được sử dụng thường xuyên nhất

* **ADC (add with carry - cộng với cờ nhớ)**: Cộng các giá trị, tăng kết quả nếu cờ CF được thiết lập. ADC thường được sử dụng để cộng các giá trị lớn, ví dụ, để cộng hai giá trị 64-bit trong môi trường 32-bit bằng cách sử dụng hai lệnh ADD và ADC. Ví dụ:

  ```assembly
  ; Làm việc với các giá trị 64-bit: cộng val1 vào val2.
  ; .lo có nghĩa là 32 bit thấp nhất, .hi có nghĩa là cao nhất.
  ADD val1.lo, val2.lo
  ADC val1.hi, val2.hi ; sử dụng CF được thiết lập hoặc xóa tại lệnh trước đó
  ```
* **ADD**: Cộng hai giá trị.
* **AND**: "and" logic.
* **CALL**: Gọi một hàm khác: `PUSH address_after_CALL_instruction; JMP label`.
* **CMP**: So sánh các giá trị và thiết lập các cờ, giống như SUB nhưng không ghi kết quả.
* **DEC**: Giảm. Không giống như các lệnh số học khác, INC không sửa đổi cờ CF.
* **IMUL**: Nhân có dấu. IMUL thường được sử dụng thay cho MUL, đọc thêm về nó: 31.1.
* **INC**: Tăng. Không giống như các lệnh số học khác, INC không sửa đổi cờ CF.
* **JCXZ, JECXZ, JRCXZ (M)**: Nhảy nếu CX/ECX/RCX=0.
* **JMP**: Nhảy đến một địa chỉ khác. Mã lệnh có một offset nhảy.
* **Jcc (where cc—condition code - trong đó cc—mã điều kiện)**:

  * Rất nhiều lệnh trong số này có từ đồng nghĩa (được biểu thị bằng AKA), điều này được thực hiện để thuận tiện. Các lệnh đồng nghĩa được dịch thành cùng một mã lệnh. Mã lệnh có một offset nhảy.
  * **JAE AKA JNC**: Nhảy nếu lớn hơn hoặc bằng (không dấu): CF=0.
  * **JA AKA JNBE**: Nhảy nếu lớn hơn (không dấu): CF=0 và ZF=0.
  * **JBE**: Nhảy nếu bé hơn hoặc bằng (không dấu): CF=1 hoặc ZF=1.
  * **JB AKA JC**: Nhảy nếu bé hơn (không dấu): CF=1.
  * **JC AKA JB**: Nhảy nếu CF=1.
  * **JE AKA JZ**: Nhảy nếu bằng hoặc không: ZF=1.
  * **JGE**: Nhảy nếu lớn hơn hoặc bằng (có dấu): SF=OF.
  * **JG**: Nhảy nếu lớn hơn (có dấu): ZF=0 và SF=OF.
  * **JLE**: Nhảy nếu bé hơn hoặc bằng (có dấu): ZF=1 hoặc SF≠OF.
  * **JL**: Nhảy nếu bé hơn (có dấu): SF≠OF.
  * **JNAE AKA JC**: Nhảy nếu không lớn hơn hoặc bằng (không dấu): CF=1.
  * **JNA**: Nhảy nếu không lớn hơn (không dấu): CF=1 và ZF=1.
  * **JNBE**: Nhảy nếu không bé hơn hoặc bằng (không dấu): CF=0 và ZF=0.
  * **JNB AKA JNC**: Nhảy nếu không bé hơn (không dấu): CF=0.
  * **JNC AKA JAE**: Nhảy nếu CF=0, đồng nghĩa với JNB.
  * **JNE AKA JNZ**: Nhảy nếu không bằng hoặc không không: ZF=0.
  * **JNGE**: Nhảy nếu không lớn hơn hoặc bằng (có dấu): SF≠OF.
  * **JNG**: Nhảy nếu không lớn hơn (có dấu): ZF=1 hoặc SF≠OF
  * **JNLE**: Nhảy nếu không bé hơn hoặc bằng (có dấu): ZF=0 và SF=OF
  * **JNL**: Nhảy nếu không bé hơn (có dấu): SF=OF
  * **JNO**: Nhảy nếu không tràn: OF=0
  * **JNS**: Nhảy nếu cờ SF bị xóa
  * **JNZ AKA JNE**: Nhảy nếu không bằng hoặc không không: ZF=0
  * **JO**: Nhảy nếu tràn: OF=1
  * **JPO**: Nhảy nếu cờ PF bị xóa (Nhảy tính chẵn lẻ lẻ)
  * **JP AKA JPE**: Nhảy nếu cờ PF được thiết lập
  * **JS**: Nhảy nếu cờ SF được thiết lập
  * **JZ AKA JE**: Nhảy nếu bằng hoặc không: ZF=1
  * **LAHF**: Sao chép một số bit cờ vào AH:
  *

  ```
  <figure><img src="/files/WuAwIGGD7LF5VlJKyi0V" alt=""><figcaption></figcaption></figure>
  ```
* **LEAVE**: Tương đương với cặp lệnh `MOV ESP, EBP` và `POP EBP`—nói cách khác, lệnh này đặt con trỏ ngăn xếp (ESP) trở lại và khôi phục thanh ghi EBP về trạng thái ban đầu.
* **LEA (Load Effective Address - Tải Địa chỉ Hiệu dụng)**: Hình thành một địa chỉ.

## 6. Bit and Byte

Kích thước của các kiểu dữ liệu khác nhau tùy thuộc vào kiến trúc hệ thống. Dưới đây là những kích thước phổ biến nhất mà bạn sẽ gặp khi làm việc với Windows và Linux trên máy tính để bàn.

* **Bit:** Một chữ số **nhị phân**. Có thể là 0 hoặc 1.
* **Nibble:** 4 **bit**.
* **Byte:** 8 **bit**.
* **Word:** 2 **byte**.
* **Double Word (DWORD):** 4 **byte**. Gấp đôi kích thước của một Word.
* **Quad Word (QWORD):** 8 **byte**. Gấp bốn lần kích thước của một Word.

Trước khi đi sâu vào các kiểu dữ liệu khác, hãy nói về "có dấu" (signed) và "không dấu" (unsigned). Số có dấu có thể là dương hoặc âm. Số không dấu chỉ có thể là dương. Tên gọi xuất phát từ cách chúng hoạt động. Số có dấu cần một **bit dấu** để phân biệt chúng có âm hay không, tương tự như cách chúng ta sử dụng dấu + và -.

### **Kích thước các kiểu dữ liệu:**

* **Char:** 1 **byte** (8 **bit**).
* **Int:** Có số nguyên 16-bit, 32-bit và 64-bit. Khi nói về số nguyên, thường là 32-bit. Đối với số nguyên có dấu, một **bit** được sử dụng để chỉ định xem số nguyên đó là dương hay âm.
  * **Signed Int:**
    * 16 bit: từ -32,768 đến 32,767.
    * 32 bit: từ -2,147,483,648 đến 2,147,483,647.
    * 64 bit: từ -9,223,372,036,854,775,808 đến 9,223,372,036,854,775,807.
  * **Unsigned Int:** Giá trị nhỏ nhất là 0, giá trị lớn nhất gấp đôi giá trị lớn nhất của một số nguyên có dấu (cùng kích thước). Ví dụ: số nguyên 32-bit không dấu đi từ 0 đến 4,294,967,295. Đó là gấp đôi giá trị lớn nhất của số nguyên có dấu 2,147,483,647, tuy nhiên, giá trị nhỏ nhất của nó là 0. Điều này là do số nguyên có dấu sử dụng **bit dấu**, làm cho nó không thể đại diện cho một giá trị.
* **Bool:** 1 **byte**. Điều thú vị là, một biến **bool** chỉ cần 1 **bit** vì nó là 1 hoặc 0 nhưng nó vẫn chiếm toàn bộ **byte**. Điều này là do máy tính không có xu hướng làm việc với các **bit** riêng lẻ do căn chỉnh (sẽ nói đến sau). Thay vào đó, chúng làm việc theo các khối như 1 **byte**, 2 **byte**, 4 **byte**, 8 **byte**, v.v.

### **Offset (Độ lệch)**

Vị trí dữ liệu được tham chiếu bằng khoảng cách của chúng từ địa chỉ của **byte** dữ liệu đầu tiên, được gọi là **BaseAddress (địa chỉ cơ sở)** (hoặc chỉ là địa chỉ) của biến. Khoảng cách của một phần dữ liệu từ **địa chỉ cơ sở** của nó được coi là **độ lệch (offset)**. Ví dụ: giả sử chúng ta có một số dữ liệu, 12345678. Để nhấn mạnh, chúng ta cũng giả sử mỗi số là 2 **byte**. Với thông tin này, 1 nằm ở **Offset** 0x0, 2 nằm ở **Offset** 0x2, 3 nằm ở **Offset** 0x4, 4 nằm ở **Offset** 0x6, v.v. Bạn có thể tham chiếu các giá trị này với định dạng `BaseAddress+0x##`. `BaseAddress+0x0` hoặc chỉ `BaseAddress` sẽ chứa 1, `BaseAddress+0x2` sẽ là 2, v.v.

## 7. Registers (Thanh ghi)

Có 8 **thanh ghi đa năng** chính:

Có một số **GPR**, mỗi **thanh ghi** có một nhiệm vụ được gán. Tuy nhiên, nhiệm vụ này giống như một mẫu hơn vì các **thanh ghi** thường được sử dụng cho bất cứ điều gì, ngoại trừ một số ít. Bất kể, việc biết mục đích được gán của chúng là tốt cho khi chúng được sử dụng theo chỉ định của chúng.

* **`RAX`** - Được gọi là **thanh ghi tích lũy (accumulator register)**. Thường được sử dụng để lưu trữ giá trị trả về của một hàm.
* **`RBX`** - Đôi khi được gọi là **thanh ghi cơ sở (base register)**, không nên nhầm lẫn với **con trỏ cơ sở (base pointer)**. Đôi khi được sử dụng làm **con trỏ cơ sở** để truy cập bộ nhớ.
* **`RDX`** - Đôi khi được gọi là **thanh ghi dữ liệu (data register)**.
* **`RCX`** - Đôi khi được gọi là **thanh ghi đếm (counter register)**. Được sử dụng làm bộ đếm vòng lặp.
* **`RSI`** - Được gọi là **chỉ số nguồn (source index)**. Được sử dụng làm **con trỏ nguồn** trong các thao tác chuỗi.
* **`RDI`** - Được gọi là **chỉ số đích (destination index)**. Được sử dụng làm **con trỏ đích** trong các thao tác chuỗi.
* **`RSP`** - **Con trỏ ngăn xếp (stack pointer)**. Giữ địa chỉ đỉnh ngăn xếp.
* **`RBP`** - **Con trỏ cơ sở (base pointer)**. Giữ địa chỉ cơ sở (đáy) ngăn xếp

### **Con trỏ lệnh (Instruction Pointer)**

RIP có lẽ là **thanh ghi** quan trọng nhất. RIP là "**Con trỏ lệnh**". Đó là địa chỉ của dòng mã tiếp theo sẽ được thực thi. Bạn không thể trực tiếp ghi vào **thanh ghi** này, chỉ một số lệnh nhất định như ret mới có thể ảnh hưởng đến **con trỏ lệnh**.

### **Phân tích các thanh ghi**

Mỗi **thanh ghi** có thể được chia thành các phân đoạn nhỏ hơn có thể được tham chiếu bằng tên **thanh ghi** khác. RAX là 64 bit, 32 bit thấp hơn có thể được tham chiếu bằng EAX và 16 bit thấp hơn có thể được tham chiếu bằng AX. AX được chia thành hai phần 8 bit. 8 bit cao/trên của AX có thể được tham chiếu bằng AH. 8 bit thấp hơn có thể được tham chiếu bằng AL.

<figure><img src="/files/8PxdqHWBlaMH2zi1CruG" alt=""><figcaption></figcaption></figure>

RAX bao gồm tất cả 8 **byte**, sẽ là **byte** 0-7. EAX bao gồm các **byte** 4-7, AX bao gồm các **byte** 6-7, AH chỉ bao gồm **byte** 6 và AL chỉ bao gồm **byte** 7 (**byte** cuối cùng).

### **Các kiểu dữ liệu khác nhau**

* Giá trị dấu phẩy động (Floating Point Values) - Floats và Doubles.
* Giá trị số nguyên (Integer Values) - Số nguyên, Booleans, Chars, Con trỏ, v.v.

Các kiểu dữ liệu khác nhau không thể được đặt trong bất kỳ **thanh ghi** nào. Các giá trị dấu phẩy động được biểu diễn khác với số nguyên. Vì điều này, các giá trị dấu phẩy động có các **thanh ghi** đặc biệt. Các **thanh ghi** này bao gồm YMM0 đến YMM15 (64-bit) và XMM0 đến XMM15 (32-bit). Các **thanh ghi** XMM là nửa dưới của các **thanh ghi** YMM, tương tự như EAX là 32 bit thấp hơn của RAX. Một điều độc đáo về các **thanh ghi** này là chúng có thể được coi là mảng. Nói cách khác, chúng có thể chứa nhiều giá trị. Ví dụ: các **thanh ghi** YMM# rộng 256-bit mỗi cái và có thể chứa 4 giá trị 64-bit hoặc 8 giá trị 32-bit. Tương tự, các **thanh ghi** XMM# rộng 128-bit và có thể chứa 2 giá trị 64-bit hoặc 4 giá trị 32-bit. Cần có các lệnh đặc biệt để sử dụng các **thanh ghi** này làm vectơ.

### **Các thanh ghi bổ sung**

Có các **thanh ghi** bổ sung cần được đề cập. Các **thanh ghi** này không có bất kỳ mục đích sử dụng đặc biệt nào. Có các **thanh ghi** r8 đến r15 được thiết kế để sử dụng bởi các giá trị kiểu số nguyên (không phải số floats hoặc doubles). 4 **byte** (32 bit), 2 **byte** (16 bit) và 8 **bit** (1 **byte**) thấp hơn đều có thể được truy cập. Chúng có thể được truy cập bằng cách thêm chữ cái "d", "w" hoặc "b". Ví dụ:

* R8 - **Thanh ghi** 64-bit (8 **byte**) đầy đủ.
* R8D - Double word (4 **byte**) thấp hơn.
* R8W - Word (2 **byte**) thấp hơn
* R8B - **Byte** thấp hơn.

## **8. Các lệnh (Instructions)**

Khả năng đọc và hiểu **mã Assembly** là rất quan trọng đối với **kỹ thuật đảo ngược**. Có khoảng 1.500 lệnh, tuy nhiên, phần lớn các lệnh không được sử dụng phổ biến hoặc chúng chỉ là các biến thể (chẳng hạn như MOV và MOVS). Giống như trong lập trình cấp cao, đừng ngần ngại tra cứu một thứ gì đó bạn không biết.

Trước khi chúng ta bắt đầu, có ba thuật ngữ khác nhau mà bạn nên biết: immediate, register và memory.

* Giá trị immediate (hoặc chỉ immediate, đôi khi là IM) là một cái gì đó như số 12. Giá trị immediate không phải là địa chỉ bộ nhớ hoặc **thanh ghi**, thay vào đó, nó là một số loại dữ liệu hằng.
* **Thanh ghi** là đề cập đến một cái gì đó như RAX, RBX, R12, AL, v.v.
* Memory (bộ nhớ) hoặc địa chỉ bộ nhớ là đề cập đến một vị trí trong bộ nhớ (một địa chỉ bộ nhớ) chẳng hạn như 0x7FFF842B.

Điều quan trọng là phải biết định dạng của các lệnh, như sau:

**(Instruction/Opcode/Mnemonic) \<Toán hạng đích>, \<Toán hạng nguồn>**

Tôi sẽ đề cập đến Instruction/Opcode/Mnemonic là lệnh, chỉ cần lưu ý rằng một số người gọi nó bằng những thứ khác nhau.

```nasm
mov RAX, 5
```

MOV là lệnh, RAX là toán hạng đích và 5 là toán hạng nguồn. Việc viết hoa các lệnh hoặc toán hạng không quan trọng. Bạn sẽ thấy tôi sử dụng hỗn hợp tất cả các chữ cái viết hoa và tất cả các chữ cái viết thường. Trong ví dụ đã cho, 5 là giá trị immediate vì nó không phải là địa chỉ bộ nhớ hợp lệ và chắc chắn không phải là một **thanh ghi**.

### **Các lệnh thông dụng**

**Di chuyển dữ liệu (Data Movement)**

* <mark style="color:red;">`MOV`</mark> được sử dụng để di chuyển/lưu trữ toán hạng nguồn vào đích. Nguồn không nhất thiết phải là giá trị immediate như trong ví dụ sau. Trong ví dụ sau, giá trị immediate 5 đang được di chuyển vào RAX.

Điều này tương đương với RAX = 5.

```nasm
MOV RAX, 5
```

* <mark style="color:red;">`LEA`</mark> là viết tắt của Load Effective Address. Về cơ bản, điều này giống như MOV ngoại trừ địa chỉ. Sự khác biệt chính giữa MOV và LEA là LEA không giải tham chiếu. Nó cũng thường được sử dụng để tính toán địa chỉ. Trong ví dụ sau, RAX sẽ chứa địa chỉ bộ nhớ/vị trí của num1.

```nasm
lea RAX, num1
```

```nasm
lea RAX, [struct+8]
```

```nasm
mov RBX, 5
lea RAX, [RBX+1]
```

Trong ví dụ đầu tiên, RAX được đặt thành địa chỉ của num1.&#x20;

Trong ví dụ thứ hai, RAX được đặt thành địa chỉ của thành viên trong một cấu trúc cách 8 **byte** từ đầu cấu trúc. Đây thường sẽ là thành viên thứ hai.&#x20;

Ví dụ thứ ba `RBX` được đặt thành 5, sau đó `LEA` được sử dụng để đặt `RAX` thành `RBX + 1`. RAX sẽ là 6.

* <mark style="color:red;">`PUSH`</mark> được sử dụng để đẩy dữ liệu lên ngăn xếp. Đẩy đề cập đến việc đặt một thứ gì đó lên trên cùng của ngăn xếp. Trong ví dụ sau, RAX được đẩy lên ngăn xếp. Đẩy sẽ hoạt động như một bản sao nên RAX vẫn sẽ chứa giá trị mà nó có trước khi nó được đẩy. Đẩy thường được sử dụng để lưu dữ liệu bên trong một **thanh ghi** bằng cách đẩy nó lên ngăn xếp, sau đó khôi phục nó sau này bằng `pop`.

```nasm
push RAX
```

* <mark style="color:red;">`POP`</mark> được sử dụng để lấy bất cứ thứ gì trên đỉnh ngăn xếp và lưu trữ nó vào đích. Trong ví dụ sau, bất cứ thứ gì trên đỉnh ngăn xếp sẽ được đưa vào RAX.

```nasm
pop RAX
```

**Số học (Arithmetic):**

* <mark style="color:red;">`INC`</mark> sẽ tăng dữ liệu thêm một. Trong ví dụ sau RAX được đặt thành 8, sau đó được tăng lên. RAX sẽ là 9 vào cuối.

```nasm
mov RAX, 8
inc RAX
```

* <mark style="color:red;">`DEC`</mark> giảm một giá trị. Trong ví dụ sau, RAX kết thúc với giá trị là 7.

```nasm
mov RAX, 8
dec RAX
```

* <mark style="color:red;">`ADD`</mark> thêm một nguồn vào một đích và lưu trữ kết quả trong đích. Trong ví dụ sau, 2 được di chuyển vào RAX, 3 vào RBX, sau đó chúng được thêm vào nhau. Kết quả (5) sau đó được lưu trữ trong RAX.

Tương tự như RAX = RAX + RBX hoặc RAX += RBX.

```nasm
mov RAX, 2
mov RBX, 3
add RAX, RBX
```

* <mark style="color:red;">`SUB`</mark> trừ một nguồn từ một đích và lưu trữ kết quả trong đích. Trong ví dụ sau, RAX sẽ kết thúc với giá trị là 2.

Tương tự như RAX = RAX - RBX hoặc RAX -= RBX.

```nasm
mov RAX, 5
mov RBX, 3
sub RAX, RBX
```

Phép nhân và phép chia hơi khác một chút.

Vì kích thước của dữ liệu có thể khác nhau và thay đổi rất nhiều khi nhân và chia, chúng sử dụng sự ghép nối của hai **thanh ghi** để lưu trữ kết quả. Nửa trên của kết quả được lưu trữ trong RDX và nửa dưới là trong RAX. Tổng kết quả của thao tác là <mark style="color:red;">`RDX:RAX`</mark>, tuy nhiên, chỉ tham chiếu RAX thường là đủ tốt. Hơn nữa, chỉ một toán hạng được cung cấp cho lệnh. Bất cứ điều gì bạn muốn nhân hoặc chia đều được lưu trữ trong RAX và những gì bạn muốn nhân hoặc chia cho được truyền dưới dạng toán hạng. Các ví dụ được cung cấp trong các mô tả sau.

* <mark style="color:red;">`MUL`</mark> (không dấu) hoặc <mark style="color:red;">`IMUL`</mark> (có dấu) nhân RAX với toán hạng. Kết quả được lưu trữ trong RDX:RAX. Trong ví dụ sau, RDX:RAX sẽ kết thúc với giá trị là 125.

```nasm
mov RAX, 25
mov RBX, 5
mul RBX ; Nhân RAX (25) với RBX (5)
```

Sau khi mã đó chạy, kết quả được lưu trữ trong RDX:RAX nhưng trong trường hợp này và trong hầu hết các trường hợp, RAX là đủ.

* <mark style="color:red;">`DIV`</mark> (không dấu) và <mark style="color:red;">`IDIV`</mark> (không dấu) hoạt động giống như MUL. Những gì bạn muốn chia (số bị chia) được lưu trữ trong RAX và những gì bạn muốn chia nó cho (số chia) được truyền dưới dạng toán hạng. Kết quả được lưu trữ trong RDX:RAX, nhưng một lần nữa chỉ RAX thường là đủ.

```nasm
mov RAX, 18
mov RBX, 3
div RBX ; Chia RAX (18) cho RBX (3)
```

Sau khi mã đó thực thi, RAX sẽ là 6.

**Kiểm soát luồng (Flow Control):**

* <mark style="color:red;">`RET`</mark> là viết tắt của return (trả về). Điều này sẽ trả lại việc thực thi cho hàm đã gọi hàm đang thực thi, hay còn gọi là người gọi. Như bạn sẽ sớm biết, một trong những mục đích của RAX là giữ các giá trị trả về. Ví dụ sau đặt RAX thành 10 sau đó trả về. Điều này tương đương với return 10; trong các ngôn ngữ lập trình cấp cao hơn.

```nasm
mov RAX, 10
ret
```

* <mark style="color:red;">`CMP`</mark> so sánh hai toán hạng và đặt các cờ thích hợp tùy thuộc vào kết quả. Điều này sẽ đặt **`Zero Flag (ZF)`** thành 1 có nghĩa là so sánh xác định rằng RAX bằng năm. Cờ được nói đến trong phần tiếp theo. Tóm lại, cờ được sử dụng để biểu thị kết quả của một so sánh, chẳng hạn như hai số có bằng nhau hay không. Nó hoạt động như lệnh trừ. ZF đặt nếu cả 2 bằng nhau. Nếu toán hạng nguồn lớn hơn toán hạng đích từ CF đặt.

```nasm
mov RAX, 5
cmp RAX, 5
```

* Các lệnh <mark style="color:red;">`JCC`</mark> là các lệnh nhảy có điều kiện, nhảy dựa trên các cờ hiện đang được đặt. JCC không phải là một lệnh mà là một thuật ngữ được sử dụng để chỉ tập hợp các lệnh bao gồm <mark style="color:red;">`JNE`</mark>, <mark style="color:red;">`JLE`</mark>, <mark style="color:red;">`JNZ`</mark> và nhiều lệnh khác. Các lệnh JCC thường dễ hiểu để đọc. <mark style="color:red;">`JNE`</mark> sẽ nhảy nếu so sánh không bằng nhau và <mark style="color:red;">`JLE`</mark> nhảy nếu nhỏ hơn hoặc bằng, <mark style="color:red;">`JG`</mark> nhảy nếu lớn hơn, v.v. Đây là phiên bản **Assembly** của câu lệnh if.

Ví dụ sau sẽ trả về nếu RAX không bằng 5. Nếu nó bằng 5 thì nó sẽ đặt RBX thành 10, sau đó trả về.

{% code lineNumbers="true" %}

```nasm
mov RAX, 5
cmp RAX, 5
jne 5 ; Nhảy đến dòng 5 (ret) nếu không bằng.
mov RBX, 10
ret
```

{% endcode %}

* <mark style="color:red;">`NOP`</mark> là viết tắt của No Operation (Không thao tác). Lệnh này thực sự không làm gì cả. Nó thường được sử dụng để đệm vì một số phần của mã thích nằm trên các ranh giới cụ thể như ranh giới 16-bit hoặc 32-bit.
* <mark style="color:red;">`TEST`</mark> thực hiện một thao tác AND thoe bit và thay vì lưu kết quả trong toán hạng đích như lệnh AND, nó đặt ZF nếu kết quả là 0. Lệnh này thường được sử dụng để kiểm tra xem một toán hạng có giá trị NULL hay không, ví dụ: bằng cách kiểm tra toán hạng với nó. Điều này được thực hiện vì cần ít byte hơn để sử dụng lệnh test so sánh với 0.&#x20;

```nasm
test destiantion, source
```

### Pointers (con trỏ)

Hai trong số những điều quan trọng nhất cần biết khi làm việc với con trỏ và địa chỉ trong **Assembly** là LEA và dấu ngoặc vuông.

* **Dấu ngoặc vuông** - Dấu ngoặc vuông giải tham chiếu trong **Assembly**. Ví dụ: \[var] là địa chỉ được trỏ đến bởi var. Nói cách khác, khi sử dụng \[var] chúng ta muốn truy cập địa chỉ bộ nhớ mà var đang giữ.
* **LEA** - Bỏ qua mọi thứ về dấu ngoặc vuông khi làm việc với LEA. LEA là viết tắt của Load Effective Address và nó được sử dụng để tính toán và tải địa chỉ.

Đây là một ví dụ đơn giản về giải tham chiếu và con trỏ trong **Assembly**:

```nasm
lea RAX, [var]
mov [RAX], 12
```

Trong ví dụ trên, địa chỉ của var được tải vào RAX. Đây là LEA chúng ta đang làm việc, không có giải tham chiếu. RAX hiện đang hoạt động như một con trỏ vì nó giữ địa chỉ đến biến. Sau đó, 12 được di chuyển vào địa chỉ được trỏ đến bởi RAX). Địa chỉ được trỏ đến bởi RAX là biến var. Nếu **Assembly** đó được thực thi, var sẽ là 12. Điều này hoàn toàn giống như thực hiện `mov var, 12`.

### **Mở rộng Zero (Zero Extension)**

Mở rộng Zero là đặt phần còn lại của các **bit** còn lại trong một **thanh ghi** thành zero khi sửa đổi các **bit** khác. Ví dụ: nếu bạn di chuyển một giá trị vào EAX, thì 32 **bit** trên của RAX có nên thay đổi không?

Nói chung, việc di chuyển đến 32 **bit** thấp hơn của RAX thông qua EAX sẽ zero out/zero extend 32 **bit** trên. Việc di chuyển đến bất cứ thứ gì ít hơn sẽ không mở rộng zero. Vì vậy, di chuyển một thứ gì đó vào AX sẽ không zero out phần còn lại của RAX. Nếu bạn muốn mở rộng zero bất kể điều gì, hãy sử dụng <mark style="color:red;">`movzx`</mark> để thực hiện mở rộng zero bất kể điều gì.

### The JMP's

Đối với so sánh không dấu:

* JB/JNAE (CF = 1) ; Nhảy nếu dưới/không trên hoặc bằng
* JAE/JNB (CF = 0) ; Nhảy nếu trên hoặc bằng/không dưới
* JBE/JNA (CF = 1 hoặc ZF = 1) ; Nhảy nếu dưới hoặc bằng/không trên
* JA/JNBE (CF = 0 và ZF = 0); Nhảy nếu trên/không dưới hoặc bằng

Đối với so sánh có dấu:

* JL/JNGE (SF <> OF) ; Nhảy nếu ít hơn/không lớn hơn hoặc bằng
* JGE/JNL (SF = OF) ; Nhảy nếu lớn hơn hoặc bằng/không ít hơn
* JLE/JNG (ZF = 1 hoặc SF <> OF); Nhảy nếu ít hơn hoặc bằng/không lớn hơn
* JG/JNLE (ZF = 0 và SF = OF); Nhảy nếu lớn hơn/không ít hơn hoặc bằng

## 9. Flags

Cờ được sử dụng để biểu thị kết quả của thao tác hoặc so sánh đã thực hiện trước đó. Ví dụ: nếu hai số được so sánh với nhau, các cờ sẽ phản ánh các kết quả như chúng bằng nhau. Các cờ được chứa trong một **thanh ghi** có tên là EFLAGS (x86) hoặc RFLAGS (x64). Tôi thường chỉ gọi nó là **thanh ghi cờ**.

### **Cờ trạng thái (Status Flags)**

Dưới đây là các cờ bạn nên biết. Lưu ý rằng khi tôi nói "cờ được đặt", ý tôi là cờ được đặt thành 1, có nghĩa là đúng/bật. 0 là sai/tắt.

* **Zero Flag (ZF)** - Được đặt nếu kết quả của một thao tác là zero. Không được đặt nếu kết quả của một thao tác không phải là zero.
* **Carry Flag (CF)** - Được đặt nếu thao tác số học không dấu cuối cùng mang (cộng) hoặc mượn (trừ) một **bit** vượt ra ngoài **thanh ghi**. Nó cũng được đặt khi một thao tác sẽ là âm nếu không phải vì thao tác đó không dấu.
* **Overflow Flag (OF)** - Được đặt nếu một thao tác số học có dấu quá lớn để **thanh ghi** chứa.
* **Sign Flag (SF)** - Được đặt nếu kết quả của một thao tác là âm.
* **Adjust/Auxiliary Flag (AF)** - Giống như cờ carry nhưng dành cho các thao tác Binary Coded Decimal (BCD).
* **Parity Flag (PF)** - Được đặt thành 1 nếu số lượng **bit** được đặt trong 8 **bit** cuối cùng là chẵn. (10110100, PF=1; 10110101, PF=0)
* **Trap Flag (TF)** - Cho phép thực hiện từng bước các chương trình.

**Phép trừ (Subtraction)**

Ví dụ sau đây sẽ chứng minh một thao tác có dấu. SF sẽ được đặt thành 1 vì thao tác trừ dẫn đến một số âm. Sử dụng lệnh cmp thay vì sub sẽ có cùng kết quả, ngoại trừ giá trị của thao tác (-6) sẽ không được lưu trong bất kỳ **thanh ghi** nào.

```nasm
mov RAX, 2
sub RAX, 8  ; 2 - 8 = -6.
; ZF = 0, OF = 0, SF = 1
```

**Phép cộng (Addition)**

Sau đây là một ví dụ trong đó kết quả quá lớn để vừa vào một **thanh ghi**. Ở đây tôi đang sử dụng các **thanh ghi** 8-bit để chúng ta có thể làm việc với các số nhỏ. Số lớn nhất có thể vừa trong một **thanh ghi** 8-bit có dấu là 128. AL được tải với 75 sau đó 60 được thêm vào. Kết quả của việc thêm hai số với nhau sẽ cho kết quả là 135, vượt quá giá trị tối đa. Vì điều này, số được bao quanh và AL sẽ là -121. Điều này đặt OF vì kết quả quá lớn cho **thanh ghi** và **cờ** SF được đặt vì kết quả là âm. Nếu đây là một thao tác không dấu, CF sẽ được đặt.

```nasm
mov AL, 75
add AL, 60
; ZF = 0, OF = 1, SF = 1
```

## 10. Windows x64 Calling Convention

Khi một hàm được gọi, về mặt lý thuyết, bạn có thể truyền các tham số thông qua **thanh ghi (registers)**, ngăn xếp (stack) hoặc thậm chí trên đĩa. Bạn chỉ cần đảm bảo rằng hàm bạn đang gọi biết nơi bạn đang đặt các tham số. Điều này không phải là một vấn đề quá lớn nếu bạn đang sử dụng các hàm của riêng mình, nhưng mọi thứ sẽ trở nên lộn xộn khi bạn bắt đầu sử dụng thư viện. Để giải quyết vấn đề này, chúng ta có các **quy ước gọi** xác định cách các tham số được truyền cho một hàm, ai phân bổ không gian cho các biến và ai dọn dẹp ngăn xếp.

Callee đề cập đến hàm đang được gọi và caller là hàm thực hiện cuộc gọi.

### **Fastcall**

Fastcall là **quy ước gọi** cho x64 Windows. Windows sử dụng **quy ước gọi** fastcall bốn **thanh ghi** theo mặc định. Thông tin nhanh cho bạn, khi nói về các **quy ước gọi**, bạn sẽ nghe nói về một thứ gọi là "Application Binary Interface" (ABI). ABI xác định các quy tắc khác nhau cho các chương trình, chẳng hạn như **quy ước gọi**, xử lý tham số, v.v.

**Quy ước gọi x64 Windows hoạt động như thế nào?**

* Bốn tham số đầu tiên được truyền trong **thanh ghi**, TỪ TRÁI SANG PHẢI. Các tham số không phải là giá trị dấu phẩy động, chẳng hạn như số nguyên, con trỏ và char, sẽ được truyền qua RCX, RDX, R8 và R9 (theo thứ tự đó). Các tham số dấu phẩy động sẽ được truyền qua XMM0, XMM1, XMM2 và XMM3 (theo thứ tự đó).
* Nếu có sự kết hợp giữa các giá trị dấu phẩy động và số nguyên, chúng vẫn sẽ được truyền qua **thanh ghi** tương ứng với vị trí của chúng. Ví dụ: `func(1, 3.14, 6, 6.28)` sẽ truyền tham số đầu tiên qua RCX, tham số thứ hai qua XMM1, tham số thứ ba qua R8 và tham số cuối cùng qua XMM3.
* Nếu tham số được truyền quá lớn để vừa trong một **thanh ghi** thì nó được truyền bằng tham chiếu (con trỏ đến dữ liệu trong bộ nhớ).
* Các tham số có thể được truyền qua bất kỳ **thanh ghi** tương ứng có kích thước nào. Ví dụ: RCX, ECX, CX, CH và CL đều có thể được sử dụng cho tham số đầu tiên.
* Bất kỳ tham số nào khác đều được đẩy lên ngăn xếp, **TỪ PHẢI SANG TRÁI.**
* Luôn có không gian được phân bổ trên ngăn xếp cho 4 tham số, ngay cả khi không có tham số nào. Không gian này không hoàn toàn bị lãng phí vì **trình biên dịch** có thể và thường sẽ sử dụng nó. Thông thường, nếu đó là một bản dựng gỡ lỗi, **trình biên dịch** sẽ đặt một bản sao của các tham số vào không gian. Trên các bản dựng phát hành, **trình biên dịch** sẽ sử dụng nó để lưu trữ biến tạm thời hoặc biến cục bộ.

Dưới đây là một số quy tắc khác của **quy ước gọi**:

* **Con trỏ cơ sở** (RBP) được lưu khi một hàm được gọi để nó có thể được khôi phục.
* Giá trị trả về của một hàm được truyền qua RAX nếu đó là một số nguyên, bool, char, v.v. hoặc XMM0 nếu đó là một số float hoặc double.
* Các hàm thành viên có một tham số đầu tiên ngầm định cho **con trỏ "this"**. Vì nó là một con trỏ và nó là tham số đầu tiên, nó sẽ được truyền qua RCX. Điều này có thể rất hữu ích để biết.
* Caller chịu trách nhiệm phân bổ không gian cho các tham số cho callee. Caller phải luôn phân bổ không gian cho 4 tham số ngay cả khi không có tham số nào được truyền.
* Các **thanh ghi** RAX, RCX, RDX, R8, R9, R10, R11 và XMM0-XMM5 được coi là dễ bay hơi (volatile) và phải được coi là bị hủy trên các cuộc gọi hàm.
* Các **thanh ghi** RBX, RBP, RDI, RSI, RSP, R12, R13, R14, R15 và XMM6-XMM15 được coi là không dễ bay hơi (nonvolatile) và nên được lưu và khôi phục bởi một hàm sử dụng chúng.

### Stack Access

Dữ liệu trên ngăn xếp, chẳng hạn như các biến cục bộ và các tham số hàm, thường được truy cập bằng RBP hoặc RSP. Trên x64, rất phổ biến khi thấy RSP được sử dụng thay vì RBP để truy cập các tham số. Hãy nhớ rằng bốn tham số đầu tiên, mặc dù chúng được truyền qua **thanh ghi**, vẫn có không gian được dành riêng cho chúng trên ngăn xếp. Không gian này sẽ là 32 **byte** (0x20), 8 **byte** cho mỗi **thanh ghi** trong số 4 **thanh ghi**. Hãy nhớ điều này vì tại một thời điểm nào đó bạn sẽ thấy **độ lệch (offset)** này khi truy cập các tham số được truyền trên ngăn xếp.

* 1-4 Tham số:
  * Các đối số sẽ được đẩy thông qua các **thanh ghi** tương ứng của chúng, từ trái sang phải. **Trình biên dịch** có thể sẽ sử dụng RSP+0x0 đến RSP+0x18 cho các mục đích khác.
* Hơn 4 Tham số:
  * Bốn đối số đầu tiên được truyền qua **thanh ghi**, từ trái sang phải và phần còn lại được đẩy lên ngăn xếp bắt đầu từ **độ lệch** RSP+0x20, từ phải sang trái. Điều này làm cho RSP+0x20 là đối số thứ năm và RSP+0x28.

Đây là một ví dụ rất đơn giản trong đó các số 1 đến 8 được truyền từ một hàm sang một hàm khác. Lưu ý thứ tự chúng được đặt vào.

```c
function(1,2,3,4,5,6,7,8)
```

```nasm
MOV RCX 0x1 ; Going left to right.
MOV RDX 0x2
MOV R8 0x3
MOV R9 0x4
PUSH 0x8 ; Now going right to left.
PUSH 0x7
PUSH 0x6
PUSH 0x5
CALL function
```

Trong trường hợp này, các tham số ngăn xếp nên được truy cập thông qua RSP+0x20 đến RSP+0x28.

Đặt chúng vào các **thanh ghi** từ trái sang phải và sau đó đẩy chúng lên ngăn xếp từ phải sang trái có thể không có ý nghĩa, nhưng nó sẽ có ý nghĩa một khi bạn nghĩ về nó. Bằng cách thực hiện điều này, nếu bạn bật các tham số ra khỏi ngăn xếp, chúng sẽ theo thứ tự.

```nasm
POP R10 ; = 5
POP R11 ; = 6
POP R12 ; = 6
POP R13 ; = 7
```

Bây giờ bạn có thể truy cập chúng, từ trái sang phải theo thứ tự: RCX, RDX, R8, R9, R10, R11, R12, R13.

### **cdecl (C Declaration)**

* Các tham số được truyền trên ngăn xếp ngược (từ phải sang trái).
* **Con trỏ cơ sở** (RBP) được lưu để nó có thể được khôi phục.
* Giá trị trả về được truyền qua EAX.
* Caller dọn dẹp ngăn xếp. Đây là điều làm cho cdecl trở nên thú vị. Vì caller dọn dẹp ngăn xếp, cdecl cho phép một số lượng tham số biến đổi.

## 11. Memory Layout

Bộ nhớ của hệ thống được tổ chức theo một cách cụ thể. Điều này được thực hiện để đảm bảo mọi thứ đều có một nơi để cư trú.

### **Các phân đoạn bộ nhớ (Memory Segments)**

Có các phân đoạn/phần khác nhau trong đó dữ liệu hoặc mã được lưu trữ trong bộ nhớ. Chúng là các phân đoạn sau:

* **Ngăn xếp (Stack)** - Giữ các biến cục bộ không tĩnh (non-static).
* **Heap** - Chứa dữ liệu được cấp phát động có thể chưa được khởi tạo lúc đầu.
* **.data** - Chứa dữ liệu toàn cục và tĩnh được khởi tạo thành một giá trị khác không.
* **.bss** - Chứa dữ liệu toàn cục và tĩnh chưa được khởi tạo hoặc được khởi tạo thành zero.
* **.text** - Chứa mã của chương trình.

### Overview of Memory Sections

<figure><img src="/files/mBKYIbJrFdf8VDhEQgqc" alt=""><figcaption></figcaption></figure>

**Quan trọng:**

Sơ đồ trên cho thấy hướng các biến (và bất kỳ dữ liệu nào được đặt tên, ngay cả cấu trúc) được đưa vào hoặc lấy ra khỏi bộ nhớ. Dữ liệu thực tế được đưa vào bộ nhớ theo một cách khác. Đây là lý do tại sao sơ đồ ngăn xếp khác nhau rất nhiều. Bạn thường thấy sơ đồ ngăn xếp với ngăn xếp và heap phát triển về phía nhau hoặc các địa chỉ bộ nhớ cao ở trên cùng. Sơ đồ đang hiển thị là phù hợp nhất cho **kỹ thuật đảo ngược**. Các địa chỉ thấp ở trên cùng cũng là mô tả thực tế nhất.

**Giải thích từng phần:**

* **Ngăn xếp (Stack)** - Vùng trong bộ nhớ có thể được sử dụng nhanh chóng để cấp phát dữ liệu tĩnh. Hãy tưởng tượng ngăn xếp với các địa chỉ thấp ở trên cùng và các địa chỉ cao ở phía dưới. Điều này giống hệt như một danh sách số bình thường. Dữ liệu được đọc và ghi là "vào sau ra trước" (last-in-first-out - LIFO). Cấu trúc LIFO của ngăn xếp thường được biểu diễn bằng một chồng đĩa. Bạn không thể đơn giản lấy ra chiếc đĩa thứ ba từ trên xuống, bạn phải lấy ra từng chiếc đĩa một để đến được nó. Bạn chỉ có thể truy cập phần dữ liệu ở trên cùng của ngăn xếp, vì vậy để truy cập dữ liệu khác, bạn cần di chuyển những gì ở trên cùng ra ngoài. Khi tôi nói rằng ngăn xếp giữ dữ liệu tĩnh, tôi đang đề cập đến dữ liệu có độ dài đã biết, chẳng hạn như một số nguyên. Kích thước của một số nguyên được xác định tại thời điểm biên dịch, kích thước thường là 4 **byte**, vì vậy chúng ta có thể ném nó lên ngăn xếp. Trừ khi độ dài tối đa được chỉ định, đầu vào của người dùng nên được lưu trữ trên heap vì dữ liệu có kích thước thay đổi. Tuy nhiên, địa chỉ/vị trí của đầu vào có thể sẽ được lưu trữ trên ngăn xếp để tham khảo trong tương lai. Khi bạn đặt dữ liệu lên trên cùng của ngăn xếp, bạn đẩy nó lên ngăn xếp. Khi dữ liệu được đẩy lên ngăn xếp, ngăn xếp phát triển lên trên, về phía các địa chỉ bộ nhớ thấp hơn. Khi bạn loại bỏ một phần dữ liệu khỏi đỉnh ngăn xếp, bạn bật nó ra khỏi ngăn xếp. Khi dữ liệu được bật ra khỏi ngăn xếp, ngăn xếp co lại, về phía các địa chỉ cao hơn. Tất cả điều đó có vẻ kỳ lạ nhưng hãy nhớ rằng, nó giống như một danh sách số bình thường trong đó 1, số thấp hơn, ở trên cùng. 10, số cao hơn, ở phía dưới. Hai **thanh ghi (registers)** được sử dụng để theo dõi ngăn xếp. **Con trỏ ngăn xếp** (RSP/ESP/SP) được sử dụng để theo dõi đỉnh của ngăn xếp và **con trỏ cơ sở** (RBP/EBP/BP) được sử dụng để theo dõi cơ sở/đáy của ngăn xếp. Điều này có nghĩa là khi dữ liệu được đẩy lên ngăn xếp, **con trỏ ngăn xếp** giảm xuống vì ngăn xếp phát triển lên trên về phía các địa chỉ thấp hơn. Tương tự, **con trỏ ngăn xếp** tăng lên khi dữ liệu được bật ra khỏi ngăn xếp. **Con trỏ cơ sở** không có lý do gì để thay đổi khi chúng ta đẩy hoặc bật một cái gì đó đến/từ ngăn xếp. Chúng ta sẽ nói về cả **con trỏ ngăn xếp** và **con trỏ cơ sở** nhiều hơn khi thời gian trôi qua. Hãy cảnh giác, bạn đôi khi sẽ thấy ngăn xếp được biểu diễn theo cách khác, nhưng cách tôi đang dạy nó là cách bạn sẽ thấy nó trong thế giới thực.
* **Heap** - Tương tự như ngăn xếp nhưng được sử dụng để cấp phát động và truy cập chậm hơn một chút. Heap thường được sử dụng cho dữ liệu động (thay đổi hoặc không thể đoán trước). Những thứ như cấu trúc và đầu vào của người dùng có thể được lưu trữ trên heap. Nếu kích thước của dữ liệu không được biết tại thời điểm biên dịch, nó thường được lưu trữ trên heap. Khi bạn thêm dữ liệu vào heap, nó phát triển về phía các địa chỉ cao hơn.
* **Hình ảnh chương trình (Program Image)** - Đây là chương trình/file thực thi được tải vào bộ nhớ. Trên Windows, đây thường là Portable Executable (PE).

Đừng lo lắng quá nhiều về TEB và PEB ngay bây giờ. Đây chỉ là một giới thiệu ngắn gọn về chúng.

* **TEB** - Thread Environment Block (TEB) lưu trữ thông tin về (các) luồng đang chạy hiện tại.
* **PEB** - Process Environment Block (PEB) lưu trữ thông tin về quá trình và các mô-đun đã tải. Một thông tin mà PEB chứa là "BeingDebugged" có thể được sử dụng để xác định xem quá trình hiện tại có đang được gỡ lỗi hay không.

Dưới đây là một sơ đồ ví dụ nhanh về ngăn xếp và heap với một số dữ liệu trên chúng.

<figure><img src="/files/jUH4AUSnqOJLxANlPkNl" alt=""><figcaption></figcaption></figure>

Trong sơ đồ trên, stackVar1 được tạo trước stackVar2, tương tự đối với các biến heap.

### **Khung ngăn xếp (Stack Frames)**

Khung ngăn xếp là các khối dữ liệu cho các hàm. Dữ liệu này bao gồm các biến cục bộ, **con trỏ cơ sở** đã lưu, địa chỉ trả về của người gọi và các tham số hàm. Xem xét ví dụ sau:

```c
int Square(int x){
    return x*x;
}
int main(){
    int num = 5;
    Square(5);
}
```

Trong ví dụ này, hàm main() được gọi trước. Khi main() được gọi, một khung ngăn xếp được tạo cho nó. Khung ngăn xếp cho main(), trước cuộc gọi hàm đến Square(), bao gồm biến cục bộ num và các tham số được truyền cho nó (trong trường hợp này không có tham số nào được truyền cho main). Khi main() gọi Square(), **con trỏ cơ sở** (RBP) và địa chỉ trả về đều được lưu. Hãy nhớ rằng, **con trỏ cơ sở** trỏ đến cơ sở/đáy của ngăn xếp. **Con trỏ cơ sở** được lưu vì khi một hàm được gọi, **con trỏ cơ sở** được cập nhật để trỏ đến cơ sở của ngăn xếp của hàm đó. Khi hàm trả về, **con trỏ cơ sở** được khôi phục để nó trỏ đến cơ sở của khung ngăn xếp của người gọi. Địa chỉ trả về được lưu để khi hàm trả về, chương trình biết nơi tiếp tục thực thi. Địa chỉ trả về là lệnh tiếp theo sau cuộc gọi hàm. Vì vậy, trong trường hợp này, địa chỉ trả về là kết thúc của hàm main(). Điều đó có vẻ khó hiểu, hy vọng điều này có thể làm rõ nó:

```nasm
mov RAX, 15 ;RAX = 15
call func   ;Gọi func. Giống như func();
mov RBX, 23 ;RBX = 23. Dòng này được lưu làm địa chỉ trả về cho cuộc gọi hàm.
```

Tôi biết điều này có thể hơi khó hiểu nhưng nó khá đơn giản trong cách nó hoạt động. Nó có thể không trực quan ngay từ đầu. Nó chỉ đơn giản là cho máy tính biết nơi để đi (lệnh nào để thực thi) khi hàm trả về. Bạn không muốn nó thực thi lệnh đã gọi hàm vì điều đó sẽ gây ra một vòng lặp vô hạn. Đây là lý do tại sao lệnh tiếp theo được sử dụng làm địa chỉ trả về thay thế. Vì vậy, trong ví dụ trên, RAX được đặt thành 15, sau đó hàm có tên func được gọi. Khi nó trả về, nó sẽ bắt đầu thực thi tại địa chỉ trả về là dòng chứa `mov RBX, 23`.

Đây là bố cục của một khung ngăn xếp:

<figure><img src="/files/cRm2Zt3qJQp7IzcU0CM3" alt=""><figcaption></figcaption></figure>

Lưu ý vị trí của mọi thứ. Điều này sẽ hữu ích trong tương lai.

### **Endianness**

Với giá trị 0xDEADBEEF, nó sẽ được lưu trữ trong bộ nhớ như thế nào? Điều này đã được tranh luận trong một thời gian và vẫn gây ra tranh cãi cho đến ngày nay. Lúc đầu, có vẻ trực quan để lưu trữ nó như hiện tại, nhưng khi bạn nghĩ về nó từ góc độ của máy tính, nó không đơn giản như vậy. Vì điều này, có hai cách máy tính có thể lưu trữ dữ liệu trong bộ nhớ - big-endian và little-endian.

* **Big Endian** - **Byte** quan trọng nhất (ngoài cùng bên trái) được lưu trữ trước. Đây sẽ là 0xDEADBEEF từ ví dụ.
* **Little Endian** - **Byte** ít quan trọng nhất (ngoài cùng bên phải) được lưu trữ trước. Đây sẽ là 0xEFBEADDE từ ví dụ.

### Data Storage

Như một bản tóm tắt nhanh, không gian được cấp phát trên ngăn xếp cho các biến từ dưới lên trên, hoặc các địa chỉ cao hơn đến các địa chỉ thấp hơn.

Dữ liệu được đưa vào không gian được cấp phát này rất đơn giản. Nó giống như viết tiếng Anh: từ trái sang phải, từ trên xuống dưới. Phần dữ liệu đầu tiên trong một biến hoặc cấu trúc là ở địa chỉ thấp nhất trong bộ nhớ so với phần còn lại của dữ liệu. Khi dữ liệu được thêm vào, nó được đặt ở một địa chỉ cao hơn xuống ngăn xếp.

<figure><img src="/files/x4cjuKqC3ODJilFvVxCP" alt=""><figcaption></figcaption></figure>

Sơ đồ này minh họa hai điều. Đầu tiên, cách dữ liệu được đưa vào không gian được cấp phát của nó. Thứ hai, một tác dụng phụ của cách dữ liệu được đưa vào bộ nhớ được cấp phát của nó. Tôi sẽ chia nhỏ sơ đồ. Ở bên trái là các biến đang được tạo. Ở bên phải là kết quả của việc tạo biến đó. Tôi sẽ chỉ tập trung vào ngăn xếp cho lời giải thích này.

Ở bên trái, ba biến được gán giá trị. Biến đầu tiên, như đã giải thích trước đó, được đặt ở phía dưới. Biến tiếp theo được đặt trên đầu, và biến tiếp theo trên đầu đó. Sau khi cấp phát không gian cho các biến, dữ liệu được đưa vào các biến đó. Mọi thứ đều khá đơn giản nhưng có một điều thú vị đang xảy ra với mảng. Lưu ý cách nó chỉ cấp phát một mảng gồm 2 phần tử stackArr\[2], nhưng nó được cung cấp 3 = {3,4,5}. Vì dữ liệu được viết từ các địa chỉ thấp hơn đến cao hơn hoặc từ trái sang phải và từ trên xuống dưới, nên nó ghi đè dữ liệu của biến bên dưới nó. Vì vậy, thay vì stackVar2 là 2, nó bị ghi đè bởi 5 có ý định nằm trong stackArr\[2]. Hy vọng rằng tất cả đều có ý nghĩa. Dưới đây là một bản tóm tắt nhanh:

* Các biến được cấp phát trên ngăn xếp cái này trên đầu cái kia như một chồng khay. Điều này có nghĩa là chúng được đặt trên ngăn xếp bắt đầu từ các địa chỉ cao hơn và đi đến các địa chỉ thấp hơn.
* Dữ liệu được đưa vào các biến từ trái sang phải, từ trên xuống dưới. Đó là, từ địa chỉ thấp hơn đến cao hơn.

Đó là một khái niệm đơn giản, hãy cố gắng không làm phức tạp nó chỉ vì tôi đã đưa ra một lời giải thích dài dòng. Điều quan trọng là bạn phải hiểu nó, đó là lý do tại sao tôi đã dành rất nhiều thời gian để giải thích khái niệm này. Chính vì những khái niệm này mà có rất nhiều mô tả về bộ nhớ ngoài kia đi theo các hướng khác nhau.

### **RBP & RSP trên x64**

Trên x64, rất phổ biến khi thấy RBP được sử dụng theo một cách không truyền thống (so với x86). Đôi khi chỉ RSP được sử dụng để trỏ đến dữ liệu trên ngăn xếp, chẳng hạn như các biến cục bộ và các tham số hàm, và RBP được sử dụng cho dữ liệu chung (tương tự như RAX). Điều này sẽ được thảo luận chi tiết hơn sau.

## 12. Registers Overview

Các **Thanh ghi đa năng** trong hệ thống x86 đều là các **thanh ghi** 32-bit. Như tên cho thấy, chúng được sử dụng trong quá trình thực thi chung các lệnh bởi CPU. Trong các hệ thống 64-bit, các **thanh ghi** này được mở rộng thành các **thanh ghi** 64-bit. Chúng chứa các **thanh ghi** sau.

* EAX hoặc RAX:
  * Đây là **Thanh ghi tích lũy (Accumulator Register)**. Kết quả của các phép toán số học thường được lưu trữ trong **thanh ghi** này. Trong các hệ thống 32-bit, một **thanh ghi** EAX 32-bit tồn tại, trong khi một **thanh ghi** RAX 64-bit tồn tại trong các hệ thống 64-bit. 16 **bit** cuối cùng của **thanh ghi** này có thể được truy cập bằng cách định địa chỉ AX. Tương tự, nó cũng có thể được định địa chỉ trong 8 **bit** bằng cách sử dụng AL cho 8 **bit** thấp hơn và AH cho 8 **bit** cao hơn.
* EBX hoặc RBX:
  * **Thanh ghi** này còn được gọi là **Thanh ghi cơ sở (Base Register)**, thường được sử dụng để lưu trữ địa chỉ cơ sở để tham chiếu một **độ lệch (offset)**. Tương tự như EAX/RAX, nó có thể được định địa chỉ là **thanh ghi** RBX 64-bit, EBX 32-bit, BX 16-bit và BH và BL 8-bit.
* ECX hoặc RCX:
  * **Thanh ghi** này còn được gọi là **Thanh ghi bộ đếm (Counter Register)** và thường được sử dụng trong các thao tác đếm như vòng lặp, v.v. Tương tự như hai **thanh ghi** trên, nó có thể được định địa chỉ là **thanh ghi** RCX 64-bit, ECX 32-bit, CX 16-bit và CH và CL 8-bit.
* EDX hoặc RDX:
  * **Thanh ghi** này còn được gọi là **Thanh ghi dữ liệu (Data Register)**. Nó thường được sử dụng trong các phép toán nhân/chia. Tương tự như các **thanh ghi** trên, nó có thể được định địa chỉ là **thanh ghi** RDX 64-bit, EDX 32-bit, DX 16-bit và DH và DL 8-bit.
* ESP hoặc RSP:
  * **Thanh ghi** này được gọi là **Con trỏ ngăn xếp (Stack Pointer)**. Nó trỏ đến đỉnh của ngăn xếp và được sử dụng kết hợp với **thanh ghi phân đoạn ngăn xếp**. Nó là một **thanh ghi** 32-bit có tên là ESP trong các hệ thống 32-bit và một **thanh ghi** 64-bit có tên là RSP trong các hệ thống 64-bit. Nó không thể được định địa chỉ dưới dạng các **thanh ghi** nhỏ hơn.
* EBP hoặc RBP:
  * **Thanh ghi** này được gọi là **Con trỏ cơ sở (Base Pointer)**. Nó được sử dụng để truy cập các tham số được truyền bởi ngăn xếp. Nó cũng được sử dụng kết hợp với **thanh ghi phân đoạn ngăn xếp**. Nó là một **thanh ghi** 32-bit có tên là EBP trong các hệ thống 32-bit và một **thanh ghi** 64-bit có tên là RBP trong các hệ thống 64-bit.
* ESI hoặc RSI:
  * **Thanh ghi** này được gọi là **Thanh ghi chỉ số nguồn (Source Index register)**. Nó được sử dụng cho các thao tác chuỗi. Nó được sử dụng với **thanh ghi phân đoạn dữ liệu (DS)** làm **độ lệch (offset)**. Nó là một **thanh ghi** 32-bit có tên là ESI trong các hệ thống 32-bit và một **thanh ghi** 64-bit có tên là RSI trong các hệ thống 64-bit.
* EDI hoặc RDI:
  * **Thanh ghi** này được gọi là **Thanh ghi chỉ số đích (Destination Index register)**. Nó cũng được sử dụng cho các thao tác chuỗi. Nó được sử dụng với **thanh ghi phân đoạn bổ sung (ES)** làm **độ lệch (offset)**. Nó là một **thanh ghi** 32-bit có tên là EDI trong các hệ thống 32-bit và một **thanh ghi** 64-bit có tên là RDI trong các hệ thống 64-bit.
* R8-R15:
  * Các **thanh ghi đa năng** 64-bit này không có trong các hệ thống 32-bit. Chúng được giới thiệu trong các hệ thống 64-bit. Chúng cũng có thể được định địa chỉ ở các chế độ 32-bit, 16-bit và 8-bit. Ví dụ: đối với **thanh ghi** R8, chúng ta có thể sử dụng R8D để định địa chỉ 32-bit thấp hơn, R8W để định địa chỉ 16-bit thấp hơn và R8B để định địa chỉ 8-bit thấp hơn. Ở đây, hậu tố D là viết tắt của Double-word, W là viết tắt của Word và B là viết tắt của Byte.

<figure><img src="/files/SArLA0ujXOyFp7WOZFHw" alt=""><figcaption></figcaption></figure>

### **Thanh ghi phân đoạn (Segment Registers):**

Các **Thanh ghi phân đoạn** là các **thanh ghi** 16-bit chuyển đổi không gian bộ nhớ phẳng thành các phân đoạn khác nhau để định địa chỉ dễ dàng hơn. Có sáu **thanh ghi phân đoạn**, như được giải thích dưới đây:

* **Phân đoạn mã (Code Segment):** **Thanh ghi** Phân đoạn mã (CS) trỏ đến phần Mã trong bộ nhớ.
* **Phân đoạn dữ liệu (Data Segment):** **Thanh ghi** Phân đoạn dữ liệu (DS) trỏ đến phần dữ liệu của chương trình trong bộ nhớ.
* **Phân đoạn ngăn xếp (Stack Segment):** **Thanh ghi** Phân đoạn ngăn xếp (SS) trỏ đến Ngăn xếp của chương trình trong bộ nhớ.
* **Phân đoạn bổ sung (Extra Segments) (ES, FS và GS):** Các **thanh ghi** phân đoạn bổ sung này trỏ đến các phần dữ liệu khác nhau. Chúng và **thanh ghi** DS chia bộ nhớ của chương trình thành bốn phần dữ liệu riêng biệt.

## 13. Memory overview

Khi một chương trình được tải vào Bộ nhớ (Memory) trong Hệ điều hành Windows, nó nhìn thấy một khung nhìn đã được trừu tượng hóa (abstracted view) của Bộ nhớ. Điều này có nghĩa là chương trình không có quyền truy cập vào toàn bộ Bộ nhớ; thay vào đó, nó chỉ có quyền truy cập vào phần Bộ nhớ của riêng nó. Đối với chương trình đó, đó là tất cả Bộ nhớ mà nó cần.

## 14. Opcodes and Operands

**Opcodes** là các số tương ứng với các lệnh được thực hiện bởi CPU. Khi chúng ta sử dụng trình gỡ rối (disassembler - chúng ta sẽ tìm hiểu về trình gỡ rối trong các phần tiếp theo) để gỡ rối một chương trình, nó sẽ đọc **opcodes**. Nó dịch chúng thành các lệnh assembly để làm cho chúng dễ đọc hơn đối với con người. Ví dụ, lệnh để di chuyển 0x5F vào thanh ghi eax là:

```nasm
mov eax, 0x5f
```

Khi nhìn vào nó trong một trình gỡ rối, chúng ta sẽ thấy:

```nasm
040000:    b8 5f 00 00 00    mov eax, 0x5f
```

Ở đây, `040000:` tương ứng với địa chỉ nơi lệnh được đặt. `b8` đề cập đến **opcode** của lệnh `mov eax`, và `5F 00 00 00` biểu thị toán hạng khác `0x5f`. Xin lưu ý rằng do thứ tự byte (endianness), toán hạng `0x5f` được viết là `5f 00 00 00`, thực tế là `00 00 00 5f` nhưng theo ký hiệu little-endian. Tương tự, có một **opcode** cho mỗi lệnh trong ngôn ngữ assembly. Có các tài liệu tham khảo để chuyển đổi **opcodes** thành các lệnh assembly. Tuy nhiên, trừ khi chúng ta đang viết một trình gỡ rối, chúng ta sẽ không cần chúng, vì trình gỡ rối thực hiện công việc đó khá tốt. Tuy nhiên, điều quan trọng là phải hiểu những gì đang xảy ra bên dưới để có một bức tranh tổng thể tốt hơn.

**Các loại toán hạng**

Nói chung, có ba loại toán hạng trong ngôn ngữ assembly.

* **Toán hạng tức thời (Immediate Operands)**: Cũng có thể được coi là hằng số. Đây là các giá trị cố định như chúng ta đã có `0x5f` trong ví dụ trên.
* **Thanh ghi (Registers)**: Cũng có thể là toán hạng. Ví dụ trên hiển thị `eax` là một thanh ghi nơi toán hạng tức thời được lưu trữ.
* **Toán hạng bộ nhớ (Memory Operands)**: Được biểu thị bằng dấu ngoặc vuông và chúng tham chiếu đến các vị trí bộ nhớ. Ví dụ: nếu chúng ta thấy `[eax]` làm toán hạng, nó có nghĩa là giá trị trong `eax` là vị trí bộ nhớ mà thao tác phải được thực hiện.

Ta có thể thấy chúng ta sử dụng dấu ngoặc vuông khi tham chiếu bộ nhớ.

Tương tự, giả sử chúng ta thấy một thanh ghi trong dấu ngoặc vuông. Trong trường hợp đó, điều đó có nghĩa là giá trị trong thanh ghi đó sẽ được coi là một vị trí bộ nhớ và giá trị trong vị trí bộ nhớ đó sẽ được di chuyển đến đích. Điều này có nghĩa là ví dụ `mov eax, [0x5fc53e]` và ví dụ dưới đây sẽ có cùng kết quả.

```nasm
mov ebx, 0x5fc53e
mov eax, [ebx]
```

### Flags

<figure><img src="/files/5qD57OaAi9KWueBT8oBf" alt=""><figcaption></figcaption></figure>

Đối với thao tác trừ, cờ Zero Flag (ZF) được đặt nếu kết quả của phép trừ là zero. Nếu đích nhỏ hơn giá trị bị trừ, thì cờ Carry Flag (CF) được đặt.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://viettaliii.gitbook.io/home/education/reverse-engineering/important-x86-x64-assembly.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
