堆栈和寄存器如何在汇编程序中工作?
我知道有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()