堆栈和寄存器如何在汇编程序中工作?

我知道有EBP,ESP,EAX等等,并且使用这些寄存器,堆栈和所有的堆栈。 如果某个寄存器(如EBP)是堆栈,并且ESP和其他寄存器堆叠在EBP之上以堆栈在EBP上,我很困惑。

或者栈只是内存分配(边界)的可视化,更好地理解内存,寄存器是真正的内存。

让我困惑的是当主函数调用函数时:

主要的是,在调用一个函数之前,函数的任何参数从EAX被推送到ESP,然后对返回地址(main中的“call”后面的下一个地址)推入堆栈的函数进行“调用”(我认为返回地址被堆积在ESP上,并且该函数的参数被调用以便在函数被调用时堆叠在EBP上,并且我认为这是错误的?),那么EIP被移到函数的开头。

然后当函数被调用时,EBP被再次推入(这是因为在函数内部,EBP没有任何作用,但是EBP寄存器中已经包含了来自主函数的一些值),并且ESP值被EBP(this这就是为什么我认为EBP是堆栈,所有东西都堆叠在EBP上,不是这样吗?)然后,ESP是“sub”,它有一定的价值,为函数的局部变量提供空间。 (当ESP在功能输入处被推入EBP时,ESP是否具有ESP值?还是被清空?)

在函数结束时,函数会使用“leave”和“ret”来擦除函数的堆栈帧(EBP?或ESP?或只是“堆栈帧”,既不是EBP也不是ESP?),如果它擦除了EBP或ESP, EBP对main有什么影响?我读过EBP是从堆栈指针重新初始化的,但是当堆栈指针被压入堆栈时)然后是“ret”,EIP在执行之前移动到返回地址,该地址被推入“main” “打电话”功能。

所以这一切都让我感到困惑,因为我不确定“堆栈”是一个特定的寄存器还是一个灵活的内存边界,以便更好地理解。 而且我不确定堆栈指针的位置和时间。


“堆栈”只是内存。 你的处理器中有一个“堆栈指针”。 关于堆栈的事情是,你并不关心它在内存中的位置,一切都是相对于堆栈指针,堆栈指针加或减一些内存位置。

希望/假设你的栈有足够的空间来做你的程序需要做的事(这是另一个话题)。 所以在这方面,堆栈只是一堆内存,不仅仅是一个寄存器值的数据。

把堆栈看作是一堆东西,实际上是堆栈的内存位置,但也许是一堆索引卡片,你可以在其中编写各种东西。 图片通常有帮助。

[     ]  <- sp

我不记得x86的细节,有些处理器的堆栈指针指向栈顶“当前项目”。 其他处理器的堆栈指针指向第一个空闲位置。 我将选择一种方法并运行,然后根据需要进行调整。 另外一些处理器堆栈自然增长为“向下”,这意味着当您向堆栈添加某些内容时,堆栈指针地址变得更小。 一些堆栈成长起来,但这并不常见,但视觉上,如果你的一堆笔记卡堆放在桌子上,而不是相反的重力,并且它们以某种力量推入天花板,那么它会更有意义。

因此,在准备调用函数之前,我们有上述图片。 比方说,堆栈指针指向堆栈顶部,我们不关心堆栈顶部是谁或什么,除了它是我们不应该触及的某些数据,堆栈的另一个属性,堆栈指针的边是公平的游戏,堆栈指针的另一边是某人的数据,除非是你自己的数据,否则你不应该触摸它。 当你调用另一个函数时,确保堆栈指针是堆栈指针指向堆栈的顶部,推送不会破坏任何东西。

所以我们想传递两个参数给我们的函数,我们推倒顺序,这样当函数被调用时它们看起来更自然,这是任意的,并且基于编译器调用约定。 只要规则始终如一,无论顺序如何,逆向推进是非常普遍的,尽管如此。

fun( a, b);

在我们推动b之前

[stuff] <-sp

我们推b后

[  b  ] <- sp
[stuff] 

其中每个[项目]是某个固定大小的堆栈中的一个内存位置,现在假定32位,但它可能是64位。

然后我们推一个

[  a  ] <- sp
[  b  ] 
[stuff] 

并且我们准备调用该函数,所以假定一个调用将返回地址放在堆栈上

叫好玩

[retadd] <- sp
[  a  ] 
[  b  ] 
[stuff] 

所以现在在相对于堆栈指针的fun函数中,我们可以解决堆栈中的各种项目:

[retadd] <- sp + 0
[  a  ]  <- sp + 4
[  b  ]  <- sp + 8
[stuff]  <- sp + 12

在这个例子中假定一个32位宽的堆栈。

通常不需要堆栈帧,它们有助于使代码更具可读性,从而更易于编译器人员进行调试,但它只是刻录一个寄存器(根据您的体系结构,该寄存器可能不是通用目的)。 但是,这是怎样的图片

push fp since we are going to modify it we don't want to mess up the callers fp register
fp = sp;  (Frame pointer (ebp) = stack pointer (esp));


[  fp ]  <- sp + 0  <- fp + 0
[retadd] <- sp + 4  <- fp + 4
[  a  ]  <- sp + 8  <- fp + 8
[  b  ]  <- sp + 12 <- fp + 12
[stuff]  <- sp + 16 <- fp + 16

所以如果我想访问传入我的函数的第一个参数,我可以在fp + 8的内存地址访问它。

现在说我想要有两个局部变量,这些变量通常位于堆栈中,因此我需要为这些变量腾出空间,我可以推送虚拟数据,或者只是修改堆栈指针,以最终结果

[  x  ]  <- sp + 0  <- fp - 8
[  x  ]  <- sp + 4  <- fp - 4
[  fp ]  <- sp + 8  <- fp + 0
[retadd] <- sp + 12 <- fp + 4
[  a  ]  <- sp + 16 <- fp + 8
[  b  ]  <- sp + 20 <- fp + 12
[stuff]  <- sp + 24 <- fp + 16

现在帧指针开始变得非常有意义,因为我将堆栈指针与我的参数相对于堆栈指针混合在一起,同样也会被忽略,第一个参数曾经是sp + 8,现在它处于sp + 16,编译器或程序员将不得不跟踪函数中的每一点,以知道哪里是一切,相当可行,但有时候并非如此。

但即使我们与堆栈指针混淆,帧指针也不会移动; 我们没有碰它,所以我们的第一个参数仍然是在fp + 8。 由于堆栈添加和删除了东西,或者即使它没有触及从初始保存和设置中指定的帧指针,在函数的最后,我们可以使用传入的参数和局部变量整个功能中已知的偏移量。

在返回之前,我们将堆栈指针重新调整到指向帧指针的位置

[  fp ]  <- sp + 0  <- fp + 0
[retadd] <- sp + 4  <- fp + 4
[  a  ]  <- sp + 8  <- fp + 8
[  b  ]  <- sp + 12 <- fp + 12
[stuff]  <- sp + 16 <- fp + 16

那么我们弹出框架指针来恢复调用者帧指针,这样它们就不会被其他函数搞乱

[retadd] <- sp + 0
[  a  ]  <- sp + 4
[  b  ]  <- sp + 8
[stuff]  <- sp + 12

那么我们从使用栈指针指向的地址的函数返回

[  a  ]  <- sp + 0
[  b  ]  <- sp + 4
[stuff]  <- sp + 8

那么调用函数会清理堆栈,使其开始使调用变得有趣

[stuff]  <- sp + 0

有许多网页和书籍谈论堆栈基础知识,太多了。


你明白堆栈只是内存中的一个地方,你是正确的。 与寄存器相比,堆栈相当大。

你可以看看堆栈,就像一堆煎饼。 栈的属性是,yu只能添加或删除顶层的元素。

有两个寄存器,帮助组织这个内存结构。 第一个是(E)SP,它是堆栈指针的简称。 另一个是(E)BP,它是一个基本指针。

为了理解为什么我们需要这两个寄存器,我们需要查看堆栈允许的操作。 有PUSH和POP。

PUSH有两件事:

SUB ESP,4
MOV [ESP],REGISTER,

这减少了堆栈指针,并将寄存器保存到新的位置。

POP做相反的事情:

MOV REGISTER,[ESP]
ADD ESP,4

这将堆栈顶部的内容移动到寄存器,并相应地移动指针。

现在让我们看看函数使用它的参数的方式。

在函数开始时,我们可以通过[ESP + 4],[ESP + 8]访问参数。 但是当我们想要一些局部变量时会发生什么? 改变ESP将使上述语句无效。

这就是Base Pointer的用途。在每个函数的开始,我们都有所谓的prolog:

PUSH EBP
MOV EBP,ESP

这保存了以前的基本指针,并保存了堆栈指针,这样我们就可以获得参数的偏移量,而不用担心堆栈指针发生变化。

在该函数结束时,您将看到一个epilog,它涉及弹回EBP的旧值。


使用EBP作为基础或框架指针是可选的。 有些编译器(如Microsoft)可以选择禁用帧指针,在这种情况下,EPB被释放为用作通用寄存器,并且所有堆栈相对引用都作为ESP的偏移量。

在16位实模式下,SP不能用作内存操作数的基址寄存器或索引,因此BP必须用于堆栈相对引用。

链接地址: http://www.djcxy.com/p/79897.html

上一篇: How do stack and registers work in assembler?

下一篇: Passport.js is not passing user to request in req.login()