Tuesday, May 26, 2015

Hàm trong C/C++ được gọi thế nào?

Những đoạn code bên dưới thực thi trên cấu trúc x86(32bit). Xem thêm Deassembly trong Visual Studio

- 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.
Ví dụ:

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í đó.
    
     push ebp
     mov ebp, esp
    
    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.
  • 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).
    
     sub esp, 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).
  • 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.
Đoạn code assembly của hàm Add trên:
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.

No comments:

Post a Comment