堆栈如何以汇编语言工作?


83

我目前正在尝试了解堆栈的工作原理,因此我决定自学一些汇编语言,我正在使用这本书:

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

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

我对某些东西感到困惑:

据我所知,堆栈只是一个数据结构。因此,我假设如果我要在汇编中进行编码,则必须自己实现堆栈。但是,似乎并非如此,因为有些命令像

pushl
popl

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

我知道这是一个愚蠢的问题,但是我对此感到非常困惑。


2
这些答案中的大多数都谈论语言所使用的堆栈,特别是谈论在堆栈上传递参数。在许多CPU中,这仅仅是为了简化语言的实现-如果您是对汇编进行手工编码,则通常会将参数传递给寄存器中的函数(至少在CPU针对堆栈操作进行优化之前,因为语言使用它的程度很高)。堆栈主要用于保持调用/返回的顺序,同时中断(需要保存CPU的状态)会将现有值压入将要使用的寄存器中,并在返回之前将其弹出。
Bill K

Answers:


82

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

一堆

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

程序堆栈

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

程序堆栈通常不是硬件(尽管它一直保存在内存中,因此可以这样争论),但是指向堆栈当前区域的堆栈指针通常是CPU寄存器。这使它比LIFO堆栈更具灵活性,因为您可以更改堆栈要寻址的点。

您应该阅读并确保您了解Wikipedia文章,因为它很好地描述了您要处理的硬件堆栈。

本教程中还用旧的16位寄存器解释了堆栈,但可能会有所帮助,而另外一个则专门介绍了堆栈。

来自Nils Pipenbrinck:

值得注意的是,某些处理器并未实现用于访问和操作堆栈的所有指令(推,弹出,堆栈指针等),但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  

2
顺便说一句-x86具有这些特殊的堆栈指令,因为从堆栈中推送和弹出内容的情况经常发生,因此最好为它们使用短操作码(较少的代码空间)。MIPS和ARM之类的体系结构没有这些,因此您必须自己实现堆栈。
Nils Pipenbrinck,2009年

4
请记住,您的热门新处理器在某种程度上与8086二进制兼容,并且与8080是第一款微处理器8008的开发源兼容。其中一些决定可以追溯很久。
David Thornley,2009年

4
在ARM中,只有一条用于操作堆栈的指令,它们并不是很明显,因为它们被称为STMDB SP!(用于PUSH)和LDMIA SP!(对于POP)。
亚当·古德

1
我的天啊,这个答案需要+500。。。考虑到现在
Gabriel


34

(如果您想使用它,我已经在此答案中列出了所有代码的要点

在2003年的CS101课程中,我只做过关于asm的最基本的事情。直到我意识到这基本上就像用C或C ++编程一样,我才真正真正了解过asm和堆栈的工作原理。但没有局部变量,参数和函数。可能听起来还不容易:)让我告诉你(对于采用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堆栈的一件奇怪的事情是,您从最后一个索引开始添加内容,然后移到较低的索引:stack [999],然后stack [998],依此类推(代码段#2.2):

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

而且(警告,您现在会感到困惑)“官方”名称stack[999]仍然是堆栈的底部
最后使用的单元格(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
}

推送/弹出的常用用法是暂时保存一些价值。说,我们在变量中有一些有用的东西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的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](包括IDexes)。
帧从函数的BP开始(在函数内部更新后),一直持续到下一个堆栈帧。因此,堆栈上的参数是调用方堆栈框架的一部分(请参见注释8a)。

注意:
8a。 维基百科对参数另有说明,但在这里我遵守英特尔软件开发人员手册,请参见第211页。1,第6.2.4.1节“堆栈框架基本指针”和第6.3.2节的“图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下获得许可。完整Stack Overflow文档内容的存档可在archive.org中找到,其中本示例由主题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. tripple代码复制到myAlgo正文的开头
  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(清理本地,恢复旧BP,返回)“结束”处的最后3条指令称为函数的结尾


11.组装

现在,让我们来看一下的实际asm myAlgo_withCalls。为此,请在Visual Studio中:

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

与类似asm的C ++的一个区别是,asm的堆栈操作的是字节而不是ints。因此,为了保留一个空间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  

以及tripple片段#11.2)的asm :

 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  

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


以下是该帖子正文的链接以及更多阅读内容:


很久以前,我问过这个问题,这是一个非常深刻的答案。谢谢。
bplus

为什么在答案的开头部分使用寄存器的16位名称?如果您在谈论实际的16位代码,那[SP]不是有效的16位寻址模式。大概最好用ESP。另外,如果声明SP为an int,则应为每个元素而不是1对其进行4修改(如果声明为long *SP,则将SP += 2由进行递增2 * sizeof(int),从而删除2个元素。但是对于intSP,应该SP += 8add esp, 832中的。位组件
彼得·科德斯,

迷人!我认为您尝试使用C解释汇编很有趣。我之前从未见过。整齐。我可能建议将“无局部变量”重命名为“局部变量的工作方式”,或者仅重命名为“局部变量”。
戴夫·多普森

@PeterCordes使用16位名称(SP,BP)的原因很清楚-SP很容易转换为“堆栈指针”。如果我使用正确的32位名称,则需要解释16/32/64位模式之间的差异,或者无法解释。我的意图是只知道Java或Python的人就可以关注这篇文章,而不会费力。而且我认为内存寻址只会分散读者的注意力。另外,在帖子末尾,我已将wikibook链接放在该主题上,以引起好奇,并就ESP说了几句话。
亚历山大·马拉霍夫

1
为避免这种情况,我们需要一个锚索引,该索引在堆栈增长时不会改变。 需求是错误的词;-fomit-frame-pointer多年来一直是gcc和clang的默认设置。看着真正的asm的人需要知道EBP / RBP通常不会被用作帧指针。我会说“传统上,人们希望锚不会随推/弹出而改变,但编译器可以跟踪偏移的变化。” 然后,您可以更新有关回溯的部分,以说这是旧方法,当DWARF.eh_frame元数据或Windows x86-64元数据可用时,默认情况下不使用该方法。
彼得·科德斯

7

关于堆栈是否在硬件中实现,此Wikipedia文章可能会有所帮助。

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

该文章及其链接到的其他文章可能对了解处理器中的堆栈用法很有用。


4

这个概念

首先将整个事情想像成是您发明它的人。像这样:

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

实施

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

mov edx,0FFFFFFFFh

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

sub 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指令的作用。

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



3

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

x86计算机启动时,未设置堆栈。程序员必须在启动时明确设置它。但是,如果您已经在使用操作系统,则可以解决此问题。下面是一个简单的引导程序的代码示例。

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


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

在此代码之后,可以使用堆栈。现在,我相信可以通过多种不同的方式来完成此操作,但是我认为这可以说明这一想法。



1

堆栈已经存在,因此您可以在编写代码时假定。堆栈包含函数的返回地址,局部变量和在函数之间传递的变量。您还可以使用诸如BP,SP(堆栈指针)内置的堆栈寄存器,因此您已经提到了内置命令。如果尚未实现堆栈,则功能将无法运行,并且代码流将无法正常工作。


1

堆栈是通过堆栈指针“实现”的,该指针(此处假定为x86体系结构)指向堆栈。每次将某事物压入堆栈(通过pushl,call或类似的堆栈操作码)时,都会将其写入堆栈指针指向的地址,并且堆栈指针递减(堆栈向下增长,即,较小的地址) 。当您从堆栈中弹出某些内容时(popl,ret),堆栈指针将递增,并且该值将从堆栈中读取。

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


1

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

在大多数体系结构中,pop和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

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


1

什么是堆栈?堆栈是一种数据结构,一种在计算机中存储信息的方式。将新对象输入堆栈时,它将放置在所有先前输入的对象之上。换句话说,堆栈数据结构就像一堆卡片,纸张,信用卡邮件或您可以想到的任何其他现实对象一样。从堆栈中删除对象时,最上面的对象将首先被删除。此方法称为LIFO(后进先出)。

术语“堆栈”也可以是网络协议堆栈的缩写。在联网中,计算机之间的连接是通过一系列较小的连接进行的。这些连接或层的行为就像堆栈数据结构一样,因为它们的构建和处理方式相同。


0

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

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

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


错误。这不是一个“指令堆栈”(有这样的事情吗?),这仅仅是通过堆栈寄存器访问的存储器。用于临时存储,过程参数和(最重要的)函数调用的返回地址
Javier

0

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

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

x86堆栈从高地址到低地址“增长”的事实使该体系结构更容易受到缓冲区溢出攻击。


1
为什么x86堆栈变小这一事实使它更容易受到缓冲区溢出的影响?扩大细分范围是否会产生相同的溢出现象?
内森·费尔曼

@nathan:仅当您可以让应用程序在堆栈上分配负数的内存时。
哈维尔

1
缓冲区溢出攻击的内容超出了基于堆栈的数组的末尾-char userName [256],这会从低到高写入内存,从而使您可以覆盖返回地址等内容。如果堆栈向同一方向增长,则您将只能覆盖未分配的堆栈。
莫里斯·弗拉纳根

0

您是正确的,堆栈只是“数据结构”。但是,此处指的是用于特殊目的的硬件实现的堆栈-“堆栈”。

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

  1. 调用堆栈-您正在询问的那个!它存储函数参数和返回地址等。请务必阅读该书中的第4章(第4页,共53页)。有一个很好的解释。
  2. 一个通用堆栈,您可以在程序中使用它来做一些特殊的事情...

  3. 我不确定通用硬件堆栈,但是我记得在某处读到在某些体系结构中有通用硬件实现的堆栈。如果有人知道这是否正确,请发表评论。

首先要知道的是您要为其编程的体系结构,这本书对此进行了解释(我只是在--link上进行了查找)。为了真正理解事物,我建议您了解x86的内存,地址,寄存器和体系结构(我想这就是您从书中学到的东西)。


0

调用函数需要以LIFO方式保存和恢复本地状态(相对于通用协程方法而言),这是一种非常普遍的需求,汇编语言和CPU体系结构基本上可以在其中建立此功能。理论上,您可以实现自己的堆栈,调用约定等,但是我假设某些操作码和大多数现有的运行时都依赖于本机的“堆栈”概念。


0

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

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

stackesp通过硬件来实现。您也可以自己实施。这会使您的程序非常慢。

例:

nop // esp= 0012ffc4

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

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

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


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语言解释,因此如果您愿意,可以看看。


Criptoix网站已死,web.archive.org上没有任何副本
Alexander Malakhov

1
由于托管问题,@ AlexanderMalakhov Cryptroix无法正常工作。Cryptroix现在可以正常工作了。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.