Skip to content

Bài 4: Machine-Level Programming¤

1. Thanh ghi (Registers)¤

1.1 Kiến trúc IA32 — 8 thanh ghi 32 bit¤

IA32 có 8 thanh ghi 32 bit. Mỗi thanh ghi có thể truy cập ở nhiều kích thước khác nhau (32-bit, 16-bit, 8-bit cao, 8-bit thấp):

Thanh ghi 32-bit 16-bit 8-bit cao 8-bit thấp Mục đích
%eax %ax %ah %al Kết quả (Accumulator)
%ecx %cx %ch %cl Counter
%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

Lưu ý

%esp%ebp được dành riêng cho quản lý stack. Không nên dùng tùy tiện trong các phép tính toán thông thường.

1.2 Kiến trúc x86-64 — 16 thanh ghi 64 bit¤

x86-64 mở rộng các thanh ghi cũ lên 64 bit và bổ sung 8 thanh ghi mới:

  • Các thanh ghi cũ: %rax, %rbx, %rcx, %rdx, %rsi, %rdi, %rsp, %rbp
  • Thanh ghi mới: %r8 đến %r15
  • Mỗi thanh ghi 64-bit có thể truy cập phần 32-bit thấp (ví dụ: %r8d), 16-bit (%r8w), và 8-bit (%r8b)
  • %rbp trong x86-64 trở thành thanh ghi mục đích chung (không còn bắt buộc làm base pointer nữa)

2. Chuyển dữ liệu — mov¤

2.1 Các suffix của lệnh mov¤

Suffix quyết định số byte được chuyển:

Suffix Kích thước Ví dụ
movb 1 byte movb $123, %al
movw 2 bytes movw %ax, %bx
movl 4 bytes movl %eax, %ebx
movq 8 bytes (x86-64) movq %rax, %rbx
mov Tự động Dùng được với tất cả

Quy tắc quan trọng

Kích thước của thanh ghi trong lệnh phải khớp với suffix. Ví dụ:

GAS
movl %eax, %ebx    ; ✅ Hợp lệ — cả hai đều 32-bit
movb $123, %bl     ; ✅ Hợp lệ — %bl là 8-bit, suffix b
movl %eax, %bl     ; ❌ Sai — %eax 32-bit nhưng %bl 8-bit
movb $3, (%ecx)    ; ✅ Hợp lệ — ghi 1 byte vào bộ nhớ
mov (%eax), %bl    ; ✅ Hợp lệ — tự suy suffix từ %bl

2.2 Các kiểu toán hạng (Operands)¤

Text Only
Immediate  →  $0x400, $-533        ; Hằng số, tiền tố $
Register   →  %eax, %esi           ; Giá trị trong thanh ghi
Memory     →  (%eax), 0x100        ; Giá trị tại địa chỉ bộ nhớ

2.3 Bảng tổ hợp toán hạng hợp lệ của movl¤

Source Dest Ví dụ C tương đương
Imm Reg movl $0x4, %eax temp = 4;
Imm Mem movl $-147, (%eax) *p = -147;
Reg Reg movl %eax, %edx temp2 = temp;
Reg Mem movl %eax, (%edx) *p = temp;
Mem Reg movl (%eax), %edx temp = *p;

Không thể Mem → Mem

Không thể chuyển trực tiếp từ bộ nhớ sang bộ nhớ với một lệnh duy nhất. Phải dùng thanh ghi trung gian.


3. Các chế độ địa chỉ bộ nhớ¤

3.1 Dạng tổng quát¤

Text Only
D(Rb, Ri, S)   →   Mem[Reg[Rb] + S * Reg[Ri] + D]

Trong đó: - D: Hằng số dịch chuyển (displacement) — 1, 2, hoặc 4 bytes - Rb: Base register — bất kỳ thanh ghi nào - Ri: Index register — bất kỳ thanh ghi nào trừ %esp/%rsp - S: Scale — 1, 2, 4, hoặc 8

Tại sao S chỉ có thể là 1, 2, 4, hoặc 8?

Vì đây chính xác là kích thước của các kiểu dữ liệu phổ biến trong C: - char = 1 byte → S=1 - short = 2 bytes → S=2 - int / float = 4 bytes → S=4 - long / double / con trỏ 64-bit = 8 bytes → S=8

Điều này giúp truy cập phần tử mảng a[i] rất tự nhiên: địa chỉ = base + i * sizeof(type).

3.2 Các dạng đặc biệt¤

Dạng Công thức Ví dụ
(Rb, Ri) Mem[Reg[Rb] + Reg[Ri]] (%eax, %ecx)
D(Rb, Ri) Mem[Reg[Rb] + Reg[Ri] + D] 4(%eax, %ecx)
(Rb, Ri, S) Mem[Reg[Rb] + S*Reg[Ri]] (%eax, %ecx, 4)
(,Ri, S) Mem[Reg[Ri] * S] (, %ecx, 8)
D(Rb) Mem[Reg[Rb] + D] 8(%ebp)

4. Lệnh leal — Load Effective Address¤

4.1 Cú pháp và tác dụng¤

GAS
leal Src, Dst

leal tính địa chỉ theo biểu thức, rồi gán giá trị địa chỉ đó vào Dstkhông đọc bộ nhớ.

Hai công dụng chính:

  1. Tính địa chỉ: p = &x[i]
  2. Tính toán biểu thức dạng x + k*i + d (compiler hay dùng để tối ưu nhân/cộng)

4.2 Ví dụ: Compiler tối ưu phép nhân¤

C
int mul12(int x) {
    return x * 12;
}

Thay vì dùng imull $12, %eax, compiler sinh ra:

GAS
leal (%eax, %eax, 2), %eax    ; %eax = x + 2*x = 3x
sall $2, %eax                  ; %eax = 3x << 2 = 12x

Mẹo

leal không ảnh hưởng đến các cờ (flags) như addl hay imull. Đây là lý do compiler thích dùng leal để tính toán số học khi có thể.


5. Các phép tính toán học và logic¤

5.1 Lệnh 2 toán hạng¤

Lệnh Phép tính Ghi chú
addl Src, Dest Dest = Dest + Src
subl Src, Dest Dest = Dest - Src
imull Src, Dest Dest = Dest * Src
sall Src, Dest Dest = Dest << Src Còn gọi là shll
sarl Src, Dest Dest = Dest >> Src Arithmetic — giữ dấu (sign bit)
shrl Src, Dest Dest = Dest >> Src Logical — điền 0 vào bit cao
xorl Src, Dest Dest = Dest ^ Src
andl Src, Dest Dest = Dest & Src
orl Src, Dest Dest = Dest \| Src

Thứ tự toán hạng AT&T

Trong cú pháp AT&T (dùng trong Linux/GCC), Source đứng trước, Dest đứng sau — ngược với Intel syntax. addl %ecx, %eax nghĩa là %eax = %eax + %ecx.

Signed vs Unsigned

Không có sự khác biệt giữa signed và unsigned cho addl, subl, imull ở cấp độ bit. Sự khác biệt chỉ xuất hiện khi xử lý overflow và phép chia.

5.2 Lệnh 1 toán hạng¤

Lệnh Phép tính
incl Dest Dest = Dest + 1
decl Dest Dest = Dest - 1
negl Dest Dest = -Dest
notl Dest Dest = ~Dest (đảo toàn bộ bit)

6. Bài tập có lời giải¤

Bài tập 1 — Trace giá trị thanh ghi và bộ nhớ¤

Cho trước:

Thanh ghi Giá trị Địa chỉ Giá trị
%eax 0x100 0x100 0xF9
%ecx 0x1 0x104 0x11
%edx 0x3 0x108 0x15
0x10C 0xAB
Xem đáp án
Câu lệnh Vị trí thay đổi Giá trị mới Giải thích
addl %ecx, (%eax) Ô nhớ 0x100 0xFA 0xF9 + 0x1 = 0xFA
imull $2, (%eax,%edx,4) Ô nhớ 0x10C 0x22 Địa chỉ = 0x100 + 4*0x3 = 0x10C; 0x11 * 2 = 0x22
subl %ecx, %eax %eax 0xFF 0x100 - 0x1 = 0xFF
movl (%eax,%ecx,8), %eax %eax 0x15 Địa chỉ = 0x100 + 8*0x1 = 0x108; đọc giá trị 0x15
leal (%eax,%ecx,8), %edx %edx 0x108 leal không đọc bộ nhớ, chỉ tính địa chỉ: 0x100 + 8*0x1 = 0x108

Bài tập 2 — Lệnh nào gán %ebx = 2?¤

Cho trước: %eax = 0x1, %ebx = 0x2, %ecx = 0x1, %edx = 0x2

Bộ nhớ: 0x100 = 0x1, 0x104 = 0x2, 0x108 = 0x1

Text Only
A. movl %eax, %ebx          ; %ebx = %eax = 0x1  → ❌
B. movl 2, %ebx             ; Sai cú pháp (thiếu $) → ❌ không hợp lệ
C. addl %eax, %ebx          ; %ebx = 0x2 + 0x1 = 0x3 → ❌
D. imull %eax, %ebx         ; %ebx = 0x2 * 0x1 = 0x2 → ✅
E. movb $2, %bl             ; 8 bit thấp của %ebx = 2; %ebx = 0x2 → ✅
F. movl (%edx,%eax,2), %ebx ; địa chỉ = 0x2 + 2*0x1 = 0x4 → không hợp lệ ở đây
G. movl 1(%eax), %ebx       ; địa chỉ = 0x1 + 1 = 0x2, đọc Mem[0x2] → không xác định
H. addl (%ecx), %ebx        ; %ebx += Mem[0x1] → không xác định
Đáp án

DE là chắc chắn đúng trong ngữ cảnh cho sẵn.


Bài tập 3 — Dịch assembly sang C¤

GAS
; x tại (%ebp+8), y tại (%ebp+12), z tại (%ebp+16)
movl 12(%ebp), %eax    ; %eax = y
xorl 8(%ebp), %eax     ; %eax = y ^ x
sall $5, %eax          ; %eax = (y^x) << 5
incr %eax              ; %eax = ((y^x)<<5) + 1   [lưu ý: đây là lỗi đánh máy, phải là incl]
subl 16(%ebp), %eax    ; %eax = ((y^x)<<5) + 1 - z
Hàm C tương đương
C
int arith(int x, int y, int z) {
    int t1 = y ^ x;
    int t2 = t1 << 5;
    int t3 = t2 + 1;
    int t4 = t3 - z;
    return t4;
}
Kiểm tra với x=2, y=5, z=3
  • t1 = 5 ^ 2 = 7 (vì 0b101 ^ 0b010 = 0b111)
  • t2 = 7 << 5 = 224
  • t3 = 224 + 1 = 225
  • t4 = 225 - 3 = 222

Bài tập 4 — Điền assembly tương ứng với C¤

C
int fun2(int x, int y, int z) {
    int t1 = y - x;
    int t2 = 6 * z;
    return t1 ^ t2;
}
GAS
movl 16(%ebp), %edx         ; %edx = z
movl 12(%ebp), %eax         ; %eax = y
subl 8(%ebp),  %eax         ; %eax = y - x  (t1)
leal (%edx,%edx,2), %edx    ; %edx = z + 2z = 3z
addl %edx, %edx             ; %edx = 3z + 3z = 6z  (t2)
xorl %edx, %eax             ; %eax = t1 ^ t2  → kết quả
ret
Tại sao không dùng imull $6, %edx?

leal + addl nhanh hơn imull vì phép nhân có latency cao hơn trên nhiều kiến trúc. Đây là kỹ thuật strength reduction — compiler thay nhân bằng tổ hợp shift/add.


Bài tập 5 — Decode assembly¤

GAS
; x tại (%ebp+8), y tại (%ebp+12)
movl 8(%ebp),  %eax     ; %eax = x
subl 12(%ebp), %eax     ; %eax = x - y
sarl $31, %eax          ; %eax = (x-y) >> 31  → toàn bit 0 hoặc toàn bit 1 (mask)
movl %eax,   %edx       ; %edx = mask
andl 12(%ebp), %eax     ; %eax = mask & y
notl %edx               ; %edx = ~mask
andl 8(%ebp),  %edx     ; %edx = ~mask & x
orl  %edx,     %eax     ; %eax = (mask & y) | (~mask & x)
Hàm C và ý nghĩa
C
int decode(int x, int y) {
    int mask = (x - y) >> 31;   // toàn 1 nếu x<y, toàn 0 nếu x>=y
    int t1 = mask & y;           // nếu x<y: lấy y, ngược lại 0
    int t2 = ~mask & x;          // nếu x>=y: lấy x, ngược lại 0
    return t1 | t2;
}

Đây là hàm max(x, y) không dùng nhánh (branchless): - Nếu x < y: mask = 0xFFFFFFFF → kết quả = y - Nếu x >= y: mask = 0x00000000 → kết quả = x

sarl $31 với số 32-bit: dịch phải số học 31 bit — sign bit nhân rộng ra toàn bộ register.


Bài tập 6 — Viết lệnh assembly¤

Tác vụ Các cách viết
Tăng %eax lên 1 incl %eax hoặc leal 1(%eax), %eax hoặc addl $1, %eax
Nhân %ecx với 4 imull $4, %ecx hoặc sall $2, %ecx hoặc leal (,%ecx,4), %ecx
%ebx = %eax + 12 (chỉ địa chỉ) leal 12(%eax), %ebx
Trừ %edx từ ô nhớ 0x104 subl %edx, 0x104
Giữ 4 bit thấp của %ecx andl $0xF, %ecx
Đọc 2 byte từ -4(%eax) vào %cx movw -4(%eax), %cx

Bài tập 7 — Tính 10a + 4¤

a ở địa chỉ 0x102, %ebx = 0x100. Điền assembly:

GAS
movl 0x102, %eax               ; %eax = a
leal (%eax,%eax,4), %eax       ; %eax = a + 4a = 5a
leal 2(%eax), %eax             ; %eax = 5a + 2
leal (%eax,%eax), %eax         ; %eax = 2*(5a+2) = 10a + 4
Các cách khác cho từng bước
  • Bước 2: imull $5, %eax
  • Bước 3: addl $2, %eax
  • Bước 4: imull $2, %eax hoặc sall $1, %eax hoặc leal (,%eax,2), %eax

Bài tập 8 — Tính 6a + b + 4 trong 2 lệnh¤

Cho %eax = a, %ebx = b:

GAS
; Cách 1:
leal 2(%eax,%eax,2), %eax      ; %eax = 2 + 3a
leal (%ebx,%eax,2), %eax       ; %eax = b + 2*(3a+2) = 6a + b + 4 ✅

; Cách 2:
leal 4(%ebx,%eax,4), %ebx      ; %ebx = b + 4a + 4
leal (%ebx,%eax,2), %eax       ; %eax = (b+4a+4) + 2a = 6a + b + 4 ✅

Bài tập 9 — Tính (x+y)² / 2¤

x8(%ebp), y12(%ebp):

GAS
movl 8(%ebp),  %eax    ; %eax = x
addl 12(%ebp), %eax    ; %eax = x + y
imull %eax, %eax       ; %eax = (x+y)²
sarl $1, %eax          ; %eax = (x+y)² >> 1 = (x+y)²/2 (lấy phần nguyên)

Tại sao sarl $1 thay vì chia 2?

Dịch phải 1 bit = chia 2 (lấy phần nguyên, về phía âm vô cực với số âm). sarl (arithmetic shift right) giữ nguyên dấu của số.


Bài tập 10 — Tìm lỗi trong assembly¤

Hàm C cần chuyển đổi:

C
int func5(char* str) {
    int a = str[0] - '0';  // '0' = 0x30 = 48
    int b = str[1] - '0';
    return a + b;
}

Assembly có lỗi:

GAS
movl  8(%ebp), %eax      ; (1) %eax = địa chỉ str  ✅
movl  (%eax),  %al       ; (2) %al = str[0]  ⚠️ suffix không khớp
subl  $0x48,   %eax      ; (3) str[0] - '0'  ⚠️ dùng %eax thay vì %al
mov   1(%eax), %bh       ; (4) str[1] → vào %bh ❌ địa chỉ sai
subl  $'0,     %ebx      ; (5) - '0' ❌ cú pháp sai
addl  %ebx,    %eax      ; (6) a + b  ⚠️ size không nhất quán

Phân tích lỗi và sửa

Lỗi (2): Dùng movl để đọc 4 byte vào %al (1 byte) — không khớp suffix. Phải dùng movb.

Lỗi (3): Sau khi đọc str[0] vào %al, phải trừ từ %al, không phải %eax (vì %eax vẫn chứa địa chỉ).

Lỗi (4): Sau bước (3), %eax đã bị thay đổi → 1(%eax) không còn trỏ đến str[1]. Phải lưu địa chỉ gốc trước.

Lỗi (5): Cú pháp hằng số phải là $0x30 hoặc $48, không phải $'0.

Phiên bản sửa đúng:

GAS
movl  8(%ebp), %ecx        ; %ecx = địa chỉ str (giữ nguyên)
movb  (%ecx),  %al         ; %al = str[0]
subb  $0x30,   %al         ; %al = str[0] - '0'
movsx %al,     %eax        ; mở rộng sang 32-bit có dấu
movb  1(%ecx), %bl         ; %bl = str[1]
subb  $0x30,   %bl         ; %bl = str[1] - '0'
movsx %bl,     %ebx        ; mở rộng sang 32-bit
addl  %ebx,    %eax        ; %eax = a + b


Bài tập Bonus — Phân tích đoạn assembly ẩn¤

GAS
; x tại (%ebp+8), n tại (%ebp+12)
movl 12(%ebp), %ecx    ; (1) %ecx = n
movl  8(%ebp), %edx    ; (2) %edx = x
xorl %eax, %eax        ; (3) %eax = 0
addl $1, %eax          ; (4) %eax = 1
sall %ecx, %eax        ; (5) %eax = 1 << n   ← LỖI
subl $1, %eax          ; (6) %eax = (1<<n) - 1
andl %edx, %eax        ; (7) %eax = x & ((1<<n)-1)
Câu 1: Lệnh (3) xorl %eax, %eax có tác dụng gì?

Gán %eax = 0 — đây là kỹ thuật chuẩn để zero một register trong assembly x86. Tại sao dùng xor thay vì movl $0, %eax? - Lệnh xor reg, reg ngắn hơn (2 bytes vs 5 bytes với movl) - Trên nhiều CPU, CPU nhận ra pattern này và tối ưu đặc biệt (không cần đọc giá trị cũ)

Câu 2: Lỗi ở lệnh (5) sall %ecx, %eax là gì?

Trong IA32, lệnh shift chỉ chấp nhận %cl (8-bit thấp của %ecx) làm operand count, không nhận %ecx 32-bit. Sửa lại: sall %cl, %eax.

Câu 3: Hàm C tương đương
C
int bonus(int x, int n) {
    return x & ((1 << n) - 1);
}

Chức năng: Lấy n bit thấp nhất của x.

Ví dụ: x = 0b11011010, n = 4 - 1 << 4 = 0b10000 - (1<<4) - 1 = 0b01111 ← mask n bit 1 - x & mask = 0b11011010 & 0b00001111 = 0b00001010


7. Tổng kết — Sơ đồ tư duy¤

Text Only
Assembly cơ bản
├── Thanh ghi
│   ├── IA32: %eax..%ebp (32-bit)
│   └── x86-64: %rax..%r15 (64-bit)
├── Chuyển dữ liệu
│   ├── mov{b,w,l,q} — suffix khớp với kích thước thanh ghi
│   └── Không có Mem→Mem trực tiếp
├── Địa chỉ bộ nhớ
│   └── D(Rb, Ri, S) = Mem[Rb + S*Ri + D]
├── leal — tính địa chỉ/biểu thức, không đọc bộ nhớ
└── Phép tính
    ├── 2 toán hạng: add, sub, imul, sal, sar, shr, xor, and, or
    └── 1 toán hạng: inc, dec, neg, not