首页 文章

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

提问于
浏览
2

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

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

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

在main中,在调用函数之前,函数的任何参数都从EAX推送到ESP,然后对函数进行“调用”,该函数将返回地址(main中“call”之后的下一个地址)推送到堆栈(我认为返回地址)用函数的参数堆叠在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

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

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

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

    [     ]  <- sp
    

    我不记得x86的细节,一些处理器的堆栈指针指向堆栈“顶部”的当前项 . 和其他处理器堆栈指针指向第一个空闲位置 . 我将选择一种方法并使用它运行,然后根据需要进行调整 . 此外,一些处理器堆栈自然地“向下”增长,这意味着当您向堆栈添加内容时,堆栈指针地址会变小 . 有些堆叠会长大,但这种情况不太常见,但是如果你的堆叠的便条卡堆叠在一些 table 上而不是反向重力并且它们通过某种力量推入天花板会更有意义 .

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

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

    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
    

    有许多网页和书籍谈论堆栈基础知识,太多不足以提及 .

  • 5

    你理解堆栈只是内存中的一个位置,你是对的 . 与寄存器相比,堆栈非常大 .

    你可以看一下堆栈,就像一堆煎饼 . 堆栈的属性是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

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

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

相关问题