Ngăn xếp hoạt động như thế nào trong hợp ngữ?


83

Tôi hiện đang cố gắng hiểu cách ngăn xếp hoạt động, vì vậy tôi quyết định tự học một số ngôn ngữ hợp ngữ , tôi đang sử dụng cuốn sách này:

http://savannah.nongnu.org/projects/pgubook/

Tôi đang sử dụng Gas và đang phát triển trên Linux Mint .

Tôi hơi bối rối vì điều gì đó:

Theo như tôi đã biết, một ngăn xếp chỉ đơn giản là một cấu trúc dữ liệu. Vì vậy, tôi cho rằng nếu tôi viết mã trong assembly, tôi sẽ phải tự triển khai ngăn xếp. Tuy nhiên, điều này dường như không đúng vì có những lệnh như

pushl
popl

Vì vậy, khi mã hóa hợp ngữ cho kiến trúc x86 và sử dụng cú pháp Gas: ngăn xếp có phải chỉ là một cấu trúc dữ liệu đã được thực hiện không? Hay nó thực sự được triển khai ở cấp độ phần cứng? Hay nó là cái gì khác? Ngoài ra, hầu hết các ngôn ngữ hợp ngữ cho các bộ chip khác đã được triển khai ngăn xếp chưa?

Tôi biết đây là một câu hỏi hơi ngu ngốc nhưng tôi thực sự khá bối rối vì điều này.


2
Hầu hết các câu trả lời này nói về ngăn xếp vì nó được các ngôn ngữ sử dụng, đặc biệt là chúng nói về việc truyền các đối số trên ngăn xếp. Trong nhiều CPU, điều này chỉ để làm cho việc triển khai các ngôn ngữ dễ dàng hơn - Nếu bạn đang lắp ráp mã hóa thủ công, bạn thường truyền các tham số cho các hàm trong các thanh ghi (Ít nhất là trước khi CPU được tối ưu hóa cho các hoạt động ngăn xếp vì các ngôn ngữ sử dụng nó rất nhiều). Ngăn xếp chủ yếu là để giữ cho các lệnh gọi / trả về theo thứ tự, còn một ngắt (cần lưu trạng thái của CPU) sẽ đẩy các giá trị hiện có trong các thanh ghi mà nó sẽ sử dụng và bật chúng trước khi trả về.
Bill K

Câu trả lời:


82

Tôi nghĩ chủ yếu là bạn đang nhầm lẫn giữa a program's stackany old stack .

Một chồng

Là một cấu trúc dữ liệu trừu tượng bao gồm thông tin trong hệ thống Cuối cùng Trong Đầu ra. Bạn đặt các đối tượng tùy ý vào ngăn xếp và sau đó bạn lại lấy chúng ra, giống như khay vào / ra, mục trên cùng luôn là vật được lấy ra và bạn luôn đặt lên trên cùng.

Một ngăn xếp chương trình

Là một ngăn xếp, nó là một phần bộ nhớ được sử dụng trong quá trình thực thi, nó thường có kích thước tĩnh cho mỗi chương trình và thường được sử dụng để lưu trữ các tham số của hàm. Bạn đẩy các tham số vào ngăn xếp khi bạn gọi một hàm và hàm này giải quyết trực tiếp ngăn xếp hoặc bật ra các biến từ ngăn xếp.

Một ngăn xếp chương trình nói chung không phải là phần cứng (mặc dù nó được giữ trong bộ nhớ nên có thể lập luận như vậy), nhưng Con trỏ ngăn xếp trỏ đến vùng hiện tại của Ngăn xếp nói chung là một thanh ghi CPU. Điều này làm cho nó linh hoạt hơn một chút so với ngăn xếp LIFO vì bạn có thể thay đổi điểm mà tại đó ngăn xếp đang xử lý.

Bạn nên đọc và đảm bảo rằng bạn hiểu bài viết wikipedia vì nó mô tả tốt về Ngăn xếp phần cứng, nơi bạn đang xử lý.

Ngoài ra còn có hướng dẫn này giải thích ngăn xếp theo các thanh ghi 16bit cũ nhưng có thể hữu ích và một hướng dẫn khác cụ thể về ngăn xếp.

Từ Nils Pipenbrinck:

Cần lưu ý rằng một số bộ xử lý không thực hiện tất cả các hướng dẫn để truy cập và thao tác với ngăn xếp (đẩy, bật, con trỏ ngăn xếp, v.v.) nhưng x86 thì có do tần suất sử dụng của nó. Trong những tình huống này, nếu bạn muốn có một ngăn xếp, bạn sẽ phải tự thực hiện nó (một số MIPS và một số bộ xử lý ARM được tạo mà không có ngăn xếp).

Ví dụ, trong MIP, một lệnh đẩy sẽ được thực hiện như:

addi $sp, $sp, -4  # Decrement stack pointer by 4  
sw   $t0, ($sp)   # Save $t0 to stack  

và một hướng dẫn Pop sẽ giống như sau:

lw   $t0, ($sp)   # Copy from stack to $t0  
addi $sp, $sp, 4   # Increment stack pointer by 4  

2
Btw - x86 có các hướng dẫn ngăn xếp đặc biệt này vì việc đẩy và đưa nội dung từ ngăn xếp diễn ra thường xuyên đến mức bạn nên sử dụng một opcode ngắn cho chúng (ít không gian mã hơn). Các kiến ​​trúc như MIPS và ARM không có những thứ này, vì vậy bạn phải tự triển khai ngăn xếp.
Nils Pipenbrinck 17/02/09

4
Hãy nhớ rằng bộ xử lý mới nóng của bạn tương thích nhị phân với 8086 ở một mức độ nào đó và tương thích nguồn với 8080, một sự phát triển của 8008, bộ vi xử lý đầu tiên. Một số quyết định trong số này có một chặng đường dài.
David Thornley

4
Trong ARM, có những hướng dẫn duy nhất để thao tác ngăn xếp, chúng không quá rõ ràng vì chúng được gọi là STMDB SP! (dành cho PUSH) và LDMIA SP! (đối với POP).
Adam Goode

1
Chúa ơi, câu trả lời này cần +500 ... Tôi đã không tìm thấy bất cứ điều gì giải thích cho điều này từ mãi mãi. Đang cân nhắc tạo các tài khoản mới để +1 tài khoản này ngay bây giờ ...
Gabriel

1
@bplus Bạn cũng có thể tham khảo cs.umd.edu/class/sum2003/cmsc311/Notes/Mips/stack.html
Suraj Jain

34

(Tôi đã đưa ra ý chính của tất cả mã trong câu trả lời này trong trường hợp bạn muốn chơi với nó)

Tôi chỉ từng làm những điều cơ bản nhất với asm trong khóa học CS101 của tôi vào năm 2003. Và tôi chưa bao giờ thực sự hiểu được cách asm và ngăn xếp hoạt động cho đến khi tôi nhận ra rằng tất cả đều cơ bản như lập trình trong C hoặc C ++ ... nhưng không có biến cục bộ, tham số và hàm. Nghe có vẻ chưa dễ dàng :) Để tôi chỉ cho bạn (đối với asm x86 với cú pháp Intel ).


1. Ngăn xếp là gì

Stack thường là một đoạn bộ nhớ liền kề được phân bổ cho mọi luồng trước khi chúng bắt đầu. Bạn có thể lưu trữ ở đó bất cứ thứ gì bạn muốn. Theo thuật ngữ C ++ ( đoạn mã # 1 ):

const int STACK_CAPACITY = 1000;
thread_local int stack[STACK_CAPACITY];

2. Trên và dưới của ngăn xếp

Về nguyên tắc, bạn có thể lưu trữ các giá trị trong các ô ngẫu nhiên của stackmảng ( đoạn mã # 2.1 ):

stack[333] = 123;
stack[517] = 456;
stack[555] = stack[333] + stack[517];

Nhưng hãy tưởng tượng bạn sẽ khó nhớ những ô stacknào đã được sử dụng và ô nào là "miễn phí". Đó là lý do tại sao chúng tôi lưu trữ các giá trị mới trên ngăn xếp cạnh nhau.

Một điều kỳ lạ về ngăn xếp của asm (x86) là bạn thêm những thứ vào đó bắt đầu bằng chỉ mục cuối cùng và chuyển đến các chỉ mục thấp hơn: ngăn xếp [999], sau đó ngăn xếp [998], v.v. ( đoạn mã # 2.2 ):

stack[999] = 123;
stack[998] = 456;
stack[997] = stack[999] + stack[998];

Và vẫn còn (thận trọng, bạn đang gonna bị nhầm lẫn bây giờ) cái tên "chính thức" cho stack[999]đáy của ngăn xếp .
Ô được sử dụng cuối cùng ( stack[997]trong ví dụ trên) được gọi là đỉnh của ngăn xếp (xem Vị trí trên cùng của ngăn xếp trên x86 ).


3. Con trỏ ngăn xếp (SP)

Với mục đích của cuộc thảo luận này, hãy giả sử các thanh ghi CPU được biểu diễn dưới dạng các biến toàn cục (xem Thanh ghi Mục đích Chung ).

int AX, BX, SP, BP, ...;
int main(){...}

Có thanh ghi CPU (SP) đặc biệt theo dõi phần trên cùng của ngăn xếp. SP là một con trỏ (giữ một địa chỉ bộ nhớ như 0xAAAABBCC). Nhưng với mục đích của bài đăng này, tôi sẽ sử dụng nó như một chỉ mục mảng (0, 1, 2, ...).

Khi một luồng bắt đầu, SP == STACK_CAPACITYsau đó chương trình và hệ điều hành sẽ sửa đổi nó khi cần thiết. Quy tắc là bạn không thể ghi vào ngăn xếp các ô ngoài đỉnh ngăn xếp và bất kỳ chỉ mục nào ít hơn thì SP không hợp lệ và không an toàn (do hệ thống ngắt ), vì vậy trước tiên bạn giảm SP rồi sau đó ghi giá trị vào ô mới được cấp phát.

Khi bạn muốn đẩy nhiều giá trị trong ngăn xếp liên tiếp, bạn có thể đặt trước không gian cho tất cả chúng ( đoạn mã số 3 ):

SP -= 3;
stack[999] = 12;
stack[998] = 34;
stack[997] = stack[999] + stack[998];

Ghi chú. Bây giờ bạn có thể thấy lý do tại sao phân bổ trên ngăn xếp lại nhanh như vậy - đó chỉ là một đợt giảm thanh ghi duy nhất.


4. Biến cục bộ

Hãy cùng xem hàm đơn giản này ( đoạn mã # 4.1 ):

int triple(int a) {
    int result = a * 3;
    return result;
}

và viết lại nó mà không sử dụng biến cục bộ ( đoạn mã # 4.2 ):

int triple_noLocals(int a) {
    SP -= 1; // move pointer to unused cell, where we can store what we need
    stack[SP] = a * 3;
    return stack[SP];
}

và xem nó được gọi như thế nào ( đoạn mã # 4.3 ):

// SP == 1000
someVar = triple_noLocals(11);
// now SP == 999, but we don't need the value at stack[999] anymore
// and we will move the stack index back, so we can reuse this cell later
SP += 1; // SP == 1000 again

5. Đẩy / bật

Việc bổ sung một phần tử mới trên đầu ngăn xếp là một hoạt động thường xuyên như vậy, CPU có một lệnh đặc biệt cho điều đó push,. Chúng tôi sẽ ghép nó như thế này ( đoạn mã 5.1 ):

void push(int value) {
    --SP;
    stack[SP] = value;
}

Tương tự như vậy, lấy phần tử trên cùng của ngăn xếp ( đoạn mã 5.2 ):

void pop(int& result) {
    result = stack[SP];
    ++SP; // note that `pop` decreases stack's size
}

Kiểu sử dụng phổ biến cho push / pop đang tạm thời tiết kiệm một số giá trị. Giả sử, chúng tôi có thứ gì đó hữu ích trong biến myVarvà vì lý do nào đó, chúng tôi cần thực hiện các phép tính sẽ ghi đè lên biến đó ( đoạn mã 5.3 ):

int myVar = ...;
push(myVar); // SP == 999
myVar += 10;
... // do something with new value in myVar
pop(myVar); // restore original value, SP == 1000

6. Tham số chức năng

Bây giờ, hãy chuyển các tham số bằng cách sử dụng ngăn xếp ( đoạn mã # 6 ):

int triple_noL_noParams() { // `a` is at index 999, SP == 999
    SP -= 1; // SP == 998, stack[SP + 1] == a
    stack[SP] = stack[SP + 1] * 3;
    return stack[SP];
}

int main(){
    push(11); // SP == 999
    assert(triple(11) == triple_noL_noParams());
    SP += 2; // cleanup 1 local and 1 parameter
}

7. return tuyên bố

Hãy trả về giá trị trong thanh ghi AX ( đoạn mã số 7 ):

void triple_noL_noP_noReturn() { // `a` at 998, SP == 998
    SP -= 1; // SP == 997

    stack[SP] = stack[SP + 1] * 3;
    AX = stack[SP];

    SP += 1; // finally we can cleanup locals right in the function body, SP == 998
}

void main(){
    ... // some code
    push(AX); // save AX in case there is something useful there, SP == 999
    push(11); // SP == 998
    triple_noL_noP_noReturn();
    assert(triple(11) == AX);
    SP += 1; // cleanup param
             // locals were cleaned up in the function body, so we don't need to do it here
    pop(AX); // restore AX
    ...
}

8. Con trỏ cơ sở ngăn xếp (BP) (còn được gọi là con trỏ khung ) và khung ngăn xếp

Hãy sử dụng hàm "nâng cao" hơn và viết lại nó trong C ++ giống asm của chúng tôi ( đoạn mã # 8.1 ):

int myAlgo(int a, int b) {
    int t1 = a * 3;
    int t2 = b * 3;
    return t1 - t2;
}

void myAlgo_noLPR() { // `a` at 997, `b` at 998, old AX at 999, SP == 997
    SP -= 2; // SP == 995

    stack[SP + 1] = stack[SP + 2] * 3; 
    stack[SP]     = stack[SP + 3] * 3;
    AX = stack[SP + 1] - stack[SP];

    SP += 2; // cleanup locals, SP == 997
}

int main(){
    push(AX); // SP == 999
    push(22); // SP == 998
    push(11); // SP == 997
    myAlgo_noLPR();
    assert(myAlgo(11, 22) == AX);
    SP += 2;
    pop(AX);
}

Bây giờ, hãy tưởng tượng chúng tôi quyết định giới thiệu biến cục bộ mới để lưu trữ kết quả ở đó trước khi trả về, như chúng tôi đã làm trong tripple(đoạn mã # 4.1). Nội dung của hàm sẽ là ( đoạn mã # 8.2 ):

SP -= 3; // SP == 994
stack[SP + 2] = stack[SP + 3] * 3; 
stack[SP + 1] = stack[SP + 4] * 3;
stack[SP]     = stack[SP + 2] - stack[SP + 1];
AX = stack[SP];
SP += 3;

Bạn thấy đấy, chúng tôi đã phải cập nhật mọi tham chiếu đến các tham số hàm và biến cục bộ. Để tránh điều đó, chúng ta cần một chỉ số neo, chỉ số này không thay đổi khi ngăn xếp phát triển.

Chúng tôi sẽ tạo neo ngay khi nhập hàm (trước khi chúng tôi phân bổ không gian cho các local) bằng cách lưu giá trị hàng đầu (giá trị của SP) hiện tại vào thanh ghi BP. Đoạn mã số 8.3 :

void myAlgo_noLPR_withAnchor() { // `a` at 997, `b` at 998, SP == 997
    push(BP);   // save old BP, SP == 996
    BP = SP;    // create anchor, stack[BP] == old value of BP, now BP == 996
    SP -= 2;    // SP == 994

    stack[BP - 1] = stack[BP + 1] * 3;
    stack[BP - 2] = stack[BP + 2] * 3;
    AX = stack[BP - 1] - stack[BP - 2];

    SP = BP;    // cleanup locals, SP == 996
    pop(BP);    // SP == 997
}

Phần ngăn xếp thuộc về và toàn quyền kiểm soát của chức năng được gọi là khung ngăn xếp của chức năng . Ví dụ: myAlgo_noLPR_withAnchorkhung ngăn xếp của là stack[996 .. 994](bao gồm cả hai Idexes).
Khung bắt đầu ở BP của chức năng (sau khi chúng tôi đã cập nhật nó bên trong chức năng) và kéo dài cho đến khung ngăn xếp tiếp theo. Vì vậy, các tham số trên ngăn xếp là một phần của khung ngăn xếp của người gọi (xem chú thích 8a).

Ghi chú:
8a. Wikipedia nói khác về các thông số, nhưng ở đây tôi tuân thủ hướng dẫn của nhà phát triển phần mềm Intel , xem vol. 1, phần 6.2.4.1 Con trỏ cơ sở Stack-Frame và Hình 6-2 trong phần 6.3.2 Hoạt động Far CALL và RET . Các tham số của hàm và khung ngăn xếp là một phần của bản ghi kích hoạt của hàm (xem Gen trên các chu kỳ của hàm ).
8b. các hiệu số dương từ điểm BP đến tham số hàm và hiệu số âm trỏ đến các biến cục bộ. Điều đó khá tiện dụng để gỡ lỗi
8c. stack[BP]lưu trữ địa chỉ của khung ngăn xếp trước đó,stack[stack[BP]]lưu trữ khung ngăn xếp trước đó, v.v. Sau chuỗi này, bạn có thể khám phá các khung của tất cả các chức năng trong chương trình, chưa trả về. Đây là cách trình gỡ lỗi hiển thị cho bạn gọi ngăn xếp
8d. 3 hướng dẫn đầu tiên myAlgo_noLPR_withAnchor, nơi chúng tôi thiết lập khung (lưu BP cũ, cập nhật BP, dự trữ không gian cho người dân địa phương) được gọi là phần mở đầu hàm


9. Quy ước gọi điện

Trong đoạn mã 8.1, chúng tôi đã đẩy các thông số myAlgotừ phải sang trái và trả về kết quả AX. Chúng tôi cũng có thể vượt qua các đoạn từ trái sang phải và quay trở lại BX. Hoặc chuyển các tham số trong BX và CX và quay trở lại trong AX. Rõ ràng, hàm caller ( main()) và hàm được gọi phải thống nhất với nhau về vị trí và thứ tự lưu trữ tất cả những thứ này.

Quy ước gọi điện là một tập hợp các quy tắc về cách các tham số được truyền và kết quả được trả về.

Trong đoạn mã trên, chúng tôi đã sử dụng quy ước gọi cdecl :

  • Các tham số được truyền trên ngăn xếp, với đối số đầu tiên ở địa chỉ thấp nhất trên ngăn xếp tại thời điểm gọi (đẩy cuối cùng <...>). Người gọi có trách nhiệm đưa các tham số trở lại ngăn xếp sau cuộc gọi.
  • giá trị trả về được đặt trong AX
  • EBP và ESP phải được giữ nguyên bởi callee ( myAlgo_noLPR_withAnchorhàm trong trường hợp của chúng tôi), sao cho người gọi ( mainhàm) có thể dựa vào các thanh ghi đó mà không bị thay đổi bởi một cuộc gọi.
  • Tất cả các thanh ghi khác (EAX, <...>) có thể được chỉnh sửa tự do bởi callee; nếu một người gọi muốn bảo toàn một giá trị trước và sau khi gọi hàm, nó phải lưu giá trị đó ở nơi khác (chúng tôi làm điều này với AX)

(Nguồn: ví dụ "32-bit cdecl" từ Tài liệu Tràn ngăn xếp; bản quyền 2016 của icktoofayPeter Cordes ; được cấp phép theo CC BY-SA 3.0. Bạn có thể tìm thấy kho lưu trữ toàn bộ nội dung Tài liệu Tràn Ngăn xếp tại archive.org, trong đó ví dụ này được lập chỉ mục theo ID chủ đề 3261 và ID ví dụ 11196.)


10. Các lệnh gọi hàm

Bây giờ là phần thú vị nhất. Cũng giống như dữ liệu, mã thực thi cũng được lưu trong bộ nhớ (hoàn toàn không liên quan đến bộ nhớ cho ngăn xếp) và mọi lệnh đều có địa chỉ.
Khi không được ra lệnh khác, CPU sẽ thực hiện lần lượt các lệnh, theo thứ tự chúng được lưu trữ trong bộ nhớ. Nhưng chúng ta có thể ra lệnh cho CPU "nhảy" đến một vị trí khác trong bộ nhớ và thực hiện các lệnh từ đó trở đi. Trong asm, nó có thể là bất kỳ địa chỉ nào và trong các ngôn ngữ cấp cao hơn như C ++, bạn chỉ có thể chuyển đến các địa chỉ được đánh dấu bằng nhãn ( có những cách giải quyết nhưng chúng không đẹp, ít nhất là).

Hãy sử dụng hàm này ( đoạn mã # 10.1 ):

int myAlgo_withCalls(int a, int b) {
    int t1 = triple(a);
    int t2 = triple(b);
    return t1 - t2;
}

Và thay vì gọi trippleC ++ theo cách, hãy làm như sau:

  1. sao chép tripplemã của vào đầumyAlgo
  2. lúc myAlgonhập cảnh nhảy quatripple mã của vớigoto
  3. khi chúng ta cần thực thi tripplemã của, hãy lưu trên địa chỉ ngăn xếp của dòng mã ngay sau khi tripplegọi, vì vậy chúng ta có thể quay lại đây sau và tiếp tục thực thi ( PUSH_ADDRESSmacro bên dưới)
  4. nhảy đến địa chỉ của dòng đầu tiên ( tripplehàm) và thực thi nó đến cuối (3. và 4. cùng là CALLmacro)
  5. ở cuối tripple(sau khi chúng tôi đã dọn dẹp người dân địa phương), lấy địa chỉ trả lại từ đầu ngăn xếp và chuyển đến đó ( RETmacro)

Vì không có cách nào dễ dàng để chuyển đến địa chỉ mã cụ thể trong C ++, chúng tôi sẽ sử dụng nhãn để đánh dấu vị trí của các bước nhảy. Tôi sẽ không đi vào chi tiết cách các macro bên dưới hoạt động, chỉ cần tôi tin rằng họ làm những gì tôi nói ( đoạn mã # 10.2 ):

// pushes the address of the code at label's location on the stack
// NOTE1: this gonna work only with 32-bit compiler (so that pointer is 32-bit and fits in int)
// NOTE2: __asm block is specific for Visual C++. In GCC use https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html
#define PUSH_ADDRESS(labelName) {               \
    void* tmpPointer;                           \
    __asm{ mov [tmpPointer], offset labelName } \
    push(reinterpret_cast<int>(tmpPointer));    \
}

// why we need indirection, read https://stackoverflow.com/a/13301627/264047
#define TOKENPASTE(x, y) x ## y
#define TOKENPASTE2(x, y) TOKENPASTE(x, y)

// generates token (not a string) we will use as label name. 
// Example: LABEL_NAME(155) will generate token `lbl_155`
#define LABEL_NAME(num) TOKENPASTE2(lbl_, num)

#define CALL_IMPL(funcLabelName, callId)    \
    PUSH_ADDRESS(LABEL_NAME(callId));       \
    goto funcLabelName;                     \
    LABEL_NAME(callId) :

// saves return address on the stack and jumps to label `funcLabelName`
#define CALL(funcLabelName) CALL_IMPL(funcLabelName, __LINE__)

// takes address at the top of stack and jump there
#define RET() {                                         \
    int tmpInt;                                         \
    pop(tmpInt);                                        \
    void* tmpPointer = reinterpret_cast<void*>(tmpInt); \
    __asm{ jmp tmpPointer }                             \
}

void myAlgo_asm() {
    goto my_algo_start;

triple_label:
    push(BP);
    BP = SP;
    SP -= 1;

    // stack[BP] == old BP, stack[BP + 1] == return address
    stack[BP - 1] = stack[BP + 2] * 3;
    AX = stack[BP - 1];

    SP = BP;     
    pop(BP);
    RET();

my_algo_start:
    push(BP);   // SP == 995
    BP = SP;    // BP == 995; stack[BP] == old BP, 
                // stack[BP + 1] == dummy return address, 
                // `a` at [BP + 2], `b` at [BP + 3]
    SP -= 2;    // SP == 993

    push(AX);
    push(stack[BP + 2]);
    CALL(triple_label);
    stack[BP - 1] = AX;
    SP -= 1;
    pop(AX);

    push(AX);
    push(stack[BP + 3]);
    CALL(triple_label);
    stack[BP - 2] = AX;
    SP -= 1;
    pop(AX);

    AX = stack[BP - 1] - stack[BP - 2];

    SP = BP; // cleanup locals, SP == 997
    pop(BP);
}

int main() {
    push(AX);
    push(22);
    push(11);
    push(7777); // dummy value, so that offsets inside function are like we've pushed return address
    myAlgo_asm();
    assert(myAlgo_withCalls(11, 22) == AX);
    SP += 1; // pop dummy "return address"
    SP += 2;
    pop(AX);
}

Ghi chú:
10a. bởi vì địa chỉ trả về được lưu trữ trên ngăn xếp, về nguyên tắc chúng ta có thể thay đổi nó. Đây là cách hoạt động của đòn tấn công đập ngăn xếp
10b. 3 hướng dẫn cuối cùng ở "cuối" của triple_label(dọn dẹp cục bộ , khôi phục BP cũ, quay lại) được gọi là phần kết của hàm


11. hội

Bây giờ chúng ta hãy nhìn vào asm thực sự cho myAlgo_withCalls. Để làm điều đó trong Visual Studio:

  • đặt nền tảng xây dựng thành x86 ( không phải x86_64)
  • loại xây dựng: Gỡ lỗi
  • đặt điểm ngắt ở đâu đó bên trong myAlgo_withCalls
  • chạy và khi quá trình thực thi dừng lại tại điểm ngắt, hãy nhấn Ctrl + Alt + D

Một điểm khác biệt với C ++ giống asm của chúng tôi là ngăn xếp của asm hoạt động trên byte thay vì int. Vì vậy, để dành không gian cho một int, SP sẽ giảm đi 4 byte.
Ở đây chúng ta bắt đầu ( đoạn mã # 11.1 , số dòng trong nhận xét là từ ý chính ):

;   114: int myAlgo_withCalls(int a, int b) {
 push        ebp        ; create stack frame 
 mov         ebp,esp  
; return address at (ebp + 4), `a` at (ebp + 8), `b` at (ebp + 12)
 
 sub         esp,0D8h   ; reserve space for locals. Compiler can reserve more bytes then needed. 0D8h is hexadecimal == 216 decimal 
 
 push        ebx        ; cdecl requires to save all these registers
 push        esi  
 push        edi  
 
 ; fill all the space for local variables (from (ebp-0D8h) to (ebp)) with value 0CCCCCCCCh repeated 36h times (36h * 4 == 0D8h)
 ; see https://stackoverflow.com/q/3818856/264047
 ; I guess that's for ease of debugging, so that stack is filled with recognizable values
 ; 0CCCCCCCCh in binary is 110011001100...
 lea         edi,[ebp-0D8h]     
 mov         ecx,36h    
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
 
;   115:    int t1 = triple(a);
 mov         eax,dword ptr [ebp+8]   ; push parameter `a` on the stack
 push        eax  
 
 call        triple (01A13E8h)  
 add         esp,4                   ; clean up param 
 mov         dword ptr [ebp-8],eax   ; copy result from eax to `t1`
 
;   116:    int t2 = triple(b);
 mov         eax,dword ptr [ebp+0Ch] ; push `b` (0Ch == 12)
 push        eax  
 
 call        triple (01A13E8h)  
 add         esp,4  
 mov         dword ptr [ebp-14h],eax ; t2 = eax
 
 mov         eax,dword ptr [ebp-8]   ; calculate and store result in eax
 sub         eax,dword ptr [ebp-14h]  

 pop         edi  ; restore registers
 pop         esi  
 pop         ebx  
 
 add         esp,0D8h  ; check we didn't mess up esp or ebp. this is only for debug builds
 cmp         ebp,esp  
 call        __RTC_CheckEsp (01A116Dh)  
 
 mov         esp,ebp  ; destroy frame
 pop         ebp  
 ret  

Và asm cho tripple( đoạn mã # 11.2 ):

 push        ebp  
 mov         ebp,esp  
 sub         esp,0CCh  
 push        ebx  
 push        esi  
 push        edi  
 lea         edi,[ebp-0CCh]  
 mov         ecx,33h  
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
 imul        eax,dword ptr [ebp+8],3  
 mov         dword ptr [ebp-8],eax  
 mov         eax,dword ptr [ebp-8]  
 pop         edi  
 pop         esi  
 pop         ebx  
 mov         esp,ebp  
 pop         ebp  
 ret  

Hy vọng, sau khi đọc bài đăng này, lắp ráp không còn khó hiểu như trước nữa :)


Dưới đây là các liên kết từ nội dung bài đăng và một số bài đọc thêm:


Đã lâu lắm rồi tôi mới hỏi điều này, đó là một câu trả lời thực sự tuyệt vời về chiều sâu. Cảm ơn.
bplus

Tại sao bạn lại sử dụng tên 16 bit cho các thanh ghi trong phần đầu của câu trả lời? Nếu bạn đang nói về mã 16 bit thực tế, [SP]thì chế độ 16 bit địa chỉ không hợp lệ. Có lẽ là tốt nhất để sử dụng ESP. Ngoài ra, nếu bạn khai báo SPlà một int, bạn nên sửa đổi nó bằng 4 cho mọi phần tử, không phải 1. (Nếu bạn đã khai báo long *SP, thì SP += 2sẽ tăng dần 2 * sizeof(int)và do đó loại bỏ 2 phần tử. Nhưng với intSP, điều đó phải SP += 8như thế add esp, 8. -bit asm.
Peter Cordes

Hấp dẫn! Tôi nghĩ thật thú vị khi bạn cố gắng giải thích lắp ráp bằng C. Tôi chưa từng thấy điều đó trước đây. Khéo léo. Tôi có thể đề xuất đổi tên "Không có biến cục bộ" thành "Cách biến cục bộ hoạt động" hoặc chỉ "Biến cục bộ".
Dave Dopson

@PeterCordes lý do cho tên 16 bit (SP, BP) là sự rõ ràng - SP dễ dàng dịch thành "con trỏ ngăn xếp". Nếu tôi sử dụng các tên 32-bit thích hợp, tôi sẽ cần giải thích sự khác biệt giữa các chế độ 16/32/64 bit hoặc không giải thích được. Ý định của tôi là ai đó chỉ biết Java hoặc Python có thể theo dõi bài viết mà không cần vò đầu bứt tai. Và tôi nghĩ việc giải quyết bộ nhớ sẽ chỉ làm người đọc phân tâm. Thêm vào đó, tôi đã đặt liên kết wikibook về chủ đề cho những người tò mò và đã nói vài lời về ESP ở cuối bài đăng.
Alexander Malakhov

1
Để tránh điều đó, chúng ta cần một chỉ số neo, chỉ số này không thay đổi khi ngăn xếp phát triển. Cần là từ sai; -fomit-frame-pointerđã được mặc định trong gcc và clang trong nhiều năm. Những người nhìn vào asm thực cần biết rằng EBP / RBP thường không được sử dụng như một con trỏ khung. Tôi muốn nói "theo truyền thống, con người muốn có một mỏ neo không thay đổi với push / pop, nhưng các trình biên dịch có thể theo dõi các hiệu số thay đổi." Sau đó, bạn có thể cập nhật phần về dấu vết để nói rằng đó là phương pháp kế thừa, không được sử dụng theo mặc định khi .eh_framesiêu dữ liệu DWARF hoặc siêu dữ liệu Windows x86-64 khả dụng.
Peter Cordes

7

Về việc liệu ngăn xếp có được triển khai trong phần cứng hay không, bài viết Wikipedia này có thể hữu ích.

Một số họ bộ xử lý, chẳng hạn như x86, có hướng dẫn đặc biệt để thao tác với ngăn xếp của luồng hiện đang thực thi. Các họ bộ xử lý khác, bao gồm PowerPC và MIPS, không có hỗ trợ ngăn xếp rõ ràng, mà thay vào đó dựa vào quy ước và ủy quyền quản lý ngăn xếp cho Giao diện nhị phân ứng dụng (ABI) của hệ điều hành.

Bài báo đó và những bài viết khác mà nó liên kết có thể hữu ích để có được cảm nhận về việc sử dụng ngăn xếp trong bộ xử lý.


4

Khái niệm

Đầu tiên, hãy nghĩ về toàn bộ sự việc như thể bạn là người đã phát minh ra nó. Như thế này:

Đầu tiên hãy nghĩ về một mảng và cách nó được thực thi ở mức thấp -> về cơ bản nó chỉ là một tập hợp các vị trí bộ nhớ liền kề (các vị trí bộ nhớ nằm cạnh nhau). Bây giờ bạn có hình ảnh tinh thần đó trong đầu, hãy nghĩ đến thực tế là bạn có thể truy cập BẤT KỲ vị trí bộ nhớ nào và xóa nó theo ý muốn của bạn khi bạn xóa hoặc thêm dữ liệu trong mảng của mình. Bây giờ hãy nghĩ về cùng một mảng đó nhưng thay vì khả năng xóa bất kỳ vị trí nào, bạn quyết định rằng bạn sẽ chỉ xóa vị trí CUỐI CÙNG khi bạn xóa hoặc thêm dữ liệu trong mảng của mình. Bây giờ ý tưởng mới của bạn để thao tác dữ liệu trong mảng đó theo cách đó được gọi là LIFO có nghĩa là Lần xuất trước. Ý tưởng của bạn rất hay vì nó giúp bạn dễ dàng theo dõi nội dung của mảng đó hơn mà không cần phải sử dụng thuật toán sắp xếp mỗi khi bạn xóa nội dung nào đó khỏi mảng đó. Cũng thế, để luôn biết địa chỉ của đối tượng cuối cùng trong mảng là gì, bạn dành một Đăng ký trong Cpu để theo dõi nó. Bây giờ, cách mà thanh ghi theo dõi nó là mỗi khi bạn xóa hoặc thêm thứ gì đó vào mảng của mình, bạn cũng giảm hoặc tăng giá trị của địa chỉ trong thanh ghi của mình bằng số lượng đối tượng bạn đã xóa hoặc thêm khỏi mảng (bằng cách số lượng không gian địa chỉ mà họ đã chiếm). Bạn cũng muốn đảm bảo rằng số lượng mà bạn giảm hoặc tăng thanh ghi đó được cố định thành một số lượng (như 4 vị trí bộ nhớ tức là 4 byte) cho mỗi đối tượng, để giúp dễ dàng theo dõi hơn và cũng để làm cho nó có thể để sử dụng thanh ghi đó với một số cấu trúc vòng lặp vì các vòng lặp sử dụng số gia tăng cố định cho mỗi lần lặp (ví dụ:. để lặp lại mảng của bạn bằng một vòng lặp, bạn xây dựng vòng lặp để tăng thanh ghi của mình lên 4 mỗi lần lặp, điều này sẽ không thể thực hiện được nếu mảng của bạn có các đối tượng có kích thước khác nhau trong đó). Cuối cùng, bạn chọn gọi cấu trúc dữ liệu mới này là "Ngăn xếp", bởi vì nó gợi cho bạn về một chồng đĩa trong nhà hàng nơi họ luôn loại bỏ hoặc thêm đĩa trên đầu ngăn xếp đó.

Việc thực hiện

Như bạn có thể thấy, một ngăn xếp không hơn gì một mảng các vị trí bộ nhớ liền kề nơi bạn quyết định cách thao tác với nó. Do đó, bạn có thể thấy rằng bạn thậm chí không cần sử dụng các lệnh và thanh ghi đặc biệt để điều khiển ngăn xếp. Bạn có thể tự mình thực hiện nó với các hướng dẫn mov, thêm và phụ cơ bản và sử dụng các thanh ghi mục đích chung thay vì ESP và EBP như sau:

mov edx, 0FFFFFFFFh

; -> đây sẽ là địa chỉ bắt đầu của ngăn xếp, cách xa nhất với mã và dữ liệu của bạn, nó cũng sẽ đóng vai trò là thanh ghi theo dõi đối tượng cuối cùng trong ngăn xếp mà tôi đã giải thích trước đó. Bạn gọi nó là "con trỏ ngăn xếp", vì vậy bạn chọn thanh ghi EDX làm ESP thường được sử dụng.

phụ edx, 4

mov [edx], dword ptr [someVar]

; -> hai hướng dẫn này sẽ giảm con trỏ ngăn xếp của bạn đi 4 vị trí bộ nhớ và sao chép 4 byte bắt đầu từ vị trí bộ nhớ [someVar] vào vị trí bộ nhớ mà EDX hiện trỏ đến, giống như lệnh PUSH giảm ESP, chỉ ở đây bạn đã làm nó theo cách thủ công và bạn đã sử dụng EDX. Vì vậy, lệnh PUSH về cơ bản chỉ là một mã opcode ngắn hơn thực sự thực hiện điều này với ESP.

mov eax, dword ptr [edx]

thêm edx, 4

; -> và ở đây chúng ta làm ngược lại, đầu tiên chúng ta sao chép 4 byte bắt đầu từ vị trí bộ nhớ mà EDX bây giờ trỏ đến vào thanh ghi EAX (được chọn tùy ý ở đây, chúng ta có thể sao chép nó ở bất cứ đâu chúng ta muốn). Và sau đó chúng tôi tăng con trỏ ngăn xếp EDX của chúng tôi lên 4 vị trí bộ nhớ. Đây là những gì lệnh POP thực hiện.

Bây giờ bạn có thể thấy rằng các lệnh PUSH và POP và thanh ghi ESP ans EBP vừa được Intel thêm vào để làm cho khái niệm ở trên về cấu trúc dữ liệu "ngăn xếp" dễ viết và đọc hơn. Vẫn còn một số Cpu-s RISC (Bộ lệnh rút gọn) không có lệnh PUSH ans POP và thanh ghi chuyên dụng cho thao tác ngăn xếp, và trong khi viết chương trình hợp ngữ cho các Cpu đó, bạn phải tự triển khai ngăn xếp giống như Tôi chỉ cho bạn.


3

Bạn nhầm lẫn giữa ngăn xếp trừu tượng và ngăn xếp được triển khai phần cứng. Sau này đã được thực hiện.


3

Tôi nghĩ rằng câu trả lời chính mà bạn đang tìm kiếm đã được gợi ý.

Khi máy tính x86 khởi động, ngăn xếp chưa được thiết lập. Lập trình viên phải thiết lập nó một cách rõ ràng tại thời điểm khởi động. Tuy nhiên, nếu bạn đang sử dụng một hệ điều hành, điều này đã được giải quyết. Dưới đây là một mẫu mã từ một chương trình bootstrap đơn giản.

Đầu tiên, dữ liệu và các thanh ghi phân đoạn ngăn xếp được đặt, sau đó con trỏ ngăn xếp được đặt 0x4000 sau đó.


    movw    $BOOT_SEGMENT, %ax
    movw    %ax, %ds
    movw    %ax, %ss
    movw    $0x4000, %ax
    movw    %ax, %sp

Sau mã này, ngăn xếp có thể được sử dụng. Bây giờ tôi chắc chắn nó có thể được thực hiện theo một số cách khác nhau, nhưng tôi nghĩ điều này sẽ minh họa cho ý tưởng.


2

Ngăn xếp chỉ là một cách mà các chương trình và chức năng sử dụng bộ nhớ.

Ngăn xếp luôn làm tôi bối rối, vì vậy tôi đã làm một minh họa:

Chồng giống như thạch nhũ

( bản svg tại đây )


1

Ngăn xếp đã tồn tại, vì vậy bạn có thể giả định điều đó khi viết mã của mình. Ngăn xếp chứa các địa chỉ trả về của các hàm, các biến cục bộ và các biến được chuyển giữa các hàm. Ngoài ra còn có các thanh ghi ngăn xếp như BP, SP (Stack Pointer) được tích hợp sẵn mà bạn có thể sử dụng, do đó các lệnh tích hợp mà bạn đã đề cập. Nếu ngăn xếp chưa được triển khai, các chức năng không thể chạy và luồng mã không thể hoạt động.


1

Ngăn xếp được "thực thi" bằng con trỏ ngăn xếp, con trỏ này (giả sử kiến ​​trúc x86 ở đây) trỏ vào phân đoạn ngăn xếp . Mỗi khi một thứ gì đó được đẩy lên ngăn xếp (bằng pushl, lệnh gọi hoặc một opcode ngăn xếp tương tự), nó sẽ được ghi vào địa chỉ mà con trỏ ngăn xếp trỏ đến và con trỏ ngăn xếp giảm dần (ngăn xếp đang phát triển xuống dưới , tức là các địa chỉ nhỏ hơn) . Khi bạn bật thứ gì đó ra khỏi ngăn xếp (popl, ret), con trỏ ngăn xếp được tăng lên và giá trị được đọc khỏi ngăn xếp.

Trong ứng dụng không gian người dùng, ngăn xếp đã được thiết lập cho bạn khi ứng dụng của bạn khởi động. Trong môi trường không gian nhân, trước tiên bạn phải thiết lập phân đoạn ngăn xếp và con trỏ ngăn xếp ...


1

Tôi chưa nhìn thấy cụ thể trình lắp ráp Gas, nhưng nói chung ngăn xếp được "triển khai" bằng cách duy trì một tham chiếu đến vị trí trong bộ nhớ nơi ở trên cùng của ngăn xếp. Vị trí bộ nhớ được lưu trong một thanh ghi, có các tên khác nhau cho các kiến ​​trúc khác nhau, nhưng có thể được coi là thanh ghi con trỏ ngăn xếp.

Các lệnh pop và push được thực hiện trong hầu hết các kiến ​​trúc cho bạn bằng cách xây dựng dựa trên các hướng dẫn vi mô. Tuy nhiên, một số "Kiến trúc giáo dục" yêu cầu bạn tự thực hiện chúng. Về mặt chức năng, push sẽ được thực hiện giống như sau:

   load the address in the stack pointer register to a gen. purpose register x
   store data y at the location x
   increment stack pointer register by size of y

Ngoài ra, một số kiến ​​trúc lưu trữ địa chỉ bộ nhớ được sử dụng cuối cùng dưới dạng Con trỏ ngăn xếp. Một số lưu trữ địa chỉ có sẵn tiếp theo.


1

Stack là gì? Ngăn xếp là một kiểu cấu trúc dữ liệu - một phương tiện lưu trữ thông tin trong máy tính. Khi một đối tượng mới được nhập vào một ngăn xếp, nó sẽ được đặt trên tất cả các đối tượng đã nhập trước đó. Nói cách khác, cấu trúc dữ liệu ngăn xếp giống như một chồng thẻ, giấy tờ, thư tín dụng hoặc bất kỳ đối tượng nào khác trong thế giới thực mà bạn có thể nghĩ đến. Khi xóa một đối tượng khỏi ngăn xếp, đối tượng trên cùng sẽ bị xóa trước. Phương pháp này được gọi là LIFO (nhập sau cùng, xuất trước).

Thuật ngữ "ngăn xếp" cũng có thể là viết tắt của ngăn xếp giao thức mạng. Trong mạng, kết nối giữa các máy tính được thực hiện thông qua một loạt các kết nối nhỏ hơn. Các kết nối hoặc lớp này hoạt động giống như cấu trúc dữ liệu ngăn xếp, ở chỗ chúng được xây dựng và xử lý theo cùng một cách.


0

Bạn đúng rằng ngăn xếp là một cấu trúc dữ liệu. Thông thường, các cấu trúc dữ liệu (bao gồm cả ngăn xếp) mà bạn làm việc là trừu tượng và tồn tại như một biểu diễn trong bộ nhớ.

Ngăn xếp bạn đang làm việc trong trường hợp này có sự tồn tại vật chất hơn - nó ánh xạ trực tiếp tới các thanh ghi vật lý thực trong bộ xử lý. Là một cấu trúc dữ liệu, ngăn xếp là cấu trúc FILO (vào trước, ra sau cùng) đảm bảo dữ liệu được xóa theo thứ tự ngược lại mà nó được nhập. Hãy xem logo StackOverflow để có hình ảnh! ;)

Bạn đang làm việc với ngăn xếp hướng dẫn . Đây là chồng các hướng dẫn thực tế mà bạn đang cung cấp cho bộ xử lý.


Sai lầm. đây không phải là 'ngăn xếp hướng dẫn' (có điều gì như vậy không?), đây chỉ đơn giản là một bộ nhớ được truy cập thông qua thanh ghi Ngăn xếp. được sử dụng để lưu trữ tạm thời, tham số thủ tục và địa chỉ trả về (quan trọng nhất) cho các lệnh gọi hàm
Javier

0

Ngăn xếp cuộc gọi được thực hiện bởi tập lệnh x86 và hệ điều hành.

Các hướng dẫn như push và pop điều chỉnh con trỏ ngăn xếp trong khi hệ điều hành xử lý phân bổ bộ nhớ khi ngăn xếp phát triển cho mỗi luồng.

Thực tế là ngăn xếp x86 "phát triển xuống" từ các địa chỉ cao hơn xuống thấp hơn làm cho kiến ​​trúc này dễ bị tấn công tràn bộ đệm hơn.


1
Tại sao thực tế là ngăn xếp x86 phát triển xuống làm cho nó dễ bị tràn bộ đệm hơn? Bạn không thể nhận được cùng một phần tràn với một phân đoạn mở rộng?
Nathan Fellman 17/02/09

@nathan: chỉ khi bạn có thể yêu cầu ứng dụng phân bổ lượng bộ nhớ âm trên ngăn xếp.
Javier

1
Các cuộc tấn công tràn bộ đệm ghi qua phần cuối của một mảng dựa trên ngăn xếp - char userName [256], điều này sẽ ghi bộ nhớ từ thấp hơn lên cao hơn cho phép bạn ghi đè lên những thứ như địa chỉ trả về. Nếu ngăn xếp tăng theo cùng một hướng, bạn sẽ chỉ có thể ghi đè ngăn xếp chưa được phân bổ.
Maurice Flanagan 17/02/09

0

Bạn đúng rằng một ngăn xếp 'chỉ' là một cấu trúc dữ liệu. Tuy nhiên, ở đây, nó đề cập đến một ngăn xếp được triển khai phần cứng được sử dụng cho một mục đích đặc biệt - "Ngăn xếp".

Nhiều người đã nhận xét về việc thực thi ngăn xếp phần cứng so với cấu trúc dữ liệu ngăn xếp (phần mềm). Tôi muốn nói thêm rằng có ba kiểu cấu trúc ngăn xếp chính:

  1. Ngăn xếp cuộc gọi - Đó là ngăn xếp cuộc gọi bạn đang hỏi! Nó lưu trữ các tham số chức năng và địa chỉ trả về, v.v. Hãy đọc Chương 4 (Tất cả về trang thứ 4 tức là trang 53) các chức năng trong cuốn sách đó. Có một lời giải thích tốt.
  2. Một ngăn xếp chung chung mà bạn có thể sử dụng trong chương trình của mình để làm điều gì đó đặc biệt ...
  3. Một ngăn xếp phần cứng chung
    Tôi không chắc chắn về điều này, nhưng tôi nhớ đã đọc ở đâu đó rằng có một ngăn xếp phần cứng được triển khai cho mục đích chung có sẵn trong một số kiến ​​trúc. Nếu bất cứ ai biết liệu điều này là chính xác, xin vui lòng bình luận.

Điều đầu tiên cần biết là kiến ​​trúc mà bạn đang lập trình, cuốn sách giải thích (tôi vừa mới tra cứu - liên kết). Để thực sự hiểu mọi thứ, tôi khuyên bạn nên tìm hiểu về bộ nhớ, địa chỉ, thanh ghi và kiến ​​trúc của x86 (tôi cho rằng đó là những gì bạn đang học - từ cuốn sách).


0

Gọi các chức năng, yêu cầu lưu và khôi phục trạng thái cục bộ theo kiểu LIFO (trái ngược với cách tiếp cận theo quy trình chung tổng quát), hóa ra là một nhu cầu cực kỳ phổ biến đến mức các ngôn ngữ hợp ngữ và kiến ​​trúc CPU về cơ bản xây dựng chức năng này. có lẽ có thể được nói đến các khái niệm về phân luồng, bảo vệ bộ nhớ, mức độ bảo mật, v.v. Về lý thuyết, bạn có thể triển khai ngăn xếp, quy ước gọi, v.v. của riêng mình, nhưng tôi giả sử một số mã opcodes và hầu hết các thời gian chạy hiện có dựa trên khái niệm gốc này về "ngăn xếp" .


0

stacklà một phần của bộ nhớ. nó sử dụng cho inputoutputcủa functions. nó cũng sử dụng để ghi nhớ trả về của hàm.

esp đăng ký là ghi nhớ địa chỉ ngăn xếp.

stackesp được thực hiện bằng phần cứng. bạn cũng có thể tự mình thực hiện. nó sẽ làm cho chương trình của bạn rất chậm.

thí dụ:

nop // esp = 0012ffc4

đẩy 0 //esp = 0012ffc0, Dword [0012ffc0] = 00000000

gọi proc01 // esp= 0012ffbc, Dword [0012ffbc] = eip,eip = adrr [proc01]

pop eax// eax= Dword [ esp], esp= esp+ 4


0

Tôi đã tìm kiếm về cách ngăn xếp hoạt động về mặt chức năng và tôi đã tìm thấy blog này thật tuyệt vời và giải thích khái niệm ngăn xếp từ đầu và cách ngăn xếp lưu trữ giá trị trong ngăn xếp.

Bây giờ về câu trả lời của bạn. Tôi sẽ giải thích bằng python nhưng bạn sẽ hiểu rõ cách hoạt động của ngăn xếp trong bất kỳ ngôn ngữ nào.

nhập mô tả hình ảnh ở đây

Nó là một chương trình:

def hello(x):
    if x==1:
        return "op"
    else:
        u=1
        e=12
        s=hello(x-1)
        e+=1
        print(s)
        print(x)
        u+=1
    return e

hello(3)

nhập mô tả hình ảnh ở đây

nhập mô tả hình ảnh ở đây

Nguồn: Cryptroix

một số chủ đề của nó mà nó đề cập trong blog:

How Function work ?
Calling a Function
 Functions In a Stack
 What is Return Address
 Stack
Stack Frame
Call Stack
Frame Pointer (FP) or Base Pointer (BP)
Stack Pointer (SP)
Allocation stack and deallocation of stack
StackoverFlow
What is Heap?

Nhưng giải thích của nó bằng ngôn ngữ python nên nếu muốn bạn có thể xem qua.


Trang web Criptoix đã chết và không có bản sao trên web.archive.org
Alexander Malakhov

1
@AlexanderMalakhov Cryptroix không hoạt động do sự cố lưu trữ. Cryptroix hiện đã hoạt động và đang hoạt động.
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.