91

我目前正在尝试了解堆栈是如何工作的,所以我决定自学一些汇编语言,我正在使用这本书:

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

我正在使用Gas并在Linux Mint上进行开发。

我有点困惑:

据我所知,堆栈只是一种数据结构。所以我假设如果我在汇编中编码,我必须自己实现堆栈。然而,情况似乎并非如此,因为有类似的命令

pushl
popl

因此,在为x86架构编写汇编代码并使用 Gas 语法时:堆栈只是已经实现的数据结构吗?或者它实际上是在硬件级别实现的?或者是别的什么?其他芯片组的大多数汇编语言也是否已经实现了堆栈?

我知道这是一个有点愚蠢的问题,但我实际上对此感到很困惑。

4

17 回答 17

90

我认为主要是您在 aprogram's stack和之间感到困惑any old stack

堆栈

是一种抽象数据结构,由后进先出系统中的信息组成。您将任意对象放在堆栈上,然后再次取下它们,就像进/出托盘一样,最上面的物品总是被取下的物品,而您总是放在最上面。

程序堆栈

是一个堆栈,它是在执行期间使用的一段内存,它通常具有每个程序的静态大小,并且经常用于存储函数参数。当您调用函数时,您将参数压入堆栈,并且该函数直接寻址堆栈或从堆栈中弹出变量。

程序堆栈通常不是硬件(尽管它保存在内存中,因此可以这样说),但指向堆栈当前区域的堆栈指针通常是 CPU 寄存器。这使它比后进先出堆栈更灵活,因为您可以更改堆栈寻址的点。

您应该阅读并确保您理解维基百科文章,因为它很好地描述了您正在处理的硬件堆栈。

还有本教程根据旧的 16 位寄存器解释了堆栈,但可能会有所帮助,另一个专门关于堆栈的教程。

来自尼尔斯·派彭布林克:

值得注意的是,一些处理器没有实现访问和操作堆栈的所有指令(推送、弹出、堆栈指针等),但 x86 却因为它的使用频率而做到了。在这些情况下,如果您想要一个堆栈,您必须自己实现它(一些 MIPS 和一些 ARM 处理器是在没有堆栈的情况下创建的)。

例如,在 MIP 中,推送指令的实现方式如下:

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

Pop 指令如下所示:

lw   $t0, ($sp)   # Copy from stack to $t0  
addi $sp, $sp, 4   # Increment stack pointer by 4  
于 2009-02-17T13:24:28.540 回答
58

(我已经对这个答案中的所有代码做了一个要点,以防你想玩它)

在 2003 年的 CS101 课程中,我只在 asm 中做过最基本的事情。而且我从来没有真正“了解”asm 和堆栈的工作原理,直到我意识到这一切基本上就像用 C 或 C++ 编程......但没有局部变量、参数和函数。可能听起来还不容易 :) 让我向您展示(对于带有Intel 语法的 x86 asm )。


1.什么是栈

堆栈通常是在每个线程启动之前为其分配的连续内存块。你可以在那里存储任何你想要的东西。用 C++ 术语(代码片段 #1):

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

2.堆栈的顶部和底部

stack原则上,您可以将值存储在数组的随机单元格中(片段 #2.1):

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

stack但是想象一下,记住哪些单元已经在使用以及哪些单元是“免费的”将是多么困难。这就是为什么我们将新值存储在彼此相邻的堆栈上。

关于 (x86) asm 的堆栈的一个奇怪的事情是,您从最后一个索引开始添加东西并移动到较低的索引:堆栈 [999],然后是堆栈 [998] 等等(片段 #2.2):

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

并且(小心,你现在会感到困惑)“官方”名称stack[999]仍然是bottom of the stack
最后使用的单元格(stack[997]在上面的示例中)称为堆栈顶部(请参阅堆栈顶部在 x86 上的位置)。


3. 堆栈指针(SP)

出于讨论的目的,我们假设 CPU 寄存器表示为全局变量(请参阅通用寄存器)。

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

有一个特殊的 CPU 寄存器 (SP) 跟踪堆栈的顶部。SP 是一个指针(保存一个像 0xAAAABBCC 这样的内存地址)。但出于本文的目的,我将使用它作为数组索引 (0, 1, 2, ...)。

当一个线程启动时,SP == STACK_CAPACITY程序和操作系统会根据需要对其进行修改。规则是您不能写入超出堆栈顶部的堆栈单元,并且任何小于 SP 的索引都是无效且不安全的(因为系统中断),因此您 首先减少 SP,然后将值写入新分配的单元。

当您想将多个值连续推送到堆栈中时,您可以预先为所有这些值保留空间(片段 #3):

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

笔记。现在您可以看到为什么堆栈上的分配如此之快 - 它只是单个寄存器递减。


4.局部变量

让我们看一下这个简单的函数(片段 #4.1):

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

并在不使用局部变量的情况下重写它(片段 #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];
}

看看它是如何被调用的(片段#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.推/弹出

在栈顶添加一个新元素是一个非常频繁的操作,CPU 有一个特殊的指令,push. 我们将像这样实现它(片段 5.1):

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

同样,取堆栈的顶部元素(片段 5.2):

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

push/pop 的常见使用模式是暂时节省一些价值。比如说,我们在变量中有一些有用的东西myVar,出于某种原因,我们需要进行覆盖它的计算(片段 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.功能参数

现在让我们使用堆栈(片段#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声明

让我们在 AX 寄存器中返回值(片段 #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.栈基指针(BP)(也称帧指针)和栈帧

让我们使用更多“高级”函数并用我们的 asm-like C++ 重写它(片段 #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);
}

现在想象一下,我们决定在返回之前引入新的局部变量来存储结果,就像我们在tripple(片段#4.1)中所做的那样。函数的主体将是(片段 #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;

你看,我们必须更新对函数参数和局部变量的每一个引用。为了避免这种情况,我们需要一个锚索引,它不会随着堆栈的增长而改变。

我们将通过将当前顶部(SP 的值)保存到 BP 寄存器中来在函数进入时(在我们为局部变量分配空间之前)创建锚点。片段#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
}

属于并完全控制函数的堆栈切片称为函数的堆栈帧。例如myAlgo_noLPR_withAnchor的堆栈帧是stack[996 .. 994](包括两个 idex)。
帧从函数的 BP 开始(在我们在函数内部更新它之后)并持续到下一个堆栈帧。所以栈上的参数是调用者栈帧的一部分(见注8a)。

注释:
8a。 维基百科对参数另有说明,但在这里我坚持英特尔软件开发人员手册,请参阅第 1 卷。1,第6.2.4.1 节 Stack-Frame Base Pointer和第6.3.2 节 Far CALL 和 RET 操作中的图 6-2 。函数的参数和堆栈帧是函数激活记录的一部分(请参阅函数 perilogues 的 gen)。
8b。BP 的正偏移指向函数参数,负偏移指向局部变量。
这对于调试8c 非常方便。stack[BP]存储前一个堆栈帧的地址,stack[stack[BP]]存储前一个堆栈帧等。沿着这条链,你可以发现程序中所有函数的帧,这些帧还没有返回。这就是调试器显示您调用堆栈
8d 的方式。的前 3 条指令myAlgo_noLPR_withAnchor,我们设置框架(保存旧 BP,更新 BP,为本地人保留空间)称为函数序言


9. 调用约定

在代码片段 8.1 中,我们将参数myAlgo从右向左推送,并在AX. 我们也可以从左到右传递参数并返回BX。或者在 BX 和 CX 中传递参数并在 AX 中返回。显然,调用者(main())和被调用函数必须同意所有这些东西的存储位置和顺序。

调用约定是一组关于如何传递参数和返回结果的规则。

在上面的代码中,我们使用了cdecl 调用约定

  • 参数在堆栈上传递,第一个参数在调用时位于堆栈的最低地址(最后推送 <...>)。调用者负责在调用后将参数从堆栈中弹出。
  • 返回值放在 AX
  • EBP 和 ESP 必须由被调用者(myAlgo_noLPR_withAnchor在我们的例子中是函数)保存,以便调用者(main函数)可以依赖那些未被调用更改的寄存器。
  • 所有其他寄存器(EAX,<...>)都可以由被调用者自由修改;如果调用者希望在函数调用之前和之后保存一个值,它必须将这个值保存在其他地方(我们使用 AX 执行此操作)

(来源:来自 Stack Overflow 文档的示例“32 位 cdecl”;icktoofayPeter Cordes拥有 2016 年版权;根据 CC BY-SA 3.0 许可。可在 archive.org 上找到完整的 Stack Overflow 文档内容的存档,其中此示例由主题 ID 3261 和示例 ID 11196 索引。)


10.函数调用

现在是最有趣的部分。就像数据一样,可执行代码也存储在内存中(与堆栈的内存完全无关),每条指令都有一个地址。
如果没有其他命令,CPU 会按照指令存储在内存中的顺序一个接一个地执行指令。但是我们可以命令 CPU “跳转”到内存中的另一个位置并从那里执行指令。在 asm 中,它可以是任何地址,而在 C++ 等更高级的语言中,您只能跳转到由标签标记的地址(有一些变通方法,但至少可以说它们并不漂亮)。

让我们使用这个函数(片段 #10.1):

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

而不是调用trippleC++ 方式,请执行以下操作:

  1. 将代码复制tripplemyAlgo正文的开头
  2. myAlgo入口处跳过tripple的代码goto
  3. 当我们需要执行tripple的代码时,将调用后的代码行的堆栈地址保存起来tripple,以便稍后返回这里继续执行(PUSH_ADDRESS下面的宏)
  4. 跳转到第一行的地址(tripple函数)并执行到最后(3.和4.一起是CALL宏)
  5. 在结束时tripple(在我们清理了局部变量之后),从堆栈顶部获取返回地址并跳转到那里(RET宏)

因为在 C++ 中没有简单的方法可以跳转到特定的代码地址,所以我们将使用标签来标记跳转的位置。我不会详细介绍下面的宏是如何工作的,相信我他们会按照我说的去做(片段 #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);
}

注释:
10a。因为返回地址是存储在栈上的,原则上我们可以改变它。这就是堆栈粉碎攻击的工作原理
10b。triple_label(cleanup locals, restore old BP, return) “结束”的最后 3 条指令称为函数的结尾


11. 组装

现在让我们看看真正的 asm for myAlgo_withCalls. 在 Visual Studio 中执行此操作:

  • 将构建平台设置为 x86(不是x86_64)
  • 构建类型:调试
  • 在 myAlgo_withCalls 中的某处设置断点
  • 运行,当执行在断点处停止时按Ctrl+ Alt+D

与我们类似 asm 的 C++ 的一个区别是 asm 的堆栈对字节而不是整数进行操作。所以要为 1 保留空间int,SP 将减少 4 个字节。
我们开始(片段#11.1,注释中的行号来自要点):

;   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  

和 asm for tripple(片段#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  

希望,在阅读了这篇文章之后,汇编看起来不像以前那么神秘了 :)


以下是帖子正文的链接和一些进一步阅读:

于 2017-02-09T20:03:30.677 回答
7

关于堆栈是否在硬件中实现,这篇维基百科文章可能会有所帮助。

某些处理器系列,例如 x86,具有用于操作当前执行线程的堆栈的特殊指令。其他处理器系列,包括 PowerPC 和 MIPS,没有明确的堆栈支持,而是依赖约定并将堆栈管理委托给操作系统的应用程序二进制接口 (ABI)。

那篇文章和它链接的其他文章可能有助于了解处理器中的堆栈使用情况。

于 2009-02-17T13:22:25.690 回答
4

您混淆了抽象堆栈和硬件实现的堆栈。后者已经实施。

于 2009-02-17T13:19:02.483 回答
4

这个概念

首先把整个事情想象成你是发明它的人。像这样:

首先考虑一个数组以及它是如何在底层实现的——>它基本上只是一组连续的内存位置(彼此相邻的内存位置)。现在您的脑海中已经有了那个图像,请考虑这样一个事实,即您可以访问任何这些内存位置并在您删除或添加阵列中的数据时随意删除它。现在考虑同一个数组,但不是删除任何位置,而是决定在删除或添加数组中的数据时只删除最后一个位置。现在,您以这种方式操作该数组中的数据的新想法称为 LIFO,这意味着后进先出。您的想法非常好,因为它可以更轻松地跟踪该数组的内容,而不必在每次从中删除某些内容时都使用排序算法。还,要始终知道数组中最后一个对象的地址是什么,您可以在 Cpu 中专用一个寄存器来跟踪它。现在,寄存器跟踪它的方式是,每次你删除或添加一些东西到你的数组时,你也会根据你从数组中删除或添加的对象的数量来减少或增加寄存器中地址的值(通过他们占用的地址空间量)。您还想确保您减少或增加该寄存器的数量固定为每个对象一个数量(如 4 个内存位置,即 4 个字节),再次,以便更容易跟踪并使其成为可能将该寄存器与某些循环结构一起使用,因为循环每次迭代使用固定增量(例如。用一个循环遍历你的数组,你构造循环以在每次迭代中将你的寄存器增加 4,如果你的数组中有不同大小的对象,这是不可能的)。最后,您选择将此新数据结构称为“堆栈”,因为它使您想起餐厅中的一堆盘子,他们总是在该堆栈顶部删除或添加一个盘子。

实施

如您所见,堆栈只不过是您决定如何操作它的一系列连续内存位置。因此,您可以看到您甚至不需要使用特殊指令和寄存器来控制堆栈。您可以使用基本的 mov、add 和 sub 指令自己实现它,并使用通用寄存器而不是 ESP 和 EBP,如下所示:

移动 edx, 0FFFFFFFFh

; -->这将是您的堆栈的起始地址,离您的代码和数据最远,它还将用作跟踪我之前解释的堆栈中最后一个对象的寄存器。您将其称为“堆栈指针”,因此您选择寄存器 EDX 作为 ESP 通常用于的内容。

子 edx, 4

mov [edx], dword ptr [someVar]

; -->这两条指令将使您的堆栈指针减少 4 个内存位置,并将从 [someVar] 内存位置开始的 4 个字节复制到 EDX 现在指向的内存位置,就像 PUSH 指令减少 ESP 一样,只有在这里你做了它是手动的,你使用了 EDX。所以 PUSH 指令基本上只是一个更短的操作码,它实际上是用 ESP 做的。

mov eax, dword ptr [edx]

添加 edx, 4

; -->在这里我们做相反的事情,我们首先将 EDX 现在指向的内存位置开始的 4 个字节复制到寄存器 EAX 中(在这里任意选择,我们可以将它复制到任何我们想要的地方)。然后我们将堆栈指针 EDX 增加 4 个内存位置。这就是 POP 指令的作用。

现在您可以看到指令 PUSH 和 POP 以及寄存器 ESP 和 EBP 是 Intel 刚刚添加的,以使上述“堆栈”数据结构的概念更易于读写。仍然有一些 RISC(精简指令集)Cpu-s 没有 PUSH ans POP 指令和用于堆栈操作的专用寄存器,并且在为这些 Cpu-s 编写汇编程序时,您必须自己实现堆栈,就像我给你看了。

于 2016-02-05T22:18:13.870 回答
3

我认为您正在寻找的主要答案已经被暗示了。

x86 计算机启动时,未设置堆栈。程序员必须在启动时明确设置它。但是,如果您已经在操作系统中,这已经被处理了。下面是一个简单引导程序的代码示例。

首先设置数据和堆栈段寄存器,然后将堆栈指针设置为0x4000。


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

在此代码之后,可以使用堆栈。现在我确信它可以通过多种不同的方式完成,但我认为这应该说明这个想法。

于 2009-02-17T13:57:36.230 回答
3

堆栈只是程序和函数使用内存的一种方式。

堆栈总是让我感到困惑,所以我做了一个说明:

堆栈就像钟乳石

这里是 svg 版本

  • 推动“将新的钟乳石附着在天花板上”。
  • 流行音乐“从钟乳石中弹出”。

希望它比混乱更有帮助。

随意使用 SVG 图像(CC0 许可)。

于 2013-11-10T12:42:26.097 回答
2

堆栈是通过堆栈指针“实现”的,堆栈指针(此处假设 x86 架构)指向堆栈。每次将东西压入堆栈(通过 pushl、call 或类似的堆栈操作码)时,都会将其写入堆栈指针指向的地址,并且堆栈指针会递减(堆栈向下增长,即较小的地址) . 当你从堆栈中弹出一些东西(popl,ret)时,堆栈指针会递增并且值会从堆栈中读取。

在用户空间应用程序中,当您的应用程序启动时,堆栈已经为您设置好了。在内核空间环境中,您必须首先设置堆栈段和堆栈指针......

于 2009-02-17T13:18:25.963 回答
1

堆栈已经存在,因此您可以在编写代码时假设它。堆栈包含函数的返回地址、局部变量和函数之间传递的变量。您还可以使用内置的堆栈寄存器,例如 BP、SP(堆栈指针),因此您提到了内置命令。如果堆栈还没有实现,函数就不能运行,代码流也不能工作。

于 2009-02-17T13:16:03.457 回答
1

我没有专门看到 Gas 汇编器,但总的来说,堆栈是通过维护对堆栈顶部所在的内存位置的引用来“实现”的。内存位置存储在一个寄存器中,对于不同的架构有不同的名称,但可以认为是堆栈指针寄存器。

pop 和 push 命令是在大多数架构中通过构建在微指令上为您实现的。但是,一些“教育架构”需要您自己实现它们。从功能上讲,push 的实现有点像这样:

   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

此外,一些体系结构将最后使用的内存地址存储为堆栈指针。有些存储下一个可用地址。

于 2009-02-17T13:26:10.203 回答
1

调用栈由 x86 指令集和操作系统实现。

诸如 push 和 pop 之类的指令会调整堆栈指针,而操作系统会在堆栈为每个线程增长时负责分配内存。

x86 堆栈从较高地址到较低地址“向下增长”的事实使得该架构更容易受到缓冲区溢出攻击。

于 2009-02-17T13:43:31.457 回答
0

堆栈是一种数据结构是正确的。通常,您使用的数据结构(包括堆栈)是抽象的,并且作为内存中的表示存在。

在这种情况下,您正在使用的堆栈具有更重要的存在——它直接映射到处理器中的真实物理寄存器。作为一种数据结构,堆栈是 FILO(先进后出)结构,可确保以与输入相反的顺序删除数据。查看 StackOverflow 徽标以获得视觉效果!;)

您正在使用指令堆栈。这是您为处理器提供的实际指令堆栈。

于 2009-02-17T13:23:00.960 回答
0

堆栈“只是”一种数据结构是正确的。然而,在这里,它指的是用于特殊目的的硬件实现的堆栈——“堆栈”。

许多人评论了硬件实现的堆栈与(软件)堆栈数据结构。我想补充一下,有三种主要的堆栈结构类型 -

  1. 调用堆栈——你要问的是哪个!它存储函数参数和返回地址等。请阅读该书中的第 4 章(所有关于第 4 页,即第 53 页)函数。有一个很好的解释。
  2. 一个通用堆栈,您可以在程序中使用它来做一些特别的事情......
  3. 通用硬件堆栈
    我对此不确定,但我记得在某处读过,在某些架构中存在通用硬件实现堆栈。如果有人知道这是否正确,请发表评论。

首先要知道的是您正在为其编程的架构,这本书解释了(我只是查了一下--link)。要真正理解事物,我建议您了解 x86 的内存、寻址、寄存器和体系结构(我假设这就是您正在学习的内容——从书中)。

于 2009-02-17T13:42:04.910 回答
0

调用函数需要以 LIFO 方式保存和恢复本地状态(而不是说,通用的协同程序方法),结果证明是一种非常普遍的需求,以至于汇编语言和 CPU 架构基本上都在其中构建了这个功能。同样可以说是线程、内存保护、安全级别等概念。理论上你可以实现自己的堆栈、调用约定等,但我假设一些操作码和大多数现有运行时都依赖于“堆栈”的本机概念.

于 2011-10-21T04:59:26.227 回答
0

什么是堆栈?堆栈是一种数据结构——一种在计算机中存储信息的方法。当一个新对象进入堆栈时,它被放置在所有先前输入的对象之上。换句话说,堆栈数据结构就像一堆卡片、文件、信用卡邮件或您能想到的任何其他现实世界对象。从堆栈中删除对象时,首先删除顶部的对象。这种方法称为 LIFO(后进先出)。

术语“堆栈”也可以简称为网络协议堆栈。在网络中,计算机之间的连接是通过一系列较小的连接建立的。这些连接或层的作用类似于堆栈数据结构,因为它们以相同的方式构建和处理。

于 2012-12-10T09:16:35.430 回答
0

stack是记忆的一部分。它使用 forinputoutputof functions。它也用于记住函数的返回。

esp寄存器是记住堆栈地址。

stack并由esp硬件实现。你也可以自己实现它。它会使你的程序非常慢。

例子:

nop // esp= 0012ffc4

推 0 // esp= 0012ffc0 ,Dword[0012ffc0]=00000000

调用 proc01 // esp= 0012ffbc ,Dword[0012ffbc] = eip, eip= adrr[proc01]

弹出eax// eax= Dword[ esp], esp= esp+ 4

于 2015-07-21T20:47:57.870 回答
0

我正在搜索堆栈在功能方面的工作原理,我发现这个博客很棒,它从头开始解释堆栈的概念以及堆栈如何在堆栈中存储值。

现在就你的答案。我将用 python 解释,但你会很好地了解堆栈在任何语言中是如何工作的。

在此处输入图像描述

它是一个程序:

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)

在此处输入图像描述

在此处输入图像描述

资料来源:Cryptroix

它在博客中涵盖的一些主题:

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?

但它用python语言解释,所以如果你愿意,你可以看看。

于 2016-10-18T09:57:57.503 回答