汇编语言如何工作?
我目前正试图理解堆栈是如何工作的,所以我决定教自己一些汇编语言 ,我正在使用这本书:
http://savannah.nongnu.org/projects/pgubook/
我正在使用Gas,并在Linux Mint上进行开发。
我有些困惑的东西:
据我所知,一个堆栈只是一个数据结构。 所以我认为如果我在汇编代码,我不得不自己实现堆栈。 然而,这似乎不是如此,因为有像这样的命令
pushl popl
所以当编译x86架构的程序集时,使用Gas语法:堆栈只是一个已经实现的数据结构? 还是实际上是在硬件层面上实现的? 或者是别的什么? 另外大多数其他芯片组的汇编语言已经实现了堆栈?
我知道这是一个愚蠢的问题,但我实际上很困惑。
我想主要是你在program's stack
和any old stack
之间感到困惑。
一个堆栈
是一个抽象数据结构,它由“后进先出”系统中的信息组成。 你把任意东西放到堆叠上,然后再把它们取出来,就像进出盘一样,最上面的东西总是被取下来的,而且总是放在最上面。
程序堆栈
是一个堆栈,它是执行期间使用的一段内存,每个程序通常具有一个静态大小,并经常用于存储函数参数。 当你调用一个函数时,你可以将这些参数压入堆栈,并且这个函数可以直接访问堆栈,或者从堆栈中popupvariables。
程序堆栈通常不是硬件(虽然它保存在内存中,所以可以这样说),但指向堆栈当前区域的堆栈指针通常是一个CPU寄存器。 这使得它比LIFO堆栈更加灵活,因为您可以改变堆栈正在寻址的点。
您应该阅读并确保您了解维基百科文章,因为它可以很好地描述您正在处理的硬件堆栈。
还有这个教程 ,它解释了旧的16位寄存器堆栈,但可能是有用的, 另一个具体的堆栈。
来自Nils Pipenbrinck:
值得注意的是,有些处理器并没有实现访问和操作堆栈的所有指令(push,pop,stack pointer等),但是x86的使用频率很高。 在这些情况下,如果你想要一个堆栈,你将不得不自己实现它(一些MIPS和一些ARM处理器是在没有堆栈的情况下创build的)。
例如,在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
关于堆栈是否在硬件中实现,这篇维基百科文章可能会有所帮助。
某些处理器系列(如x86)具有用于操作当前正在执行的线程的堆栈的特殊指令。 其他处理器系列(包括PowerPC和MIPS)没有明确的堆栈支持,而是依赖于约定和委托堆栈pipe理到操作系统的应用程序二进制接口(ABI)。
该文章及其链接的其他文章可能对处理器中的堆栈使用感觉有用。
这个概念
首先想到整个事情,就好像你是发明它的人一样。 喜欢这个:
首先考虑一个数组,以及它是如何在低级别实现的 – >它基本上只是一组连续的内存位置(彼此相邻的内存位置)。 现在你脑海中已经有了这个心理图像,想想你可以访问任何这些内存位置,并在你的数组中删除或添加数据时,按照你的意愿删除它。 现在想想那个相同的数组,但不是删除任何位置的可能性,而是决定在删除或添加数组中的数据时只删除最后一个位置。 现在,您以这种方式操作数组中的数据的新想法被称为LIFO,意思是“后进先出”。 你的想法是非常好的,因为它使得跟踪数组的内容变得更容易,而不必每次从中移除一些sortingalgorithm。 另外,要知道数组中最后一个对象的地址是什么,你需要在Cpu中专用一个寄存器来跟踪它。 现在,寄存器跟踪它的方式是,每当你删除或添加一些东西到你的数组中时,你也将你的寄存器中的地址值减去或增加你从数组中删除或添加的对象的数量他们占用的地址空间量)。 你还要确保你减less或增加寄存器的数量固定为每个对象的一个数量(比如4个内存位置,即4个字节),以便更容易跟踪并使其成为可能在一些循环结构中使用这个寄存器,因为循环每次迭代使用固定的增量(例如,用循环来循环你的数组,循环中每次迭代构build一个循环来增加你的寄存器的数量,如果你的数组的对象是不同的尺寸)。 最后,您select将这个新的数据结构称为“堆栈”,因为这会让您想起餐厅中的一堆盘子,他们总是在堆叠的顶部移除或添加一块盘子。
实施
正如你所看到的,堆栈不过是一系列连续的内存位置,你决定如何操纵它。 正因为如此,你可以看到你甚至不需要使用特殊的指令和寄存器来控制堆栈。 您可以使用基本的mov,add和sub指令自己实现它,并使用通用寄存器来代替ESP和EBP:
mov edx,0FFFFFFFFh
; – >这将是你的堆栈的起始地址,离你的代码和数据最远,它也将作为那个寄存器来跟踪我前面解释过的堆栈中的最后一个对象。 你把它称为“堆栈指针”,所以你select寄存器EDX作为ESP通常使用的。
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寄存器(在这里任意select,我们可以将它复制到我们想要的任何地方)。 然后我们将堆栈指针EDX增加4个内存位置。 这是POP指令的作用。
现在你可以看到,PUSH和POP指令以及ESP和EBP寄存器都是由Intel增加的,使得上述“堆栈”数据结构的概念更易于编写和读取。 还有一些RISC(精简指令集)Cpu没有PUSH和POP指令,也没有用于堆栈操作的专用寄存器,在为这些Cpu编写汇编程序时,你必须自己实现堆栈我给你看了
您混淆了抽象堆栈和硬件实现的堆栈。 后者已经实施。
我想你正在寻找的主要答案已经暗示了。
x86计算机启动时,堆栈未安装。 程序员必须在启动时明确地设置它。 但是,如果你已经在操作系统中,这已经被处理了。 下面是一个简单的引导程序的代码示例。
首先设置数据和堆栈段寄存器,然后将堆栈指针设置为0x4000。
movw $BOOT_SEGMENT, %ax movw %ax, %ds movw %ax, %ss movw $0x4000, %ax movw %ax, %sp
在此代码之后,可以使用堆栈。 现在我确信可以用不同的方法来完成,但我认为这应该说明这个想法。
堆栈只是程序和函数使用内存的一种方式。
堆栈总是困惑我,所以我做了一个例子:
( 这里是svg版本 )
(我已经在这个答案中的所有代码的要点 ,如果你想玩它)
在2003年的CS101课程中,我只使用过最基本的东西。我从来没有真正懂得如何使用和堆栈,直到我意识到它像C或C ++编程一样基本上是…但没有局部variables,参数和function。 可能听起来并不容易:)让我告诉你(对于Intel语法的 x86 asm)。
1.什么是堆栈
堆栈是为每个线程启动时分配的连续内存块。 你可以在那里存储任何你想要的。 用C ++的说法( 代码片段#1 ):
const int STACK_CAPACITY = 1000; thread_local int stack[STACK_CAPACITY];
2.堆栈的顶部和底部
原则上,您可以将值存储在stack
数组的随机单元格中( 片段#2.1 ):
cin >> stack[333]; cin >> stack[517]; stack[555] = stack[333] + stack[517];
但想象一下,记住stack
哪些单元已经被使用,哪些单元是“空闲的”将会是多么的艰难。 这就是为什么我们将新的值存储在堆栈旁边。
关于(x86)asm堆栈的一个奇怪的事情是,你可以从最后一个索引开始添加东西,并移动到较低的索引:stack [999],stack [998]等等( 片段#2.2 ):
cin >> stack[999]; cin >> stack[998]; stack[997] = stack[999] + stack[998];
仍然(coution,你现在会感到困惑) stack[999]
的“官方”名称是stack[999]
底部 。
最后一个使用过的单元(上面例子中的stack[997]
)被称为栈顶 (请参阅栈 的顶部在x86上 )。
3.堆栈指针(SP)
有专门的CPU寄存器(SP)跟踪添加到堆栈的最后一个元素。 顾名思义,它就是一个指针(包含一个像0xAAAABBCC的地址)。 但为了这篇文章的目的,我将使用它作为索引。
在线程开始时, SP == STACK_CAPACITY
,然后根据需要减less它。 规则是你不能写入堆栈顶部以外的栈单元,并且任何小于SP的索引都是无效的,所以你首先减lessSP, 然后给新分配的单元写一个值。
当你知道你将在堆栈中增加几个值时,你可以为它们预留所有的空间( 片段#3 ):
SP -= 3; cin >> stack[999]; cin >> stack[998]; stack[997] = stack[999] + stack[998];
4.没有局部variables
让我们来看看这个简单的函数( 片段#4.1 ):
int triple(int a) { int result = a * 3; return result; }
并重写它没有本地variables( 片段#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.按下/popup
堆栈顶部增加一个新的元素是一个频繁的操作,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使用模式暂时保存一些值。 说,我们有一些有用的variablesmyVar
,出于某种原因,我们需要做的计算,将覆盖它( 片段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.没有返回值
堆栈不是在你的asm代码中唯一可见的地方。 您也可以操纵CPU寄存器(请参见通用寄存器 )。 他们真的像全局variables:
int AX, BX, ...;
让我们返回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) (也称为帧指针 )和堆栈帧
让我们采取更多的“高级”function,并重写在我们的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); }
现在想象我们决定在返回之前引入新的局部variables来存储结果,就像我们在tripple
(片段#4.1)中做的tripple
。 该函数的主体将是( 片段#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;
你看,我们必须更新对函数参数和局部variables的每一个引用。 为了避免这种情况,我们需要一个锚索引,当堆栈增长时它不会改变。
我们将通过将当前顶部(SP的值)保存到BP寄存器中来创build函数入口(在为本地分配空间之前)的锚定权。 代码段#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]
996..994 stack[996 .. 994]
(包括idexes)。
框架从函数的BP开始(在函数内部更新之后)并持续到下一个堆栈帧。 所以堆栈中的参数是调用者堆栈帧的一部分(参见注释8a)。
笔记:
8A。 维基百科说,否则有关参数,但在这里我坚持英特尔软件开发人员手册 ,见卷。 1,章节6.2.4.1堆栈帧指针和6.3.2节远程调用和RET操作中的图6-2。 函数的参数和堆栈框架是函数激活logging的一部分 (请参阅函数perilogues上的gen )。
8B。 从BP点到function参数的正偏移以及负偏移指向局部variables。 这对debugging非常方便
8C。 stack[BP]
存储先前的堆栈帧的地址, stack[stack[BP]]
存储先前的堆栈帧等等。 在这个链之后,你可以发现程序中所有function的框架,它们还没有返回。 这是debugging器显示你调用堆栈的方式
8D。 myAlgo_noLPR_withAnchor
的前3条指令,我们在这里设置框架(保存旧的BP,更新BP,为当地人保留空间)称为函数序言
9.调用约定
在代码片段8.1中,我们从右向左推送了myAlgo
参数, myAlgo
结果返回给AX
。 我们也可以从左到右传递参数,并在BX
返回。 或者在BX和CX中传递参数,并在AX中返回。 显然,调用者( main()
)和被调用的函数必须同意在哪里存储这些东西。
调用约定是关于如何传递参数和返回结果的一组规则。
在上面的代码中,我们使用了cdecl调用约定 :
- 参数在堆栈上传递,第一个参数在调用时位于堆栈的最低地址(push last <…>)。 调用者负责在调用之后将参数从堆栈popup。
- 返回值放在AX中
- EBP和ESP必须由被调用者来保存(在我们的例子中,
myAlgo_noLPR_withAnchor
函数),这样调用者(main
函数)可以依赖那些没有被调用改变的寄存器。 - 所有其他寄存器(EAX,<…>)可由被调用者自由修改; 如果调用者希望在函数调用之前和之后保存一个值,则必须将值保存在别处(我们使用AX来执行此操作)
(来源:Stack Overflow文档中的示例“32位cdecl”;由icktoofay和Peter Cordes颁发的2016版; CC BY-SA 3.0授权的完整的Stack Overflow文档内容的存档可以在archive.orgfind,其中此示例由主题ID 3261和示例ID 11196进行索引。
10.没有电话
现在最有趣的部分。 就像数据一样,可执行代码也存储在内存中(与内存完全无关),每条指令都有一个地址。
如果没有其他指令,CPU将按照它们存储在内存中的顺序依次执行指令。 但是我们可以命令CPU“跳转”到内存中的另一个位置,并从那里执行指令。 在asm中,它可以是任何地址,而在更高级的语言(如C ++)中,只能跳转到标有标签的地址( 有解决方法,但不是很漂亮,至less可以说)。
我们来看看这个函数( 代码片段#10.1 ):
int myAlgo_withCalls(int a, int b) { int t1 = triple(a); int t2 = triple(b); return t1 - t2; }
而不是调用tripple
C ++方法,请执行以下操作:
- 保存在
tripple
调用之后的代码行的堆栈地址,所以我们稍后可以返回并确认执行(下面的PUSH_ADDRESS
macros) - 跳到
tripple
函数的地址并执行到最后(1.和2.一起是CALL
macros) - 在
tripple
结束(我们已经清理本地人之后),从栈顶取回地址并跳到那里(RET
macros)
因为在C ++中没有简单的方法跳转到特定的代码地址,我们将使用标签来标记跳转的地方。 我不会详细探讨下面的macroses如何工作,只要相信我,他们做我所说的话( 代码段#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.大会
现在让我们来看看myAlgo_withCalls
真正的asm。 在Visual Studio中这样做:
- 将构build平台设置为x86
- 构buildtypes:debugging
- 在myAlgo_withCalls里设置一个断点
- 运行,当执行在中断点停止时按Ctrl + Alt + D
与我们的asm-like C ++的区别在于,asm的堆栈在字节而不是ints上运行。 所以要为一个int
保留空间,SP会减less4个字节。
在这里我们去( 代码#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
( snippet#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
希望看到这个post后,程序集看起来不像以前那么神秘:)
这里是post的正文和一些进一步阅读的链接:
- Eli Bendersky, 其中堆栈的顶部是在x86 – 顶部/底部,push / pop,SP,堆栈帧,调用约定
- Eli Bendersky, x86-64上的堆栈帧布局 – 在x64 上传递的参数,堆栈帧,红色区域
- Mariland大学, 了解堆栈 – 一本关于堆栈概念的非常好的介绍。 (这是MIPS(而不是x86)和GAS语法,但这是不重要的话题)。 如果感兴趣,请参阅有关MIPS ISA编程的其他说明。
- x86 Asm wikibook, 通用寄存器
- x86反汇编wikibook, 堆栈
- x86反汇编wikibook, 函数和堆栈框架
- 英特尔软件开发人员手册 – 我期望它是真正的核心,但令人惊讶的是它很容易阅读(虽然信息量是压倒性的)
- 乔纳森·德·博伊内·波拉德(Jonathan de Boyne Pollard), function上的“perilogues” – 序言/尾声,堆栈帧/激活logging,红色区域
堆栈已经存在,所以你可以假设你写代码的时候。 堆栈包含函数的返回地址,局部variables和函数之间传递的variables。 还有一些堆栈寄存器,如内build的BP,SP(堆栈指针),您可以使用,因此您提到了内置的命令。 如果堆栈尚未实现,则函数将无法运行,并且代码stream将无法工作。
堆栈是通过堆栈指针“实现的”,(这里假设x86架构)指向堆栈段 。 每次将某个东西压入堆栈(通过pushl,call或类似的堆栈操作码),它被写入堆栈指针所指向的地址,并且堆栈指针递减 (堆栈向下增长,即较小的地址) 。 当你从堆栈中popup某些东西(popl,ret)时,堆栈指针会递增,并从堆栈中读取值。
在用户空间应用程序中,应用程序启动时已经为您设置了堆栈。 在内核空间环境中,您必须先设置堆栈段和堆栈指针…
我没有具体的看到Gas汇编程序,但是一般来说,通过维护对堆栈顶部所在的内存位置的引用来“实现”堆栈。 存储单元存储在一个寄存器中,对于不同的体系结构,寄存器名称不同,但可以认为是堆栈指针寄存器。
pop和push命令通过构build微指令在大多数架构中实现。 然而,一些“教育架构”要求你自己实施。 从function上来说,推送会像这样实现:
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
而且,一些体系结构将最后使用的内存地址存储为堆栈指针。 一些存储下一个可用的地址。
什么是堆栈? 堆栈是一种数据结构 – 一种将信息存储在计算机中的手段。 当一个新的对象被input到一个堆栈中时,它被放置在所有先前input的对象之上。 换句话说,堆栈数据结构就像一堆卡片,文件,信用卡邮件或任何其他您可以想到的现实世界的对象。 从堆栈中移除对象时,顶部的对象首先被移除。 这种方法被称为LIFO(后进先出)。
对于networking协议栈,术语“堆栈”也可以是简短的。 在networking中,计算机之间的连接是通过一系列较小的连接来完成的。 这些连接或层像堆栈数据结构一样工作,因为它们是以相同的方式构build和处理的。
你是正确的,一个堆栈是一个数据结构。 通常,您所使用的数据结构(包括堆栈)是抽象的,并以内存中的表示forms存在。
在这种情况下,您正在使用的堆栈具有更多的物质存在 – 它直接映射到处理器中的实际物理寄存器。 作为一种数据结构,堆栈是FILO(先进先出)结构,确保以与input相反的顺序删除数据。 请参阅StackOverflow徽标以获得视觉效果! ;)
您正在使用指令堆栈 。 这是你正在喂给处理器的实际指令的堆栈。
调用堆栈由x86指令集和操作系统实现。
像push和pop这样的指令调整堆栈指针,而当操作系统负责为每个线程堆栈增长时分配内存。
x86堆栈从较高地址向较低地址“增长”的事实使这种架构更容易受到缓冲区溢出攻击。
你是正确的,一个堆栈只是一个数据结构。 但是,这里指的是用于特殊用途的硬件实现的堆栈 – “堆栈”。
许多人已经评论了硬件实现的堆栈与(软件)堆栈数据结构。 我想补充一点,有三个主要的堆栈结构types –
- 一个调用堆栈 – 这是你问的那个! 它存储函数参数和返回地址等。请阅读该书的第4章(关于第4页,即第53页的所有函数)。 有一个很好的解释。
- 一个通用的堆栈,你可以在你的程序中使用一些特殊的东西
- 通用硬件堆栈
我不确定这一点,但是我记得在某些地方读到,在某些体系结构中有一个通用硬件实现的栈。 如果有人知道这是否正确,请发表评论。
The first thing to know is the architecture you are programming for, which the book explains (I just looked it up –link). To really understand things, I suggest that you learn about the memory, addressing, registers and architecture of x86 (I assume thats what you are learning –from the book).
Calling functions, which requires saving and restoring local state in LIFO fashion (as opposed to say, a generalized co-routine approach), turns out to be such an incredibly common need that assembly languages and CPU architectures basically build this functionality in. The same could probably be said for notions of threading, memory protection, security levels, etc. In theory you could implement your own stack, calling conventions, etc., but I assume some opcodes and most existing runtimes rely on this native concept of "stack".
stack
is part of memory. it use for input
and output
of functions
. also it use for remembering function's return.
esp
register is remember the stack address.
stack
and esp
are implemented by hardware. also you can implement it yourself. it will make your program very slow.
例:
nop // esp
= 0012ffc4
push 0 // esp
= 0012ffc0 ,Dword[0012ffc0]=00000000
call proc01 // esp
= 0012ffbc ,Dword[0012ffbc] = eip
, eip
= adrr[proc01]
pop eax
// eax
= Dword[ esp
], esp
= esp
+ 4
I was searching about how stack works in terms of function and i found this blog its awesome and its explain concept of stack from scratch and how stack store value in stack.
Now on your answer . I will explain with python but you will get good idea how stack works in any language.
Its a program :
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)
Source : Cryptroix
some of its topic which it cover in 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?
But its explain with python language so if you want you can take a look.