Thứ Sáu, 26 tháng 7, 2013

Stack Frames

Trong bài viết này tôi sẽ trình bày cách thức subroutines có thể khai báo parameters mà đã được định vị trí trong runtime stack.

Stack frame là một vùng của stack được thiết lập dành cho arguments được đưa vào, subroutine return address, local variables, và saved registers. Stack frame được tạo ra là kết quả của thứ tự các bước sau:

  • Passed arguments, nếu có, được pushed vào stack.
  • Subroutine được gọi, địa chỉ trả về của subroutine được pushed vào stack.
  • Khi subroutin bắt đầu thực thi, EBP được pushed vào stack.
  • EBP được set bằng với ESP. Từ đây, EBP giống như một tham chiếu cơ bản (base reference ) cho tất cả subroutine parameters. 
  • Nếu có local variables, ESP được giảm đi để dự trữ không gian cho các biến trên stack. 
  • Nếu bất cứ thanh ghi nào cần được saved, chúng được pushed vào stack. 
Cấu trúc của một một stack frame bị tác động trực tiếp bởi mô hình bộ nhớ của một chương trình và phụ thuộc vào quy ước passing argument. 

Có một lý do chính đáng để học về passing arguments lên stack; gần như hầu hết các ngôn ngữ lập trình bậc cao sử dụng chúng. Ví dụ, nếu bạn muốn gọi functions trong MS-Windows Application Programmer Interface (API), bạn phải pass arguments vào stack. 

Stack Parameters

Chúng ta đã biết một cách để pass arguments đến procedures thông qua việc sử dụng registers. Chúng ta có thể nói rằng các procedures sử dụng register parameters.  Register parameters được tối ưu hóa cho tốc độ thực thi của chương trình và dễ dàng sử dụng. Không may mắn rằng, register parameters thường tạo code "rác" trong các chương trình gọi. Các contents tồn tại trên thanh ghi phải được lưu lại trước khi chúng có thể nạp các giá trị argument vào. Một trường hợp là khi gọi DumpMem từ thư viện Irvine32:


pushad
mov esi,OFFSET array                       ; starting OFFSET
mov ecx,LENGTHOF array                ; size, in units
mov ebx,TYPE array                          ; doubleword format
call DumpMem                                   ; display memory
popad

Stack parameters cho phép ta một cách tiếp cận linh hoạt hơn. Chỉ là trước subroutine call, arguments được pushed vào stack. Cho ví dụ, nếu DumpMem sử dụng stack parameters, chúng ta gọi nó sử dụng code sau:

push TYPE array
push LENGTHOF array
push OFFSET array
call DumpMem

Hai kiểu arguments được pushed vào stack trong suốt subroutine calls:
  • Value arguments (các giá trị của các biến và constants)
  • Reference arguments (các địa chỉ của các biến)
Passing by Value khi một argument được passed bằng value , một bản sao của giá trị này được pushed vào stack. Giả sử chúng ta gọi một subroutine có tên là AddTwo, passing nó 2 số nguyên 32-bit:

.data
val1    DWORD  5
val2    DWORD  6
.code
push   val2
push   val1
call AddTwo

Dưới đây là hình ảnh của stack trước CALL instruction 

Một function call tương đương viết trong C++ là :
  int sum = AddTwo( val1, val2);

Quan sát ta thấy rằng arguments được pushed vào stack theo thứ tự ngược lại với thứ tự khi ta khai báo, đây là một chuẩn của C và C++. 

Passing by Reference

Một argument passed thông qua reference gồm địa chỉ (offset) của một đối tượng. Các statements sau gọi Swap , passing 2 arguments bởi reference:

push   OFFSET  val2
push   OFFSET  val1
call     Swap

Phía dưới là hình ảnh của stack trước khi gọi Swap:

Hàm Swap trong C/C++ có thể được viết như sau:
  Swap(&val1, &val2);

Passing Arrays 

Các ngôn ngữ lập trình bậc cao luôn luôn pass arrays tới subroutines qua địa chỉ. Là như sau, chúng push địa chỉ của một mảng vào stack. Subroutine sau đó lấy địa chỉ từ stack và sử dụng nó để truy nhập đến mảng. Dễ dàng để nhận ra tại sao ta không muốn pass một mảng bởi value, bởi vì nếu làm như vậy yêu cầu phải push từng phần tử của mảng vào stack một cách riêng lẻ. Những hành động như vậy thường rất chậm và có thể chiếm dụng nhiều không gian trong stack. Statements phía dưới đây thực hiện đúng cách thông qua việc passing offset của một mảng đến một subroutine có tên là ArrayFill:

.data
array DWORD 50 DUP(?)
.code
push OFFSET array
call ArrayFill

Accessing Stack Parameters

Các ngôn ngữ lập trình bậc cao có nhiều cách để khởi tạo và truy nhập đến parameters chung suốt function calls. Chúng tôi sẽ sử dụng C và C++ để làm ví dụ. Chúng bắt đầu với một prologue bao gồm các statements làm nhiệm vụ save EBP register và trỏ EBP đến đỉnh của stack. Tùy theo lựa chọn, bạn có thể push một lượng registers lên stack, các giá trị trong các thanh ghi này sẽ được restored khi function returns. Cuối của function bao gồm một epilogue nơi mà EBP register được restored và RET instruction returns đến hàm gọi. 

AddTwo Example hàm AddTwo dưới đây, được viết trong C, nhận 2 số nguyên được passed bởi giá trị và return tổng của chúng:

int AddTwo(int x, int y)
{
       return x + y;
}

Nào, hãy cài đặt hàm này trong assembly. Trong prologue của nó, AddTwo pushes EBP vào stack để bảo quản giá trị tồn tại của nó:

AddTwo PROC
              push ebp

Tiếp theo, EBP được set đến cùng giá trị với ESP, vì vậy EBP có thể là base pointer cho AddTwo's stack frame:

AddTwo PROC
              push ebp
              mov ebp, esp

Sau khi 2 instructions thực thi, figure phía dưới đây shows contents của một stack frame. Một function call ví dụ như AddTwo(5, 6) có thể tạo ra parameter thứ hai được pushed trên stack, theo sau là parameter thứ nhất:

AddTwo có thể push các thanh ghi bổ sung mà không cảnh báo offsets của stack parameters từ EBP. ESP có thể thay đổi giá trị, nhưng EBP thì không. 

Base-Offset Addressing  Chúng tôi sử dụng base-offset addressing để access stack parameters. EBP là base register và offset là một constant. 32-bit values được trả về trong EAX. Implementation phía dưới đây của AddTwo cộng parameters và trả về tổng của chúng trong EAX:

AddTwo  PROC
            push ebp
            mov ebp, esp
            mov eax, [ebp + 12]
            add eax, [ebp + 8]
            pop ebp
            ret
AddTwo ENDP

Explicit Stack Parameters

Khi stack parameters được tham chiếu với expressions như là [ebp + 8], chúng ta gọi chúng là explicit stack parameters. Lí do ta gọi như vậy là vì assembly code states rõ ràng offset của parameter như là constant value. Một vài programmers định nghĩa symbolic constants để biểu diễn explicit stack parameters, để làm cho code của họ dễ đọc hơn:

y_param  EQU [ebp + 12]
x_param  EQU [ebp + 8]

AddTwo  PROC
        push ebp
        mov ebp, esp
        mov eax, y_param
        add eax, x_param
        pop ebp
        ret
AddTwo ENDP

Cleaning Up the Stack

Phải có một cách nào đó để remove parameters khỏi stack khi một subroutine returns. Nếu không, một memory leak có thể xảy ra, và stack có thể bị corrupted. Cho ví dụ, giả sử statements sau trong main gọi AddTwo:

push   6
push   5
call     AddTwo

Giả thiết rằng AddTwo leaves 2 parameters trên stack, minh họa dưới đây shows stack sau khi returning từ call:


Bên trong main, chúng ta có thể cố lờ đi vấn đề này và hi vọng rằng chương trình của chúng ta kết thúc một cách bình thường. Nhưng nếu chúng ta call AddTwo từ một loop, stack có thể overflow. Mỗi call sử 12 bytes không gian stack - 4 bytes dành cho mỗi parameter, cộng thêm 4 bytes cho địa chỉ trả về của lệnh CALL. Vấn đề trở nên nghiêm trọng nếu chúng ta gọi Example1 từ main, những gì sau đó quay ra gọi AddTwo:

main PROC
call Example1
exit
main ENDP
Example1 PROC
push 6
push 5
call AddTwo
ret                           ; stack is corrupted!
Example1 ENDP

Khi lệnh RET trong Example1 thực thi, lúc này ESP trỏ đến số 5 chứ không phải trỏ về return address để chuyển luồng thực thi trở về main.


Như bạn đã biết, lệnh RET sẽ nạp giá trị 5 vào con trỏ lệnh và cố gắng chuyển điều khiển đến memory address 5. Nếu giá trị 5 nằm bên ngoài không gian địa chỉ của chương trình, processor có thể sinh ra một runtime exception để báo cho OS kết thúc chương trình.

The C Calling Convention  Một cách đơn giản để remove parameters ra ngoài runtime stack là cộng ESP với một giá trị nào đó bằng với kích thước của parameters. Sau đó, ESP sẽ trỏ tới stack location chứa địa chỉ trả về của subroutine. Như trong ví dụ sau:

Example1  PROC
          push   6
          push   5
          call     AddTwo
          add    esp, 8
          ret
Example1 END

Các chương trình được viết bằng C/C++ thường xuyên remove arguments ra khỏi stack trong hàm gọi sau khi một subroutine được trả về.

STDCALL Calling Convention  Một cách phổ biến khác để remove parameters khỏi stack là sử dụng một quy ước có tên là STDCALL . Trong thủ tục AddTwo, chúng ta cung cấp một tham số nguyên đến lệnh RET, những gì sau đó cộng 8 vào EBP sau khi returning về thủ tục gọi. Số nguyên phải bằng với số lượng bytes stack space được sử dụng bởi subroutin parameters:

AddTwo PROC
        push ebp
        mov ebp, esp
        mov eax, [ebp + 12]
        add  eax, [ebp + 8]
        pop  ebp
        ret 8

Ta có thể nhận ra một số điềm giống và khác giữa STDCALL và C, giống nhau ở chổ STDCALL pushes arguments vào trong stack theo thứ tự ngược lại giống như C. Bởi việc có một parameter trong lệnh RET, STDCALL giảm được số lượng code được sinh ra cho subroutine calls (giảm đi một lệnh) và đảm bảo rằng calling programs sẽ không bao giờ quên dọn dẹp stack. C calling convention, mặt khác, cho phép subroutines khai báo một số lượng parameters tùy biến. Caller có thể quyết định bao nhiêu arguments nó sẽ pass. Một ví dụ là hàm printf , hàm này có số lượng arguments phụ thuộc vào số lượng format specifiers trong initial string argument:

int  x = 5;
float y = 3.2;
char z = 'Z';
printf("Printing values:  %d, %d, %c", x, y, z);

Một C compiler pushes arguments lên stack theo thứ tự ngược lại, theo sau bởi một count argument ám chỉ số lượng arguments thực sự . Hàm lấy argument count và accesses từng arguments một. Function implementation không có một cách tiện lợi nào để encoding một constant trong RET instruction để clean up stack, vì vậy trách nhiệm này để lại cho caller.

Passing 8-Bit and 16-Bit Arguments on the Stack

Khi passing stack arguments đến procedures trong protected mode, tốt nhất là push 32-bit operands. Mặc dù bạn có thể push 16-bit operands trên stack, nhưng làm như vậy sẽ ngăn ESP aligned một doubleword boundary. Một page fault có thể xảy ra và runtime performance có thể bị degraded. Bạn nên mở rộng chúng lên 32 bits trước khi pushing chúng vào stack.

Thủ tục Uppercase sau nhận một character argument và return uppercase của nó vào trong AL:

Uppercase  PROC
          push  ebp
          mov  esp, ebp
          mov  al, [esp+8]
          cmp  al, 'a'
          jb     L1
          cmp  al, 'z'
          ja     L1
          sub  al, 32
L1:     pop   ebp
          ret    4
Uppercase  ENDP


Nếu chúng ta pass một character literal đến Uppercase, PUSH instruction sẽ tự động mở rộng character này lên 32 bits:

push   'x'
call     Uppercase

Không có nhận xét nào:

Đăng nhận xét