# Code patterns

## 1. Method

Để hiểu rõ mối quan hệ giữa mã nguồn cấp cao và mã máy cấp thấp. Có thể sử dụng phương pháp xem xét đầu ra assembly sau khi biên dịch.

* **Kỹ thuật học tập**: việc biên dịch code nhỏ và xem output assembly là một cách học hiệu quả.
* **Bài tập**: khuyến khích chúng ta tối ưu hóa code assembly để hiểu sâu hơn.
* **Thông tin gỡ lỗi**: code debug chứa thông tin liên kết giữa mã nguồn và mã máy, hữu ích cho việc gỡ lỗi.

## 2. Some basics

### 2.1 Giới thiệu ngắn gọn về CPU

CPU là thiết bị thực thi mã máy mà một chương trình bao gồm.

Một bảng chú giải ngắn:

* **Lệnh (Instruction):** Một lệnh cơ bản của CPU. Các ví dụ đơn giản nhất bao gồm: di chuyển dữ liệu giữa các thanh ghi, làm việc với bộ nhớ, các phép toán số học cơ bản. Thông thường, mỗi CPU có một kiến trúc tập lệnh (instruction set architecture - ISA) riêng.
* **Mã máy (Machine code):** Mã mà CPU trực tiếp xử lý. Mỗi lệnh thường được mã hóa bằng một vài byte.
* **Ngôn ngữ assembly:** Mã gợi nhớ và một số phần mở rộng như macro được thiết kế để giúp công việc của lập trình viên dễ dàng hơn.
* **Thanh ghi CPU (CPU register):** Mỗi CPU có một tập hợp cố định các thanh ghi đa dụng (GPR). ≈ 8 trong x86, ≈ 16 trong x86-64, ≈ 16 trong ARM. Cách dễ nhất để hiểu một thanh ghi là coi nó như một biến tạm thời không có kiểu dữ liệu. Hãy tưởng tượng bạn đang làm việc với một ngôn ngữ lập trình (PL) cấp cao và chỉ có thể sử dụng tám biến 32-bit (hoặc 64-bit). Nhưng bạn vẫn có thể làm được rất nhiều điều chỉ với những biến này!

### 2.2 Hệ thống số

* **Hệ thập phân (Decimal):** Hệ thống số cơ bản mà con người sử dụng (cơ số 10).
* **Hệ nhị phân (Binary):** Hệ thống số cơ bản của máy tính (cơ số 2), sử dụng 0 và 1.
* **Hệ thập lục phân (Hexadecimal):** Hệ thống số cơ số 16, sử dụng các chữ số 0-9 và các chữ cái A-F, thường được sử dụng để biểu diễn các số nhị phân một cách ngắn gọn.
* **Ký hiệu vị trí:** Giá trị của một chữ số phụ thuộc vào vị trí của nó trong số.
* **Chuyển đổi cơ số:** Các số có thể được chuyển đổi giữa các cơ số khác nhau mà không thay đổi giá trị của chúng, chỉ thay đổi cách biểu diễn.
* **Sự cần thiết của hệ nhị phân và thập lục phân:** Các số nhị phân cồng kềnh, vì vậy các số thập lục phân được sử dụng để biểu diễn chúng ngắn gọn hơn.
* **Cách nhận biết các cơ số khác nhau:** Các tiền tố (0b, 0x) và hậu tố (d, b, h) được sử dụng để xác định cơ số của một số.

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

#### 2.2.1 Cơ số bát phân (octal radix)

* **Định nghĩa:** Hệ thống số bát phân sử dụng 8 chữ số từ 0 đến 7 và mỗi chữ số tương ứng với 3 bit nhị phân.
* **Tiện ích `chmod`:** Một ví dụ thực tế về việc sử dụng số bát phân là trong tiện ích `chmod` của \*NIX, nơi các quyền truy cập tệp được biểu diễn bằng các số bát phân.
* **Cấu trúc `chmod`:** Các số bát phân trong `chmod` được chia thành 3 chữ số, đại diện cho quyền của chủ sở hữu, nhóm và những người khác, mỗi chữ số tương ứng với các bit quyền đọc/ghi/thực thi.

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

#### 2.2.2 Tính chia hết

Tính chất chia hết của các số trong các hệ thống số khác nhau, và cách nó có thể được sử dụng để suy luận nhanh về kích thước và vị trí của các khối dữ liệu trong bộ nhớ:

* **Quy tắc chia hết bằng 10, 100, v.v. trong hệ thập phân:** Nếu một số thập phân kết thúc bằng một hoặc nhiều số 0, nó chia hết cho 10, 100, v.v. tương ứng.
* **Quy tắc chia hết tương tự trong hệ thập lục phân và nhị phân:** Tương tự, một số thập lục phân kết thúc bằng một hoặc nhiều số 0 thập lục phân (ví dụ: `0x0`, `0x00`, `0x000`) thì chia hết cho 16, 256, 4096, v.v. tương ứng. Một số nhị phân kết thúc bằng một hoặc nhiều số 0 nhị phân (ví dụ: `0b0`, `0b00`, `0b000`) thì chia hết cho 2, 4, 8, v.v. tương ứng.

## 3. The simplest Function

Dưới đây là hàm đơn giản nhất là hàm chỉ trả về một giá trị hằng số:

```c
int f()
{
    return 123;
};
```

Hãy biên dịch nó!

### 3.1 x86

Đây là những gì cả trình biên dịch GCC và MSVC tối ưu hóa tạo ra trên nền tảng x86:

```nasm
f:
    mov eax, 123
    ret
```

Chỉ có hai lệnh: lệnh đầu tiên đặt giá trị 123 vào thanh ghi `EAX`, thanh ghi này theo quy ước được sử dụng để lưu trữ giá trị trả về, và lệnh thứ hai là `RET`, lệnh này trả lại quyền thực thi cho nơi gọi (caller).

Nơi gọi sẽ lấy kết quả từ thanh ghi `EAX`.

### 3.2 ARM

Có một vài sự khác biệt trên nền tảng ARM:

```armasm
f PROC
    MOV r0,#0x7b ; 123
    BX lr
ENDP
```

ARM sử dụng thanh ghi `R0` để trả về kết quả của các hàm, vì vậy 123 được sao chép vào `R0`.

Địa chỉ trả về không được lưu trên stack cục bộ trong ISA ARM, mà thay vào đó là trong thanh ghi liên kết (link register), vì vậy lệnh `BX LR` khiến việc thực thi nhảy đến địa chỉ đó—thực chất là trả lại quyền thực thi cho nơi gọi.

Điều đáng chú ý là `MOV` là một tên gây hiểu nhầm cho lệnh trong cả ISA x86 và ARM. Trên thực tế, dữ liệu không bị di chuyển mà là *<mark style="color:red;">được sao chép</mark>*.

## 4. Hello, world!

Sử dụng ví dụ nổi tiếng sau:

```c
#include <stdio.h>
int main()
{
    printf("hello, world\n");
    return 0;
}
```

### 4.1 x86

#### 4.1.1 **MSVC**

Hãy biên dịch nó trong MSVC 2010:

```c
cl 1.cpp /Fa1.asm
```

Code MSVC 2010

```nasm
CONST SEGMENT
$SG3830 DB 'hello, world', 0AH, 00H
CONST ENDS
PUBLIC _main
EXTRN _printf:PROC
; Function compile flags: /Odtp
_TEXT SEGMENT
_main PROC
    push ebp
    mov ebp, esp
    push OFFSET $SG3830
    call _printf
    add esp, 4
    xor eax, eax
    pop ebp
    ret 0
_main ENDP
_TEXT ENDS
```

MSVC tạo ra các danh sách assembly theo cú pháp Intel.

Trình biên dịch đã tạo ra tệp `1.obj`, tệp này sẽ được liên kết thành `1.exe`. Trong trường hợp của chúng ta, tệp chứa hai segment: `CONST` (cho các hằng số dữ liệu) và `_TEXT` (cho mã).

Chuỗi "hello, world" trong C/C++ có kiểu `const char[]`, nhưng nó không có tên riêng. Trình biên dịch cần xử lý chuỗi này bằng cách nào đó nên nó định nghĩa tên nội bộ `$SG3830` cho nó.

Đó là lý do tại sao ví dụ có thể được viết lại như sau:

```c
#include <stdio.h>
const char $SG3830[]="hello, world\n";
int main()
{
    printf($SG3830);
    return 0;
}
```

Hãy quay lại danh sách assembly. Như chúng ta có thể thấy, chuỗi được kết thúc bằng một byte zero, đây là tiêu chuẩn cho các chuỗi C/C++.

Trong code segment `_TEXT`, hiện tại chỉ có một hàm: `main()`. Hàm `main()` bắt đầu bằng mã prologue (PROC) và kết thúc bằng mã epilogue (ENDP) (giống như hầu hết mọi hàm).

Sau prologue của hàm, chúng ta thấy lệnh gọi hàm `printf()`: `CALL _printf`. Trước khi gọi, địa chỉ chuỗi (hoặc một con trỏ đến nó) chứa lời chào của chúng ta được đặt lên stack với sự trợ giúp của lệnh `PUSH`.

Khi hàm `printf()` trả lại quyền điều khiển cho hàm `main()`, địa chỉ chuỗi (hoặc con trỏ đến nó) vẫn còn trên stack. Vì chúng ta không cần nó nữa, con trỏ stack (thanh ghi ESP) cần được điều chỉnh.

`ADD ESP, 4` có nghĩa là cộng 4 vào giá trị thanh ghi ESP.

Tại sao lại là 4? Vì đây là một chương trình 32-bit, chúng ta cần chính xác 4 byte để truyền địa chỉ qua stack. Nếu là code x64, chúng ta sẽ cần 8 byte. `ADD ESP, 4` về cơ bản tương đương với `POP register` nhưng không sử dụng bất kỳ thanh ghi nào.

Với cùng mục đích, một số trình biên dịch (như Intel C++ Compiler) có thể phát ra `POP ECX` thay vì `ADD` (ví dụ: một mẫu như vậy có thể được quan sát thấy trong code Oracle RDBMS khi nó được biên dịch bằng trình biên dịch Intel C++). Lệnh này có hiệu quả gần như tương tự nhưng nội dung thanh ghi ECX sẽ bị ghi đè. Trình biên dịch Intel C++ có thể sử dụng `POP ECX` vì opcode của lệnh này ngắn hơn `ADD ESP, x` (1 byte cho `POP` so với 3 byte cho `ADD`).

Đây là một ví dụ về việc sử dụng `POP` thay vì `ADD` từ Oracle RDBMS:

```nasm
.text:0800029A push ebx
.text:0800029B call qksfroChild
.text:080002A0 pop ecx
```

Sau khi gọi `printf()`, code C/C++ gốc chứa câu lệnh `return 0` - trả về 0 làm kết quả của hàm `main()`.

Trong code đã tạo, điều này được thực hiện bằng lệnh `XOR EAX, EAX`.

`XOR` thực tế chỉ là "eXclusive OR" nhưng các trình biên dịch thường sử dụng nó thay vì `MOV EAX, 0` - lại một lần nữa vì nó có opcode ngắn hơn một chút (2 byte cho `XOR` so với 5 byte cho `MOV`).

Một số trình biên dịch phát ra `SUB EAX, EAX`, có nghĩa là trừ giá trị trong EAX khỏi giá trị trong EAX, điều này, trong mọi trường hợp, đều cho kết quả là zero.

Lệnh cuối cùng `RET` trả lại quyền điều khiển cho nơi gọi caller. Thông thường, đây là code C/C++ CRT (C runtime library), sau đó, lần lượt trả lại quyền điều khiển cho hệ điều hành.

#### 4.1.2 GCC

Bây giờ, hãy thử biên dịch cùng một code C/C++ trong trình biên dịch GCC 4.4.1 trên Linux:&#x20;

```bash
gcc 1.c -o 1
```

Tiếp theo, với sự hỗ trợ của trình disassembler IDA, hãy xem hàm `main()` đã được tạo như thế nào. IDA, giống như MSVC, sử dụng cú pháp Intel.

```nasm
main proc near
    var_10 = dword ptr -10h
    push ebp
    mov ebp, esp
    and esp, 0FFFFFFF0h
    sub esp, 10h
    mov eax, offset aHelloWorld ; "hello, world\n"
    mov [esp+10h+var_10], eax
    call _printf
    mov eax, 0
    leave
    retn
main endp
```

Kết quả gần như tương tự. Địa chỉ của chuỗi "hello, world" (được lưu trong data segment) được load vào thanh ghi EAX trước và sau đó nó được lưu vào stack.&#x20;

Ngoài ra, prologue của hàm chứa `AND ESP, 0FFFFFFF0h` - lệnh này căn chỉnh giá trị thanh ghi ESP trên ranh giới 16 byte. Điều này dẫn đến tất cả các giá trị trên stack đều được căn chỉnh theo cùng một cách (CPU hoạt động tốt hơn nếu các giá trị nó đang xử lý được đặt trong bộ nhớ tại các địa chỉ được căn chỉnh trên ranh giới 4 byte hoặc 16 byte).

`SUB ESP, 10h` cấp phát 16 byte trên stack. Mặc dù, như chúng ta có thể thấy sau này, chỉ cần 4 byte ở đây. Điều này là do kích thước của stack được cấp phát cũng được căn chỉnh trên ranh giới 16 byte.

Địa chỉ chuỗi (hoặc con trỏ đến chuỗi) sau đó được lưu trực tiếp vào stack mà không cần sử dụng lệnh `PUSH`. `var_10` - là một biến cục bộ và cũng là một đối số cho `printf()`. Đọc về nó bên dưới.

Sau đó, hàm `printf()` được gọi.

Không giống như MSVC, khi GCC biên dịch mà không bật tối ưu hóa, nó phát ra `MOV EAX, 0` thay vì một opcode ngắn hơn.

Lệnh cuối cùng, `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ỏ stack (`ESP`) trở lại và khôi phục thanh ghi `EBP` về trạng thái ban đầu. Điều này là cần thiết vì chúng ta đã sửa đổi các giá trị thanh ghi này (`ESP` và `EBP`) ở đầu hàm (bằng cách thực thi `MOV EBP, ESP / AND ESP, ...`).

#### 4.1.3 GCC: AT\&T syntax

Hãy xem cách điều này có thể được biểu diễn bằng ngôn ngữ assembly cú pháp AT\&T. Cú pháp này phổ biến hơn nhiều trong thế giới UNIX.

```bash
gcc -S 1_1.c
```

Chúng ta nhận được điều này:

```nasm
        .file "1_1.c"
        .section .rodata
.LC0:
        .string "hello, world\n"
        .text
        .globl main
        .type main, @function
main:
.LFB0:
        .cfi_startproc
        pushl %ebp
        .cfi_def_cfa_offset 8
        .cfi_offset 5, -8
        movl %esp, %ebp
        .cfi_def_cfa_register 5
        andl $-16, %esp
        subl $16, %esp
        movl $.LC0, (%esp)
        call printf
        movl $0, %eax
        leave
        .cfi_restore 5
        .cfi_def_cfa 4, 4
        ret
        .cfi_endproc
.LFE0:
        .size main, .-main
        .ident "GCC: (Ubuntu/Linaro 4.7.3-1ubuntu1) 4.7.3"
        .section .note.GNU-stack,"",@progbits
```

Danh sách chứa nhiều macro (bắt đầu bằng dấu chấm). (sẽ tìm hiểu sau)

Hiện tại, để đơn giản hóa, chúng ta có thể bỏ qua chúng (ngoại trừ macro `.string` mã hóa một chuỗi ký tự kết thúc bằng null giống như một chuỗi C). Sau đó, chúng ta sẽ thấy điều này:

```nasm
.LC0:
    .string "hello, world\n"
main:
    pushl %ebp
    movl %esp, %ebp
    andl $-16, %esp
    subl $16, %esp
    movl $.LC0, (%esp)
    call printf
    movl $0, %eax
    leave
    ret
```

Một số khác biệt chính giữa cú pháp Intel và AT\&T là:

* **Toán hạng nguồn và đích được viết theo thứ tự ngược lại.**

  Trong cú pháp Intel: `<lệnh> <toán hạng đích> <toán hạng nguồn>`.

  Trong cú pháp AT\&T: `<lệnh> <toán hạng nguồn> <toán hạng đích>`.

  Đây là một cách dễ dàng để ghi nhớ sự khác biệt: khi bạn làm việc với cú pháp Intel, bạn có thể tưởng tượng rằng có một dấu bằng (=) giữa các toán hạng và khi bạn làm việc với cú pháp AT\&T, hãy tưởng tượng có một mũi tên sang phải (→).
* **AT\&T:** Trước tên thanh ghi, một dấu phần trăm phải được viết (%) và trước các số là dấu đô la ($). Dấu ngoặc đơn được sử dụng thay vì dấu ngoặc vuông.
* **AT\&T:** Một hậu tố được thêm vào các lệnh để xác định kích thước toán hạng:
  * `q` — quad (64 bit)
  * `l` — long (32 bit)
  * `w` — word (16 bit)
  * `b` — byte (8 bit)

Hãy quay lại kết quả đã biên dịch: nó giống hệt với những gì chúng ta đã thấy trong IDA. Với một sự khác biệt tinh tế: `0FFFFFFF0h` được biểu diễn là `$-16`. Nó là cùng một thứ: 16 trong hệ thập phân là 0x10 trong hệ thập lục phân. -0x10 bằng `0xFFFFFFF0` (cho một kiểu dữ liệu 32-bit).

Một điều nữa: giá trị trả về được đặt thành 0 bằng cách sử dụng lệnh `MOV` thông thường, không phải `XOR`. `MOV` chỉ load một giá trị vào một thanh ghi. Tên của nó là một tên sai (dữ liệu không bị di chuyển mà là sao chép). Trong các kiến trúc khác, lệnh này được đặt tên là "LOAD" hoặc "STORE" hoặc một cái gì đó tương tự.

### 4.2 x86-64

#### 4.2.1 MSVC-x86-64

Hãy thử với MSVC 64-bit:

```nasm
$SG2989 DB 'hello, world', 0AH, 00H
main    PROC
        sub rsp, 40
        lea rcx, OFFSET FLAT:$SG2989
        call printf
        xor eax, eax
        add rsp, 40
        ret 0
main    ENDP
```

Trong x86-64, tất cả các thanh ghi đều được mở rộng lên 64-bit và giờ tên của chúng có tiền tố `R-`. Để sử dụng stack ít hơn (nói cách khác, để truy cập bộ nhớ ngoài/cache ít hơn), có một cách phổ biến để truyền các đối số hàm thông qua các thanh ghi (fastcall). Nghĩa là, một phần đối số hàm được truyền trong các thanh ghi, phần còn lại—thông qua stack.

Trong Win64, 4 đối số hàm được truyền trong các thanh ghi `RCX`, `RDX`, `R8`, `R9`. Đó là những gì chúng ta thấy ở đây: một con trỏ đến chuỗi cho `printf()` bây giờ không được truyền trong stack mà trong thanh ghi `RCX`. Các con trỏ bây giờ là 64-bit, vì vậy chúng được truyền trong các thanh ghi 64-bit (có tiền tố `R-`). Tuy nhiên, để tương thích ngược, vẫn có thể truy cập các phần 32-bit, bằng cách sử dụng tiền tố `E-`. Đây là cách thanh ghi `RAX` / `EAX` / `AX` / `AL` trông như thế nào trong x86-64:

<figure><img src="/files/9wk9QLZS8uxI2pO60Jtw" alt=""><figcaption></figcaption></figure>

Hàm `main()` trả về một giá trị kiểu `int`, trong C/C++, để tương thích ngược và tính di động tốt hơn, vẫn là 32-bit, vì vậy đó là lý do tại sao thanh ghi `EAX` bị xóa ở cuối hàm (tức là phần 32-bit của thanh ghi) thay vì `RAX`.

Cũng có 40 byte được cấp phát trong stack cục bộ. Điều này được gọi là "shadow space".

#### 4.2.2 GCC-x86-64

Hãy thử với GCC trên Linux 64-bit:

```nasm
.string "hello, world\n"
main:
        sub rsp, 8
        mov edi, OFFSET FLAT:.LC0 ; "hello, world\n"
        xor eax, eax ; number of vector registers passed
        call printf
        xor eax, eax
        add rsp, 8
        ret
```

6 đối số đầu tiên được truyền trong các thanh ghi `RDI`, `RSI`, `RDX`, `RCX`, `R8`, `R9` và phần còn lại - thông qua stack.

Vì vậy, con trỏ đến chuỗi được truyền trong `EDI` (phần 32-bit của thanh ghi). Nhưng tại sao không sử dụng phần 64-bit, `RDI`?

Điều quan trọng cần ghi nhớ là tất cả các lệnh `MOV` trong chế độ 64-bit mà ghi một cái gì đó vào phần thanh ghi 32-bit thấp hơn cũng xóa các bit 32-bit cao hơn. Tức là, `MOV EAX, 011223344h` ghi một giá trị vào `RAX` một cách chính xác, vì các bit cao hơn sẽ bị xóa.

Nếu chúng ta mở tệp đối tượng đã biên dịch (`.o`), chúng ta cũng có thể thấy opcode của tất cả các lệnh:

```nasm
.text:00000000004004D0                     main proc near
.text:00000000004004D0 48 83 EC 08         sub rsp, 8
.text:00000000004004D4 BF E8 05 40 00      mov edi, offset format ; "hello, world\n"
.text:00000000004004D9 31 C0               xor eax, eax
.text:00000000004004DB E8 D8 FE FF FF      call _printf
.text:00000000004004E0 31 C0               xor eax, eax
.text:00000000004004E2 48 83 C4 08         add rsp, 8
.text:00000000004004E6 C3                  retn
.text:00000000004004E6                     main endp
```

Như chúng ta có thể thấy, lệnh ghi vào `EDI` tại `0x4004D4` chiếm 5 byte. Cùng lệnh đó mà ghi một giá trị 64-bit vào `RDI` chiếm 7 byte. Rõ ràng, GCC đang cố gắng tiết kiệm một chút không gian. Bên cạnh đó, nó có thể chắc chắn rằng data segment chứa chuỗi sẽ không được cấp phát tại các địa chỉ cao hơn 4GiB.

Chúng ta cũng thấy rằng thanh ghi `EAX` đã được xóa trước khi gọi hàm `printf()`. Điều này được thực hiện bởi vì theo tiêu chuẩn ABI đã đề cập ở trên, số lượng thanh ghi vector đã sử dụng được truyền trong `EAX` trong các hệ thống \*NIX trên x86-64.

### 4.3 GCC-one more thing

Việc một chuỗi C vô danh có kiểu `const` và các chuỗi C được cấp phát trong segment hằng số được đảm bảo là bất biến, có một hệ quả thú vị: trình biên dịch có thể sử dụng một phần cụ thể của chuỗi.

Hãy thử ví dụ sau:

```c
#include <stdio.h>
int f1()
{
    printf ("world\n");
}
int f2()
{
    printf ("hello world\n");
}
int main()
{
    f1();
    f2();
}
```

Các trình biên dịch C/C++ phổ biến (bao gồm cả MSVC) cấp phát hai chuỗi, nhưng hãy xem GCC 4.8.1 làm gì:

```nasm
f1      proc near
        s = dword ptr -1Ch
        sub esp, 1Ch
        mov [esp+1Ch+s], offset s    ; "world\n"
        call _puts
        add esp, 1Ch
        retn
f1      endp
f2      proc near
        s = dword ptr -1Ch
        sub esp, 1Ch
        mov [esp+1Ch+s], offset aHello    ; "hello "
        call _puts
        add esp, 1Ch
        retn
f2      endp
aHello  db 'hello '
s       db 'world',0xa,0
```

Thật vậy: khi chúng ta in chuỗi "hello world", hai từ này được đặt liền nhau trong bộ nhớ và `puts()` được gọi từ hàm `f2()` không nhận ra rằng chuỗi này bị chia. Trên thực tế, nó không bị chia; nó chỉ bị chia "ảo", trong danh sách này.

Khi `puts()` được gọi từ `f1()`, nó sử dụng chuỗi "world" cộng với một byte zero. `puts()` không nhận ra rằng có gì đó ở trước chuỗi này!

Thủ thuật thông minh này thường được sử dụng bởi ít nhất là GCC và có thể tiết kiệm một chút bộ nhớ.

## 5. Function prologue and epilogue

Một function prologue (khúc dạo đầu hàm) là một chuỗi các lệnh ở đầu một hàm. Nó thường có dạng như đoạn code sau:

```nasm
push ebp
mov ebp, esp
sub esp, X
```

Các lệnh này làm những việc sau: lưu giá trị trong thanh ghi EBP, đặt giá trị của thanh ghi EBP thành giá trị của ESP và sau đó cấp phát không gian trên stack cho các biến cục bộ.

Giá trị trong EBP vẫn giữ nguyên trong suốt thời gian thực thi của hàm và được sử dụng để truy cập các biến cục bộ và đối số. Với cùng mục đích, người ta có thể sử dụng ESP, nhưng vì nó thay đổi theo thời gian, cách tiếp cận này không quá thuận tiện.

Function epilogue (khúc kết hàm) giải phóng không gian đã cấp phát trong stack, trả giá trị trong thanh ghi EBP về trạng thái ban đầu và trả lại luồng điều khiển cho nơi gọi (callee):

```nasm
mov esp, ebp
pop ebp
ret 0
```

Function prologues và epilogues thường được phát hiện trong các disassembler để phân định hàm.

Vai trò của chúng là quản lý stack frame và luồng thực thi của chương trình. Nó cũng làm rõ lý do tại sao EBP thường được sử dụng để truy cập biến cục bộ thay bì ESP và tại sao disassembler có thể dựa vào prologue và epilogue để xác định ranh giới hàm.

## 6. Stack

**Stack** là một trong những cấu trúc dữ liệu cơ bản nhất trong khoa học máy tính. Hay còn gọi là LIFO (Last In, First Out - Vào sau, ra trước).

Về mặt kỹ thuật, nó chỉ là một khối bộ nhớ trong bộ nhớ tiến trình cùng với thanh ghi ESP hoặc RSP trong x86 hoặc x64, hoặc thanh ghi SP trong ARM, như một con trỏ bên trong khối đó.

Các lệnh truy cập stack được sử dụng thường xuyên nhất là `PUSH` và `POP` (trong cả x86 và chế độ Thumb của ARM). `PUSH` trừ 4 khỏi ESP / RSP / SP ở chế độ 32-bit (hoặc 8 ở chế độ 64-bit) và sau đó ghi nội dung của toán hạng duy nhất của nó vào địa chỉ bộ nhớ được trỏ bởi ESP / RSP / SP.

`POP` là thao tác ngược lại: truy xuất dữ liệu từ vị trí bộ nhớ mà SP trỏ đến, tải nó vào toán hạng của lệnh (thường là một thanh ghi) và sau đó cộng 4 (hoặc 8) vào con trỏ stack.

Sau khi cấp phát stack, con trỏ stack trỏ đến đáy của stack. `PUSH` giảm con trỏ stack và `POP` tăng nó. Đáy của stack thực sự nằm ở đầu bộ nhớ được cấp phát cho khối stack. Nghe có vẻ lạ, nhưng đó là cách nó hoạt động.

### **6.1 Tại sao stack phát triển ngược?**

Theo trực giác, chúng ta có thể nghĩ rằng stack phát triển lên trên, tức là hướng tới các địa chỉ cao hơn, giống như bất kỳ cấu trúc dữ liệu nào khác.

Lý do stack phát triển ngược có lẽ là do lịch sử. Khi máy tính còn lớn và chiếm cả một căn phòng, việc chia bộ nhớ thành hai phần, một cho heap và một cho stack, rất dễ dàng. Tất nhiên, không ai biết heap và stack sẽ lớn như thế nào trong quá trình thực thi chương trình, vì vậy giải pháp này là đơn giản nhất có thể.

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

Điều này nhắc nhở chúng ta về cách một số sinh viên viết hai bài ghi chú bằng cách chỉ sử dụng một cuốn sổ: các ghi chú cho bài giảng đầu tiên được viết như bình thường và các ghi chú cho bài giảng thứ hai được viết từ cuối cuốn sổ, bằng cách lật nó. Các ghi chú có thể gặp nhau ở đâu đó ở giữa, trong trường hợp thiếu không gian trống.

### 6.2 Stack được dùng để làm gì?

#### 6.2.1 Lưu địa chỉ trả về của hàm

**x86**

Khi gọi một hàm khác bằng lệnh `CALL`, địa chỉ của điểm ngay sau lệnh `CALL` được lưu vào stack và sau đó một lệnh nhảy vô điều kiện đến địa chỉ trong toán hạng của `CALL` được thực thi.

Lệnh `CALL` tương đương với cặp lệnh `PUSH địa_chỉ_sau_call / JMP toán_hạng`.

`RET` lấy một giá trị từ stack và nhảy đến nó  - điều đó tương đương với cặp lệnh `POP tmp / JMP tmp`.

Việc làm tràn stack rất đơn giản. Chỉ cần chạy đệ quy vô tận:

```c
void f()
{
    f();
};
```

MSVC 2008 báo cáo vấn đề:

{% code overflow="wrap" %}

```
c:\tmp6>cl ss.cpp /Fass.asm
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.21022.08 for 80x86
Copyright (C) Microsoft Corporation. All rights reserved.
ss.cpp
c:\tmp6\ss.cpp(4) : warning C4717: 'f' : recursive on all control paths, function will cause 
runtime stack overflow
```

{% endcode %}

nhưng vẫn tạo ra code đúng:

```nasm
?f@@YAXXZ PROC    ; f
; File c:\tmp6\ss.cpp
; Line 2
        push    ebp
        mov     ebp, esp
; Line 3
        call    ?f@@YAXXZ    ; f
; Line 4
        pop     ebp
        ret     0
?f@@YAXXZ ENDP    ; f
```

Ngoài ra, nếu chúng ta bật tối ưu hóa trình biên dịch (tùy chọn `/Ox`), code tối ưu hóa sẽ không làm tràn stack và sẽ hoạt động chính xác:

```nasm
?f@@YAXXZ PROC    ; f
; File c:\tmp6\ss.cpp
; Line 2
$LL3@f:
; Line 3
    jmp     SHORT $LL3@f
?f@@YAXXZ ENDP    ; f
```

GCC 4.4.1 tạo ra code tương tự trong cả hai trường hợp mà không đưa ra bất kỳ cảnh báo nào về vấn đề.

#### **6.2.2 Truyền đối số hàm (passing function arguments)**

Cách phổ biến nhất để truyền tham số trong x86 được gọi là "cdecl":

```nasm
push arg3
push arg2
push arg1
call f
add esp, 12 ; 4*3=12
```

Các hàm được gọi (callee) nhận các đối số của chúng thông qua con trỏ stack.

Do đó, đây là cách các giá trị đối số được đặt trong stack trước khi thực thi lệnh đầu tiên của hàm `f()`:

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

Nhân tiện, hàm được gọi (callee) không có bất kỳ thông tin nào về việc có bao nhiêu đối số đã được truyền. Các hàm C có số lượng đối số thay đổi (như `printf()`) xác định số lượng của chúng bằng cách sử dụng các bộ định dạng chuỗi (bắt đầu bằng ký tự %).

Nếu chúng ta viết một cái gì đó như:

```cpp
printf("%d %d %d", 1234);
```

`printf()` sẽ in ra `1234`, và sau đó là hai số ngẫu nhiên, nằm ngay bên cạnh nó trong stack.

Đó là lý do tại sao không quan trọng lắm việc chúng ta khai báo hàm `main()` như thế nào: như `main()`, `main(int argc, char *argv[])` hoặc `main(int argc, char *argv[], char *envp[])`.

Trên thực tế, code CRT đang gọi `main()` đại khái như sau:

```nasm
push envp
push argv
push argc
call main
...
```

Nếu bạn khai báo `main()` là `main()` mà không có đối số, thì chúng vẫn có mặt trong stack, nhưng không được sử dụng. Nếu bạn khai báo `main()` là `main(int argc, char *argv[])`, bạn sẽ có thể sử dụng hai đối số đầu tiên và đối số thứ ba sẽ vẫn "vô hình" đối với hàm của bạn. Thậm chí, có thể khai báo `main(int argc)` và nó vẫn sẽ hoạt động.

**Các cách truyền đối số thay thế**

Điều đáng chú ý là không có gì bắt buộc các lập trình viên phải truyền đối số thông qua stack. Nó không phải là một yêu cầu bắt buộc. Người ta có thể triển khai bất kỳ phương pháp nào khác mà không cần sử dụng stack.

Một cách khá phổ biến trong số những người mới làm quen với ngôn ngữ assembly là truyền đối số thông qua các biến toàn cục, như:

```nasm
...
mov X, 123
mov Y, 456
call do_something
...
X dd ?
Y dd ?
do_something proc near
    ; lấy X
    ; lấy Y
    ; làm gì đó
    retn
do_something endp
```

Nhưng phương pháp này có nhược điểm rõ ràng: hàm `do_something()` không thể tự gọi đệ quy (hoặc thông qua một hàm khác), vì nó phải ghi đè các đối số của chính nó. Câu chuyện tương tự với các biến cục bộ: nếu giữ chúng trong các biến toàn cục, hàm sẽ không thể tự gọi chính nó. Và điều này cũng không an toàn cho luồng. Một phương pháp để lưu trữ thông tin như vậy trong stack làm cho điều này dễ dàng hơn—nó có thể chứa nhiều đối số hàm và/hoặc giá trị, nhiều không gian như nó có.

MS-DOS có cách truyền tất cả các đối số hàm thông qua các thanh ghi, ví dụ, đây là một đoạn code cho MS-DOS 16-bit cổ in ra "Hello, world!":

```nasm
mov dx, msg ; địa chỉ của thông báo
mov ah, 9 ; 9 có nghĩa là hàm "in chuỗi"
int 21h ; "syscall" của DOS
mov ah, 4ch ; hàm "kết thúc chương trình"
int 21h ; "syscall" của DOS
msg db 'Hello, World!\$'
```

Nếu một hàm MS-DOS sẽ trả về một giá trị boolean (tức là một bit duy nhất, thường biểu thị trạng thái lỗi), thì flag `CF` thường được sử dụng.

Ví dụ:"

```nasm
mov ah, 3ch ; tạo tệp
lea dx, filename
mov cl, 1
int 21h
jc error
mov file_handle, ax
...
error:
...
```

Trong trường hợp có lỗi, flag `CF` được nâng lên. Nếu không, handle của tệp mới tạo sẽ được trả về thông qua `AX`.

#### **6.2.3 Lưu trữ biến cục bộ**

Một hàm có thể cấp phát không gian trong stack cho các biến cục bộ của nó chỉ bằng cách giảm con trỏ stack (stack pointer) về phía đáy stack.

Do đó, nó rất nhanh, bất kể có bao nhiêu biến cục bộ được định nghĩa. Việc lưu trữ các biến cục bộ trong stack cũng không phải là một yêu cầu bắt buộc. Bạn có thể lưu trữ các biến cục bộ ở bất cứ đâu bạn thích, nhưng theo truyền thống thì đây là cách nó được thực hiện.

**6.2.4 x86: Hàm alloca()**

Điều đáng chú ý là hàm `alloca()`. Hàm này hoạt động giống như `malloc()`, nhưng cấp phát bộ nhớ trực tiếp trên stack. Khối bộ nhớ đã cấp phát không cần phải được giải phóng thông qua lệnh gọi hàm `free()`, vì function epilogue trả lại `ESP` về trạng thái ban đầu của nó và bộ nhớ đã cấp phát sẽ bị bỏ qua.

Điều đáng chú ý là cách `alloca()` được triển khai. Nói một cách đơn giản, hàm này chỉ cần dịch chuyển `ESP` xuống dưới về phía đáy stack theo số byte bạn cần và đặt `ESP` làm con trỏ đến khối đã cấp phát.

Hãy thử:

```c
#ifdef __GNUC__
#include <alloca.h> // GCC
#else
#include <malloc.h> // MSVC
#endif
#include <stdio.h>
void f()
{
    char *buf=(char*)alloca (600);
    #ifdef __GNUC__
        snprintf (buf, 600, "hi! %d, %d, %d\n", 1, 2, 3); // GCC
    #else
        _snprintf (buf, 600, "hi! %d, %d, %d\n", 1, 2, 3); // MSVC
    #endif
    puts (buf);
};
```

Hàm `_snprintf()` hoạt động giống như `printf()`, nhưng thay vì kết xuất kết quả vào stdout (ví dụ: vào terminal hoặc console), nó ghi kết quả vào buffer `buf`. Hàm `puts()` sao chép nội dung của `buf` vào stdout. Tất nhiên, hai lệnh gọi hàm này có thể được thay thế bằng một lệnh gọi `printf()`, nhưng chúng ta phải minh họa việc sử dụng buffer nhỏ.

**MSVC**

Hãy biên dịch (MSVC 2010)

```nasm
...
    mov     eax, 600         ; 00000258H
    call    __alloca_probe_16
    mov     esi, esp
    push    3
    push    2
    push    1
    push    OFFSET $SG2672
    push    600            ; 00000258H
    push    esi
    call    __snprintf
    push    esi
    call    _puts
    add     esp, 28         ; 0000001cH
...
```

Đối số duy nhất của `alloca()` được truyền thông qua `EAX` (thay vì đẩy nó vào stack).

**GCC + Intel syntax**

GCC 4.4.1 thực hiện tương tự mà không cần gọi các hàm bên ngoài:

```
.LC0:
    .string "hi! %d, %d, %d\n"
f:
    push    ebp
    mov     ebp, esp
    push    ebx
    sub     esp, 660
    lea     ebx, [esp+39]
    and     ebx, -16 ; căn chỉnh con trỏ theo biên 16 bit
    mov     DWORD PTR [esp], ebx ; s
    mov     DWORD PTR [esp+20], 3
    mov     DWORD PTR [esp+16], 2
    mov     DWORD PTR [esp+12], 1
    mov     DWORD PTR [esp+8], OFFSET FLAT:.LC0 ; "hi! %d, %d, %d\n"
    mov     DWORD PTR [esp+4], 600 ; maxlen
    call    _snprintf
    mov     DWORD PTR [esp], ebx ; s
    call    puts
    mov     ebx, DWORD PTR [ebp-4]
    leave
    ret
```

**6.2.5 (Windows) SEH**

Các bản ghi SEH cũng được lưu trữ trên stack (nếu chúng có mặt).

**6.2.6 Bảo vệ chống tràn buffer**

**6.2.7 Tự động giải phóng dữ liệu trong stack**

Có lẽ lý do để lưu trữ các biến cục bộ và các bản ghi SEH trong stack là vì chúng được giải phóng tự động khi hàm kết thúc, chỉ bằng một lệnh để điều chỉnh con trỏ stack (thường là `ADD`). Các đối số hàm, như chúng ta có thể nói, cũng được giải phóng tự động ở cuối hàm. Ngược lại, mọi thứ được lưu trữ trong heap phải được giải phóng một cách rõ ràng.

### **6.3 Bố cục stack điển hình**

Một bố cục stack điển hình trong môi trường 32-bit ở đầu một hàm, trước khi thực thi lệnh đầu tiên, trông như sau:

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

### 6.4 Noise in stack (nhiễu, rác trong stack)

Chúng đến từ đâu? Đây là những gì còn sót lại sau khi các hàm khác thực thi. Một ví dụ ngắn:

```c
#include <stdio.h>
void f1()
{
    int a=1, b=2, c=3;
};
void f2()
{
    int a, b, c;
    printf ("%d, %d, %d\n", a, b, c);
};
int main()
{
    f1();
    f2();
};
```

Biên dịch MSVC 2010 không tối ưu hóa:

```nasm
$SG2752     DB '%d, %d, %d', 0aH, 00H
_c$ = -12 ; size = 4
_b$ = -8 ; size = 4
_a$ = -4 ; size = 4
_f1         PROC
            push ebp
            mov ebp, esp
            sub esp, 12
            mov DWORD PTR _a$[ebp], 1
            mov DWORD PTR _b$[ebp], 2
            mov DWORD PTR _c$[ebp], 3
            mov esp, ebp
            pop ebp
            ret 0
_f1         ENDP
_c$ = -12 ; size = 4
_b$ = -8 ; size = 4
_a$ = -4 ; size = 4
_f2         PROC
            push ebp
            mov ebp, esp
            sub esp, 12
            mov eax, DWORD PTR _c$[ebp]
            push eax
            mov ecx, DWORD PTR _b$[ebp]
            push ecx
            mov edx, DWORD PTR _a$[ebp]
            push edx
            push OFFSET $SG2752  ; '%d, %d, %d'
            call DWORD PTR __imp__printf
            add esp, 16
            mov esp, ebp
            pop ebp
            ret 0
_f2         ENDP
_main       PROC
            push ebp
            mov ebp, esp
            call _f1
            call _f2
            xor eax, eax
            pop ebp
            ret 0
_main       ENDP
```

Trình biên dịch sẽ càu nhàu một chút …

```
c:\Polygon\c>cl st.c /Fast.asm /MD
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
Copyright (C) Microsoft Corporation. All rights reserved.
st.c
c:\polygon\c\st.c(11) : warning C4700: uninitialized local variable 'c' used
c:\polygon\c\st.c(11) : warning C4700: uninitialized local variable 'b' used
c:\polygon\c\st.c(11) : warning C4700: uninitialized local variable 'a' used
Microsoft (R) Incremental Linker Version 10.00.40219.01
Copyright (C) Microsoft Corporation. All rights reserved.
/out:st.exe
st.obj
```

Nhưng khi chúng ta chạy chương trình đã biên dịch …

```
c:\Polygon\c>st
1, 2, 3
```

Ồ, thật là một điều kỳ lạ! Chúng ta không đặt bất kỳ biến nào trong `f2()`. Đây là các giá trị "ma", vẫn còn trong stack.

## 7. printf() with several arguments

Bây giờ, hãy mở rộng ví dụ Hello, world!  thay thế `printf()` trong thân hàm `main()` bằng nội dung sau:

```c
#include <stdio.h>
int main()
{
    printf("a=%d; b=%d; c=%d", 1, 2, 3);
    return 0;
}
```

### 7.1 x86

#### 7.1.1 x86: 3 arguments

**MSVC**

Khi chúng ta biên dịch nó bằng MSVC 2010 Express, chúng ta nhận được:

```nasm
$SG3830 DB 'a=%d; b=%d; c=%d', 00H
...
    push    3
    push    2
    push    1
    push    OFFSET $SG3830
    call    _printf
    add     esp, 16       ; 00000010H
```

Gần như giống nhau, nhưng bây giờ chúng ta có thể thấy các đối số của `printf()` được đẩy vào stack theo thứ tự ngược lại. Đối số đầu tiên được đẩy vào sau cùng.

Nhân tiện, các biến kiểu `int` trong môi trường 32-bit có độ rộng 32-bit, tức là 4 byte.

Vì vậy, chúng ta có 4 đối số ở đây. 4 \* 4 = 16 — chúng chiếm chính xác 16 byte trong stack: một con trỏ 32-bit đến một chuỗi và 3 số kiểu `int`.

Khi con trỏ stack (thanh ghi `ESP`) đã thay đổi trở lại bằng lệnh `ADD ESP, X` sau khi gọi hàm, thông thường, số lượng đối số hàm có thể được suy ra bằng cách đơn giản chia X cho 4.

Tất nhiên, điều này là cụ thể đối với quy ước gọi `cdecl`, và chỉ dành cho môi trường 32-bit.

Trong một số trường hợp nhất định, khi một số hàm trả về ngay sau nhau, trình biên dịch có thể hợp nhất nhiều lệnh “`ADD ESP, X`” thành một, sau lần gọi cuối cùng:

```nasm
push a1
push a2
call ...
...
push a1
call ...
...
push a1
push a2
push a3
call ...
add esp, 24
```

Đây là một ví dụ thực tế:

```nasm
.text:100113E7 push 3
.text:100113E9 call sub_100018B0 ; nhận một đối số (3)
.text:100113EE call sub_100019D0 ; không nhận đối số nào
.text:100113F3 call sub_10006A90 ; không nhận đối số nào
.text:100113F8 push 1
.text:100113FA call sub_100018B0 ; nhận một đối số (1)
.text:100113FF add esp, 8 ; loại bỏ hai đối số khỏi stack cùng một lúc
```

**GCC**

Bây giờ, hãy biên dịch cùng chương trình này trong Linux bằng GCC 4.4.1 và xem chúng ta có gì trong IDA:

```nasm
main proc near
    var_10 = dword ptr -10h
    var_C = dword ptr -0Ch
    var_8 = dword ptr -8
    var_4 = dword ptr -4
    push    ebp
    mov     ebp, esp
    and     esp, 0FFFFFFF0h
    sub     esp, 10h
    mov     eax, offset aADBDCD ; "a=%d; b=%d; c=%d"
    mov     [esp+10h+var_4], 3
    mov     [esp+10h+var_8], 2
    mov     [esp+10h+var_C], 1
    mov     [esp+10h+var_10], eax
    call    _printf
    mov     eax, 0
    leave
    retn
main endp
```

Có thể nhận thấy rằng sự khác biệt giữa code MSVC và code GCC chỉ nằm ở cách các đối số được lưu trữ trên stack. Ở đây, GCC làm việc trực tiếp với stack mà không sử dụng `PUSH`/`POP`.

**GCC và GDB**

Hãy thử ví dụ này cũng trong GDB trên Linux.

Tùy chọn `-g` hướng dẫn trình biên dịch đưa thông tin gỡ lỗi vào tệp thực thi.

```bash
$ gcc 1.c -g -o 1
```

```bash
$ gdb 1
GNU gdb (GDB) 7.6.1-ubuntu
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/dennis/polygon/1...done.
```

```bash
(gdb) b printf
Breakpoint 1 at 0x80482f0
```

Chạy. Chúng ta không có source code của hàm `printf()` ở đây, vì vậy GDB không thể hiển thị nó, nhưng có thể làm được.

```bash
(gdb) run
Starting program: /home/dennis/polygon/1
Breakpoint 1, __printf (format=0x80484f0 "a=%d; b=%d; c=%d") at printf.c:29
29 printf.c: No such file or directory.
```

In 10 phần tử stack. Cột ngoài cùng bên trái chứa các địa chỉ trên stack.

```bash
(gdb) x/10w $esp
0xbffff11c: 0x0804844a 0x080484f0 0x00000001 0x00000002
0xbffff12c: 0x00000003 0x08048460 0x00000000 0x00000000
0xbffff13c: 0xb7e29905 0x00000001
```

Phần tử đầu tiên là RA (`0x0804844a`). Chúng ta có thể xác minh điều này bằng cách disassemly bộ nhớ tại địa chỉ này:

```bash
(gdb) x/5i 0x0804844a
0x804844a <main+45>:  mov    $0x0,%eax
0x804844f <main+50>:  leave  
0x8048450 <main+51>:  ret    
0x8048451:  xchg   %ax,%ax
0x8048453:  xchg   %ax,%ax
```

Hai lệnh `XCHG` là các lệnh nhàn rỗi, tương tự như `NOP`.

Phần tử thứ hai (0x080484f0) là địa chỉ của chuỗi format:

```bash
(gdb) x/s 0x080484f0
0x80484f0: "a=%d; b=%d; c=%d"
```

3 phần tử tiếp theo (1, 2, 3) là các đối số của `printf()`. Các phần tử còn lại có thể chỉ là "rác" trên stack, nhưng cũng có thể là các giá trị từ các hàm khác, các biến cục bộ của chúng, v.v. Chúng ta có thể bỏ qua chúng vào lúc này.

Chạy lệnh "`finish`". Lệnh này hướng dẫn GDB "thực thi tất cả các lệnh cho đến khi kết thúc hàm". Trong trường hợp này: thực thi cho đến khi kết thúc hàm `printf()`.

```bash
(gdb) finish
Run till exit from #0  __printf (format=0x80484f0 "a=%d; b=%d; c=%d") at printf.c:29
main () at 1.c:6
6       return 0;
Value returned is $2 = 13
```

GDB hiển thị những gì `printf()` trả về trong `EAX` (13).

Chúng ta cũng thấy "return 0;" và thông tin rằng biểu thức này nằm trong tệp `1.c` tại dòng 6. Thật vậy, tệp `1.c` nằm trong thư mục hiện tại và GDB tìm thấy chuỗi ở đó. Làm thế nào GDB biết dòng code C nào đang được thực thi? Điều này là do thực tế là trình biên dịch, trong khi tạo thông tin gỡ lỗi, cũng lưu một bảng các mối quan hệ giữa số dòng code nguồn và địa chỉ lệnh. GDB là một trình gỡ lỗi cấp source code, sau cùng.

Hãy xem xét các thanh ghi. 13 trong `EAX`:

```
(gdb) info registers
eax            0xd                 13
ecx            0x0                 0
edx            0x0                 0
ebx            0xb7fc0000          -1208221696
esp            0xbffff120          0xbffff120
ebp            0xbffff138          0xbffff138
esi            0x0                 0
edi            0x0                 0
eip            0x804844a           0x804844a <main+45>
...
```

Hãy disassemble các lệnh hiện tại. Mũi tên trỏ đến lệnh sẽ được thực thi tiếp theo.

```
(gdb) disas
Dump of assembler code for function main:
0x0804841d <+0>:     push   %ebp
0x0804841e <+1>:     mov    %esp,%ebp
0x08048420 <+3>:     and    $0xfffffff0,%esp
0x08048423 <+6>:     sub    $0x10,%esp
0x08048426 <+9>:     movl   $0x3,0xc(%esp)
0x0804842e <+17>:    movl   $0x2,0x8(%esp)
0x08048436 <+25>:    movl   $0x1,0x4(%esp)
0x0804843e <+33>:    movl   $0x80484f0,(%esp)
0x08048445 <+40>:    call   0x80482f0 <printf@plt>
=> 0x0804844a <+45>:  mov    $0x0,%eax
0x0804844f <+50>:    leave
0x08048450 <+51>:    ret
End of assembler dump.
```

GDB sử dụng cú pháp AT\&T theo mặc định. Nhưng có thể chuyển sang cú pháp Intel:

```bash
(gdb) set disassembly-flavor intel
(gdb) disas
Dump of assembler code for function main:
0x0804841d <+0>:     push   ebp
0x0804841e <+1>:     mov    ebp,esp
0x08048420 <+3>:     and    esp,0xfffffff0
0x08048423 <+6>:     sub    esp,0x10
0x08048426 <+9>:     mov    DWORD PTR [esp+0xc],0x3
0x0804842e <+17>:    mov    DWORD PTR [esp+0x8],0x2
0x08048436 <+25>:    mov    DWORD PTR [esp+0x4],0x1
0x0804843e <+33>:    mov    DWORD PTR [esp],0x80484f0
0x08048445 <+40>:    call   0x80482f0 <printf@plt>
=> 0x0804844a <+45>:  mov    eax,0x0
0x0804844f <+50>:    leave
0x08048450 <+51>:    ret
End of assembler dump.
```

Thực thi lệnh tiếp theo. GDB hiển thị dấu ngoặc kết thúc, có nghĩa là nó kết thúc khối.

```
(gdb) step
7       };
```

Hãy xem xét các thanh ghi sau khi thực thi lệnh `MOV EAX, 0`. Thật vậy, `EAX` bằng zero tại thời điểm đó.

```bash
(gdb) info registers
eax            0x0                 0
ecx            0x0                 0
edx            0x0                 0
ebx            0xb7fc0000          -1208221696
esp            0xbffff120          0xbffff120
ebp            0xbffff138          0xbffff138
esi            0x0                 0
edi            0x0                 0
eip            0x804844f           0x804844f <main+50>
...
```

#### 7.1.2 x64: 8 arguments

Để xem cách các đối số khác được truyền qua stack, chúng ta hãy thay đổi ví dụ của mình một lần nữa bằng cách tăng số lượng đối số lên 9 (`printf()` format string + 8 biến `int`):

```c
#include <stdio.h>
int main()
{
    printf("a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n", 1, 2, 3, 4, 5, 6, 7, 8);
    return 0;
};
```

**MSVC**

Như đã đề cập trước đó, 4 đối số đầu tiên phải được truyền qua các thanh ghi `RCX`, `RDX`, `R8`, `R9` trong Win64, trong khi tất cả các đối số còn lại - thông qua stack. Đó chính xác là những gì chúng ta thấy ở đây. Tuy nhiên, lệnh `MOV`, thay vì `PUSH`, được sử dụng để chuẩn bị stack, vì vậy các giá trị được lưu vào stack một cách trực tiếp.

```nasm
$SG2923 DB 'a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d', 0aH, 00H
main PROC
    sub rsp, 88
    mov DWORD PTR [rsp+64], 8
    mov DWORD PTR [rsp+56], 7
    mov DWORD PTR [rsp+48], 6
    mov DWORD PTR [rsp+40], 5
    mov DWORD PTR [rsp+32], 4
    mov     r9d, 3
    mov     r8d, 2
    mov     edx, 1
    lea     rcx, OFFSET FLAT:$SG2923
    call    printf
    ; return 0
    xor     eax, eax
    add     rsp, 88
    ret     0
main ENDP
_TEXT ENDS
END
```

Người đọc tinh ý có thể hỏi tại sao 8 byte được cấp phát cho các giá trị `int`, khi 4 byte là đủ? Đúng vậy, người ta phải nhớ: 8 byte được cấp phát cho bất kỳ kiểu dữ liệu nào ngắn hơn 64 bit. Điều này được thiết lập vì sự thuận tiện: nó giúp dễ dàng tính toán địa chỉ của một đối số tùy ý. Bên cạnh đó, tất cả chúng đều được đặt tại các địa chỉ bộ nhớ được căn chỉnh. Điều tương tự cũng xảy ra trong môi trường 32-bit: 4 byte được dành riêng cho tất cả các kiểu dữ liệu.

### 7.4 Khung thô (kết luận)

Đây là một bộ khung thô về cách gọi hàm:

**x86**

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

**x64 (MSVC)**

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

**x64 (GCC)**

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

**ARM**

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

**ARM64**

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

## 8. scanf()

### 8.1 Simple exemple

```c
#include <stdio.h>
int main()
{
    int x;
    printf ("Enter X:\n");
    scanf ("%d", &x);
    printf ("You entered %d...\n", x);
    return 0;
};
```

Ngày nay, việc sử dụng `scanf()` cho các tương tác người dùng là không thông minh. Tuy nhiên, chúng ta có thể minh họa việc truyền một con trỏ đến một biến kiểu `int`.

#### **8.1.1 Về con trỏ**

* **Con trỏ:** Định nghĩa con trỏ là địa chỉ của một vị trí bộ nhớ.
* **Kích thước con trỏ:** Con trỏ chiếm 4 byte trong x86 (32-bit) và 8 byte trong x86-64 (64-bit).
* **Tính hiệu quả của con trỏ:** Con trỏ giúp truyền các cấu trúc lớn hoặc mảng mà không cần sao chép toàn bộ dữ liệu, do đó tiết kiệm chi phí.
* **Con trỏ và sửa đổi dữ liệu:** Con trỏ cho phép các hàm được gọi sửa đổi trực tiếp dữ liệu gốc.
* **Con trỏ untyped (`void*`):** Có thể làm việc với các con trỏ không có kiểu, như trong `memcpy()`.
* **Con trỏ và nhiều giá trị trả về:** Con trỏ được sử dụng để trả về nhiều giá trị từ một hàm, như trong trường hợp của `scanf()`.
* **Kiểm tra kiểu compile-time:** Loại con trỏ chỉ được dùng khi compile, không tồn tại khi chương trình đã được build xong.

#### 8.1.2 x86

**MSVC**

Đây là những gì chúng ta nhận được sau khi biên dịch với MSVC 2010:

```nasm
CONST     SEGMENT
$SG3831     DB 'Enter X:', 0aH, 00H
$SG3832     DB '%d', 00H
$SG3833     DB 'You entered %d...', 0aH, 00H
CONST     ENDS
PUBLIC     _main
EXTRN     _scanf:PROC
EXTRN     _printf:PROC
; Function compile flags: /Odtp
_TEXT     SEGMENT
_x$ = -4 ; size = 4
_main     PROC
    push    ebp
    mov     ebp, esp
    push    ecx
    push    OFFSET $SG3831    ; 'Enter X:'
    call    _printf
    add     esp, 4
    lea     eax, DWORD PTR _x$[ebp]
    push    eax
    push    OFFSET $SG3832    ; '%d'
    call    _scanf
    add     esp, 8
    mov     ecx, DWORD PTR _x$[ebp]
    push    ecx
    push    OFFSET $SG3833    ; 'You entered %d...'
    call    _printf
    add     esp, 8
    ; return 0
    xor     eax, eax
    mov     esp, ebp
    pop     ebp
    ret     0
_main     ENDP
_TEXT     ENDS
```

`x` là một biến cục bộ.

Theo tiêu chuẩn C/C++, nó chỉ hiển thị trong hàm này và không từ bất kỳ phạm vi bên ngoài nào khác. Theo truyền thống, các biến cục bộ được lưu trữ trên stack. Có thể có các cách khác để cấp phát chúng, nhưng trong x86 thì đó là cách nó hoạt động.

Mục tiêu của lệnh theo sau function prologue, `PUSH ECX`, không phải là để lưu trạng thái `ECX` (lưu ý rằng không có `POP ECX` tương ứng ở cuối hàm).

Trên thực tế, nó cấp phát 4 byte trên stack để lưu trữ biến `x`.

`x` sẽ được truy cập với sự trợ giúp của macro `_x$` (nó bằng -4) và thanh ghi `EBP` trỏ đến frame hiện tại.

Trong suốt quá trình thực thi của hàm, `EBP` trỏ đến stack frame hiện tại, cho phép truy cập các biến cục bộ và các đối số hàm thông qua `EBP+offset`.

Cũng có thể sử dụng `ESP` cho cùng mục đích, mặc dù điều đó không thuận tiện lắm vì nó thay đổi thường xuyên. Giá trị của `EBP` có thể được coi là trạng thái đóng băng của giá trị trong `ESP` khi bắt đầu thực thi hàm.

Đây là bố cục stack frame điển hình trong môi trường 32-bit:

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

Hàm `scanf()` trong ví dụ của chúng ta có hai đối số.

Đối số đầu tiên là một con trỏ đến chuỗi chứa `%d` và đối số thứ hai là địa chỉ của biến `x`.

Đầu tiên, địa chỉ của biến `x` được load vào thanh ghi `EAX` bằng lệnh `lea eax, DWORD PTR _x$[ebp]`.

`LEA` là viết tắt của "load effective address", và thường được sử dụng để tạo địa chỉ.

Chúng ta có thể nói rằng trong trường hợp này, `LEA` chỉ đơn giản là lưu tổng của giá trị thanh ghi `EBP` và macro `_x$` vào thanh ghi `EAX`.

Điều này giống như `lea eax, [ebp-4]`.

Vì vậy, 4 đang được trừ đi từ giá trị thanh ghi `EBP` và kết quả được load vào thanh ghi `EAX`. Tiếp theo, giá trị thanh ghi `EAX` được đẩy vào stack và hàm `scanf()` đang được gọi.

`printf()` đang được gọi sau đó với đối số đầu tiên của nó — một con trỏ đến chuỗi: `You entered %d...`.

Đối số thứ hai được chuẩn bị bằng: `mov ecx, [ebp-4]`. Lệnh này lưu giá trị biến `x`, chứ không phải địa chỉ của nó, vào thanh ghi `ECX`.

Tiếp theo, giá trị trong `ECX` được lưu vào stack và hàm `printf()` cuối cùng đang được gọi.

**Các lệnh `add esp` liên tục trong code assembly là do:**

* Quy ước gọi `cdecl` yêu cầu hàm gọi dọn dẹp stack.
* MSVC thường tạo ra lệnh `add esp` ngay sau mỗi lệnh gọi hàm để dọn dẹp stack.
* Việc này giúp code dễ đọc, linh hoạt và dễ bảo trì.

### 8.2 Global variables

Điều gì xảy ra nếu biến `x` từ ví dụ trước không phải là biến cục bộ mà là biến toàn cục? Khi đó, nó sẽ có thể truy cập được từ bất kỳ điểm nào, không chỉ từ thân hàm. Các biến toàn cục được coi là một anti-pattern, nhưng vì mục đích thử nghiệm, chúng ta có thể làm điều này.

```c
#include <stdio.h>
// bây giờ x là biến toàn cục
int x;
int main()
{
    printf ("Enter X:\n");
    scanf ("%d", &x);
    printf ("You entered %d...\n", x);
    return 0;
}
```

#### 8.2.1 MSVC: x86

```nasm
_DATA     SEGMENT
COMM     _x:DWORD
$SG2456     DB 'Enter X:', 0aH, 00H
$SG2457     DB '%d', 00H
$SG2458     DB 'You entered %d...', 0aH, 00H
_DATA     ENDS
PUBLIC     _main
EXTRN     _scanf:PROC
EXTRN     _printf:PROC
; Function compile flags: /Odtp
_TEXT     SEGMENT
_main     PROC
    push    ebp
    mov     ebp, esp
    push    OFFSET $SG2456    ; 'Enter X:'
    call    _printf
    add     esp, 4
    push    OFFSET _x
    push    OFFSET $SG2457    ; '%d'
    call    _scanf
    add     esp, 8
    mov     eax, DWORD PTR _x
    push    eax
    push    OFFSET $SG2458    ; 'You entered %d...'
    call    _printf
    add     esp, 8
    xor     eax, eax
    pop     ebp
    ret     0
_main     ENDP
_TEXT     ENDS
```

Trong trường hợp này, biến `x` được định nghĩa trong segment `_DATA` và không có bộ nhớ nào được cấp phát trong stack cục bộ. Nó được truy cập trực tiếp, không thông qua stack. Các biến toàn cục chưa được khởi tạo không chiếm dung lượng trong tệp thực thi (thật vậy, tại sao người ta cần cấp phát dung lượng cho các biến ban đầu được đặt thành zero?), nhưng khi ai đó truy cập địa chỉ của chúng, hệ điều hành sẽ cấp phát một khối chứa toàn số không ở đó.

Bây giờ hãy gán một giá trị rõ ràng cho biến:

```c
int x=10; // giá trị mặc định
```

Chúng ta có:

```nasm
_DATA SEGMENT
_x DD 0aH
...
```

Ở đây chúng ta thấy một giá trị `0xA` kiểu `DWORD` (DD là viết tắt của DWORD = 32 bit) cho biến này.

Nếu bạn mở tệp `.exe` đã biên dịch trong IDA, bạn có thể thấy biến `x` được đặt ở đầu segment `_DATA`, và sau đó bạn có thể thấy các chuỗi văn bản.

Nếu bạn mở tệp `.exe` đã biên dịch từ ví dụ trước trong IDA, trong đó giá trị của `x` chưa được đặt, bạn sẽ thấy một cái gì đó như thế này:

```nasm
.data:0040FA80 _x dd ?                           ; DATA XREF: _main+10
.data:0040FA80                                   ; _main+22
.data:0040FA84 dword_40FA84 dd ?                 ; DATA XREF: _memset+1E
.data:0040FA84                                   ; unknown_libname_1+28
.data:0040FA88 dword_40FA88 dd ?                 ; DATA XREF: ___sbh_find_block+5
.data:0040FA88                                   ; ___sbh_free_block+2BC
.data:0040FA8C                                   ; LPVOID lpMem
.data:0040FA8C lpMem dd ?                        ; DATA XREF: ___sbh_find_block+B
.data:0040FA8C                                   ; ___sbh_free_block+2CA
.data:0040FA90 dword_40FA90 dd ?                 ; DATA XREF: _V6_HeapAlloc+13
.data:0040FA90                                   ; __calloc_impl+72
.data:0040FA94 dword_40FA94 dd ?                 ; DATA XREF: ___sbh_free_block+2FE
```

* **Biến toàn cục:** Lưu trữ trong segment `_DATA`, không trên stack.
* **Không tăng kích thước:** Biến chưa khởi tạo không làm tăng kích thước tệp.
* **Khởi tạo zero:** Hệ điều hành cấp phát và khởi tạo zero cho các biến chưa khởi tạo khi chương trình chạy.
* **`DD`:** Chỉ thị để khai báo giá trị 32-bit trong DATA segment.
* **Đánh dấu `?`:** IDA đánh dấu các biến chưa khởi tạo bằng `?`.
* **So sánh stack và data segment:** Cho thấy các biến cục bộ dùng stack, biến toàn cục dùng data segment.

## 9. Accessing passed arguments

Bây giờ chúng ta đã biết rằng hàm gọi (caller) đang truyền các đối số cho hàm được gọi (callee) thông qua stack. Nhưng làm thế nào hàm được gọi truy cập chúng?

Ví dụ đơn giản:

```c
#include <stdio.h>
int f (int a, int b, int c)
{
    return a*b+c;
};
int main()
{
    printf ("%d\n", f(1, 2, 3));
    return 0;
};
```

**x86 MSVC**

Đây là những gì chúng ta nhận được sau khi biên dịch (MSVC 2010 Express):

```nasm
_TEXT     SEGMENT
_a$ = 8    ; size = 4
_b$ = 12   ; size = 4
_c$ = 16   ; size = 4
_f      PROC
        push    ebp
        mov     ebp, esp
        mov     eax, DWORD PTR _a$[ebp]
        imul    eax, DWORD PTR _b$[ebp]
        add     eax, DWORD PTR _c$[ebp]
        pop     ebp
        ret     0
_f      ENDP
_main   PROC
        push    ebp
        mov     ebp, esp
        push    3        ; 3rd argument
        push    2        ; 2nd argument
        push    1        ; 1st argument
        call    _f
        add     esp, 12
        push    eax
        push    OFFSET $SG2463   ; '%d', 0aH, 00H
        call    _printf
        add     esp, 8
        ; return 0
        xor     eax, eax
        pop     ebp
        ret     0
_main   ENDP
```

Những gì chúng ta thấy là hàm `main()` đẩy 3 số lên stack và gọi `f(int,int,int)`.

Việc truy cập đối số bên trong `f()` được tổ chức với sự trợ giúp của các macro như: `_a$ = 8`, tương tự như các biến cục bộ, nhưng với các offset dương (được địa chỉ hóa với dấu cộng). Vì vậy, chúng ta đang địa chỉ hóa phía bên ngoài của stack frame bằng cách thêm macro `_a$` vào giá trị trong thanh ghi `EBP`.

Sau đó, giá trị của `a` được lưu vào `EAX`. Sau khi thực thi lệnh `IMUL`, giá trị trong `EAX` là tích của giá trị trong `EAX` và nội dung của `_b`.

Sau đó, `ADD` cộng giá trị trong `_c` vào `EAX`.

Giá trị trong `EAX` không cần phải được di chuyển: nó đã ở đúng nơi mà nó phải ở. Khi trả về hàm gọi, nó lấy giá trị `EAX` và sử dụng nó làm đối số cho `printf()`.

## 10. More about results returning

Trong x86, kết quả thực thi của hàm thường được trả về trong thanh ghi `EAX`. Nếu nó là kiểu byte hoặc một ký tự (`char`), thì phần thấp nhất của thanh ghi `EAX` (`AL`) được sử dụng. Nếu một hàm trả về một số kiểu `float`, thì thanh ghi FPU `ST(0)` được sử dụng thay thế.

Trong ARM, kết quả thường được trả về trong thanh ghi `R0`.

### **10.1 Cố gắng sử dụng kết quả của hàm trả về kiểu&#x20;***<mark style="color:red;">**void**</mark>*

Vậy, điều gì xảy ra nếu giá trị trả về của hàm `main()` được khai báo là kiểu `void` chứ không phải `int`? Cái gọi là startup code đang gọi `main()` đại khái như sau:

```nasm
push envp
push argv
push argc
call main
push eax
call exit
```

Nói cách khác:

```excel-formula
exit(main(argc,argv,envp));
```

Nếu bạn khai báo `main()` là `void`, không có gì được trả về một cách rõ ràng (sử dụng câu lệnh `return`), thì một thứ gì đó ngẫu nhiên, thứ đã được lưu trữ trong thanh ghi `EAX` ở cuối `main()`, sẽ trở thành đối số duy nhất của hàm `exit()`. Rất có thể, sẽ có một giá trị ngẫu nhiên, còn sót lại từ quá trình thực thi hàm của bạn, vì vậy mã thoát của chương trình là giả ngẫu nhiên.

Chúng ta có thể minh họa thực tế này. Xin lưu ý rằng ở đây hàm `main()` có kiểu trả về là `void`:

```c
#include <stdio.h>
void main()
{
    printf ("Hello, world!\n");
};
```

Hãy biên dịch nó trong Linux.

GCC 4.8.1 thay thế `printf()` bằng `puts()`, nhưng điều đó không sao, vì `puts()` trả về số ký tự đã in ra, giống như `printf()`. Xin lưu ý rằng `EAX` không được thiết lập về zero trước khi kết thúc `main()`.

Điều này ngụ ý rằng giá trị của `EAX` ở cuối `main()` chứa những gì mà `puts()` đã để lại ở đó.

```nasm
.LC0:
    .string "Hello, world!"
main:
    push ebp
    mov ebp, esp
    and esp, -16
    sub esp, 16
    mov DWORD PTR [esp], OFFSET FLAT:.LC0
    call puts
    leave
    ret
```

Hãy viết một bash script hiển thị trạng thái thoát:

```bash
#!/bin/sh
./hello_world
echo $?
```

Và chạy nó:

```
$ tst.sh
Hello, world!
14
```

14 là số ký tự đã in.

### 10.2 **Điều gì xảy ra nếu chúng ta không sử dụng kết quả của hàm?**

`printf()` trả về số lượng ký tự đầu ra thành công, nhưng kết quả của hàm này hiếm khi được sử dụng trong thực tế.

Cũng có thể gọi một hàm mà bản chất của nó là trả về một giá trị, và không sử dụng nó:

```c
int f()
{
    // bỏ qua 3 giá trị ngẫu nhiên đầu tiên:
    rand();
    rand();
    rand();
    // và sử dụng giá trị thứ 4:
    return rand();
};
```

Kết quả của hàm `rand()` được để lại trong `EAX`, trong cả bốn trường hợp.

Nhưng trong 3 trường hợp đầu tiên, giá trị trong `EAX` không được sử dụng.

### **10.3 Trả về một cấu trúc (a structure)**

Hãy quay lại thực tế là giá trị trả về được để lại trong thanh ghi `EAX`.

Đó là lý do tại sao các trình biên dịch C cũ không thể tạo ra các hàm có khả năng trả về thứ gì đó không vừa trong một thanh ghi (thường là `int`), nhưng nếu cần, người ta phải trả lại thông tin thông qua các con trỏ được truyền làm đối số của hàm.

Vì vậy, thông thường, nếu một hàm cần trả về một số giá trị, nó chỉ trả về một giá trị và tất cả các giá trị còn lại — thông qua con trỏ.

Bây giờ, có thể trả lại, giả sử, toàn bộ một cấu trúc, nhưng điều đó vẫn không phổ biến lắm. Nếu một hàm phải trả về một cấu trúc lớn, hàm gọi phải cấp phát nó và truyền một con trỏ đến nó thông qua đối số đầu tiên, một cách trong suốt đối với lập trình viên. Điều đó gần giống như việc truyền một con trỏ vào đối số đầu tiên theo cách thủ công, nhưng trình biên dịch ẩn nó đi.

Một ví dụ nhỏ:

```c
struct s
{
    int a;
    int b;
    int c;
};
struct s get_some_values (int a)
{
    struct s rt;
    rt.a=a+1;
    rt.b=a+2;
    rt.c=a+3;
    return rt;
};
```

...những gì chúng ta nhận được (MSVC 2010 /Ox ):

```nasm
$T3853 = 8    ; size = 4
_a$ = 12     ; size = 4
?get_some_values@@YA?AUs@@H@Z PROC ; get_some_values
    mov     ecx, DWORD PTR _a$[esp-4]
    mov     eax, DWORD PTR $T3853[esp-4]
    lea     edx, DWORD PTR [ecx+1]
    mov     DWORD PTR [eax], edx
    lea     edx, DWORD PTR [ecx+2]
    add     ecx, 3
    mov     DWORD PTR [eax+4], edx
    mov     DWORD PTR [eax+8], ecx
    ret     0
?get_some_values@@YA?AUs@@H@Z ENDP ; get_some_values
```

Tên macro để truyền con trỏ nội bộ đến một cấu trúc ở đây là `$T3853`.

Ví dụ này có thể được viết lại bằng cách sử dụng các tiện ích mở rộng ngôn ngữ C99:

```c
struct s
{
    int a;
    int b;
    int c;
};
struct s get_some_values (int a)
{
    return (struct s){.a=a+1, .b=a+2, .c=a+3};
};
```

**GCC 4.8.1**

```nasm
_get_some_values proc near

ptr_to_struct = dword ptr 4
a             = dword ptr 8

                mov     edx, [esp+a]
                mov     eax, [esp+ptr_to_struct]
                lea     ecx, [edx+1]
                mov     [eax], ecx
                lea     ecx, [edx+2]
                add     edx, 3
                mov     [eax+4], ecx
                mov     [eax+8], edx
                retn
_get_some_values endp
```

Như chúng ta thấy, hàm chỉ điền vào các trường của cấu trúc được cấp phát bởi hàm gọi, như thể một con trỏ đến cấu trúc đã được truyền. Vì vậy, không có nhược điểm về hiệu suất.

## 11. Pointers

Con trỏ thường được sử dụng để trả về giá trị từ các hàm (nhớ lại trường hợp scanf()).

## 12. GOTO operator

Toán tử GOTO thường được coi là một anti-pattern (mẫu thiết kế xấu).

Đây là một ví dụ đơn giản:

```c
#include <stdio.h>
int main()
{
    printf ("begin\n");
    goto exit;
    printf ("skip me!\n");
exit:
    printf ("end\n");
};
```

Đây là những gì chúng ta có trong MSVC 2012:

```nasm
$SG2934 DB     'begin', 0aH, 00H
$SG2936 DB     'skip me!', 0aH, 00H
$SG2937 DB     'end', 0aH, 00H

_main   PROC
        push ebp
        mov ebp, esp
        push OFFSET $SG2934 ; 'begin'
        call _printf
        add esp, 4
        jmp SHORT $exit$3
        push OFFSET $SG2936 ; 'skip me!'
        call _printf
        add esp, 4
$exit$3:
        push OFFSET $SG2937 ; 'end'
        call _printf
        add esp, 4
        xor eax, eax
        pop ebp
        ret 0
_main   ENDP
```

Câu lệnh goto đã được thay thế đơn giản bằng một lệnh `JMP`, có cùng tác dụng: nhảy vô điều kiện đến một vị trí khác. Lệnh `printf()` thứ hai chỉ có thể được thực thi với sự can thiệp của con người, bằng cách sử dụng trình gỡ lỗi hoặc bằng cách vá mã.

## 13. Conditional jumps

Ví dụ đơn giản:

```c
#include <stdio.h>
void f_signed (int a, int b)
{
    if (a>b)
        printf ("a>b\n");
    if (a==b)
        printf ("a==b\n");
    if (a<b)
        printf ("a<b\n");
};

void f_unsigned (unsigned int a, unsigned int b)
{
    if (a>b)
        printf ("a>b\n");
    if (a==b)
        printf ("a==b\n");
    if (a<b)
        printf ("a<b\n");
};

int main()
{
    f_signed(1, 2);
    f_unsigned(1, 2);
    return 0;
};
```

**x86 + MSVC**

Đây là cách hàm `f_signed()` trông như thế nào:

```
_a$ = 8
_b$ = 12
_f_signed PROC
    push ebp
    mov ebp, esp
    mov eax, DWORD PTR _a$[ebp]
    cmp eax, DWORD PTR _b$[ebp]
    jle SHORT $LN3@f_signed
    push OFFSET $SG737 ; 'a>b'
    call _printf
    add esp, 4
$LN3@f_signed:
    mov ecx, DWORD PTR _a$[ebp]
    cmp ecx, DWORD PTR _b$[ebp]
    jne SHORT $LN2@f_signed
    push OFFSET $SG739 ; 'a==b'
    call _printf
    add esp, 4
$LN2@f_signed:
    mov edx, DWORD PTR _a$[ebp]
    cmp edx, DWORD PTR _b$[ebp]
    jge SHORT $LN4@f_signed
    push OFFSET $SG741 ; 'a<b'
    call _printf
    add esp, 4
$LN4@f_signed:
    pop ebp
    ret 0
_f_signed ENDP
```

Lệnh đầu tiên, `JLE`, là viết tắt của Jump if Less or Equal (Nhảy nếu Nhỏ hơn hoặc Bằng). Nói cách khác, nếu toán hạng thứ hai lớn hơn hoặc bằng toán hạng thứ nhất, luồng điều khiển sẽ được chuyển đến địa chỉ hoặc nhãn được chỉ định trong lệnh. Nếu điều kiện này không được kích hoạt vì toán hạng thứ hai nhỏ hơn toán hạng thứ nhất, luồng điều khiển sẽ không bị thay đổi và lệnh `printf()` đầu tiên sẽ được thực thi.&#x20;

Kiểm tra thứ hai là `JNE`: Jump if Not Equal (Nhảy nếu Không Bằng). Luồng điều khiển sẽ không thay đổi nếu các toán hạng bằng nhau.&#x20;

Kiểm tra thứ ba là `JGE`: Jump if Greater or Equal (Nhảy nếu Lớn hơn hoặc Bằng) — nhảy nếu toán hạng thứ nhất lớn hơn toán hạng thứ hai hoặc nếu chúng bằng nhau.&#x20;

Vì vậy, nếu tất cả ba bước nhảy có điều kiện được kích hoạt, không có lệnh gọi `printf()` nào sẽ được thực thi. Điều này là không thể nếu không có sự can thiệp đặc biệt.&#x20;

Bây giờ hãy xem hàm `f_unsigned()`. Hàm `f_unsigned()` giống như `f_signed()`, ngoại trừ việc các lệnh `JBE` và `JAE` được sử dụng thay vì `JLE` và `JGE`, như sau:

**GCC**

```
_a$ = 8 ; size = 4
_b$ = 12 ; size = 4
_f_unsigned PROC
    push ebp
    mov ebp, esp
    mov eax, DWORD PTR _a$[ebp]
    cmp eax, DWORD PTR _b$[ebp]
    jbe SHORT $LN3@f_unsigned
    push OFFSET $SG2761 ; 'a>b'
    call _printf
    add esp, 4
$LN3@f_unsigned:
    mov ecx, DWORD PTR _a$[ebp]
    cmp ecx, DWORD PTR _b$[ebp]
    jne SHORT $LN2@f_unsigned
    push OFFSET $SG2763 ; 'a==b'
    call _printf
    add esp, 4
$LN2@f_unsigned:
    mov edx, DWORD PTR _a$[ebp]
    cmp edx, DWORD PTR _b$[ebp]
    jae SHORT $LN4@f_unsigned
    push OFFSET $SG2765 ; 'a<b'
    call _printf
    add esp, 4
$LN4@f_unsigned:
    pop ebp
    ret 0
_f_unsigned ENDP
```

Như đã đề cập, các lệnh rẽ nhánh khác nhau: `JBE` — Jump if Below or Equal (Nhảy nếu Nhỏ hơn hoặc Bằng) và `JAE` — Jump if Above or Equal (Nhảy nếu Lớn hơn hoặc Bằng). Các lệnh này (`JA` / `JAE` / `JB` / `JBE`) khác với `JG` / `JGE` / `JL` / `JLE` ở chỗ chúng hoạt động với các số không dấu.

Đó là lý do tại sao nếu chúng ta thấy `JG` / `JL` được sử dụng thay vì `JA` / `JB` hoặc ngược lại, chúng ta gần như chắc chắn rằng các biến tương ứng là có dấu hoặc không dấu. Đây cũng là hàm `main()`, nơi không có gì mới đối với chúng ta:

**main()**

```nasm
_main   PROC
        push ebp
        mov ebp, esp
        push 2
        push 1
        call _f_signed
        add esp, 8
        push 2
        push 1
        call _f_unsigned
        add esp, 8
        xor eax, eax
        pop ebp
        ret 0
_main   ENDP
```

**Conclustion x86**

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

**Conclustion ARM**

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

## 14. switch()/case/default

### 14.1 Số lượng trường hợp nhỏ

```c
#include <stdio.h>
void f (int a) {
  switch (a) {
    case 0: printf ("zero\n"); break;
    case 1: printf ("one\n"); break;
    case 2: printf ("two\n"); break;
    default: printf ("something unknown\n"); break;
  };
};
int main() {
  f (2); // test
};
```

**x86 (MSVC)**

```nasm
tv64 = -4 ; size = 4
_a$ = 8 ; size = 4
_f     PROC
    push ebp
    mov ebp, esp
    push ecx
    mov eax, DWORD PTR _a$[ebp]
    mov DWORD PTR tv64[ebp], eax
    cmp DWORD PTR tv64[ebp], 0
    je SHORT $LN4@f
    cmp DWORD PTR tv64[ebp], 1
    je SHORT $LN3@f
    cmp DWORD PTR tv64[ebp], 2
    je SHORT $LN2@f
    jmp SHORT $LN1@f
$LN4@f:
    push OFFSET $SG739 ; 'zero', 0aH, 00H
    call _printf
    add esp, 4
    jmp SHORT $LN7@f
$LN3@f:
    push OFFSET $SG741 ; 'one', 0aH, 00H
    call _printf
    add esp, 4
    jmp SHORT $LN7@f
$LN2@f:
    push OFFSET $SG743 ; 'two', 0aH, 00H
    call _printf
    add esp, 4
    jmp SHORT $LN7@f
$LN1@f:
    push OFFSET $SG745 ; 'something unknown', 0aH, 00H
    call _printf
    add esp, 4
$LN7@f:
    mov esp, ebp
    pop ebp
    ret 0
_f     ENDP
```

Hàm của chúng ta với một vài trường hợp trong `switch()` thực tế tương tự với cấu trúc này:

```c
void f (int a)
{
    if (a==0)
        printf ("zero\n");
    else if (a==1)
        printf ("one\n");
    else if (a==2)
        printf ("two\n");
    else
        printf ("something unknown\n");
};
```

Nếu chúng ta làm việc với `switch()` với một vài trường hợp, chúng ta không thể chắc chắn liệu nó có phải là một `switch()` thực sự trong mã nguồn hay chỉ là một loạt các câu lệnh `if()`.

Điều này ngụ ý rằng `switch()` giống như một cú pháp đường ngọt cho một số lượng lớn các `if()` lồng nhau.

Không có gì đặc biệt mới đối với chúng ta trong mã được tạo ra, ngoại trừ việc trình biên dịch di chuyển biến đầu vào `a` đến một biến cục bộ tạm thời `tv64`.

Nếu chúng ta biên dịch điều này trong GCC 4.4.1, chúng ta sẽ nhận được kết quả gần như tương tự, ngay cả khi bật tối ưu hóa tối đa (tùy chọn `-O3`).

Bây giờ hãy bật tối ưu hóa trong MSVC ( `/Ox` ): `cl 1.c /Fa1.asm /Ox`

```nasm
_a$ = 8 ; size = 4
_f PROC
    mov eax, DWORD PTR _a$[esp-4]
    sub eax, 0
    je SHORT $LN4@f
    sub eax, 1
    je SHORT $LN3@f
    sub eax, 1
    je SHORT $LN2@f
    mov DWORD PTR _a$[esp-4], OFFSET $SG791 ; 'something unknown', 0aH, 00H
    jmp _printf
$LN2@f:
    mov DWORD PTR _a$[esp-4], OFFSET $SG789 ; 'two', 0aH, 00H
    jmp _printf
$LN3@f:
    mov DWORD PTR _a$[esp-4], OFFSET $SG787 ; 'one', 0aH, 00H
    jmp _printf
$LN4@f:
    mov DWORD PTR _a$[esp-4], OFFSET $SG785 ; 'zero', 0aH, 00H
    jmp _printf
_f ENDP
```

Ở đây chúng ta có thể thấy một số hack bẩn.

Đầu tiên: giá trị của `a` được đặt trong EAX và 0 được trừ khỏi nó. Nghe có vẻ vô lý, nhưng nó được thực hiện để kiểm tra xem giá trị trong EAX có phải là 0 hay không. Nếu có, cờ ZF sẽ được đặt (ví dụ: trừ từ 0 là 0) và bước nhảy có điều kiện đầu tiên `JE` (Jump if Equal hoặc từ đồng nghĩa `JZ` —Jump if Zero) sẽ được kích hoạt và luồng điều khiển sẽ được chuyển đến nhãn `$LN4@f`, nơi thông báo 'zero' đang được in. Nếu bước nhảy đầu tiên không được kích hoạt, 1 được trừ khỏi giá trị đầu vào và nếu ở một giai đoạn nào đó kết quả là 0, bước nhảy tương ứng sẽ được kích hoạt.

Và nếu không có bước nhảy nào được kích hoạt cả, luồng điều khiển sẽ chuyển đến `printf()` với đối số chuỗi 'something unknown'.

Thứ hai: chúng ta thấy điều gì đó bất thường đối với chúng ta: một con trỏ chuỗi được đặt vào biến `a`, và sau đó `printf()` được gọi không qua `CALL`, mà qua `JMP`. Có một lời giải thích đơn giản cho điều đó: người gọi đẩy một giá trị vào stack và gọi hàm của chúng ta thông qua `CALL`. Bản thân `CALL` đẩy địa chỉ trả về (RA) vào stack và thực hiện một bước nhảy vô điều kiện đến địa chỉ hàm của chúng ta. Hàm của chúng ta tại bất kỳ thời điểm thực thi nào (vì nó không chứa bất kỳ lệnh nào di chuyển con trỏ stack) có bố cục stack sau:

* ESP — trỏ đến RA (return address)
* ESP+4 — trỏ đến biến `a`

Mặt khác, khi chúng ta cần gọi `printf()` ở đây, chúng ta cần chính xác bố cục stack tương tự, ngoại trừ đối số `printf()` đầu tiên, cần phải trỏ đến chuỗi. Và đó là những gì mã của chúng ta làm.

Nó thay thế đối số đầu tiên của hàm bằng địa chỉ của chuỗi và nhảy đến `printf()`, như thể chúng ta không gọi hàm `f()`, mà trực tiếp `printf()`. `printf()` in một chuỗi ra stdout và sau đó thực thi lệnh `RET`, lệnh này POP RA (return address) từ stack và luồng điều khiển được trả về không phải đến `f()` mà đến người gọi của `f()`, bỏ qua phần cuối của hàm `f()`.

Tất cả điều này có thể thực hiện được vì `printf()` được gọi ngay ở cuối hàm `f()` trong tất cả các trường hợp. Bằng một cách nào đó, nó tương tự với hàm `longjmp()`. Và tất nhiên, tất cả đều được thực hiện vì tốc độ.

### 14.2 Khung sườn sơ bộ của `switch()`

```nasm
MOV REG, input       ; Di chuyển giá trị đầu vào vào thanh ghi
CMP REG, 4         ; So sánh giá trị đầu vào với số lượng trường hợp tối đa
JA default         ; Nhảy đến nhãn 'default' nếu giá trị đầu vào lớn hơn số lượng trường hợp tối đa
SHL REG, 2         ; Tìm phần tử trong bảng nhảy, dịch trái 2 bit (3 bit ở x64)
MOV REG, jump_table[REG] ; Lấy địa chỉ nhảy từ bảng nhảy
JMP REG           ; Nhảy đến địa chỉ đã lấy
case1:              ; Nhãn của trường hợp 1
    ; do something      ; Thực hiện một công việc gì đó
    JMP exit           ; Nhảy đến nhãn 'exit'
case2:              ; Nhãn của trường hợp 2
    ; do something      ; Thực hiện một công việc gì đó
    JMP exit           ; Nhảy đến nhãn 'exit'
case3:              ; Nhãn của trường hợp 3
    ; do something      ; Thực hiện một công việc gì đó
    JMP exit           ; Nhảy đến nhãn 'exit'
case4:              ; Nhãn của trường hợp 4
    ; do something      ; Thực hiện một công việc gì đó
    JMP exit           ; Nhảy đến nhãn 'exit'
case5:              ; Nhãn của trường hợp 5
    ; do something      ; Thực hiện một công việc gì đó
    JMP exit           ; Nhảy đến nhãn 'exit'
default:            ; Nhãn của trường hợp 'default'
    
    ...               ; Thực hiện một công việc gì đó

exit:              ; Nhãn 'exit'

    ....               ; Tiếp tục thực hiện

jump_table  dd case1 ; Mục nhập trong bảng nhảy: địa chỉ của trường hợp 1
            dd case2 ; Mục nhập trong bảng nhảy: địa chỉ của trường hợp 2
            dd case3 ; Mục nhập trong bảng nhảy: địa chỉ của trường hợp 3
            dd case4 ; Mục nhập trong bảng nhảy: địa chỉ của trường hợp 4
            dd case5 ; Mục nhập trong bảng nhảy: địa chỉ của trường hợp 5
```

Bước nhảy đến địa chỉ trong bảng nhảy cũng có thể được thực hiện bằng lệnh này: `JMP jump_table[REG*4]`. Hoặc `JMP jump_table[REG*8]` trong x64.

## 15. Loops

Trong C/C++, các vòng lặp thường được xây dựng bằng cách sử dụng các câu lệnh `for()`, `while()` hoặc `do/while()`.

Hãy bắt đầu với `for()`.

```
for (initialization; condition; at each iteration)
{
    loop_body;
}
```

Hãy cùng nhau với một ví dụ đơn giản:

```c
#include <stdio.h>
void printing_function(int i)
{
    printf ("f(%d)\n", i);
};
int main()
{
    int i;
    for (i=2; i<10; i++)
        printing_function(i);
    return 0;
};
```

**MSVC**

```nasm
_i$ = -4
_main     PROC
    push ebp
    mov ebp, esp
    push ecx
    mov DWORD PTR _i$[ebp], 2 ; loop initialization
    jmp SHORT $LN3@main
$LN2@main:
    mov eax, DWORD PTR _i$[ebp] ; here is what we do after each iteration:
    add eax, 1 ; add 1 to (i) value
    mov DWORD PTR _i$[ebp], eax
$LN3@main:
    cmp DWORD PTR _i$[ebp], 10 ; this condition is checked *before* each iteration
    jge SHORT $LN1@main ; if (i) is biggest or equals to 10, lets finish loop'
    mov ecx, DWORD PTR _i$[ebp] ; loop body: call printing_function(i)
    push ecx
    call _printing_function
    add esp, 4
    jmp SHORT $LN2@main ; jump to loop begin
$LN1@main: ; loop end
    xor eax, eax
    mov esp, ebp
    pop ebp
    ret 0
_main     ENDP
```

### **15.1 Conclusion**

Khung sườn sơ bộ của vòng lặp từ 2 đến 9 (bao gồm cả 9):

**x86**

```
    mov [counter], 2  ; Khởi tạo
    jmp check       ; Nhảy đến phần kiểm tra
body:            ; Nhãn của thân vòng lặp
    ; loop body       ; Thân vòng lặp
    ; do something here ; Thực hiện một công việc gì đó
    ; use counter variable in local stack ; Sử dụng biến bộ đếm trong stack cục bộ
    add [counter], 1  ; Tăng bộ đếm
check:           ; Nhãn của phần kiểm tra điều kiện
    cmp [counter], 9  ; So sánh bộ đếm với 9
    jle body        ; Nhảy về thân vòng lặp nếu bộ đếm <= 9
```

Hoạt động tăng bộ đếm có thể được biểu diễn thành 3 lệnh trong mã không tối ưu hóa:

**x86**

```nasm
    MOV [counter], 2    ; Khởi tạo
    JMP check        ; Nhảy đến phần kiểm tra
body:             ; Nhãn của thân vòng lặp
    ; loop body         ; Thân vòng lặp
    ; do something here  ; Thực hiện một công việc gì đó
    ; use counter variable in local stack ; Sử dụng biến bộ đếm trong stack cục bộ
    MOV REG, [counter] ; Tăng bộ đếm
    INC REG
    MOV [counter], REG
check:            ; Nhãn của phần kiểm tra điều kiện
    CMP [counter], 9   ; So sánh bộ đếm với 9
    JLE body        ; Nhảy về thân vòng lặp nếu bộ đếm <= 9
```

Nếu thân vòng lặp ngắn, toàn bộ thanh ghi có thể được dành cho biến bộ đếm:

**x86**

```nasm
    MOV EBX, 2        ; Khởi tạo
    JMP check         ; Nhảy đến phần kiểm tra
body:             ; Nhãn của thân vòng lặp
    ; loop body         ; Thân vòng lặp
    ; do something here  ; Thực hiện một công việc gì đó
    ; use counter in EBX, but do not modify it! ; Sử dụng bộ đếm trong EBX, nhưng không sửa đổi nó!
    INC EBX         ; Tăng bộ đếm
check:            ; Nhãn của phần kiểm tra điều kiện
    CMP EBX, 9        ; So sánh bộ đếm với 9
    JLE body         ; Nhảy về thân vòng lặp nếu bộ đếm <= 9
```

## 16. Thay thế các lệnh số học bằng các lệnh khác

**SHL**

<figure><img src="/files/9GtYK2mv2spkWWvrP8K1" alt=""><figcaption></figcaption></figure>

**SHR**

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


---

# 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/code-patterns.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.
