我目前正在尝试理解堆栈是如何工作的,所以我决定自学一些assembly language,我正在使用这本书:
http://savannah.nongnu.org/projects/pgubook/
我正在使用Gas并在Linux Mint上进行开发。
我有些困惑:
据我所知,堆栈只是一种数据结构。所以我假设如果我在汇编编码我必须自己实现堆栈。然而,似乎并非如此,因为有像
这样的命令pushl
popl
因此,在汇编中为x86架构编码并使用Gas语法时:堆栈只是一个已经实现的数据结构吗?或者它实际上是在硬件级别实现的?或者是别的什么?其他芯片组的大多数汇编语言也已经实现了堆栈吗?
我知道这是一个愚蠢的问题,但实际上我很困惑。
答案 0 :(得分:71)
我认为主要是你在program's stack
和any old stack
之间感到困惑。
筹码
是一个抽象数据结构,由Last In First Out系统中的信息组成。你把任意物品放在堆叠上,然后再将它们取下来,就像进/出托盘一样,顶部的物品总是被取下的,你总是把它放在顶部。
A程序堆栈
是一个堆栈,它是执行期间使用的一部分内存,它通常具有每个程序的静态大小,并且经常用于存储函数参数。在调用函数时将参数压入堆栈,函数可以直接寻址堆栈,也可以从堆栈中弹出变量。
程序堆栈通常不是硬件(虽然它保存在内存中因此可以这样说),但是指向堆栈当前区域的堆栈指针通常是CPU寄存器。这使得它比LIFO堆栈更灵活,因为您可以更改堆栈的寻址点。
您应该阅读并确保您理解wikipedia文章,因为它可以很好地描述您正在处理的硬件堆栈。
还有this tutorial根据旧的16位寄存器解释堆栈,但可能有用,another one具体关于堆栈。
来自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
答案 1 :(得分:20)
(我已经提供了此答案中所有代码的gist,以防您想要使用它。)
在2003年的CS101课程中,我只在asm做过最基本的事情。而且我从来没有真正去过#34;得到它"如何asm和堆栈工作 直到我意识到它基本上都像C或C ++中的编程......但没有局部变量,参数和函数。 可能听起来不容易:)让我告诉你(对于x86 asm with Intel syntax)。
<强> 1。什么是堆栈
Stack是每个线程启动时分配的连续内存块。你可以随意存放。用C ++的说法(代码片段#1 ):
const int STACK_CAPACITY = 1000;
thread_local int stack[STACK_CAPACITY];
<强> 2。 Stack的顶部和底部
原则上,您可以将值存储在stack
数组(代码段#2.1 )的随机单元格中:
cin >> stack[333];
cin >> stack[517];
stack[555] = stack[333] + stack[517];
但是想象一下,记住stack
的哪些细胞已经被使用并且哪些细胞是免费的&#34;是多么困难。这就是为什么我们将新值存储在堆栈中的原因。
关于(x86)asm的堆栈的一个奇怪的事情是你从最后一个索引开始添加东西并移动到较低的索引:stack [999],然后堆栈[998]等等(摘录#2.2 ):
cin >> stack[999];
cin >> stack[998];
stack[997] = stack[999] + stack[998];
仍然(好吧,你现在会感到困惑)&#34;官员&#34; stack[999]
的名称是堆栈底部
最后使用的单元格(上例中的stack[997]
)称为堆栈顶部(请参阅Where the top of the stack is on x86)。
第3。堆栈指针(SP)
Stack不是你的asm代码中唯一可见的东西。您还可以操作CPU寄存器(请参阅General-Purpose Registers)。它们就像全局变量一样:
int AX, BX, SP, BP, ...;
int main(){...}
有专用的CPU寄存器(SP)来跟踪添加到堆栈的最后一个元素。 顾名思义,它是一个指针(保存一个像0xAAAABBCC这样的存储器地址)。但就本文而言,我将其用作索引。
在线程的开始SP == STACK_CAPACITY
然后在需要时减少它。规则是你不能写入堆栈顶部以外的堆栈单元格,任何索引少于SP无效,所以你
首先递减SP,然后将值写入新分配的单元格。
如果您知道将在堆栈中连续添加多个值,则可以预先为所有这些值保留空间(代码段#3 ):
SP -= 3;
cin >> stack[999];
cin >> stack[998];
stack[997] = stack[999] + stack[998];
注意。现在你可以看到为什么&#34;分配&#34;在堆栈上是如此之快。您实际上没有分配任何内容(如new
关键字或malloc
),它只是一个整数减量。
<强> 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
中有一些有用的东西,由于某种原因,我们需要进行覆盖它的计算( snippet 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)(也称为帧指针)和堆栈帧
让我们采取更多&#34;高级&#34;函数并在我们类似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寄存器中,在函数输入时(在我们为本地分配空间之前)创建锚。 Snippet#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。 Wikipedia says otherwise,但在此我遵守Intel software developer's manual,请参阅第一卷。 1,部分 6.2.4.1堆栈帧基址指针和图6-2的 6.3.2远程CALL和RET操作部分。函数的参数和堆栈框架是功能的激活记录的一部分(参见The gen on function perilogues)。
8b。从BP点到函数参数的正偏移量和负偏移量指向局部变量。这对于调试来说非常方便
8c。 stack[BP]
存储前一个堆栈帧的地址,stack[stack[BP]]
存储前一个堆栈帧,依此类推。在这个链之后,你可以发现程序中所有函数的框架,这些函数还没有返回。这是调试器向您显示调用堆栈的方式
8d。 myAlgo_noLPR_withAnchor
的前3条指令,其中我们设置框架(保存旧BP,更新BP,为本地人预留空间)称为功能序言 < / p>
<强> 9。调用约定
在代码段8.1中,我们已将myAlgo
的参数从右向左推送,并在AX
中返回结果。
我们也可以从左到右传递参数并在BX
中返回。或者在BX和CX中传递params并在AX中返回。显然,来电者(main()
)和
被叫函数必须同意存储所有这些东西的位置和顺序。
调用约定是一组关于如何传递参数和返回结果的规则。
在上面的代码中,我们使用了 cdecl调用约定:
myAlgo_noLPR_withAnchor
函数)保留,这样调用者(main
函数)可以依赖那些未被调用更改的寄存器。 / LI>
(来源:示例&#34; 32位cdecl&#34;来自Stack Overflow文档;版权所有2016 icktoofay和Peter Cordes ;根据CC BY-SA 3.0获得许可。可以在archive.org找到archive of the full Stack Overflow Documentation content,其中此示例按主题ID 3261和示例ID 11196编制索引。)
<强> 10。摆脱函数调用
现在最有趣的部分。就像数据一样,可执行代码也存储在内存中(与堆栈内存完全无关),每条指令都有一个地址 当没有另外命令时,CPU按照它们存储在存储器中的顺序一个接一个地执行指令。但我们可以命令CPU到#34;跳跃&#34;到内存中的另一个位置并从那里执行指令。 在asm中,它可以是任何地址,在更高级的语言(如C ++)中,您只能跳转到标签标记的地址(there are workarounds,但它们并不漂亮,至少可以说。)
我们采用此功能(代码段#10.1 ):
int myAlgo_withCalls(int a, int b) {
int t1 = triple(a);
int t2 = triple(b);
return t1 - t2;
}
而不是以tripple
C ++方式调用,请执行以下操作:
tripple
myAlgo
的正文
myAlgo
条目中使用tripple
goto
代码
tripple
代码时,在tripple
调用之后保存代码行的堆栈地址,以便稍后返回并继续执行({{ 1}}下面的宏)PUSH_ADDRESS
函数的地址并执行到最后(3.和4.一起是tripple
宏)CALL
的末尾(在我们清理了本地人之后),从堆栈顶部获取返回地址并跳转到那里(tripple
宏)因为在C ++中没有简单的方法可以跳转到特定的代码地址,所以我们将使用标签来标记跳转的位置。 我不会详细介绍下面的宏如何工作,只要相信我他们按照我说的去做(片段#10.2 ):
RET
注意:
10a。因为返回地址存储在堆栈中,原则上我们可以更改它。这就是stack smashing attack的工作方式
10b。&#34; end&#34;的最后3条指令// 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);
}
(清理本地人,恢复旧BP,返回)被称为函数的结尾
<强> 11。装配强>
现在让我们看看triple_label
的真实主题。要在Visual Studio中执行此操作:
与我们类似asm的C ++的一个区别是asm的堆栈是以字节而不是整数运行的。因此,为一个myAlgo_withCalls
预留空间,SP将减少4个字节
我们在这里(代码段#11.1 ,评论中的行号来自gist):
int
asm for ; 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
( snippet#11.2 ):
tripple
希望,在阅读这篇文章之后,装配看起来并不像以前那样神秘:)
以下是帖子主体的链接以及其他一些内容:
答案 2 :(得分:7)
关于堆栈是否在硬件中实现,Wikipedia article可能有所帮助。
某些处理器系列,例如 x86,有特殊说明 操纵堆栈 目前正在执行线程其他 处理器系列,包括PowerPC 和MIPS,没有显式堆栈 支持,而是依靠 约定和委托堆栈 管理到操作系统 应用程序二进制接口(ABI)。
该文章及其链接的其他文章可能有助于了解处理器中的堆栈使用情况。
答案 3 :(得分:4)
概念
首先想到整个事情就好像你是发明它的人一样。像这样:
首先想到一个数组以及它是如何在低级别实现的 - &gt;它基本上只是一组连续的内存位置(彼此相邻的内存位置)。现在您已经掌握了这种心理形象,想一想您可以访问任何这些内存位置,并在删除或添加数组中的数据时随意删除它。 现在想一下相同的数组,但不是删除任何位置的可能性,而是在删除或添加数组中的数据时,决定只删除最后的位置。 现在,以这种方式操作该数组中数据的新想法称为LIFO,这意味着后进先出。您的想法非常好,因为它可以更容易地跟踪该数组的内容,而无需在每次从中删除某些内容时使用排序算法。此外,要始终知道数组中最后一个对象的地址是什么,您可以在Cpu中使用一个寄存器来跟踪它。现在,寄存器跟踪它的方式是每次删除或向数组中添加内容时,您还会减少或增加寄存器中地址的值,方法是从数组中删除或添加的对象数量(按他们占用的地址空间量)。您还需要确保将每个对象减少或增加该寄存器的数量固定为一个量(例如4个存储器位置,即4个字节),以便更容易跟踪并使其成为可能使用该寄存器与一些循环结构,因为循环每次迭代使用固定的增量(例如,通过循环循环你的数组,你构造循环,每次迭代增加你的寄存器4,如果你的数组有对象的话,这是不可能的它的大小不同)。最后,您选择将这个新数据结构称为“堆栈”,因为它会让您想起餐馆中的一堆盘子,他们总是在该堆栈的顶部移除或添加一个盘子。
实施
正如您所看到的,堆栈只不过是一系列连续的内存位置,您决定如何操作它。因此,您可以看到您甚至不需要使用特殊指令和寄存器来控制堆栈。您可以使用基本的mov,add和sub指令自己实现它,并使用通用寄存器代替ESP和EBP,如下所示:
mov edx,0FFFFFFFFh
<强>; - &gt; 这将是您的堆栈的起始地址,离您的代码和数据最远,它也将作为跟踪我之前解释的堆栈中最后一个对象的寄存器。你称之为“堆栈指针”,因此你选择寄存器EDX来正常使用ESP。
sub edx,4
mov [edx],dword ptr [someVar]
<强>; - &gt; 这两条指令会将堆栈指针递减4个内存位置,并将从[someVar]内存位置开始的4个字节复制到EDX现在指向的内存位置,就像PUSH指令递减ESP一样,只有在这里你手动完成并使用了EDX。因此,PUSH指令基本上只是一个较短的操作码,实际上是用ESP做的。
mov eax,dword ptr [edx]
添加edx,4
<强>; - &gt; ,在这里我们相反,我们首先将从EDX现在指向的内存位置开始的4个字节复制到寄存器EAX中(在这里任意选择,我们可以将它复制到我们想要的任何地方)。然后我们将堆栈指针EDX增加4个内存位置。这就是POP指令的作用。
现在您可以看到英特尔刚刚添加了PUSH和POP指令以及ESP和EBP寄存器,使“堆栈”数据结构的上述概念更易于编写和读取。还有一些RISC(精简指令集)Cpu-s没有PUSH ans POP指令和用于堆栈操作的专用寄存器,在为这些Cpu-s编写汇编程序时,你必须自己实现堆栈我给你看了。
答案 4 :(得分:3)
您混淆了抽象堆栈和硬件实现的堆栈。后者已经实施。
答案 5 :(得分:3)
我认为您正在寻找的主要答案已被暗示过。
当x86计算机启动时,不会设置堆栈。程序员必须在启动时明确设置它。但是,如果您已经在操作系统中,那么这已经得到了解决。下面是一个简单的bootstrap程序的代码示例。
首先设置数据和堆栈段寄存器,然后将堆栈指针设置为0x4000。
movw $BOOT_SEGMENT, %ax
movw %ax, %ds
movw %ax, %ss
movw $0x4000, %ax
movw %ax, %sp
在此代码之后,可以使用堆栈。现在我确信它可以通过多种不同的方式完成,但我认为这应该说明这个想法。
答案 6 :(得分:2)
答案 7 :(得分:1)
通过堆栈指针“实现”堆栈,该堆栈指针(假设此处为x86架构)指向堆栈段。每次在堆栈上推送一些东西(通过pushl,call或类似的堆栈操作码),它就被写入堆栈指针指向的地址,堆栈指针递减(堆栈是增长向下,即更小的地址)。当您从堆栈中弹出一些东西(popl,ret)时,堆栈指针递增并从堆栈中读取值。
在用户空间应用程序中,应用程序启动时已经为您设置了堆栈。在内核空间环境中,您必须首先设置堆栈段和堆栈指针...
答案 8 :(得分:1)
什么是Stack? 堆栈是一种数据结构 - 一种在计算机中存储信息的方法。在堆栈中输入新对象时,它将放在所有先前输入的对象之上。换句话说,堆栈数据结构就像一堆卡片,纸张,信用卡邮件或您能想到的任何其他现实世界的对象。从堆栈中删除对象时,首先删除顶部的对象。此方法称为LIFO(后进先出)。
对于网络协议栈,术语“堆栈”也可以是短的。在网络中,计算机之间的连接是通过一系列较小的连接进行的。这些连接或层的作用类似于堆栈数据结构,因为它们以相同的方式构建和处理。
答案 9 :(得分:1)
我没有特别看过气体汇编程序,但一般来说,堆栈是通过维护对堆栈顶部所在的内存位置的引用来“实现”的。存储器位置存储在寄存器中,该寄存器具有不同体系结构的不同名称,但可以被认为是堆栈指针寄存器。
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
此外,某些体系结构将最后使用的内存地址存储为堆栈指针。有些人会存储下一个可用的地址。
答案 10 :(得分:1)
堆栈已经存在,因此您可以假设在编写代码时。 堆栈包含函数的返回地址,局部变量和在函数之间传递的变量。 您还可以使用内置的BP,SP(堆栈指针)等堆栈寄存器,因此您提到了内置命令。 如果堆栈尚未实现,则无法运行函数,并且代码流无法工作。
答案 11 :(得分:0)
调用堆栈由x86指令集和操作系统实现。
push和pop之类的指令调整堆栈指针,而操作系统负责在每个线程的堆栈增长时分配内存。
x86堆栈从较高地址到较低地址“逐渐减少”的事实使这种架构更加susceptible to the buffer overflow attack.
答案 12 :(得分:0)
调用函数,需要以LIFO方式保存和恢复本地状态(而不是一种通用的常规方法),结果语言和CPU架构基本上构建了这个功能,这是一个非常普遍的需求。对于线程,内存保护,安全级别等概念可能也是如此。理论上你可以实现自己的堆栈,调用约定等,但我假设一些操作码和大多数现有的运行时都依赖于这个本机概念。 “堆叠”。
答案 13 :(得分:0)
你是正确的,堆栈只是一个数据结构。但是,它指的是用于特殊目的的硬件实现堆栈 - “堆栈”。
许多人评论过硬件实现的堆栈与(软件)堆栈数据结构。我想补充一点,有三种主要的堆栈结构类型 -
首先要知道的是你正在编程的架构,这本书解释了(我只是查了一下 - 链接)。为了真正理解事物,我建议您了解x86的内存,寻址,寄存器和体系结构(我假设您正在学习的内容 - 来自本书)。
答案 14 :(得分:0)
stack
是记忆的一部分。它用于input
的{{1}}和output
。它也用于记住功能的回归。
functions
寄存器记住堆栈地址。
esp
和stack
由硬件实现。你也可以自己实现它。它会让你的程序变得非常慢。
示例:
nop // esp
= 0012ffc4
push 0 // esp
= 0012ffc0,Dword [0012ffc0] = 00000000
调用proc01 // esp
= 0012ffbc,Dword [0012ffbc] = esp
,eip
= adrr [proc01]
pop eip
// eax
= Dword [eax
],esp
= esp
+ 4
答案 15 :(得分:0)
堆栈是一种数据结构是正确的。通常,您使用的数据结构(包括堆栈)是抽象的,并作为内存中的表示形式存在。
在这种情况下,您正在使用的堆栈具有更多的物质存在 - 它直接映射到处理器中的实际物理寄存器。作为数据结构,堆栈是FILO(先进先出)结构,确保以与输入相反的顺序删除数据。请参阅StackOverflow徽标以获得视觉效果! ;)
您正在使用指令堆栈。这是您为处理器提供的实际指令堆栈。
答案 16 :(得分:0)
我正在搜索堆栈如何在函数方面工作,我发现this blog它的真棒和它从头开始堆栈的解释概念以及堆栈中的堆栈存储值。
现在回答你的问题。我将用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语言解释,所以如果你想要你可以看看。