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-bit | 16-bit | 8-bit cao | 8-bit thấp | Ý nghĩa gốc |
|---|---|---|---|---|
%eax |
%ax |
%ah |
%al |
Accumulator – Kết quả trả về |
%ecx |
%cx |
%ch |
%cl |
Counter – Đếm vòng lặp |
%edx |
%dx |
%dh |
%dl |
Data |
%ebx |
%bx |
%bh |
%bl |
Base |
%esi |
%si |
— | — | Source Index |
%edi |
%di |
— | — | Destination Index |
%esp |
%sp |
— | — | Stack Pointer |
%ebp |
%bp |
— | — | Base Pointer (Frame Pointer) |
Tính tương thích ngược
Các thanh ghi 16-bit (%ax, %cx,...) là phần thấp của thanh ghi 32-bit. Điều này đảm bảo chương trình 16-bit cũ vẫn chạy được trên CPU 32-bit.
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-bit | 32-bit thấp |
|---|---|
%rax |
%eax |
%r8 |
%r8d |
%r15 |
%r15d |
1.4 Thanh ghi đặc biệt¤
| Thanh ghi | Tên | Chức năng |
|---|---|---|
%rip / %eip |
Program Counter / Instruction Pointer | Trỏ đến địa chỉ lệnh tiếp theo sẽ được thực thi |
%rflag / %eflag |
Condition Codes | Lư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 |
|---|---|---|
CF |
Carry Flag | Có tràn số không dấu |
ZF |
Zero Flag | Kết quả bằng 0 |
SF |
Sign Flag | Kết quả âm |
OF |
Overflow Flag | Tràn số có dấu |
2. Assembly Instruction (AT&T)¤
2.1 Sự khác biệt AT&T vs Intel¤
| Đặc điểm | Intel | AT&T |
|---|---|---|
| Thứ tự toán hạng | mov dest, src |
mov src, dest |
| Thanh ghi | eax |
%eax |
| Hằng số | 5 |
$5 |
| Lệnh mov | mov |
movl, movq, movb... |
| Địa chỉ bộ nhớ | [ebp + 8] |
8(%ebp) |
Gặp ở đâu?
- Intel syntax: IDA Pro, hoặc dùng
gcc -masm=intel,objdump -M intel - AT&T syntax (mặc định):
gcc,objdump(không cần tùy chọn thêm)
2.2 Lệnh mov và các dạng hợp lệ¤
Suffix quyết định kích thước dữ liệu:
| Suffix | Kích thước |
|---|---|
movb |
1 byte |
movw |
2 bytes |
movl |
4 bytes |
movq |
8 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 |
Quy tắc quan trọng
Không thể chuyển dữ liệu trực tiếp từ ô nhớ sang ô nhớ trong một lệnh assembly. Phải dùng thanh ghi trung gian.
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?
Đáp án
movl %eax, %ebx→ ✅ Hợp lệ –movldùng 2 thanh ghi 32-bitmovb $123, %bl→ ✅ Hợp lệ –movb(1 byte) dùng với%bl(thanh ghi 8-bit)movl %eax, %bl→ ❌ Không hợp lệ –movllà 4 bytes nhưng%blchỉ 1 byte, không khớp kích thướcmovb $3, (%ecx)→ ✅ Hợp lệ – Ghi 1 byte vào địa chỉ bộ nhớ do%ecxtrỏmov (%eax), %bl→ ✅ Hợp lệ –movkhông có suffix tự suy kích thước từ thanh ghi đích (%bl= 1 byte)
Kết quả: 4 lệnh hợp lệ.
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ĩa | Ràng buộc |
|---|---|---|
D |
Displacement – hằng số dịch chuyển | 1, 2, hoặc 4 bytes |
Rb |
Base register | Bất kỳ thanh ghi nào |
Ri |
Index register | Bất kỳ, trừ %rsp/%esp |
S |
Scale | 1, 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, Dst |
leal Src, Dst |
|
|---|---|---|
| Tính địa chỉ từ biểu thức | ✅ | ✅ |
| Truy xuất ô nhớ | ✅ | ❌ |
| Kết quả gán vào Dst | Dữ liệu tại ô nhớ | Địa chỉ của ô nhớ |
Ứng dụng đặc biệt của leal
Compiler thường dùng leal để tính biểu thức toán học nhanh hơn, thay vì thực sự truy xuất bộ nhớ:
2.6 Các lệnh toán học thường gặp¤
| Lệnh | Ý nghĩa | Ví dụ |
|---|---|---|
leal S, D |
D = địa chỉ(S) – không truy xuất mem | leal (%eax,%eax,4), %eax |
incl D |
D = D + 1 | incl %eax |
decl D |
D = D − 1 | decl %ecx |
negl D |
D = −D | negl %ebx |
notl D |
D = ~D (bitwise NOT) | notl %eax |
addl S, D |
D = D + S | addl %ecx, %eax |
subl S, D |
D = D − S | subl $4, %esp |
imull S, D |
D = D × S | imull %ecx, %eax |
xorl S, D |
D = D XOR S | xorl %eax, %eax (đặt về 0) |
andl S, D |
D = D AND S | andl $0xFF, %eax |
orl S, D |
D = D OR S | orl $1, %eax |
sall k, D |
D = D << k (left shift) | sall $2, %eax (×4) |
sarl k, D |
D = D >> k (arithmetic, giữ dấu) | sarl $1, %eax (/2 có dấu) |
shrl k, D |
D = 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
.olạ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 C | 32-bit | 64-bit |
|---|---|---|
char |
1 byte | 1 byte |
short |
2 bytes | 2 bytes |
int |
4 bytes | 4 bytes |
long |
4 bytes | 8 bytes |
float |
4 bytes | 4 bytes |
double |
8 bytes | 8 bytes |
long double |
10/16 bytes | 10/16 bytes |
pointer (con trỏ) |
4 bytes | 8 bytes |
Chú ý kiểu long và pointer
long và con trỏ thay đổi kích thước theo kiến trúc. Trên 64-bit, con trỏ chiếm 8 bytes. Đây là lý do code C không portable nếu giả định sizeof(int) == sizeof(pointer).
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 |
|---|---|---|
jmp |
Luôn luôn | Nhảy không điều kiện |
je |
ZF = 1 | Equal / Zero |
jne |
ZF = 0 | Not Equal |
js |
SF = 1 | Negative (âm) |
jns |
SF = 0 | Nonnegative |
jg |
~(SF^OF) & ~ZF |
Greater (có dấu) |
jge |
~(SF^OF) |
Greater or Equal (có dấu) |
jl |
SF^OF |
Less (có dấu) |
jle |
(SF^OF) \| ZF |
Less or Equal (có dấu) |
ja |
~CF & ~ZF |
Above (không dấu) |
jb |
CF = 1 | Below (không dấu) |
Kết hợp cmpl + jX:
| Assembly | Điều kiện C tương đương |
|---|---|
cmpl src2, src1 + je |
src1 == src2 |
cmpl src2, src1 + jne |
src1 != src2 |
cmpl src2, src1 + jg |
src1 > src2 |
cmpl src2, src1 + jl |
src1 < src2 |
cmpl src2, src1 + jge |
src1 >= src2 |
cmpl src2, src1 + jle |
src1 <= src2 |
3.4 Từ C sang Assembly và ngược lại¤
Ví dụ: Rẽ nhánh if/else
# 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
# 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:
Cách đọc assembly ngược về C
- Tìm điều kiện
cmp+j→ đó là điều kiệnifhay điều kiện dừng vòng lặp - Tìm
jmpvô điều kiện quay lại → đó là vòng lặp - Tìm
call→ đó là lời gọi hàm - Tìm
push/pop+call/ret→ đó là frame của hàm
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 ghi | Vai trò |
|---|---|
%ebp |
Frame pointer – trỏ đến đáy frame hiện tại (cố định trong suốt hàm) |
%esp |
Stack 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ế call và ret:
# 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:
| IA32 | x86-64 | |
|---|---|---|
| Truyền tham số | Qua stack | 6 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:
# 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!
Quy tắc Alignment
Mỗi trường trong struct phải bắt đầu tại địa chỉ chia hết cho kích thước của nó. Compiler tự chèn padding bytes để đảm bảo điều này. Tổng kích thước struct cũng được làm tròn lên bội số của trường lớn nhất.
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 Symbol | Mô 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 symbol | Chỉ dùng trong file này (biến static trong C) |
Strong vs Weak symbol:
| Strong | Weak | |
|---|---|---|
| Ví dụ | Hàm được định nghĩa, biến toàn cục có khởi tạo | Biến toàn cục không khởi tạo |
| Quy tắc | Chỉ được có 1 strong symbol cùng tên | Nhiều weak, linker chọn 1; weak bị ghi đè bởi strong |
Các section quan trọng trong file ELF:
| Section | Nội dung |
|---|---|
.text |
Mã máy (code) |
.data |
Biến toàn cục/static đã khởi tạo |
.bss |
Biến toàn cục/static chưa khởi tạo (không chiếm dung lượng trong file) |
.symtab |
Bả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)¤
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)¤
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¤
Mở rộng checklist
- Phân biệt AT&T vs Intel syntax
- Các suffix
b/w/l/qvà thanh ghi tương ứng - Các tổ hợp hợp lệ của
mov(Imm/Reg/Mem) - Tính địa chỉ với
D(Rb, Ri, S) - Phân biệt
lealvsmovl - Đọc/viết assembly cho
if,while,for - Condition codes: CF, ZF, SF, OF – khi nào set
- Bảng lệnh
jXvà điều kiện nhảy - Stack frame:
%ebp,%esp, vị trí tham số/biến cục bộ - Khác biệt truyền tham số IA32 vs x86-64
- Alignment trong struct
- Strong/Weak symbol và quy tắc linker
- Section
.text,.data,.bss,.symtab - Buffer overflow hoạt động như thế nào