Làm thế nào các biến được lưu trữ trong và lấy từ ngăn xếp chương trình?


47

Lời xin lỗi trước cho sự ngây thơ của câu hỏi này. Tôi là một nghệ sĩ 50 tuổi lần đầu tiên cố gắng hiểu đúng về máy tính. Vì vậy, ở đây đi.

Tôi đã cố gắng hiểu làm thế nào các loại dữ liệu và các biến được xử lý bởi một trình biên dịch (theo một nghĩa rất chung, tôi biết có rất nhiều thứ cho nó). Tôi thiếu một cái gì đó trong sự hiểu biết của tôi về mối quan hệ giữa lưu trữ trong "ngăn xếp" và các loại giá trị và lưu trữ trên "heap" và các loại tham chiếu (dấu ngoặc kép có nghĩa là tôi hiểu rằng các thuật ngữ này là trừu tượng và không được thực hiện quá đúng theo nghĩa đen trong bối cảnh đơn giản như cách tôi đóng khung câu hỏi này). Dù sao, ý tưởng đơn giản của tôi là các loại như Booleans và số nguyên đi vào "ngăn xếp" bởi vì chúng có thể, bởi vì chúng là các thực thể được biết đến về không gian lưu trữ và phạm vi của chúng dễ dàng được kiểm soát theo.

Nhưng điều tôi không nhận được là làm thế nào 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 xdưới dạng một 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ị của 3nó được lưu trữ ở đó, và sau đó trong cùng một hàm tôi khai báo và gán ynhư, nói 4, và sau đó tôi sử dụng xmột biểu thức khác, (nói z = 5 + x) làm thế nào để chương trình có thể đọc xđể đánh giá zkhi nó ở dướiytrên ngăn xếp? 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? Trong tâm trí chơi chữ của tôi dường như nếu có chỉ số khác này, thì chúng ta đang nói về một cái gì đó giống như một đống? Tôi rõ ràng rất bối rối, và tôi chỉ hy vọng có một câu trả lời đơn giản cho câu hỏi đơn giản của mình.

Cảm ơn đã đọc đến đây.


7
@ fade2black Tôi không đồng ý - nên có thể đưa ra câu trả lời có độ dài hợp lý tóm tắt các điểm quan trọng.
David Richerby

9
Bạn đang mắc phải một lỗi cực kỳ phổ biến khi kết hợp loại giá trị với nơi nó được lưu trữ . Đơn giản là sai khi nói rằng bool đi trên stack. Các bool đi theo các biếncác biến đi theo stack nếu thời gian sống của chúng được biết là ngắn và trên heap nếu thời gian sống của chúng không được biết là ngắn. Để biết một số suy nghĩ về việc này liên quan đến C # như thế nào, hãy xem blog.msdn.microsoft.com/ericlippert/2010/09/30/ợi
Eric Lippert

7
Ngoài ra, đừng nghĩ về ngăn xếp như một chồng các giá trị trong các biến . Hãy nghĩ về nó như một chồng các khung kích hoạt cho các phương thức . Trong một phương thức, bạn có thể truy cập bất kỳ biến kích hoạt của phương thức đó, nhưng bạn không thể truy cập các biến của người gọi, vì chúng không nằm trong khung nằm trên cùng của ngăn xếp .
Eric Lippert

5
Ngoài ra: Tôi hoan nghênh bạn đã chủ động tìm hiểu một cái gì đó mới và đào sâu vào các chi tiết triển khai của một ngôn ngữ. Bạn đang gặp phải một vấp ngã thú vị ở đây: bạn hiểu thế nào là một ngăn xếp như một kiểu dữ liệu trừu tượng , nhưng không phải là một chi tiết triển khai để thống nhất kích hoạt và tiếp tục . Cái sau không tuân theo các quy tắc của kiểu dữ liệu trừu tượng stack; nó coi chúng là hướng dẫn hơn là quy tắc. Toàn bộ quan điểm của ngôn ngữ lập trình là đảm bảo rằng bạn không phải hiểu những chi tiết trừu tượng này để giải quyết các vấn đề lập trình.
Eric Lippert

4
Cảm ơn bạn Eric, Sava, Thumbnail, những bình luận và tài liệu tham khảo đều cực kỳ hữu ích. Tôi luôn cảm thấy như những người như bạn phải rên rỉ bên trong khi họ nhìn thấy một câu hỏi như của tôi, nhưng xin vui lòng biết sự phấn khích và sự hài lòng to lớn khi nhận được câu trả lời!
Celine Atwood

Câu trả lời:


24

Lưu trữ các biến cục bộ trên ngăn xếp là một chi tiết triển khai - về cơ bản là tối ưu hóa. Bạn có thể nghĩ về nó theo cách này. Khi nhập một hàm, không gian cho tất cả các biến cục bộ được phân bổ ở đâu đó. Sau đó, bạn có thể truy cập tất cả các biến, vì bạn biết vị trí của chúng bằng cách nào đó (đây là một phần của quá trình phân bổ). Khi để lại một chức năng, không gian được giải phóng (giải phóng).

Ngăn xếp là một cách để thực hiện quy trình này - bạn có thể nghĩ về nó như một loại "đống nhanh" có kích thước hạn chế và do đó chỉ phù hợp với các biến nhỏ. Là một tối ưu hóa bổ sung, tất cả các biến cục bộ được lưu trữ trong một khối. Vì mỗi biến cục bộ có kích thước đã biết, bạn biết phần bù của từng biến trong khối và đó là cách bạn truy cập vào nó. Điều này trái ngược với các biến được phân bổ trên heap, có địa chỉ được lưu trữ trong các biến khác.

Bạn có thể nghĩ về ngăn xếp rất giống với cấu trúc dữ liệu ngăn xếp cổ điển, với một điểm khác biệt quan trọng: bạn được phép truy cập các mục bên dưới ngăn xếp trên cùng. Thật vậy, bạn có thể truy cập mục thứ từ đầu. Đây là cách bạn có thể truy cập tất cả các biến cục bộ của mình bằng cách đẩy và bật. Việc đẩy duy nhất được thực hiện là khi vào chức năng và chỉ xuất hiện khi rời khỏi chức năng.k

Cuối cùng, hãy để tôi đề cập rằng trong thực tế, một số biến cục bộ được lưu trữ trong các thanh ghi. Điều này là do truy cập vào các thanh ghi nhanh hơn truy cập vào ngăn xếp. Đây là một cách khác để thực hiện một không gian cho các biến cục bộ. Một lần nữa, chúng ta biết chính xác nơi một biến được lưu trữ (lần này không thông qua offset, nhưng thông qua tên của một thanh ghi) và loại lưu trữ này chỉ phù hợp với dữ liệu nhỏ.


1
"Phân bổ trong một khối" là một chi tiết thực hiện khác. Nó không quan trọng, mặc dù. Trình biên dịch biết bộ nhớ cần thiết cho các biến cục bộ như thế nào, nó cấp phát bộ nhớ đó cho một hoặc nhiều khối và sau đó nó tạo các biến cục bộ trong bộ nhớ đó.
MSalters

Cảm ơn, đã sửa. Thật vậy, một số "khối" này chỉ là các thanh ghi.
Yuval Filmus

1
Bạn chỉ thực sự cần ngăn xếp để lưu trữ các địa chỉ trả lại, nếu vậy. Bạn có thể thực hiện đệ quy mà không có ngăn xếp khá dễ dàng, bằng cách chuyển một con trỏ đến địa chỉ trả về trên heap.
Yuval Filmus

1
@MikeCaron Stacks gần như không có gì để làm với đệ quy. Tại sao bạn lại "thổi bay các biến" trong các chiến lược thực hiện khác?
vườn

1
@gardenhead thay thế rõ ràng nhất (và một thay thế thực sự được / đã được sử dụng) là phân bổ tĩnh cho từng biến của thủ tục. Nhanh chóng, đơn giản, có thể dự đoán được ... nhưng không cho phép đệ quy hoặc tái cấp phép. Điều đó và ngăn xếp thông thường không phải là sự thay thế duy nhất của khóa học (phân bổ động mọi thứ là khác), nhưng chúng thường là những thứ để thảo luận khi biện minh cho ngăn xếp :)
hobbs

23

ytrên ngăn xếp không ngăn chặn xđược truy cập vật lý , như bạn đã chỉ ra, làm cho ngăn xếp máy tính khác với các ngăn xếp khác.

Khi một chương trình được biên dịch, vị trí của các biến trong ngăn xếp cũng được xác định trước (trong ngữ cảnh của hàm). Trong ví dụ của bạn, nếu ngăn xếp chứa xmột y"trên cùng" của nó, thì chương trình sẽ biết trước đó xsẽ là 1 mục bên dưới đỉnh của ngăn xếp trong khi bên trong hàm. Vì phần cứng máy tính có thể yêu cầu rõ ràng 1 mục bên dưới đỉnh của ngăn xếp, nên máy tính có thể nhận được xmặc dù ycũng tồn tại.

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?

Đúng. Khi bạn thoát khỏi một hàm, con trỏ ngăn xếp di chuyển trở lại vị trí trước đó, xóa một cách hiệu quả xy, nhưng về mặt kỹ thuật chúng sẽ vẫn ở đó cho đến khi bộ nhớ được sử dụng cho mục đích khác. Ngoài ra, nếu chức năng của bạn gọi một chức năng khác, xyvẫn sẽ ở đó và có thể được truy cập bằng cách cố ý đi quá xa trong ngăn xếp.


1
Đây có vẻ như là câu trả lời rõ ràng nhất cho đến nay về việc không nói vượt quá kiến ​​thức nền tảng mà OP mang đến. +1 để thực sự nhắm mục tiêu OP!
Ben I.

1
Tôi cũng đồng ý! Mặc dù tất cả các câu trả lời đều rất hữu ích và tôi rất biết ơn, bài viết ban đầu của tôi đã được thúc đẩy bởi vì tôi cảm thấy (d) rằng toàn bộ điều / đống này là hoàn toàn cơ bản để hiểu cách phân biệt loại giá trị / tham chiếu phát sinh, nhưng tôi không thể Nếu bạn chỉ có thể xem đỉnh của "ngăn xếp". Vì vậy, câu trả lời của bạn giải phóng tôi khỏi đó. (Tôi có cùng cảm giác khi tôi lần đầu tiên nhận ra tất cả các định luật nghịch đảo khác nhau trong vật lý chỉ đơn giản là rơi ra khỏi hình học của bức xạ phát ra từ một quả cầu, và bạn có thể vẽ một sơ đồ đơn giản để nhìn thấy nó.)
Celine Atwood

Tôi thích nó bởi vì nó luôn rất hữu ích khi bạn có thể thấy cách thức và lý do tại sao một số hiện tượng ở cấp độ cao hơn (ví dụ: trong ngôn ngữ) thực sự là do một số hiện tượng cơ bản hơn một chút nữa ở cây trừu tượng. Ngay cả khi nó được giữ khá đơn giản.
Celine Atwood

1
@CelineAtwood Xin lưu ý rằng việc cố gắng truy cập các biến "bằng vũ lực" sau khi chúng bị xóa khỏi ngăn xếp sẽ mang lại cho bạn hành vi không thể đoán trước / không xác định và không nên thực hiện. Lưu ý rằng tôi đã không nói "không thể" b / c một số ngôn ngữ sẽ cho phép bạn thử nó. Tuy nhiên, đó là một lỗi lập trình và nên tránh.
code_dredd

12

Để 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 GCCtrong 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 ):

Khung ngăn xếp CSAPP

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ở, ebptheo 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 + 4là đị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%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 %ebphoặ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.


Của tôi nếu tôi hỏi hình ảnh đó đến từ đâu? Trông có vẻ quen thuộc đáng ngờ ... :-) Nó có thể đã có trong sách giáo khoa trước đây.
Vịt lớn

1
nvmd. Tôi chỉ thấy liên kết. Đó là những gì tôi nghĩ. +1 để chia sẻ cuốn sách đó.
Vịt lớn

1
+1 cho bản demo lắp ráp gcc :)
Flow2k

9

Có hai thanh ghi đặc biệt: ESP (con trỏ ngăn xếp) và EBP (con trỏ cơ sở). Khi một thủ tục được gọi, hai thao tác đầu tiên thường là

push        ebp  
mov         ebp,esp 

Hoạt động đầu tiên lưu giá trị của EBP trên ngăn xếp và hoạt động thứ hai tải giá trị của con trỏ ngăn xếp vào con trỏ cơ sở (để truy cập các biến cục bộ). Vì vậy, EBP trỏ đến cùng một vị trí như ESP.

Trình biên dịch dịch tên biến thành phần bù EBP. Ví dụ: nếu bạn có hai biến cục bộ x,yvà bạn có một số thứ như

  x = 1;
  y = 2;
  return x + y;

sau đó nó có thể được dịch thành một cái gì đó như

   push        ebp  
   mov         ebp,esp
   mov  DWORD PTR [ ebp + 6],  1   ;x = 1
   mov  DWORD PTR [ ebp + 14], 2   ;y = 2
   mov  eax, [ ebp + 6 ]
   add  [ ebp + 14 ], eax          ; x + y 
   mov  eax, [ ebp + 14 ] 
   ...  

Giá trị offset 6 và 14 được tính toán tại thời điểm biên dịch.

Đây là cách nó hoạt động. Tham khảo một cuốn sách biên dịch để biết chi tiết.


14
Điều này là cụ thể cho intel x86. Trên ARM, đăng ký SP (R13) được sử dụng, cũng như FP (R11). Và trên x86, việc thiếu các thanh ghi có nghĩa là các trình biên dịch tích cực sẽ không sử dụng EBP vì nó có thể được lấy từ ESP. Đây là ví dụ rõ ràng cuối cùng, trong đó tất cả các địa chỉ liên quan đến EBP có thể được dịch sang tương đối ESP, không cần thay đổi nào khác.
MSalters

Bạn không thiếu SUB trên ESP để nhường chỗ cho x, y ở vị trí đầu tiên phải không?
Hagen von Eitzen

@HagenvonEitzen, có lẽ. Tôi chỉ muốn thể hiện ý tưởng về cách các biến được phân bổ trên stack được truy cập bằng các thanh ghi phần cứng.
fade2black

Downvoters, ý kiến ​​xin vui lòng !!!
fade2black

8

Bạn bối rối vì các biến cục bộ được lưu trữ trong ngăn xếp không được truy cập với quy tắc truy cập của ngăn xếp: First In Last Out, hoặc chỉ PHIM .

Vấn đề là quy tắc FILO áp dụng cho các chuỗi lệnh gọi hàm và khung ngăn xếp , thay vì cho các biến cục bộ.

Khung ngăn xếp là gì?

Khi bạn nhập một hàm, nó sẽ được cung cấp một lượng bộ nhớ trên ngăn xếp, được gọi là khung stack. Các biến cục bộ của hàm được lưu trữ trong khung stack. Bạn có thể tưởng tượng rằng kích thước của khung ngăn xếp thay đổi từ chức năng này sang chức năng khác vì mỗi chức năng có số lượng và kích thước khác nhau của các biến cục bộ.

Làm thế nào các biến cục bộ được lưu trữ trong khung ngăn xếp không liên quan gì đến FILO. (Ngay cả thứ tự xuất hiện của các biến cục bộ trong mã nguồn của bạn cũng không đảm bảo rằng các biến cục bộ sẽ được lưu trữ theo thứ tự đó.) Khi bạn suy luận chính xác trong câu hỏi của mình, "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 lấy ". Địa chỉ của các biến cục bộ thường được tính bằng một địa chỉ cơ sở , chẳng hạn như địa chỉ biên của khung ngăn xếp và các giá trị bù cụ thể cho từng biến cục bộ.

Vậy khi nào thì hành vi FILO này xuất hiện?

Bây giờ, điều gì xảy ra nếu bạn gọi một chức năng khác? Hàm callee phải có khung stack riêng và chính khung stack này được đẩy trong stack . Đó là, khung ngăn xếp của chức năng callee được đặt trên đầu khung ngăn xếp của chức năng người gọi. Và nếu hàm callee này gọi một hàm khác, thì khung stack của nó sẽ được đẩy, một lần nữa, trên đỉnh của ngăn xếp.

Điều gì xảy ra nếu một hàm trả về? Khi chức năng callee trở về chức năng người gọi, khung ngăn xếp của chức năng callee được bật ra khỏi ngăn xếp, giải phóng không gian để sử dụng trong tương lai.

Vì vậy, từ câu hỏi của bạn:

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?

bạn khá đúng ở đây vì các giá trị biến cục bộ trên khung ngăn xếp không thực sự bị xóa khi hàm trả về. Giá trị chỉ ở đó, mặc dù vị trí bộ nhớ nơi nó được lưu trữ không thuộc về khung ngăn xếp của bất kỳ chức năng nào. Giá trị bị xóa khi một số chức năng khác đạt được khung ngăn xếp của nó bao gồm vị trí và ghi trên một số giá trị khác vào vị trí bộ nhớ đó.

Sau đó, những gì khác nhau ngăn xếp từ đống?

Stack và heap giống nhau theo nghĩa là cả hai đều đề cập đến một số không gian trên bộ nhớ. Vì chúng tôi có thể truy cập bất kỳ vị trí nào trên bộ nhớ bằng địa chỉ của nó, bạn có thể truy cập bất kỳ vị trí nào trong ngăn xếp hoặc đống.

Sự khác biệt đến từ lời hứa mà hệ thống máy tính đưa ra về cách sử dụng chúng. Như bạn đã nói, heap là loại tham khảo. Vì các giá trị trong heap không liên quan đến bất kỳ khung ngăn xếp cụ thể nào, nên phạm vi của giá trị không được gắn với bất kỳ hàm nào. Tuy nhiên, một biến cục bộ nằm trong phạm vi của một hàm và mặc dù bạn có thể truy cập bất kỳ giá trị biến cục bộ nào nằm ngoài khung ngăn xếp của hàm hiện tại, hệ thống sẽ cố gắng đảm bảo rằng loại hành vi này không xảy ra, bằng cách sử dụng xếp chồng khung. Điều này cho chúng ta một số loại ảo giác rằng biến cục bộ nằm trong phạm vi một chức năng cụ thể.


4

Có nhiều cách để thực hiện các biến cục bộ bằng một hệ thống thời gian chạy ngôn ngữ. Sử dụng một ngăn xếp là một giải pháp hiệu quả phổ biến, được sử dụng trong nhiều trường hợp thực tế.

Theo trực giác, một con trỏ ngăn xếp spđược giữ xung quanh trong thời gian chạy (trong một địa chỉ cố định hoặc trong một thanh ghi - nó thực sự quan trọng). Giả sử rằng mọi "đẩy" đều tăng con trỏ ngăn xếp.

Tại thời gian biên dịch, trình biên dịch xác định địa chỉ của từng biến là sp - Knơi Khằng số chỉ phụ thuộc vào phạm vi của biến (do đó có thể được tính tại thời gian biên dịch).

Lưu ý rằng chúng tôi đang sử dụng từ "stack" ở đây theo nghĩa lỏng lẻo. Ngăn xếp này không chỉ được truy cập thông qua các hoạt động đẩy / pop / top, mà còn được truy cập bằng cách sử dụng sp - K.

Ví dụ, hãy xem xét mã giả này:

procedure f(int x, int y) {
  print(x,y);    // (1)
  if (...) {
    int z=x+y; // (2)
    print(x,y,z);  // (3)
  }
  print(x,y); // (4)
  return;
}

Khi thủ tục được gọi, các đối số x,ycó thể được truyền vào ngăn xếp. Để đơn giản, giả sử quy ước là người gọi đẩy xtrước, sau đó y.

Sau đó, trình biên dịch tại điểm (1) có thể tìm thấy xtại sp - 2ytại sp - 1.

Tại điểm (2), một biến mới được đưa vào phạm vi. Trình biên dịch tạo mã tổng x+y, tức là những gì được chỉ bởi sp - 2sp - 1, và đẩy kết quả của tổng trên ngăn xếp.

Tại điểm (3), zđược in. Trình biên dịch biết nó là biến cuối cùng trong phạm vi, vì vậy nó được trỏ bởi sp - 1. Điều này không còn nữa y, kể từ khi spthay đổi. Tuy nhiên, để in ytrình biên dịch biết rằng nó có thể tìm thấy nó, trong phạm vi này, tại sp - 2. Tương tự, xbây giờ được tìm thấy tại sp - 3.

Tại điểm (4), chúng tôi thoát khỏi phạm vi. zđược bật lên, và ymột lần nữa được tìm thấy tại địa chỉ sp - 1, và xlà tại sp - 2.

Khi chúng tôi trở lại, fhoặc người gọi bật x,yra khỏi ngăn xếp.

Vì vậy, tính toán Kcho trình biên dịch là vấn đề đếm có bao nhiêu biến trong phạm vi, đại khái. Trong thế giới thực, điều này thực sự phức tạp hơn vì không phải tất cả các biến đều có cùng kích thước, do đó việc tính toán Kphức tạp hơn một chút. Đôi khi, ngăn xếp cũng chứa địa chỉ trả về f, vì vậy Kcũng phải "bỏ qua". Nhưng đây là những kỹ thuật.

Lưu ý rằng, trong một số ngôn ngữ lập trình, mọi thứ có thể trở nên phức tạp hơn nếu các tính năng phức tạp hơn phải được xử lý. Ví dụ, các thủ tục lồng nhau đòi hỏi một phân tích rất cẩn thận, vì Kbây giờ phải "bỏ qua" nhiều địa chỉ trả lại, đặc biệt nếu thủ tục lồng nhau được đệ quy. Đóng / lambdas / chức năng ẩn danh cũng yêu cầu một số chăm sóc để xử lý các biến "bị bắt". Tuy nhiên, ví dụ trên nên minh họa ý tưởng cơ bản.


3

Ý tưởng đơn giản nhất là nghĩ về các biến như tên sửa cho các địa chỉ trong bộ nhớ. Thật vậy, một số nhà lắp ráp hiển thị mã máy theo cách đó ("lưu giá trị 5 trong địa chỉ i", trong đó icó một tên biến).

Một số địa chỉ này là "tuyệt đối", như biến toàn cục, một số là "tương đối", như biến cục bộ. Các biến (tức là địa chỉ) trong các hàm có liên quan đến một số vị trí trên "ngăn xếp" khác nhau cho mỗi lần gọi hàm; theo cách đó, cùng một tên có thể đề cập đến các đối tượng thực tế khác nhau và các cuộc gọi vòng tròn đến cùng chức năng là các lệnh độc lập làm việc trên bộ nhớ độc lập.


2

Các mục dữ liệu có thể đi trên ngăn xếp được đặt trên ngăn xếp - Có! Đó là một không gian cao cấp. Ngoài ra, một khi chúng ta đẩy xvào ngăn xếp và sau đó đẩy yvào ngăn xếp, lý tưởng là chúng ta không thể truy cập xcho đến khi ycó. Chúng tôi cần phải bật yđể truy cập x. Bạn đã nhận được chúng chính xác.

Ngăn xếp không phải là biến, mà là frames

Trường hợp bạn đã sai nó là về chính ngăn xếp. Trên ngăn xếp, nó không phải là các mục dữ liệu được đẩy trực tiếp. Thay vào đó, trên ngăn xếp một cái gì đó được gọi stack-framelà đẩy. Khung ngăn xếp này chứa các mục dữ liệu. Mặc dù bạn không thể truy cập các khung sâu trong ngăn xếp, bạn có thể truy cập vào khung trên cùng và tất cả các mục dữ liệu có trong nó.

Hãy nói rằng chúng tôi có các mục dữ liệu của chúng tôi được gói trong hai khung ngăn xếp frame-xframe-y. Chúng tôi đẩy họ hết lần này đến lần khác. Bây giờ miễn là frame-yngồi trên đầu trang frame-x, bạn không thể truy cập lý tưởng bất kỳ mục dữ liệu nào bên trong frame-x. Chỉ có frame-ythể nhìn thấy. NHƯNG được đưa ra frame-ylà có thể nhìn thấy, bạn có thể truy cập tất cả các mục dữ liệu được gói trong đó. Toàn bộ khung có thể nhìn thấy phơi bày tất cả các mục dữ liệu có trong.

Kết thúc câu trả lời. Thêm (rant) trên các khung này

Trong quá trình biên dịch, một danh sách tất cả các chức năng trong chương trình được tạo ra. Sau đó, cho mỗi chức năng, một danh sách các mục dữ liệu có thể xếp chồng được tạo ra. Sau đó, cho mỗi chức năng a stack-frame-templateđược thực hiện. Mẫu này là một cấu trúc dữ liệu chứa tất cả các biến được chọn, không gian cho dữ liệu đầu vào của hàm, dữ liệu đầu ra, v.v ... Bây giờ trong thời gian chạy, bất cứ khi nào một hàm được gọi, một bản sao của điều này templateđược đặt trên ngăn xếp - cùng với tất cả các biến đầu vào và biến trung gian . Khi chức năng này gọi một số chức năng khác, thì một bản sao mới của chức năng đó sẽ stack-frameđược đưa vào ngăn xếp. Bây giờ miễn là chức năng đó đang chạy, các mục dữ liệu của chức năng này được bảo tồn. Khi chức năng đó kết thúc, khung stack của nó được bật ra. Hiện naykhung stack này đang hoạt động và chức năng này có thể truy cập tất cả các biến của nó.

Xin lưu ý rằng cấu trúc và thành phần của khung ngăn xếp thay đổi từ ngôn ngữ lập trình sang ngôn ngữ lập trình. Ngay cả trong một ngôn ngữ cũng có thể có sự khác biệt tinh tế trong việc triển khai khác nhau.


Cảm ơn bạn đã xem xét CS. Tôi là một lập trình viên bây giờ một ngày học các bài học piano :)

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.