Để cung cấp một ví dụ cụ thể về cách trình biên dịch quản lý ngăn xếp và cách truy cập các giá trị trên ngăn xếp, chúng ta có thể xem các mô tả trực quan, cộng với mã được tạo GCC
trong môi trường Linux với i386 là kiến trúc đích.
1. Khung xếp chồng
Như bạn đã biết, ngăn xếp là một vị trí trong không gian địa chỉ của một quy trình đang chạy được sử dụng bởi các hàm hoặc các thủ tục , theo nghĩa là không gian được phân bổ trên ngăn xếp cho các biến được khai báo cục bộ, cũng như các đối số được truyền cho hàm ( không gian cho các biến được khai báo bên ngoài bất kỳ chức năng nào (tức là các biến toàn cục) được phân bổ ở một vùng khác trong bộ nhớ ảo). Không gian được phân bổ cho tất cả dữ liệu của hàm được tham chiếu đến khung ngăn xếp . Dưới đây là mô tả trực quan của nhiều khung ngăn xếp (từ Hệ thống máy tính: Phối cảnh của lập trình viên ):
2. Quản lý khung ngăn xếp và vị trí thay đổi
Để các giá trị được ghi vào ngăn xếp trong khung ngăn xếp cụ thể được trình biên dịch quản lý và đọc bởi chương trình, phải có một số phương pháp để tính toán vị trí của các giá trị này và lấy địa chỉ bộ nhớ của chúng. Các thanh ghi trong CPU được gọi là con trỏ ngăn xếp và con trỏ cơ sở giúp điều này.
Con trỏ cơ sở, ebp
theo quy ước, chứa địa chỉ bộ nhớ ở dưới cùng hoặc cơ sở của ngăn xếp. Vị trí của tất cả các giá trị trong khung ngăn xếp có thể được tính bằng cách sử dụng địa chỉ trong con trỏ cơ sở làm tham chiếu. Điều này được mô tả trong hình trên: %ebp + 4
là địa chỉ bộ nhớ được lưu trữ trong con trỏ cơ sở cộng với 4, ví dụ.
3. Mã do trình biên dịch tạo
Nhưng điều tôi không nhận được là cách các biến trên ngăn xếp được đọc bởi một ứng dụng - nếu tôi khai báo và gán x là số nguyên, giả sử x = 3 và lưu trữ được dành riêng trên ngăn xếp và sau đó giá trị 3 của nó được lưu trữ ở đó, và trong cùng một hàm tôi khai báo và gán y là 4, và sau đó tôi sử dụng x trong một biểu thức khác, (giả sử z = 5 + x) làm thế nào để chương trình có thể đọc x để đánh giá z khi nó ở dưới y trên stack?
Chúng ta hãy sử dụng một chương trình ví dụ đơn giản được viết bằng C để xem cách thức hoạt động của nó:
int main(void)
{
int x = 3;
int y = 4;
int z = 5 + x;
return 0;
}
Hãy để chúng tôi kiểm tra văn bản lắp ráp do GCC tạo ra cho văn bản nguồn C này (Tôi đã làm sạch nó một chút để rõ ràng):
main:
pushl %ebp # save previous frame's base address on stack
movl %esp, %ebp # use current address of stack pointer as new frame base address
subl $16, %esp # allocate 16 bytes of space on stack for function data
movl $3, -12(%ebp) # variable x at address %ebp - 12
movl $4, -8(%ebp) # variable y at address %ebp - 8
movl -12(%ebp), %eax # write x to register %eax
addl $5, %eax # x + 5 = 9
movl %eax, -4(%ebp) # write 9 to address %ebp - 4 - this is z
movl $0, %eax
leave
Những gì chúng ta quan sát là biến x, y, z được đặt tại địa chỉ %ebp - 12
, %ebp -8
và %ebp - 4
, tương ứng. Nói cách khác, vị trí của các biến trong khung ngăn xếp main()
được tính bằng cách sử dụng địa chỉ bộ nhớ được lưu trong thanh ghi CPU %ebp
.
4. Dữ liệu trong bộ nhớ ngoài con trỏ ngăn xếp nằm ngoài phạm vi
Tôi rõ ràng đang thiếu một cái gì đó. Có phải là vị trí trên ngăn xếp chỉ liên quan đến tuổi thọ / phạm vi của biến và toàn bộ ngăn xếp có thể truy cập được vào chương trình mọi lúc không? Nếu vậy, điều đó có nghĩa là có một số chỉ mục khác chỉ giữ địa chỉ của các biến trên ngăn xếp để cho phép các giá trị được truy xuất không? Nhưng sau đó tôi nghĩ rằng toàn bộ điểm của ngăn xếp là các giá trị được lưu trữ ở cùng một nơi với địa chỉ biến?
Ngăn xếp là một vùng trong bộ nhớ ảo, có sử dụng được quản lý bởi trình biên dịch. Trình biên dịch tạo mã theo cách mà các giá trị nằm ngoài con trỏ ngăn xếp (các giá trị nằm ngoài đỉnh của ngăn xếp) không bao giờ được tham chiếu. Khi một hàm được gọi, vị trí của con trỏ ngăn xếp thay đổi để tạo khoảng trống trên ngăn xếp được coi là không "nằm ngoài giới hạn", có thể nói như vậy.
Khi các hàm được gọi và trả về, con trỏ ngăn xếp được giảm dần và tăng lên. Dữ liệu được ghi vào ngăn xếp không biến mất sau khi nó nằm ngoài phạm vi, nhưng trình biên dịch không tạo ra các hướng dẫn tham chiếu dữ liệu này vì không có cách nào để trình biên dịch tính toán địa chỉ của các dữ liệu này bằng cách sử dụng %ebp
hoặc %esp
.
5. Tóm tắt
Mã có thể được thực thi trực tiếp bởi CPU được tạo bởi trình biên dịch. Trình biên dịch quản lý ngăn xếp, ngăn xếp khung cho các chức năng và các thanh ghi CPU. Một chiến lược được GCC sử dụng để theo dõi vị trí của các biến trong khung stack trong mã dự định thực hiện trên kiến trúc i386 là sử dụng địa chỉ bộ nhớ trong con trỏ cơ sở khung stack %ebp
, làm tham chiếu và ghi giá trị của biến vào vị trí trong khung stack tại offset cho địa chỉ trong %ebp
.