Ngăn xếp tăng lên hay hướng xuống?


89

Tôi có đoạn mã này trong c:

int q = 10;
int s = 5;
int a[3];

printf("Address of a: %d\n",    (int)a);
printf("Address of a[1]: %d\n", (int)&a[1]);
printf("Address of a[2]: %d\n", (int)&a[2]);
printf("Address of q: %d\n",    (int)&q);
printf("Address of s: %d\n",    (int)&s);

Đầu ra là:

Address of a: 2293584
Address of a[1]: 2293588
Address of a[2]: 2293592
Address of q: 2293612
Address of s: 2293608

Vì vậy, tôi thấy rằng từ ađến a[2], địa chỉ bộ nhớ tăng 4 byte mỗi. Nhưng từ qđến s, địa chỉ bộ nhớ giảm 4 byte.

Tôi thắc mắc 2 điều:

  1. Ngăn xếp tăng lên hay giảm xuống? (Có vẻ như cả hai đối với tôi trong trường hợp này)
  2. Điều gì xảy ra giữa a[2]qđịa chỉ bộ nhớ? Tại sao có sự khác biệt lớn về bộ nhớ ở đó? (20 byte).

Lưu ý: Đây không phải là câu hỏi bài tập về nhà. Tôi tò mò về cách hoạt động của ngăn xếp. Cảm ơn vì bất kì sự giúp đỡ.


Việc đặt hàng là tùy ý. Khoảng trống có thể là để lưu trữ một kết quả trung gian như & q hoặc & s - hãy nhìn vào phần tháo gỡ và tự xem.
Tom Leys

Tôi đồng ý, hãy đọc mã lắp ráp. Nếu bạn đang hỏi những câu hỏi kiểu này thì đã đến lúc học cách đọc nó.
Per Johansson

Một phiên bản lắp ráp đơn giản hơn để trả lời: stackoverflow.com/questions/664744/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Câu trả lời:


74

Hành vi của ngăn xếp (lớn lên hoặc phát triển xuống) phụ thuộc vào giao diện nhị phân ứng dụng (ABI) và cách ngăn xếp cuộc gọi (hay còn gọi là bản ghi kích hoạt) được tổ chức.

Trong suốt vòng đời của nó, một chương trình bị ràng buộc phải giao tiếp với các chương trình khác như OS. ABI xác định cách một chương trình có thể giao tiếp với một chương trình khác.

Ngăn xếp cho các kiến ​​trúc khác nhau có thể phát triển theo một trong hai cách, nhưng đối với một kiến ​​trúc thì nó sẽ nhất quán. Vui lòng kiểm tra liên kết wiki này . Tuy nhiên, sự tăng trưởng của ngăn xếp được quyết định bởi ABI của kiến ​​trúc đó.

Ví dụ: nếu bạn lấy MIPS ABI, ngăn xếp cuộc gọi được xác định như bên dưới.

Chúng ta hãy xem xét rằng hàm 'fn1' gọi 'fn2'. Bây giờ khung ngăn xếp được nhìn thấy bởi 'fn2' như sau:

direction of     |                                 |
  growth of      +---------------------------------+ 
   stack         | Parameters passed by fn1(caller)|
from higher addr.|                                 |
to lower addr.   | Direction of growth is opposite |
      |          |   to direction of stack growth  |
      |          +---------------------------------+ <-- SP on entry to fn2
      |          | Return address from fn2(callee) | 
      V          +---------------------------------+ 
                 | Callee saved registers being    | 
                 |   used in the callee function   | 
                 +---------------------------------+
                 | Local variables of fn2          |
                 |(Direction of growth of frame is |
                 | same as direction of growth of  |
                 |            stack)               |
                 +---------------------------------+ 
                 | Arguments to functions called   |
                 | by fn2                          |
                 +---------------------------------+ <- Current SP after stack 
                                                        frame is allocated

Bây giờ bạn có thể thấy ngăn xếp tăng dần xuống. Vì vậy, nếu các biến được phân bổ cho khung cục bộ của hàm, thì địa chỉ của biến sẽ thực sự tăng xuống. Trình biên dịch có thể quyết định thứ tự của các biến để cấp phát bộ nhớ. (Trong trường hợp của bạn, nó có thể là 'q' hoặc 's' là bộ nhớ ngăn xếp được cấp phát đầu tiên. Nhưng, nói chung trình biên dịch thực hiện cấp phát bộ nhớ ngăn xếp theo thứ tự khai báo của các biến).

Nhưng trong trường hợp mảng, việc cấp phát chỉ có một con trỏ duy nhất và bộ nhớ cần cấp phát sẽ thực sự được trỏ bởi một con trỏ duy nhất. Bộ nhớ cần phải liền kề cho một mảng. Vì vậy, mặc dù ngăn xếp tăng dần xuống, đối với các mảng thì ngăn xếp tăng lên.


5
Ngoài ra, nếu bạn muốn kiểm tra xem ngăn xếp tăng lên hay xuống dưới. Khai báo một biến cục bộ trong hàm main. In địa chỉ của biến. Gọi một hàm khác từ main. Khai báo một biến cục bộ trong hàm. In địa chỉ của nó. Dựa trên các địa chỉ đã in, chúng ta có thể nói ngăn xếp tăng lên hoặc giảm xuống.
Ganesh Gopalasubramanian

cảm ơn Ganesh, tôi có một câu hỏi nhỏ: trong hình u vẽ, trong khối thứ ba, ý ​​bạn là "thanh ghi lưu calleR đang được sử dụng trong CALLER" bởi vì khi f1 gọi f2, chúng ta phải lưu trữ địa chỉ f1 (là addr trả về cho thanh ghi f2) và f1 (calleR) không phải thanh ghi f2 (callee). Đúng?
CSawy

44

Đây thực sự là hai câu hỏi. Một là về cách ngăn xếp phát triển khi một hàm gọi một hàm khác (khi một khung mới được cấp phát), và hai là về cách các biến được bố trí trong khung của một hàm cụ thể.

Cả hai đều không được quy định bởi tiêu chuẩn C, nhưng các câu trả lời hơi khác một chút:

  • Ngăn xếp phát triển theo cách nào khi một khung mới được cấp phát - nếu hàm f () gọi hàm g (), fcon trỏ khung của sẽ lớn hơn hay nhỏ hơn gcon trỏ khung của? Điều này có thể xảy ra theo cả hai cách - nó phụ thuộc vào trình biên dịch và kiến ​​trúc cụ thể (tra cứu "quy ước gọi"), nhưng nó luôn nhất quán trong một nền tảng nhất định (với một vài ngoại lệ kỳ lạ, hãy xem phần nhận xét). Xuống dưới là phổ biến hơn; đó là trường hợp trong x86, PowerPC, MIPS, SPARC, EE và Cell SPU.
  • Các biến cục bộ của một hàm được bố trí như thế nào bên trong khung ngăn xếp của nó? Điều này là không xác định và hoàn toàn không thể đoán trước được; trình biên dịch có thể tự do sắp xếp các biến cục bộ của nó, tuy nhiên nó muốn có được kết quả hiệu quả nhất.

7
"nó luôn nhất quán trong một nền tảng nhất định" - không được đảm bảo. Tôi đã thấy một nền tảng không có bộ nhớ ảo, nơi ngăn xếp được mở rộng động. Các khối ngăn xếp mới có hiệu lực không đúng, có nghĩa là bạn sẽ "đi xuống" một khối ngăn xếp trong một lúc, sau đó đột ngột "đi ngang" sang một khối khác. "Sideways" có thể có nghĩa là một địa chỉ lớn hơn hoặc một địa chỉ nhỏ hơn, hoàn toàn phụ thuộc vào sự may mắn của lượt rút thăm.
Steve Jessop

2
Để biết thêm chi tiết cho mục 2 - một trình biên dịch có thể quyết định rằng một biến không bao giờ cần phải ở trong bộ nhớ (giữ nó trong một sổ đăng ký cho vòng đời của biến) và / hoặc nếu thời gian tồn tại của hai hoặc nhiều biến không ' trùng lặp, trình biên dịch có thể quyết định sử dụng cùng một bộ nhớ cho nhiều hơn một biến.
Michael Burr

2
Tôi nghĩ rằng S / 390 (IBM zSeries) có ABI nơi các khung cuộc gọi được liên kết thay vì phát triển trên một ngăn xếp.
ephemient

2
Đúng trên S / 390. Một cuộc gọi là "BALR", đăng ký nhánh và liên kết. Giá trị trả về được đưa vào một thanh ghi thay vì được đẩy vào một ngăn xếp. Hàm trả về là một nhánh đối với nội dung của thanh ghi đó. Khi ngăn xếp sâu hơn, không gian được phân bổ trong đống và chúng được liên kết với nhau. Đây là nơi mà MVS tương đương với "/ bin / true" có tên: "IEFBR14". Phiên bản đầu tiên có một lệnh duy nhất: "BR 14", được phân nhánh với nội dung của thanh ghi 14 chứa địa chỉ trả về.
janm

1
Và một số trình biên dịch trên bộ xử lý PIC phân tích toàn bộ chương trình và phân bổ vị trí cố định cho các biến tự động của mỗi hàm; ngăn xếp thực tế rất nhỏ và không thể truy cập được từ phần mềm; nó chỉ dành cho địa chỉ trả lại.
janm

13

Hướng mà các ngăn xếp phát triển là kiến ​​trúc cụ thể. Điều đó nói rằng, sự hiểu biết của tôi là chỉ một số rất ít kiến ​​trúc phần cứng có ngăn xếp lớn lên.

Hướng mà một ngăn xếp phát triển độc lập với bố cục của một đối tượng riêng lẻ. Vì vậy, trong khi ngăn xếp có thể tăng dần, các mảng sẽ không (tức là & mảng [n] sẽ luôn là <& mảng [n + 1]);


4

Không có gì trong tiêu chuẩn bắt buộc cách mọi thứ được tổ chức trên ngăn xếp. Trong thực tế, bạn có thể xây dựng một trình biên dịch phù hợp mà không lưu trữ phần tử mảng ở yếu tố tiếp giáp trên stack ở tất cả, miễn là nó có khéo léo để vẫn làm mảng yếu tố số học đúng cách (để nó biết, ví dụ, rằng một 1 là 1K từ [0] và có thể điều chỉnh cho phù hợp).

Lý do bạn có thể nhận được các kết quả khác nhau là do, trong khi ngăn xếp có thể tăng dần để thêm "đối tượng" vào nó, mảng là một "đối tượng" duy nhất và nó có thể có các phần tử mảng tăng dần theo thứ tự ngược lại. Nhưng không an toàn khi dựa vào hành vi đó vì hướng có thể thay đổi và các biến có thể được hoán đổi cho nhau vì nhiều lý do bao gồm, nhưng không giới hạn ở:

  • tối ưu hóa.
  • sự liên kết.
  • ý tưởng bất chợt của người phần quản lý ngăn xếp của trình biên dịch.

Xem ở đây cho luận thuyết xuất sắc của tôi về hướng ngăn xếp :-)

Để trả lời các câu hỏi cụ thể của bạn:

  1. Ngăn xếp tăng lên hay giảm xuống?
    Nó không quan trọng chút nào (về tiêu chuẩn) nhưng, vì bạn đã hỏi, nó có thể tăng lên hoặc giảm xuống trong bộ nhớ, tùy thuộc vào việc triển khai.
  2. Điều gì xảy ra giữa địa chỉ bộ nhớ [2] và q? Tại sao có sự khác biệt lớn về bộ nhớ ở đó? (20 byte)?
    Nó không quan trọng chút nào (về tiêu chuẩn). Xem ở trên để biết những lý do có thể.

Tôi thấy bạn liên kết rằng hầu hết kiến ​​trúc CPU áp dụng cách "phát triển xuống", bạn không biết nếu làm như vậy có lợi thế nào không?
Baiyan Huang

Không có ý tưởng, thực sự. Có thể ai đó nghĩ rằng mã đi lên từ 0 vì vậy ngăn xếp nên đi xuống từ mức cao, để giảm thiểu khả năng giao nhau. Nhưng một số CPU đặc biệt bắt đầu chạy mã tại các vị trí khác 0 nên có thể không đúng như vậy. Như với hầu hết mọi thứ, có thể nó đã được thực hiện theo cách đó đơn giản chỉ vì đó là cách đầu tiên có người nghĩ để làm điều đó :-)
paxdiablo

@lzprgmr: Có một số lợi thế nhỏ khi thực hiện một số loại phân bổ heap theo thứ tự tăng dần và trước đây nó thường xảy ra đối với stack và heap nằm ở hai đầu đối diện của một không gian địa chỉ chung. Với điều kiện là việc sử dụng kết hợp static + heap + stack không vượt quá bộ nhớ có sẵn, người ta không phải lo lắng về chính xác lượng bộ nhớ ngăn xếp mà một chương trình đã sử dụng.
supercat

3

Trên x86, "cấp phát" bộ nhớ của khung ngăn xếp chỉ đơn giản là trừ số byte cần thiết từ con trỏ ngăn xếp (tôi tin rằng các kiến ​​trúc khác cũng tương tự). Theo nghĩa này, tôi đoán ngăn xếp lớn dần "xuống", trong đó các địa chỉ nhỏ dần khi bạn gọi sâu hơn vào ngăn xếp (nhưng tôi luôn hình dung bộ nhớ bắt đầu bằng 0 ở trên cùng bên trái và nhận được các địa chỉ lớn hơn khi bạn di chuyển ở bên phải và quấn xuống, vì vậy trong hình ảnh tinh thần của tôi, chồng lên nhau ...). Thứ tự của các biến được khai báo có thể không liên quan đến địa chỉ của chúng - tôi tin rằng tiêu chuẩn cho phép trình biên dịch sắp xếp lại chúng, miễn là nó không gây ra tác dụng phụ (ai đó vui lòng sửa cho tôi nếu tôi sai) . Họ '

Khoảng trống xung quanh mảng có thể là một số loại đệm, nhưng nó rất bí ẩn đối với tôi.


1
Trên thực tế, tôi biết trình biên dịch có thể sắp xếp lại chúng, vì hoàn toàn miễn phí khi không phân bổ chúng. Nó chỉ có thể đưa chúng vào thanh ghi và không sử dụng bất kỳ không gian ngăn xếp nào.
rmeador

Nó không thể đưa chúng vào sổ đăng ký nếu bạn tham khảo địa chỉ của chúng.
florin

điểm tốt, đã không xem xét điều đó. nhưng nó vẫn cũng đủ là một bằng chứng cho thấy các trình biên dịch có thể sắp xếp lại chúng, vì chúng ta biết nó có thể làm điều đó ít nhất một số thời điểm đó :)
rmeador

1

Trước hết, 8 byte không gian chưa sử dụng trong bộ nhớ của nó (không phải là 12, ngăn xếp nhớ tăng dần xuống, Vì vậy, không gian không được cấp phát là từ 604 đến 597). và tại sao?. Bởi vì mọi kiểu dữ liệu đều chiếm không gian trong bộ nhớ bắt đầu từ địa chỉ chia hết cho kích thước của nó. Trong trường hợp của chúng ta, mảng 3 số nguyên chiếm 12 byte không gian bộ nhớ và 604 không chia hết cho 12. Vì vậy, nó để lại các khoảng trống cho đến khi gặp địa chỉ bộ nhớ chia hết cho 12, nó là 596.

Vì vậy, không gian bộ nhớ được cấp phát cho mảng là từ 596 đến 584. Nhưng vì cấp phát mảng được tiếp tục, Vì vậy, phần tử đầu tiên của mảng bắt đầu từ địa chỉ 584 chứ không phải từ 596.


1

Trình biên dịch có thể tự do phân bổ các biến cục bộ (tự động) ở bất kỳ vị trí nào trên khung ngăn xếp cục bộ, bạn không thể suy ra một cách đáng tin cậy hướng phát triển của ngăn xếp hoàn toàn từ đó. Bạn có thể suy ra hướng phát triển ngăn xếp từ việc so sánh địa chỉ của các khung ngăn xếp lồng nhau, tức là so sánh địa chỉ của một biến cục bộ bên trong khung ngăn xếp của một hàm so với địa chỉ của nó:

#include <stdio.h>
int f(int *x)
{
  int a;
  return x == NULL ? f(&a) : &a - x;
}

int main(void)
{
  printf("stack grows %s!\n", f(NULL) < 0 ? "down" : "up");
  return 0;
}

5
Tôi khá chắc rằng đó là hành vi không xác định để trừ các con trỏ đến các đối tượng ngăn xếp khác nhau - các con trỏ không thuộc cùng một đối tượng không thể so sánh được. Rõ ràng là mặc dù nó sẽ không sụp đổ trên bất kỳ kiến ​​trúc "bình thường" nào.
Steve Jessop

@SteveJessop Có cách nào chúng tôi có thể sửa lỗi này để lấy hướng ngăn xếp theo chương trình không?
xxks-kkk

@ xxks-kkk: về nguyên tắc là không, vì triển khai C không bắt buộc phải có "hướng của ngăn xếp". Ví dụ, sẽ không vi phạm tiêu chuẩn để có một quy ước gọi trong đó một khối ngăn xếp được cấp phát phía trước và sau đó một số quy trình cấp phát bộ nhớ trong giả ngẫu nhiên được sử dụng để nhảy xung quanh bên trong nó. Trong thực tế, nó thực sự hoạt động như matja mô tả.
Steve Jessop

0

Tôi không nghĩ nó mang tính xác định như vậy. Một mảng dường như "phát triển" bởi vì bộ nhớ đó nên được cấp phát liên tục. Tuy nhiên, vì q và s hoàn toàn không liên quan đến nhau, trình biên dịch chỉ gắn mỗi chúng vào một vị trí bộ nhớ trống tùy ý trong ngăn xếp, có lẽ là những cái phù hợp nhất với kích thước số nguyên.

Điều đã xảy ra giữa a [2] và q là không gian xung quanh vị trí của q không đủ lớn (tức là không lớn hơn 12 byte) để phân bổ một mảng 3 số nguyên.


nếu vậy, tại sao q, s, a không có bộ nhớ dự phòng? (Ví dụ: Địa chỉ của q: 2293612 Địa chỉ của s: 2293608 Địa chỉ của a: 2293604)

Tôi thấy "khoảng cách" giữa s và a

Bởi vì s và a không được phân bổ cùng nhau - các con trỏ duy nhất phải liền kề là các con trỏ trong mảng. Bộ nhớ khác có thể được cấp phát ở bất cứ đâu.
javanix

0

Ngăn xếp của tôi dường như mở rộng về phía các địa chỉ được đánh số thấp hơn.

Nó có thể khác trên máy tính khác hoặc thậm chí trên máy tính của riêng tôi nếu tôi sử dụng lệnh gọi trình biên dịch khác. ... hoặc trình biên dịch muigt chọn hoàn toàn không sử dụng ngăn xếp (nội tuyến mọi thứ (các hàm và biến nếu tôi không lấy địa chỉ của chúng)).

$ cat stack.c
#include <stdio.h>

int stack(int x) {
  printf("level %d: x is at %p\n", x, (void*)&x);
  if (x == 0) return 0;
  return stack(x - 1);
}

int main(void) {
  stack(4);
  return 0;
}
$ / usr / bin / gcc -Wall -Wextra -std = c89 -pedantic stack.c
$ ./a.out
cấp 4: x là 0x7fff7781190c
cấp 3: x là 0x7fff778118ec
cấp 2: x là 0x7fff778118cc
cấp 1: x là 0x7fff778118ac
cấp 0: x là 0x7fff7781188c

0

Ngăn xếp tăng dần (trên x86). Tuy nhiên, ngăn xếp được phân bổ trong một khối khi hàm tải và bạn không đảm bảo thứ tự các mục sẽ nằm trong ngăn xếp.

Trong trường hợp này, nó phân bổ không gian cho hai int và một mảng ba int trên ngăn xếp. Nó cũng phân bổ thêm 12 byte sau mảng, vì vậy nó trông giống như sau:

a [12 byte]
padding (?) [12 byte]
s [4 byte]
q [4 byte]

Vì bất kỳ lý do gì, trình biên dịch của bạn đã quyết định rằng nó cần phân bổ 32 byte cho hàm này và có thể nhiều hơn nữa. Điều đó không rõ ràng đối với bạn là một lập trình viên C, bạn không biết tại sao.

Nếu bạn muốn biết tại sao, hãy biên dịch mã sang hợp ngữ, tôi tin rằng đó là -S trên gcc và / S trên trình biên dịch C của MS. Nếu bạn nhìn vào hướng dẫn mở của hàm đó, bạn sẽ thấy con trỏ ngăn xếp cũ được lưu và sau đó 32 (hoặc cái gì đó khác!) Bị trừ khỏi nó. Từ đó, bạn có thể xem cách mã truy cập vào khối bộ nhớ 32 byte đó và tìm ra trình biên dịch của bạn đang làm gì. Khi kết thúc hàm, bạn có thể thấy con trỏ ngăn xếp được khôi phục.


0

Nó phụ thuộc vào hệ điều hành và trình biên dịch của bạn.


Không biết tại sao câu trả lời của tôi bị bỏ phiếu thấp. Nó thực sự phụ thuộc vào hệ điều hành và trình biên dịch của bạn. Trên một số hệ thống, ngăn xếp tăng dần xuống, nhưng trên một số hệ thống khác, ngăn xếp tăng lên. Và trên một số hệ thống, không có ngăn xếp khung đẩy xuống thực sự, mà nó được mô phỏng với một vùng bộ nhớ hoặc bộ thanh ghi dành riêng.
David R Tribble vào

3
Có lẽ bởi vì các câu khẳng định không phải là câu trả lời tốt.
Các cuộc đua ánh sáng trong quỹ đạo

0

Ngăn xếp không phát triển xuống. Vì vậy, f (g (h ())), ngăn xếp được phân bổ cho h sẽ bắt đầu ở địa chỉ thấp hơn sau đó g và g sẽ thấp hơn sau đó của f. Nhưng các biến trong ngăn xếp phải tuân theo đặc tả C,

http://c0x.coding-guidelines.com/6.5.8.html

1206 Nếu các đối tượng được trỏ đến là thành viên của cùng một đối tượng tổng hợp, con trỏ đến thành viên cấu trúc được khai báo sau sẽ so sánh lớn hơn con trỏ đến thành viên được khai báo trước đó trong cấu trúc và con trỏ đến phần tử mảng có giá trị chỉ số con lớn hơn so sánh con trỏ đến các phần tử của cùng một mảng có giá trị chỉ số dưới thấp hơn.

& a [0] <& a [1], phải luôn đúng, bất kể 'a' được phân bổ như thế nào


Trên hầu hết các máy, ngăn xếp phát triển xuống dưới - ngoại trừ những máy có ngăn xếp phát triển lên trên.
Jonathan Leffler

0

phát triển xuống dưới và điều này là do tiêu chuẩn thứ tự byte cuối nhỏ khi nói đến tập dữ liệu trong bộ nhớ.

Một cách bạn có thể xem xét nó là ngăn xếp KHÔNG tăng lên nếu bạn nhìn vào bộ nhớ từ 0 từ trên xuống và tối đa từ dưới cùng.

Lý do cho ngăn xếp tăng dần xuống là có thể bỏ tham chiếu từ quan điểm của ngăn xếp hoặc con trỏ cơ sở.

Hãy nhớ rằng bất kỳ loại hội nghị nào cũng tăng từ địa chỉ thấp nhất đến cao nhất. Vì ngăn xếp tăng dần xuống (địa chỉ cao nhất đến thấp nhất), điều này cho phép bạn coi ngăn xếp như bộ nhớ động.

Đây là một lý do tại sao rất nhiều ngôn ngữ lập trình và kịch bản sử dụng máy ảo dựa trên ngăn xếp thay vì dựa trên thanh ghi.


The reason for the stack growing downward is to be able to dereference from the perspective of the stack or base pointer.Lập luận rất hay
user3405291 17/11/17

0

Nó phụ thuộc vào kiến ​​trúc. Để kiểm tra hệ thống của riêng bạn, hãy sử dụng mã này từ GeeksForGeeks :

// C program to check whether stack grows 
// downward or upward. 
#include<stdio.h> 

void fun(int *main_local_addr) 
{ 
    int fun_local; 
    if (main_local_addr < &fun_local) 
        printf("Stack grows upward\n"); 
    else
        printf("Stack grows downward\n"); 
} 

int main() 
{ 
    // fun's local variable 
    int main_local; 

    fun(&main_local); 
    return 0; 
} 
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.