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
callvàret. - Khi
callđược thực thi, địa chỉ trả về (return address — địa chỉ câu lệnh assembly ngay sau lệnhcall) đượ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-bit | 32-bit | Vai trò |
|---|---|---|
%rax | %eax | Giá trị trả về (Return value) |
%rbx | %ebx | Callee-saved |
%rcx | %ecx | Tham số thứ 4 |
%rdx | %edx | Tham số thứ 3 |
%rsi | %esi | Tham số thứ 2 |
%rdi | %edi | Tham số thứ 1 |
%rsp | %esp | Stack pointer (đặc biệt) |
%rbp | %ebp | Callee-saved / Frame pointer tuỳ chọn |
%r8 | %r8d | Tham số thứ 5 |
%r9 | %r9d | Tham số thứ 6 |
%r10 | %r10d | Caller-saved |
%r11 | %r11d | Caller-saved |
%r12–%r15 | %r12d–%r15d | Callee-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ại7. 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
retToà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
ret7.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
ret8. 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
ret8.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; retLuồ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
retBà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 scanfBà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 printf10. 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