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

时间:2014-04-02 20:09:53

标签: assembly

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

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

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

在main中,在调用函数之前,函数的任何参数都从EAX推送到ESP,然后对函数进行“调用”,该函数将返回地址(main中“call”之后的下一个地址)推送到堆栈(I认为返回地址堆叠在ESP上,带有函数的参数,以便在调用函数时堆叠在EBP上。我认为这是错误的?),然后将EIP移动到函数的开头。

然后当调用该函数时,EBP被推(再次?这是因为在函数内部,EBP什么都没有?但是EBP寄存器是否已经包含来自main的某些值?)并且ESP值被推送EBP(这就是为什么我认为EBP是堆栈。此时所有东西都叠加在EBP上不是吗?)然后,ESP是“sub”的,具有一些值,为函数的局部变量提供空间。 (当ESP在函数入口处被推入EBP时ESP是否具有ESP值?还是被清空?)

在函数结束时,函数会“离开”和“退出”,它会擦除​​函数的堆栈帧(EBP?或ESP?或者只是“堆栈帧”,既不是EBP也不是ESP?如果它删除了EBP或ESP,EBP for main会发生什么?我读到EBP是从堆栈指针重新初始化但堆栈指针何时被推入堆栈?)然后“ret”,EIP移动到返回地址被推入“main”在对函数执行“调用”之前。

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

3 个答案:

答案 0 :(得分:5)

"堆栈"只是记忆。在处理器的某个地方,你有一个"堆栈指针"。关于堆栈的事情是你并不关心它在内存中的确切位置,一切都是相对于堆栈指针,堆栈指针加上或减去一些内存位置。

希望/假设您的堆栈有足够的空间来执行您的程序需要执行的操作(这是另一个主题)。所以在这方面,堆栈只是一堆内存,不仅仅是一个寄存器的数据。

将堆栈视为一堆东西,实际上是堆栈的内存位置,但也许是一堆索引卡,您可以在其上编写各种内容。图片通常有帮助。

[     ]  <- sp

我不记得x86的细节,一些处理器堆栈指针指向&#34;顶部&#34;堆栈。和其他处理器堆栈指针指向第一个空闲位置。我将选择一种方法并使用它运行,然后根据需要进行调整。还有一些处理器堆栈自然增长&#34; down&#34;意味着当您向堆栈添加内容时,堆栈指针地址会变小。有些堆叠会长大,但这种情况不太常见,但在视觉上却更有意义,如果你的一叠便条卡堆叠在一些桌子而不是反向重力上,并且它们会被一些力推入天花板。

所以我们在准备调用函数之前有上面的图片。让我们说堆栈指针指向堆栈的顶部,我们暂时不关心堆栈顶部的人或者什么,除了我们不应该触摸的某些数据之外堆栈的另一个属性,堆栈指针的一侧是公平游戏,堆栈指针的另一侧是你不应该触摸的某人的数据,除非它是你自己的数据。当你调用另一个函数时,确保堆栈指针使堆栈指针指向堆栈的顶部,推送不会破坏任何东西。

所以我们想要向函数传递两个参数,我们以相反的顺序推送它们,这样当函数被调用时它们看起来更自然,这是任意的并且基于编译器调用约定。只要规则始终如一,它与什么顺序无关紧要,反向推动虽然如此。

fun( a, b);

在我们推b之前

[stuff] <-sp

我们推b后

[  b  ] <- sp
[stuff] 

其中每个[item]是一些固定大小的堆栈上的一个内存位置,现在假定为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

有很多关于堆栈基础知识的网页和书籍,太多了。

答案 1 :(得分:2)

您理解堆栈只是内存中的一个位置,这是正确的。与寄存器相比,堆栈非常大。

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

有两个寄存器,有助于组织这种内存结构。第一个是(E)SP,它是Stack Pointer的缩写。另一个是(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

这将保存以前的Base Pointer,并保存堆栈指针,这样我们就可以获得参数的偏移量,而不必担心堆栈指针的变化。

在功能结束时,您将看到一个epilog,其中包括回显旧的EBP值。

答案 2 :(得分:0)

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

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