Tại sao chúng ta vẫn phát triển các ngăn xếp ngược?


46

Khi biên dịch mã C và nhìn vào lắp ráp, tất cả đều có ngăn xếp phát triển ngược như sau:

_main:
    pushq   %rbp
    movl    $5, -4(%rbp)
     popq    %rbp
    ret

-4(%rbp)- điều này có nghĩa là con trỏ cơ sở hoặc con trỏ ngăn xếp thực sự đang di chuyển xuống các địa chỉ bộ nhớ thay vì đi lên? Tại sao vậy?

Tôi đã thay đổi $5, -4(%rbp)đến $5, +4(%rbp), biên dịch và chạy mã và không có lỗi. Vậy tại sao chúng ta vẫn phải đi ngược vào ngăn xếp bộ nhớ?


2
Lưu ý rằng -4(%rbp)hoàn toàn không di chuyển con trỏ cơ sở và +4(%rbp)không thể có khả năng hoạt động.
Margaret Bloom

14
" Tại sao chúng ta vẫn phải đi lùi " - bạn nghĩ lợi thế của việc đi tiếp là gì? Cuối cùng, điều đó không thành vấn đề, bạn chỉ cần chọn một.
Bergi

31
"tại sao chúng ta phát triển các ngăn xếp ngược?" - bởi vì nếu chúng ta không có ai khác sẽ hỏi tại sao lại malloctăng số tiền ngược lại
slebetman

2
@MargaretBloom: Rõ ràng trên nền tảng của OP, mã khởi động CRT không quan tâm nếu mainlàm tắc nghẽn RBP của nó. Điều đó chắc chắn là có thể. (Và vâng, viết 4(%rbp)sẽ bước vào giá trị RBP đã lưu). Err thực sự, điều này không bao giờ thực hiện mov %rsp, %rbp, vì vậy quyền truy cập bộ nhớ có liên quan đến RBP của người gọi , nếu đó là những gì OP thực sự đã thử nghiệm !!! Nếu điều này thực sự được sao chép từ đầu ra của trình biên dịch, một số hướng dẫn đã bị bỏ qua!
Peter Cordes

1
Dường như với tôi rằng "ngược" hoặc "chuyển tiếp" (hoặc "xuống" và "lên") phụ thuộc vào quan điểm của bạn. Nếu bạn lập sơ đồ bộ nhớ dưới dạng cột có địa chỉ thấp ở trên cùng, thì việc tăng ngăn xếp bằng cách giảm con trỏ ngăn xếp sẽ tương tự như ngăn xếp vật lý.
jamesdlin

Câu trả lời:


86

Điều này có nghĩa là con trỏ cơ sở hoặc con trỏ ngăn xếp thực sự đang di chuyển xuống các địa chỉ bộ nhớ thay vì đi lên? Tại sao vậy?

Có, các pushhướng dẫn làm giảm con trỏ ngăn xếp và ghi vào ngăn xếp, trong khi popthực hiện ngược lại, đọc từ ngăn xếp và tăng con trỏ ngăn xếp.

Điều này có phần lịch sử ở chỗ đối với các máy có bộ nhớ hạn chế, ngăn xếp được đặt ở vị trí cao và hướng xuống dưới, trong khi đống được đặt thấp và lớn lên. Chỉ có một khoảng trống của "bộ nhớ trống" - giữa heap & stack và khoảng cách này được chia sẻ, một trong hai có thể phát triển thành khoảng trống khi cần thiết. Do đó, chương trình chỉ hết bộ nhớ khi stack và heap va chạm không để lại bộ nhớ trống. 

Nếu cả stack và heap đều phát triển theo cùng một hướng, thì có hai khoảng trống và stack không thể thực sự phát triển thành khoảng trống của heap (ngược lại cũng có vấn đề).

Ban đầu, bộ xử lý không có hướng dẫn xử lý ngăn xếp chuyên dụng. Tuy nhiên, khi hỗ trợ ngăn xếp đã được thêm vào phần cứng, nó đã đưa mô hình này đi xuống và bộ xử lý vẫn theo mô hình này ngày hôm nay.

Người ta có thể lập luận rằng trên máy 64 bit có đủ không gian địa chỉ để cho phép nhiều khoảng trống - và như một bằng chứng, nhiều khoảng trống nhất thiết phải xảy ra khi một tiến trình có nhiều luồng. Mặc dù điều này không đủ động lực để thay đổi mọi thứ xung quanh, vì với nhiều hệ thống khoảng cách, hướng phát triển được cho là tùy ý, do đó, truyền thống / khả năng tương thích sẽ mở rộng quy mô.


Bạn sẽ phải thay đổi các hướng dẫn ngăn xếp xử lý CPU để thay đổi hướng của ngăn xếp, nếu không từ bỏ việc sử dụng dành riêng đẩy & popping hướng dẫn (ví dụ như push, pop, call, ret, những người khác).

Lưu ý rằng kiến ​​trúc tập lệnh MIPS không có chuyên dụng push& pop, do đó, việc phát triển ngăn xếp theo một hướng là điều thực tế - bạn vẫn có thể muốn bố trí bộ nhớ một khoảng trống cho một quy trình xử lý đơn, nhưng có thể tăng ngăn xếp lên trên và heap hướng xuống dưới Tuy nhiên, nếu bạn đã làm điều đó, một số mã varargs C có thể yêu cầu điều chỉnh trong nguồn hoặc trong thông số dưới mức.

(Trên thực tế, vì không có xử lý ngăn xếp chuyên dụng trên MIPS, chúng tôi có thể sử dụng gia tăng trước hoặc sau hoặc giảm trước hoặc sau khi đẩy lên ngăn xếp miễn là chúng tôi sử dụng đảo ngược chính xác để bật ra khỏi ngăn xếp và cũng giả sử rằng hệ điều hành tôn trọng mô hình sử dụng ngăn xếp đã chọn. Thật vậy, trong một số hệ thống nhúng và một số hệ thống giáo dục, ngăn xếp MIPS được phát triển lên.)


32
Nó không chỉ là pushpoptrên hầu hết các kiến trúc, mà còn là ngắt xử lý quan trọng hơn nhiều, call, ret, và bất cứ điều gì khác đã nướng-in tương tác với stack.
Ded repeatator

3
ARM có thể có tất cả bốn hương vị ngăn xếp.
Margaret Bloom

14
Đối với những gì nó có giá trị, tôi không nghĩ "hướng phát triển là tùy ý" theo nghĩa là một trong hai lựa chọn đều tốt như nhau. Phát triển xuống có thuộc tính tràn ra phần cuối của bộ đệm ghi đè các khung ngăn xếp trước đó, bao gồm các địa chỉ trả lại được lưu. Lớn lên có đặc tính tràn vào phần cuối của bộ đệm chỉ lưu trữ trong cùng hoặc sau đó (nếu bộ đệm không ở trạng thái mới nhất, có thể có các cuộc gọi sau) và thậm chí chỉ có không gian không sử dụng (tất cả đều giả sử là người bảo vệ trang sau ngăn xếp). Vì vậy, từ góc độ an toàn, lớn lên có vẻ rất thích hợp
R ..

6
@R ..: lớn lên không loại bỏ khai thác tràn bộ đệm, bởi vì các hàm dễ bị tổn thương thường không phải là các hàm lá: chúng gọi các hàm khác, đặt địa chỉ trả về phía trên bộ đệm. Các hàm lá nhận được một con trỏ từ người gọi của họ có thể trở nên thô tục để ghi đè địa chỉ trả về của chính họ. ví dụ: Nếu một hàm cấp phát một bộ đệm trên ngăn xếp và chuyển nó tới gets(), hoặc không strcpy()thực hiện được nội tuyến, thì trả về trong các hàm thư viện đó sẽ sử dụng địa chỉ trả về được ghi đè. Hiện tại với các ngăn xếp giảm dần, đó là khi người gọi của họ trở lại.
Peter Cordes

5
@PeterCordes: Thật vậy, nhận xét của tôi lưu ý rằng các khung ngăn xếp cùng cấp hoặc gần đây hơn bộ đệm tràn vẫn có khả năng bị ghi đè, nhưng điều đó ít hơn rất nhiều. Trong trường hợp hàm clobbering là hàm lá được gọi trực tiếp bởi hàm có bộ đệm (ví dụ strcpy), trên một vòm nơi địa chỉ trả về được giữ trong một thanh ghi trừ khi nó cần phải bị đổ, không có quyền truy cập để ghi lại địa chỉ nhà.
R ..

8

Trong hệ thống cụ thể của bạn, ngăn xếp bắt đầu từ địa chỉ bộ nhớ cao và "phát triển" xuống địa chỉ bộ nhớ thấp. (trường hợp đối xứng từ thấp đến cao cũng tồn tại)

Và vì bạn đã thay đổi từ -4 và +4 và nó chạy, điều đó không có nghĩa là nó đúng. Bố cục bộ nhớ của một chương trình đang chạy phức tạp hơn và phụ thuộc vào nhiều yếu tố khác có thể góp phần vào thực tế là bạn đã không gặp sự cố ngay lập tức trên chương trình cực kỳ đơn giản này.


1

Con trỏ ngăn xếp chỉ vào ranh giới giữa bộ nhớ ngăn xếp được phân bổ và chưa phân bổ. Phát triển nó xuống có nghĩa là nó chỉ vào điểm bắt đầu của cấu trúc đầu tiên trong không gian ngăn xếp được phân bổ, với các mục được phân bổ khác theo sau tại các địa chỉ lớn hơn. Có con trỏ trỏ đến điểm bắt đầu của các cấu trúc được phân bổ là phổ biến hơn nhiều so với vòng khác.

Bây giờ trên nhiều hệ thống ngày nay, có một thanh ghi riêng cho các khung ngăn xếp có thể hơi khó tin để tìm ra chuỗi cuộc gọi, với bộ lưu trữ biến cục bộ xen kẽ. Cách thanh ghi khung ngăn xếp này được thiết lập trên một số kiến ​​trúc có nghĩa là nó kết thúc chỉ ra phía sau bộ lưu trữ biến cục bộ trái ngược với con trỏ ngăn xếp trước nó. Vì vậy, sử dụng thanh ghi khung ngăn xếp này sau đó yêu cầu lập chỉ mục tiêu cực.

Lưu ý rằng các khung ngăn xếp và lập chỉ mục của chúng là khía cạnh phụ của các ngôn ngữ máy tính được biên dịch, do đó, trình tạo mã của trình biên dịch phải xử lý "tính không tự nhiên" thay vì lập trình ngôn ngữ lắp ráp kém.

Vì vậy, trong khi có những lý do lịch sử tốt để chọn ngăn xếp phát triển xuống (và một số trong số chúng được giữ lại nếu bạn lập trình bằng ngôn ngữ lắp ráp và không bận tâm đến việc thiết lập khung ngăn xếp phù hợp), chúng trở nên ít nhìn thấy hơn.


2
"Bây giờ trên nhiều hệ thống ngày nay, có một thanh ghi riêng cho các khung ngăn xếp" bạn đang đứng sau thời đại. Các định dạng thông tin gỡ lỗi phong phú hơn đã loại bỏ phần lớn nhu cầu về con trỏ khung hiện nay.
Peter Green
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.