Bài 8: Machine-Level Programming: Procedures (Hàm/Thủ Tục) x86-64


1. Tổng quan về Hàm trong IA32 và x86-64

Cả hai kiến trúc đều dùng chung cơ chế gọi hàm dựa trên stack:

  • Stack hỗ trợ việc gọi/trở về từ hàm.
  • Sử dụng lệnh callret.
  • Khi call được thực thi, địa chỉ trả về (return address — địa chỉ câu lệnh assembly ngay sau lệnh call) được đẩy vào stack.
  • Trong x86-64, địa chỉ trả về có kích thước 8 bytes (64-bit).

2. Stack trong x86-64

Địa chỉ cao (Bottom)
┌─────────────────┐
│                 │
│   Stack Frame   │  ← Stack phát triển đi XUỐNG
│                 │
└─────────────────┘
Địa chỉ thấp (Top)  ← %rsp trỏ vào đây
  • %rsp (Stack Pointer): luôn trỏ vào đỉnh stack (địa chỉ thấp nhất đang dùng).
  • Lệnh push: %rsp -= 8, rồi ghi dữ liệu vào (%rsp).
  • Lệnh pop: đọc dữ liệu từ (%rsp), rồi %rsp += 8.

3. Thanh ghi x86-64

x86-64 có 16 thanh ghi đa năng (gấp đôi IA32), mỗi thanh ghi có thể truy xuất với các kích thước 8, 16, 32, 64-bit:

64-bit32-bitVai trò
%rax%eaxGiá trị trả về (Return value)
%rbx%ebxCallee-saved
%rcx%ecxTham số thứ 4
%rdx%edxTham số thứ 3
%rsi%esiTham số thứ 2
%rdi%ediTham số thứ 1
%rsp%espStack pointer (đặc biệt)
%rbp%ebpCallee-saved / Frame pointer tuỳ chọn
%r8%r8dTham số thứ 5
%r9%r9dTham số thứ 6
%r10%r10dCaller-saved
%r11%r11dCaller-saved
%r12%r15%r12d%r15dCallee-saved

4. Quy ước truyền tham số và trả về giá trị

4.1 Truyền tham số

x86-64 ưu tiên dùng thanh ghi thay vì stack để truyền tham số (khác hoàn toàn IA32):

Tham số 1  →  %rdi
Tham số 2  →  %rsi
Tham số 3  →  %rdx
Tham số 4  →  %rcx
Tham số 5  →  %r8
Tham số 6  →  %r9
Tham số 7+ →  Stack (theo thứ tự ngược)

4.2 Giá trị trả về

  • Giá trị trả về luôn nằm trong %rax.

5. Quy ước Caller-saved vs Callee-saved


6. Cấu trúc Stack Frame x86-64

┌──────────────────────┐  ← Caller's %rsp trước khi call
│  Tham số 7, 8, ...   │  (nếu có > 6 tham số)
├──────────────────────┤
│   Return Address     │  ← do lệnh call đẩy vào (8 bytes)
├──────────────────────┤  ← %rbp (tuỳ chọn, nếu dùng frame pointer)
│  Old %rbp (tuỳ chọn) │
├──────────────────────┤
│  Callee-saved regs   │  ← %rbx, %r12, ... được lưu tại đây
├──────────────────────┤
│   Local Variables    │  ← biến cục bộ không vừa thanh ghi
├──────────────────────┤
│   Argument Build     │  ← tham số 7+ chuẩn bị cho hàm cháu
└──────────────────────┘  ← %rsp hiện tại

7. Ví dụ minh họa từng bước

7.1 Hàm swap_l — Không cần stack frame

void swap_l(long *xp, long *yp) {
    long t0 = *xp;
    long t1 = *yp;
    *xp = t1;
    *yp = t0;
}
swap:
    movq  (%rdi), %rdx   ; t0 = *xp  (xp ở %rdi, tham số 1)
    movq  (%rsi), %rax   ; t1 = *yp  (yp ở %rsi, tham số 2)
    movq  %rax, (%rdi)   ; *xp = t1
    movq  %rdx, (%rsi)   ; *yp = t0
    ret

Toàn bộ thông tin lưu trong thanh ghi → không cần stack frame.


7.2 Hàm call_incr — Biến cục bộ cần địa chỉ

long call_incr() {
    long v1 = 15213;
    long v2 = incr(&v1, 3000);
    return v1 + v2;
}
call_incr:
    subq  $16, %rsp          ; Cấp phát 16 bytes trên stack
    movq  $15213, 8(%rsp)    ; v1 = 15213, lưu tại %rsp+8
    movl  $3000, %esi        ; Tham số 2: val = 3000
    leaq  8(%rsp), %rdi      ; Tham số 1: &v1
    call  incr               ; gọi incr(&v1, 3000)
    addq  8(%rsp), %rax      ; return v1 + v2  (v2 = %rax từ incr)
    addq  $16, %rsp          ; Thu hồi stack frame
    ret

7.3 Stack Frame phức tạp: swap_ele_su

long sum = 0;
void swap_ele_su(long a[], int i) {
    swap(&a[i], &a[i+1]);
    sum += (a[i] * a[i+1]);
}
swap_ele_su:
    movq   %rbx, -16(%rsp)    ; Lưu %rbx xuống stack (trước khi giảm %rsp)
    movq   %rbp, -8(%rsp)     ; Lưu %rbp xuống stack
    subq   $16, %rsp          ; Cấp phát frame (bây giờ vùng lưu ở 0(%rsp) và 8(%rsp))

    movslq %esi, %rax         ; Mở rộng i từ 32-bit → 64-bit
    leaq   8(%rdi,%rax,8), %rbx  ; %rbx = &a[i+1]  (callee-saved!)
    leaq   (%rdi,%rax,8),  %rbp  ; %rbp = &a[i]    (callee-saved!)

    movq   %rbx, %rsi         ; Tham số 2: &a[i+1]
    movq   %rbp, %rdi         ; Tham số 1: &a[i]
    call   swap

    movq   (%rbx), %rax       ; a[i+1]
    imulq  (%rbp), %rax       ; a[i] * a[i+1]
    addq   %rax, sum(%rip)    ; sum += ...  (biến global, RIP-relative)

    movq   (%rsp),   %rbx     ; Khôi phục %rbx
    movq   8(%rsp),  %rbp     ; Khôi phục %rbp
    addq   $16, %rsp          ; Thu hồi frame
    ret

8. Hàm đệ quy — pcount_r

8.1 Phiên bản IA32

int pcount_r(unsigned x) {
    if (x == 0) return 0;
    else return (x & 1) + pcount_r(x >> 1);
}
pcount_r:
    pushl  %ebp
    movl   %esp, %ebp
    pushl  %ebx            ; Lưu %ebx (callee-saved)
    subl   $4, %esp        ; Cấp phát 4 bytes cho tham số đệ quy

    movl   8(%ebp), %ebx   ; %ebx = x
    movl   $0, %eax        ; %eax = 0 (chuẩn bị return 0)
    testl  %ebx, %ebx
    je     .L3             ; if (x == 0) goto .L3

    movl   %ebx, %eax
    shrl   %eax            ; %eax = x >> 1
    movl   %eax, (%esp)    ; Đẩy (x>>1) lên stack làm tham số
    call   pcount_r        ; Đệ quy

    movl   %ebx, %edx
    andl   $1, %edx        ; %edx = x & 1
    leal   (%edx,%eax), %eax  ; %eax = (x&1) + pcount_r(x>>1)
.L3:
    addl   $4, %esp
    popl   %ebx
    popl   %ebp
    ret

8.2 Phiên bản x86-64 (gọn hơn nhiều!)

pcount_r:
    movl   $0, %eax        ; Return value = 0 (trường hợp x==0)
    testq  %rdi, %rdi
    je     .L6             ; if (x == 0) return 0

    pushq  %rbx            ; Lưu %rbx (callee-saved)
    movq   %rdi, %rbx      ; %rbx = x  (giữ x qua lời gọi đệ quy)
    andl   $1, %ebx        ; %rbx = x & 1
    shrq   %rdi            ; %rdi = x >> 1  (tham số cho đệ quy)
    call   pcount_r        ; Đệ quy → kết quả ở %rax
    addq   %rbx, %rax      ; %rax = pcount_r(x>>1) + (x&1)
    popq   %rbx            ; Khôi phục %rbx
.L6:
    rep; ret

Luồng thực thi đệ quy theo từng bước:

flowchart TD A["Gọi pcount_r(x)"] --> B{x == 0?} B -- Có --> C["return 0\n(%rax = 0)"] B -- Không --> D["pushq %rbx\nlưu x & 1 vào %rbx"] D --> E["shrq %rdi\n(x >> 1 làm tham số mới)"] E --> F["call pcount_r(x >> 1)"] F --> G["addq %rbx, %rax\n(kết quả + x & 1)"] G --> H["popq %rbx\nkhôi phục"] H --> I["ret"]

9. Bài tập có giải

Bài 1.1 — Đọc assembly IA32, viết lại C

.LC0: .string "%d"
.LC1: .string "%d %d"
example:
    pushl  %ebp
    movl   %esp, %ebp
    subl   $8, %esp
    movl   $5,  -4(%ebp)    ; a = 5
    movl   $10, -8(%ebp)    ; b = 10
    leal   -8(%ebp), %eax
    pushl  %eax
    pushl  $.LC0
    call   scanf             ; scanf("%d", &b)
    addl   $8, %esp
    movl   -8(%ebp), %eax
    pushl  -4(%ebp)
    pushl  %eax
    pushl  $.LC1
    call   printf            ; printf("%d %d", b, a)
    addl   $12, %esp
    movl   $0, %eax
    leave
    ret

Bài 1.2 — Vẽ stack khi gọi scanf

Giả sử: Trước dòng pushl %ebp: %esp = 0x100, %ebp = 0x120

Địa chỉ   | Nội dung
----------+------------------
0x120     | (old %ebp frame trước)
0x11C     | Return addr of example
0x118     | ← %ebp sau dòng 5 (movl %esp,%ebp)
0xFC      | a = 5           (−4(%ebp))
0xF8      | b = 10          (−8(%ebp))
0xF4      | ← %esp sau subl $8
          |
--- Sau push &b và push $.LC0: ---
0xF0      | &b = 0xF8
0xEC      | $.LC0 ("%%d")
0xE8      | Return addr of scanf  ← %esp khi vào scanf

Bài 2.1 — Phân tích chuỗi từ hằng số hex

movl $0x74746E61, -8(%ebp)

Bài 3.1 — Đọc assembly, xác định biến cục bộ và lời gọi hàm

sub  $0x40, %esp
movl $0x04030201, 0x3c(%esp)   ; a = 0x04030201  tại %ebp-4
movl $0x0,        0x38(%esp)   ; b = 0            tại %ebp-8
; fgets(buf, 50, stdin)
mov  0x0804a02c, %eax          ; stdin
mov  %eax, 0x8(%esp)           ; tham số 3
movl $0x32, 0x4(%esp)          ; tham số 2: 50 (0x32)
lea  0x10(%esp), %eax
mov  %eax, (%esp)              ; tham số 1: &buf
call fgets
; printf("\n[buf]: %s\n", buf)
lea  0x10(%esp), %eax
mov  %eax,         0x4(%esp)   ; tham số 2: buf
movl $0x8048610,   (%esp)      ; tham số 1: chuỗi format
call printf

10. Tóm tắt x86-64 Procedures

mindmap root((x86-64 Procedures)) Truyền tham số 6 tham số đầu qua thanh ghi Tham số 7+ qua stack Giá trị trả về trong %rax Quản lý thanh ghi Caller-saved rax rdi rsi rdx rcx r8 r9 r10 r11 Callee-saved rbx rbp r12 r13 r14 r15 Stack pointer rsp đặc biệt Stack Frame Cấp phát một lần subq rsp Truy xuất qua rsp không cần rbp Thu hồi addq rsp Đặc điểm nổi bật Có thể không cần stack frame Hỗ trợ đệ quy đầy đủ Red zone 128 bytes phía dưới rsp