为什么要保存某些寄存器?否则会出什么问题



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

如果只是按原样使用,而不是保存,会有什么后果?

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

寄存器是否在程序集之外使用?这意味着CPU在汇编中使用这些寄存器时没有任何用户操作,或者例如当用户启动程序(视频游戏或其他什么)或正在使用进程时?

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

请参阅什么是被调用者和调用者保存的寄存器?了解更多关于保留调用寄存器与删除调用寄存器的信息,以及带有其中一些寄存器的调用约定如何让您(或编译器)创建高效的asm。

为什么Windows64使用与x86-64上所有其他操作系统不同的调用约定?讨论了x86-64 System V调用约定的优点,以及如何/为什么进行设计选择。另请参阅此re:为什么(某些)参数在寄存器中传递。


当您违反呼叫约定时会发生什么

破坏保留调用的寄存器可能会遇到这样的问题:函数调用是否干扰了%rax以外的其他寄存器?,但会影响那些将本地人保存在呼叫保留寄存器中的非窃听呼叫者。(不同于在那个和其他几个SO问题中,人们试图在循环中调用函数,但将循环变量保留在允许函数崩溃的寄存器中)。

例如,如果是使用RBP作为帧指针的调试构建,那么像这样的循环可能是无限的,或者立即结束,甚至崩溃。基本上,想象一下对函数的调用踩在调用方的任何本地变量上的后果。

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

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

(编译器优化可能会使函数调用发生在寄存器中,这是你从机器代码的简单音译中所没有想到的,因为它可以假设其局部变量私有的,并且其保留调用的寄存器不会被踩到。)


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

不破坏是而不是正确性的证据;尤其是在汇编语言中,危险代码";碰巧工作";,但是仍然会被破坏,这对于不同的周围代码来说是一个问题。


寄存器是否在程序集之外使用?

CPU运行的一切都是汇编语言,没有外部。(实际上是机器代码,但与程序集近似1:1对应)。这通常是编译器从高级语言生成的代码。参见How to remove";噪声";从GCC/clang汇编输出?了解有关查看编译器生成的asm的更多信息。

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

添加到@Peter的优秀答案:

过度简化一点,一个程序告诉处理器该做什么,给它一个接一个的机器代码指令序列来执行,处理器做得很快,但完全相信程序会做一些有用的事情,而不知道或不关心会是什么

当这些机器代码指令执行时,它们会改变进程的状态(进程是这些指令的执行环境),这就是一条指令与另一条指令通信的方式;这最终构建了一个接一个的程序答案/结果。CPU寄存器是进程状态的基础,进程状态的大多数短期修改都发生在这些CPU寄存器中。一条指令修改寄存器,下一条指令在该寄存器中查找程序的下一部分。所有程序的控制及其机器代码指令序列。

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

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

为了使调用方能够正常恢复,其挂起状态必须保持不变。否则,它可能会继续使用不正确的变量值,从而导致不良行为。

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

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

最新更新