【问题标题】:Why should certain registers be saved? What could go wrong if not? [duplicate]为什么要保存某些寄存器?如果没有,会出什么问题? [复制]
【发布时间】:2021-11-23 21:38:46
【问题描述】:

在MASM中使用汇编x86_64,有一些寄存器的值在使用前必须保存;它们必须被推入堆栈。它们是:RBP、RBX、RSP、R12、R13、R14 和 R15。 以RBX为例,为什么要保存?

直接使用它而不保存它会有什么后果?

如果有后果,它会影响我当前正在运行的程序还是会影响其他东西?

寄存器是否在汇编之外使用?意思是 CPU 使用这些寄存器而无需用户在汇编中进行任何操作,或者例如当用户启动程序(视频游戏或其他)或正在使用进程时?

【问题讨论】:

  • 任何编译的程序都会使用寄存器。如果您的函数在程序中被调用,那么调用者可能希望您保留这些寄存器。在返回它们的代码时更改它们,当它们假定另一个值已被保留时,您可能会导致错误。保留寄存器的确切选择有些随意,它只是商定的调用约定的一部分。如果您编写自己的函数并从代码中调用它们,而不是其他代码,那么您可以发明任何您喜欢的调用约定。
  • 看这里:stackoverflow.com/a/56178078/421195 和这里:developer.arm.com/documentation/ihi0042/latest A subroutine must preserve the contents of the registers r4-r8, r10, r11 and SP (and r9 in PCS variants that designate r9 as v6)
  • 寄存器是否在汇编之外使用? - 一切 都是汇编语言(或真正的机器代码)。包括编译器生成的代码。在多任务操作系统下,CPU 寄存器对每个线程都是私有的;操作系统的上下文切换有效地将它们虚拟化。
  • @learn123456 不。每个程序都有自己的一组寄存器。但是您程序中的其他函数可能期望当它们调用您的某个函数时,您的函数将保留rbx
  • 不,我的意思是 software 线程。就像我在回答中所说的那样,通过操作系统的上下文切换保存/恢复或注册会虚拟化架构寄存器,因此每个软件线程都可以拥有自己的,无论它运行在哪个逻辑核心上。

标签: assembly x86-64 cpu-registers calling-convention


【解决方案1】:

调用约定是函数如何相互调用并传递/返回参数而不用踩到对方的脚趾。这包括编译器生成的代码调用你的手写函数:在你的函数返回后,它允许对寄存器内容进行假设。

请参阅What are callee and caller saved registers?,了解有关保留调用与调用破坏寄存器的更多信息,以及使用其中一些的调用约定如何让您(或编译器)创建高效的 asm。

Why does Windows64 use a different calling convention from all other OSes on x86-64? 讨论了 x86-64 System V 调用约定的优势,以及如何/为何做出其设计选择。另见this re: why (some) args are passed in registers


违反调用约定会发生什么

破坏调用保留寄存器所带来的问题类似于Is function call messing with other registers than %rax?,但会影响将本地变量保存在调用保留寄存器中的非错误调用者。 (不像那个和其他几个 SO 问题,人们试图在循环中调用一个函数,但将他们的循环变量保存在允许函数破坏的寄存器中)。

例如像这样的循环可能是无限的,或者立即结束,或者如果它是使用 RBP 作为帧指针的调试版本并且您破坏了它,甚至会崩溃。基本上,想象一下在调用者的任何局部变量上调用函数的后果。

  for (int i=0; i<10; i++) {
     int tmp = my_handwritten_asm_function(i);
     printf ("%d %d\n", i, tmp);
  }

由于循环中有函数调用,编译器会将i 保留在像EBX 这样的调用保留寄存器中(如果它没有完全展开循环并且必须mov ecx, 1 / call / mov ecx, 2 /call 等)如果你的 asm 函数修改了 EBX,那显然很糟糕。

(编译器优化可以使函数调用发生在寄存器中的东西上-保留的寄存器不会被踩到。)


当然,有些调用者可能依赖于某个寄存器值,尤其是调用main 的代码之类的东西。因此,在手写main 中违反调用约定/ABI 的情况并不少见。

不破坏是不是正确性的证据;尤其是在汇编语言中,危险代码“发生工作”的情况并不少见,但仍然会以不同的周围代码出现问题的方式被破坏。


寄存器是否在汇编之外使用?

一切 CPU 运行的都是汇编语言,没有外部。 (实际上是机器码,但与汇编大约 1:1 对应)。通常这是由高级语言编译器生成的代码。有关查看编译器生成的 asm 的更多信息,请参阅 How to remove "noise" from GCC/clang assembly output?

但是 CPU 寄存器对于多任务操作系统下的每个线程都是私有的;操作系统的上下文切换有效地将它们虚拟化。因此,只有实际调用您的函数(直接或间接)的代码才会受到影响。

【讨论】:

  • 因此,无论如何,程序员负责手动保存那些调用保留寄存器,这意味着编译器不会自动保存它们。这是一个约定。因此,如果编译器没有自动保存 RAX(这是从头开始),也没有保存 RBX(不是从头开始,应该保存),那么这就是我如何使用它们,或者我的约定跟着,对吧?
  • @learn123456:是的,完全正确。 What does it mean that "registers are preserved across function calls"? 是关于此的问答。违反调用约定(偶然或故意)在 asm 中很容易,而不仅仅是机器代码,因为汇编程序根本无法帮助您。他们不懂函数,只懂标签和跳转/调用指令,因为他们不需要。 (MASM 的 proc foo/endp 语法有点例外,例如 uses ebx 或任何使其神奇地将 ret 组装成 pop ebx/ret 或我认为的东西的声明。)跨度>
【解决方案2】:

补充@Peter 的出色回答:

有点过于简单化了,一个程序通过给它一个接一个地执行一系列机器代码指令来告诉处理器要做什么,处理器执行得非常快,但完全相信程序会在不知道或不知道的情况下做一些有用的事情关心那将是什么。

随着这些机器代码指令的执行,它们会改变进程的状态(进程是这些指令的执行环境),这就是一条指令与另一条指令通信的方式;这最终会逐步构建程序答案/结果。 CPU 寄存器是该进程状态的基础,并且大多数短期的进程状态修改都发生在这些 CPU 寄存器中。一条指令修改一个寄存器,下一条指令查看该寄存器以用于程序的下一部分。一切都在程序及其机器代码指令序列的控制之下。

进程的一部分包含所有 CPU 寄存器的值,称为线程。操作系统虚拟化 CPU,因此每个线程似乎都有自己的一组 CPU 寄存器。寄存器及其值的连续性对线程至关重要,因为该状态用于在机器代码指令之间进行通信,以逐个构建更大的结果。

机器代码程序可以做的一件事是调用函数。当一个函数调用另一个函数时,调用者实际上被挂起,等待被调用函数完成并返回其答案/数据。一旦被调用的函数返回给它的调用者,暂停的调用者就会恢复,并继续在该函数调用之后找到它自己的编程。

为了让调用者能够正常恢复,它的挂起状态必须是完整的。否则,它可能会以不正确的变量值恢复,从而导致不良行为。

当一个函数调用另一个函数时,该函数可能仍会调用更多函数,依此类推。为了支持这一点,线程有一个调用堆栈的概念,并且存在等待最近一个函数完成的当前挂起函数的概念——这就是调用链的概念。调用链中的函数(被挂起)依赖于保留的寄存器,它们的值构成了这些挂起函数的重要状态。然而,由于 CPU 寄存器数量很少,但在任何给定程序中可能有数万个函数,因此函数有必要重用已被挂起函数使用的 CPU 寄存器——尽管只要在使用中寄存器返回到相同的值,调用者将不明智,将根据需要恢复。

因此,如果被调用者未能正确保存保留的寄存器,则某些调用者的某些状态将被意外修改,结果可能发生任何事情,b/c 这与特定的编程有关一些挂起的函数,其状态被意外修改。

【讨论】:

    猜你喜欢
    • 2018-01-16
    • 2023-03-23
    • 1970-01-01
    • 1970-01-01
    • 2021-05-24
    • 2021-08-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多