- Calling convention: là một bộ nguyên tắc để gọi hàm, giúp cho việc gọi hàm và các hàm con, đệ quy.. bên trong một chương trình được thực thi bằng các thanh nhớ(register), stack... trên CPU.
- Calling convention:
Nguyên tắc này sử dụng các lệnh cơ bản của CPU: push, pop, call, ret. Có 2 nguyên tắc một dành cho Caller(Caller's rules - code nơi gọi hàm) và Callee(Callee's rules - code trong hàm được gọi). Các ngôn ngữ khác khi tương tác với cấu trúc X86 cũng sẽ tương tự
1. Nguyên tắc cho Caller:
- 1. Trước khi gọi một hàm/chương trình con(subroutine) caller sẽ lưu lại(backup) các giá trị trên thanh nhớ(register) EBX, ECX, EDX bằng cách đẩy(push) vào stack.
- 2. Đẩy các tham số của hàm vào stack theo thứ tự từ phải sang trái, tức là tham số cuối cùng sẽ được đẩy vào đầu tiên. Vì stack bắt đầu từ địa chỉ cao và mở rộng xuống địa chỉ thấp khi giá trị được push vào, tham số đầu tiên sẽ nằm ở địa chỉ thấp nhất để khi xử lí sẽ được pop ra đầu tiên.
- 3. Gọi hàm bằng chỉ thị(instruction) call. Chỉ thị này sẽ đặt địa chỉ của giá trị trả về vào đầu stack và rẽ nhánh vào(nhảy vào) đoạn code bên trong hàm để thực thi.
- 4. Sau khi thực thi xong hàm, caller sẽ xóa(remove) các tham số của hàm ra khỏi stack, và khôi phục lại top index của stack.
- 5. Caller có thể lấy giá trị trả về của hàm trong thanh ghi EAX(nếu hàm có trả về).
- 6. Caller khôi phục(pop ra khỏi stack) các giá trị trên EBX, ECX, EDX đã backup ở bước 1. Như vậy sau khi thực hiện xong, các giá trị trên các thanh nhớ không bị thay đổi bởi hàm/chương trình con.
int Add(int num1, int num2)
{
int m = 0;
int n = num1 + num2;
return n;
}
int main()
{
int a = 10;
int b = 20;
int c = Add(a, b);
return 0;
}
Hình ảnh code Deassembly (trong Visual Studio) của đoạn code trong hàm main(). Ta xem assembly của dòng lệnh gọi hàm Add(phía dưới lệnh gọi hàm Add)Mô tả code assembly:
int a = 10;
mov dword ptr [a],0Ah //Gán giá trị 10(0A ở hệ Hex) vào
//vùng nhớ của biến a
int b = 20;
mov dword ptr [b],14h //Gán giá trị 20 vào vùng nhớ của biến b
int c = Add(a, b);
mov eax,dword ptr [b] //Đưa trị của biến b vào thanh nhớ EAX
push eax //Đẩy(push) giá trị (của biến b)
// từ thanh nhớ EAX vào stack
mov ecx,dword ptr [a] //Đưa trị của biến a vào thanh nhớ EAX
push ecx //Đẩy(push) giá trị (của biến a)
// từ thanh nhớ EAX vào stack
call Add (012811C7h) //Gọi chỉ thị thực thi hàm Add.
//Sau khi thực thi hàm này,
// kết quả được đẩy vào EAX bởi callee
add esp,8 //Sau khi thực thi hàm Add xong, xóa(pop)
// 2 giá trị của a và b ra khỏi stack
// bằng cách di chuyển stack pointer lên
// 8 byte(2 biến int a và b được push vào
// mổi biến là 4 byte).
//Do stack ghi tử địa chỉ cao
// xuống địa chỉ thấp nên xóa bằng cách
// đẩy stack pointer lên.
mov dword ptr [c],eax //Gán giá trị trả về từ EAX vào biến c.
2. Nguyên tắc cho Callee:
- 1. Ở đầu mổi hàm giá trị trên EBP được đẩy vào stack(backup giá trị của EBP), và copy giá trị của ESP và EBP. EBP giống như một chỉ mục để tìm kiếm param và local variable trên stack. EBP giống như một "snapshot" của giá tri trên stack pointer khi hàm con được thực hiện. Các biến local và param nằm ở một vị trí xác định trên stack và EBP sẽ lưu lại vị trí đó.
Phải backup lại giá trị trên EBP trước khi thực hiện hàm để quá trình thực thi nội dung hàm không làm thay đổi giá trị trên EBP của Caller. Sau đó copy giá trị của ESP(stack pointer) vào EBP để sử dụng cho việc truy cập các param và local variable.push ebp mov ebp, esp
- 2. Cấp phát vùng nhớ trên stack cho biến local. Stack sẽ cấp phát mở rộng xuống vùng nhớ thấp, tùy theo số lượng biến sẽ được cấp phát một kích thước nhất đinh. Ví dụ 3 biến integer(mổi biến 4 byte) sẽ được cấp phát 12byte(stack pointer giảm xuống 12).
Cũng như param, biến local nằm trên một vị trí nhất định trên stack và được ghi lại trong EBP(base pointer).sub esp, 12
- 3. Các giá tri trên các thanh nhớ(EDI và ESI) được lưu lại vào stack.
Sau các bước trên, nội dung hàm sẽ được thực thi. - 4. Sau khi hàm kết thúc, kết quả trả về sẽ được lưu vào thanh nớ EAX.
- 5. Khôi phục các giá trị đã được lưu(backup) trên stack vào lại các thanh nhớ(EDI và ESI).
- 6. Hủy các biến local bằng cách di chuyển con trỏ stack pointer về lại vị trí trước khi cấp phát vùng nhớ cho các biến này. Giá trị này được lưu trong EBP(ở bước 1).
mov esp, ebp
- 7. Khôi phục lại giá trị EBP đã backup ở bước 1.
- 8. Thoát ra khỏi hàm và trở về đoạn code caller bằng chỉ thị ret. Chỉ thị này sẽ xóa địa chỉ của biến được return khỏi stack.
Mô tả đoạn code assembly:
int Add(int num1, int num2)
{
push ebp //Lưu giá trị base pointer củ
// vào stack
mov ebp,esp //Gán giá trị base pointer mới
// là giá trị của stack pointer
sub esp,8 //Đẩy stack pointer xuống 8 byte
// (mở rông vùng nhớ cho 2 biến
// local int là m và n).
mov dword ptr [n],0CCCCCCCCh //Khởi tạo biến
mov dword ptr [m],0CCCCCCCCh
int m = 0;
mov dword ptr [m],0 //Gán giá trị cho biến m.
int n = num1 + num2; //Các dòng lệnh bên dưới
// để thực hiện phép cộng
mov eax,dword ptr [num1]
add eax,dword ptr [num2]
mov dword ptr [n],eax
return n;
mov eax,dword ptr [n] //Gán giá trị trả về vào
// thanh nhớ EAX
}
Hình minh họa stack khi gọi hàm
Stack mở rộng từ trên xuống(địa chỉ cao xuống địa chỉ thấp). Caller sẽ đẩy các giá trị vào stack trước rồi gọi hàm. Khi vào hàm con, các stack của callee được tạo ra trong quá trình xử lí. Khi kết thúc hàm con, callee sẽ xóa các stack của mình(nâng stack pointer lên) và thoát ra khỏi hàm, trở về caller. Caller lấy giá trị trả về và cũng xóa các stack của mình.
Càng nhiều hàm lồng vào nhau thì stack sẽ càng mở rộng xuống để lưu các giá trị của caller bên ngoài. Và khi kết thúc stack cũng được xóa ngược lên.