Bài 13: Ôn Tập Cuối Kỳ


1. Các Thành Phần Của Hệ Thống

1.1 Tổng quan kiến trúc máy tính

CPU
├── ALU (Arithmetic Logic Unit) – Bộ tính toán
├── Registers – Thanh ghi (lưu trữ tạm thời tốc độ cao)
└── Cache
    ├── L1 Cache (nhanh nhất, nhỏ nhất)
    ├── L2 Cache
    └── L3 Cache (chậm hơn, lớn hơn)

Memory (RAM) – Bộ nhớ chính

Storage
├── SSD (Solid State Drive)
└── HDD (Hard Disk Drive)

Tốc độ truy cập (nhanh → chậm): Register > L1 Cache > L2 > L3 > RAM > SSD > HDD

Khi CPU thực thi chương trình, nó liên tục trao đổi dữ liệu giữa các tầng này. Thanh ghi là nơi CPU làm việc trực tiếp – mọi phép tính đều phải đưa dữ liệu vào thanh ghi trước.


1.2 Các thanh ghi IA32 (32-bit)

IA32 có 8 thanh ghi đa năng 32-bit:

Thanh ghi 32-bit16-bit8-bit cao8-bit thấpÝ nghĩa gốc
%eax%ax%ah%alAccumulator – Kết quả trả về
%ecx%cx%ch%clCounter – Đếm vòng lặp
%edx%dx%dh%dlData
%ebx%bx%bh%blBase
%esi%siSource Index
%edi%diDestination Index
%esp%spStack Pointer
%ebp%bpBase Pointer (Frame Pointer)

1.3 Các thanh ghi x86-64 (64-bit)

x86-64 mở rộng lên 16 thanh ghi 64-bit:

  • 8 thanh ghi cũ được mở rộng: %rax, %rbx, %rcx, %rdx, %rsi, %rdi, %rsp, %rbp
  • 8 thanh ghi mới hoàn toàn: %r8%r15

Mỗi thanh ghi 64-bit có thể truy cập phần thấp của nó:

64-bit32-bit thấp
%rax%eax
%r8%r8d
%r15%r15d

1.4 Thanh ghi đặc biệt

Thanh ghiTênChức năng
%rip / %eipProgram Counter / Instruction PointerTrỏ đến địa chỉ lệnh tiếp theo sẽ được thực thi
%rflag / %eflagCondition CodesLưu kết quả phụ của phép tính (cờ trạng thái)

Các bit cờ quan trọng trong %eflag:

CờTênÝ nghĩa
CFCarry FlagCó tràn số không dấu
ZFZero FlagKết quả bằng 0
SFSign FlagKết quả âm
OFOverflow FlagTràn số có dấu

2. Assembly Instruction (AT&T)

2.1 Sự khác biệt AT&T vs Intel

Đặc điểmIntelAT&T
Thứ tự toán hạngmov dest, srcmov src, dest
Thanh ghieax%eax
Hằng số5$5
Lệnh movmovmovl, movq, movb
Địa chỉ bộ nhớ[ebp + 8]8(%ebp)

2.2 Lệnh mov và các dạng hợp lệ

Suffix quyết định kích thước dữ liệu:

SuffixKích thước
movb1 byte
movw2 bytes
movl4 bytes
movq8 bytes (x86-64)

Các tổ hợp nguồn–đích hợp lệ:

Hợp lệ?Lệnh
movl Imm, Reg
movl Imm, Mem
movl Reg, Reg
movl Reg, Mem
movl Mem, Reg
movl Mem, Mem (không được chuyển thẳng mem→mem)
movl Imm, Imm
movl Reg, Imm
movl Mem, Imm

Ví dụ minh họa:

movl $0x4, %rax       # temp = 0x4  (Imm → Reg)
movl $-147, (%rax)    # *p = -147   (Imm → Mem)
movl %rax, %rdx       # rdx = rax   (Reg → Reg)
movl %rax, (%rdx)     # *p = temp   (Reg → Mem)
movl (%rax), %rdx     # temp = *p   (Mem  Reg)

2.3 Câu hỏi: Lệnh mov nào hợp lệ?

Có bao nhiêu lệnh mov hợp lệ trong các lệnh sau?

movl %eax, %ebx      # (1)
movb $123, %bl       # (2)
movl %eax, %bl       # (3)
movb $3, (%ecx)      # (4)
mov  (%eax), %bl     # (5)

2.4 Chế độ đánh địa chỉ bộ nhớ

Dạng tổng quát: D(Rb, Ri, S) → truy xuất Mem[Reg[Rb] + S × Reg[Ri] + D]

Ký hiệuÝ nghĩaRàng buộc
DDisplacement – hằng số dịch chuyển1, 2, hoặc 4 bytes
RbBase registerBất kỳ thanh ghi nào
RiIndex registerBất kỳ, trừ %rsp/%esp
SScale1, 2, 4, hoặc 8

Các dạng rút gọn:

Cú phápĐịa chỉ tính toán
(Rb, Ri)Reg[Rb] + Reg[Ri]
D(Rb, Ri)Reg[Rb] + Reg[Ri] + D
(Rb, Ri, S)Reg[Rb] + S × Reg[Ri]
(, Ri, S)S × Reg[Ri]
D(%ebp)Reg[%ebp] + D (thường dùng để đọc tham số trên stack)

Ví dụ thực tế:

# Truy cập phần tử thứ i của mảng int A[]
# A[i] có địa chỉ = base_A + 4*i
movl (%eax, %ecx, 4), %edx    # edx = A[i], với eax=&A[0], ecx=i

2.5 leal vs movl

movl Src, Dstleal Src, Dst
Tính địa chỉ từ biểu thức
Truy xuất ô nhớ
Kết quả gán vào DstDữ liệu tại ô nhớĐịa chỉ của ô nhớ

2.6 Các lệnh toán học thường gặp

LệnhÝ nghĩaVí dụ
leal S, DD = địa chỉ(S) – không truy xuất memleal (%eax,%eax,4), %eax
incl DD = D + 1incl %eax
decl DD = D − 1decl %ecx
negl DD = −Dnegl %ebx
notl DD = ~D (bitwise NOT)notl %eax
addl S, DD = D + Saddl %ecx, %eax
subl S, DD = D − Ssubl $4, %esp
imull S, DD = D × Simull %ecx, %eax
xorl S, DD = D XOR Sxorl %eax, %eax (đặt về 0)
andl S, DD = D AND Sandl $0xFF, %eax
orl S, DD = D OR Sorl $1, %eax
sall k, DD = D « k (left shift)sall $2, %eax (×4)
sarl k, DD = D » k (arithmetic, giữ dấu)sarl $1, %eax (/2 có dấu)
shrl k, DD = D » k (logical, điền 0)shrl $1, %eax (/2 không dấu)

3. Lập Trình Mức Máy Tính

3.1 Chuỗi biên dịch từ C sang file thực thi

flowchart LR A["hello.c\n(Source – text)"] B["hello.i\n(Preprocessed – text)"] C["hello.s\n(Assembly – text)"] D["hello.o\n(Object – binary)"] E["hello\n(Executable – binary)"] F["printf.o"] A -->|"cpp\n(Preprocessor)"| B B -->|"cc1\n(Compiler)"| C C -->|"as\n(Assembler)"| D D -->|"ld\n(Linker)"| E F --> E
  • Preprocessor (cpp): Xử lý #include, #define, macro → ra file .i
  • Compiler (cc1): Dịch C sang assembly → ra file .s
  • Assembler (as): Dịch assembly sang mã máy nhị phân → ra file .o (relocatable object)
  • Linker (ld): Ghép các .o lại, giải quyết tham chiếu ngoài → ra file thực thi

3.2 Kiểu dữ liệu và kích thước

Kiểu C32-bit64-bit
char1 byte1 byte
short2 bytes2 bytes
int4 bytes4 bytes
long4 bytes8 bytes
float4 bytes4 bytes
double8 bytes8 bytes
long double10/16 bytes10/16 bytes
pointer (con trỏ)4 bytes8 bytes

3.3 Điều khiển luồng – Condition Codes & Lệnh Jump

Luồng hoạt động:

Lệnh toán học / so sánh
Cập nhật %eflag (CF, ZF, SF, OF)
Lệnh jX kiểm tra cờ
Nhảy hay không nhảy

Lệnh sinh ra condition codes:

cmpl  a, b    # Tính b - a, không lưu kết quả, chỉ cập nhật cờ
testl a, b    # Tính b & a (AND), không lưu kết quả, chỉ cập nhật cờ

Bảng lệnh nhảy:

LệnhĐiều kiện nhảyÝ nghĩa
jmpLuôn luônNhảy không điều kiện
jeZF = 1Equal / Zero
jneZF = 0Not Equal
jsSF = 1Negative (âm)
jnsSF = 0Nonnegative
jg~(SF^OF) & ~ZFGreater (có dấu)
jge~(SF^OF)Greater or Equal (có dấu)
jlSF^OFLess (có dấu)
jle(SF^OF) | ZFLess or Equal (có dấu)
ja~CF & ~ZFAbove (không dấu)
jbCF = 1Below (không dấu)

Kết hợp cmpl + jX:

cmpl %ebx, %eax     # So sánh eax với ebx (tính eax - ebx)
jg   .label_true    # Nhảy nếu eax > ebx
AssemblyĐiều kiện C tương đương
cmpl src2, src1 + jesrc1 == src2
cmpl src2, src1 + jnesrc1 != src2
cmpl src2, src1 + jgsrc1 > src2
cmpl src2, src1 + jlsrc1 < src2
cmpl src2, src1 + jgesrc1 >= src2
cmpl src2, src1 + jlesrc1 <= src2

3.4 Từ C sang Assembly và ngược lại

Ví dụ: Rẽ nhánh if/else

// C code
int result;
if (a > b) {
    result = a - b;
} else {
    result = b - a;
}
# Assembly tương đương (AT&T)
    cmpl  %ebx, %eax      # so sánh a (eax) với b (ebx)
    jle   .else_branch    # nếu a <= b thì nhảy sang else
    subl  %ebx, %eax      # eax = a - b
    jmp   .end
.else_branch:
    subl  %eax, %ebx      # ebx = b - a
    movl  %ebx, %eax      # eax = kết quả
.end:

Ví dụ: Vòng lặp while

// C code
int i = 0, sum = 0;
while (i < 10) {
    sum += i;
    i++;
}
# Assembly tương đương
    movl $0, %ecx         # i = 0
    movl $0, %eax         # sum = 0
.loop_check:
    cmpl $10, %ecx        # so sánh i với 10
    jge  .loop_end        # nếu i >= 10 thì thoát
    addl %ecx, %eax       # sum += i
    incl %ecx             # i++
    jmp  .loop_check
.loop_end:

3.5 Thủ tục/Hàm và Stack

Cấu trúc stack frame (IA32):

Địa chỉ cao
┌──────────────────┐
│   ... (caller)   │  ← Caller's frame
├──────────────────┤
│   Argument n     │  ← Tham số thứ n (đẩy vào stack từ phải sang trái)
│   ...            │
│   Argument 1     │
├──────────────────┤
│  Return Address  │  ← Địa chỉ lệnh tiếp theo của caller (push khi call)
├──────────────────┤  ← %ebp trỏ vào đây
│   Saved %ebp     │  ← Lưu frame pointer cũ
├──────────────────┤
│ Saved Registers  │
│ Local Variables  │
├──────────────────┤  ← %esp trỏ vào đây (đỉnh stack)
Địa chỉ thấp
Thanh ghiVai trò
%ebpFrame pointer – trỏ đến đáy frame hiện tại (cố định trong suốt hàm)
%espStack pointer – trỏ đến đỉnh stack (thay đổi khi push/pop)

Truy cập tham số và biến cục bộ (IA32):

8(%ebp)     # Tham số thứ 1
12(%ebp)    # Tham số thứ 2
-4(%ebp)    # Biến cục bộ thứ 1
-8(%ebp)    # Biến cục bộ thứ 2

Cơ chế callret:

# Lệnh call:
# 1. Push địa chỉ lệnh kế tiếp (Return Address) vào stack
# 2. Nhảy đến địa chỉ hàm được gọi
call  func_name

# Lệnh ret:
# 1. Pop Return Address từ stack
# 2. Nhảy về địa chỉ đó
ret

Sự khác biệt IA32 vs x86-64:

IA32x86-64
Truyền tham sốQua stack6 tham số đầu qua thanh ghi: %rdi, %rsi, %rdx, %rcx, %r8, %r9; còn lại mới dùng stack
Giá trị trả về%eax%rax
Con trỏ stack/frame%esp, %ebp%rsp, %rbp

3.6 Mảng, Structure, Union

Mảng 1 chiều:

int val[5];
// val[0] tại địa chỉ x
// val[1] tại địa chỉ x + 4
// val[i] tại địa chỉ x + 4*i
# Truy cập val[i], biết &val[0] trong %eax, i trong %ecx
movl (%eax, %ecx, 4), %edx    # edx = val[i]

Structure và Alignment:

struct example {
    char  a;    // 1 byte tại offset 0
    // 3 bytes padding
    int   b;    // 4 bytes tại offset 4
    char  c;    // 1 byte tại offset 8
    // 3 bytes padding
};
// sizeof(struct example) = 12, không phải 6!

Union:

union data {
    int   i;    // 4 bytes
    float f;    // 4 bytes
    char  c;    // 1 byte
};
// sizeof(union data) = 4 (kích thước của thành phần lớn nhất)

Union chia sẻ cùng một vùng nhớ cho tất cả thành phần – chỉ một thành phần hợp lệ tại một thời điểm.


3.7 Linking

Symbol là gì?

Symbol là tên (hàm, biến toàn cục) mà linker cần giải quyết khi ghép các file .o lại.

Loại SymbolMô tả
Global symbolĐịnh nghĩa trong file này, có thể dùng ở file khác (extern sẽ dùng nó)
External symbolĐược dùng trong file này nhưng định nghĩa ở file khác
Local symbolChỉ dùng trong file này (biến static trong C)

Strong vs Weak symbol:

StrongWeak
Ví dụHàm được định nghĩa, biến toàn cục có khởi tạoBiến toàn cục không khởi tạo
Quy tắcChỉ được có 1 strong symbol cùng tênNhiều weak, linker chọn 1; weak bị ghi đè bởi strong

Các section quan trọng trong file ELF:

SectionNội dung
.textMã máy (code)
.dataBiến toàn cục/static đã khởi tạo
.bssBiến toàn cục/static chưa khởi tạo (không chiếm dung lượng trong file)
.symtabBảng symbol – danh sách tất cả các symbol

Relocation là quá trình linker điền địa chỉ thực tế vào các chỗ tham chiếu symbol, sau khi đã xác định được vị trí của từng section trong file thực thi cuối cùng.


4. Các Topic An Toàn Thông Tin

4.1 Reverse Engineering (Dịch ngược)

Là quá trình phân tích file nhị phân để hiểu logic chương trình mà không có source code. Kỹ năng cần thiết:

  • Đọc và hiểu assembly (AT&T hoặc Intel)
  • Dùng công cụ: objdump, IDA Pro, Ghidra
  • Nhận biết các pattern: hàm, vòng lặp, rẽ nhánh trong assembly

4.2 Buffer Overflow (Tràn bộ đệm)

void vulnerable(char *input) {
    char buf[8];
    strcpy(buf, input);  // Không kiểm tra độ dài!
}

Nếu input dài hơn 8 bytes, dữ liệu tràn sang vùng nhớ kế tiếp trên stack, có thể ghi đè Return Address, khiến chương trình nhảy đến mã tùy ý của kẻ tấn công.

Stack khi bị overflow:
┌──────────────┐
│ Return Addr  │ ← bị ghi đè bởi attacker!
├──────────────┤
│  Saved %ebp  │ ← bị ghi đè
├──────────────┤
│ buf[7..0]    │ ← 8 bytes buffer
└──────────────┘

4.3 Truy xuất ngoài mảng (Out-of-bounds Access)

int arr[5];
arr[10] = 42;  // Truy xuất ngoài phạm vi!

Trong C, không có kiểm tra biên. Việc đọc/ghi ngoài mảng có thể:

  • Làm hỏng dữ liệu khác trên stack/heap
  • Bị khai thác để thực thi mã độc (liên quan đến buffer overflow)
  • Gây crash chương trình (segmentation fault)

5. Tổng kết nhanh – Checklist ôn thi