assembly 在使用汇编语言x86-64时,用于存储计算值的寄存器顺序是否有约定?

x7yiwoj4  于 11个月前  发布在  其他
关注(0)|答案(2)|浏览(151)

我知道这个问题已经提了很多次了,但是我还没有找到一个明确的答案,所以很抱歉,我还是有疑问。
当用汇编语言编程时,对于个人问题,不一定要与外部库链接,但希望遵守书面或非书面约定,我应该使用什么寄存器以及以什么顺序?
我想如果我需要的话,可以使用函数参数(RDI,RSI,RDX,RCX,R8,R9)使用的相同寄存器来存储值。此外,如果我需要更多,我开始从R10-R15开始,但我不确定这是否是一种习惯做法。
另外,在寄存器之前选择堆栈的标准是什么?
PS。再次抱歉问一个问题,提交到网站上几次了,但我只是想做“正确”。谢谢。

sbdsn5lh

sbdsn5lh1#

在任何调用约定中,在调用保留寄存器之前使用call-clobbered寄存器,除了你想在函数调用中生存的值。我对链接Q&A的回答涵盖了很多关于如何/何时使用寄存器的问题。
对于Windows以外的操作系统上的x86-64,请参阅 * What registers are preserved through a linux x86-64 function call * 以了解调用约定的详细信息,例如哪些寄存器被调用。(非R12-R15)。
在x86-64上的call-clobbered编译器中,它们在大多数情况下都是等效的,尽管许多指令对AL或EAX的编码较小,分别为8位或32位立即数。(为了更好的I缓存密度和前端解码吞吐量,较小的机器码大小通常更好,其他条件相同。)

注意,add eax, 4使用标准的3字节add r/m32, imm8编码是最短的,而不是5字节add eax, imm32。8位立即操作的AL总是节省空间,但EAX适用于32或64-位操作只为不适合imm 8的常量节省空间。或者为test eax, immediate节省空间,因为没有test r/m, imm8编码。当然,您可以始终使用如果你想要低位,用test al, 1代替test eax, 1;你唯一失去的是像test eax, -128这样的东西,用负数的符号扩展来检查除了低7位之外的所有位。
有关涉及某些寄存器(涉及RBP、R12和R13作为基础)的某些寻址模式的额外代码大小的详细信息,请参见 * Why are RBP and RSP called general-purpose registers? *。
RBP/RSP Q&A还提到了一个事实,即大多数“遗留”寄存器(不是R8-R15)都有一些特殊的指令隐式地使用它们。比如RCX(特别是CL)是唯一用于可变计数移位计数的寄存器,比如shr edx, cl,除非你有BMI 2 shrx edx, eax, esi(这是大代码大小,但在Intel上更有效,是单uop)。
另一种情况下,所有其他不相等:Which Intel microarchitecture introduced the ADC reg,0 single-uop special case?-即使在Skylake上,adc al, 0短格式编码是2 uops,没有明显的原因,只固定在桤木湖。

r7xajy2e

r7xajy2e2#

另外,在寄存器之前选择堆栈的标准是什么?
有几种情况--很明显,您需要传递一个参数,而这个参数必须在堆栈上传递(因为您没有其他参数寄存器)。
此外,当我们对变量(包括临时变量)进行分析时,我们可以确定那些在调用之前定义并在调用之后使用的变量,而对于那些在调用中不存在的变量,我们更倾向于使用被调用破坏的寄存器。
那些在调用中有效的寄存器需要在函数调用后仍然存在的存储,而这种存储可以是调用保留寄存器,也可以是基于堆栈的内存--因为这两种存储选择都满足在函数调用后仍然存在的标准。
如果我们确定在调用中处于活动状态的变量具有较低的动态使用计数,则堆栈内存可能更受欢迎,而当动态使用计数较高时,调用保留寄存器可能更受欢迎。
这是由于使用调用保留寄存器带来的开销,即通常在函数序言中必须保留其传入值,而通常在函数尾声中必须恢复相同的值。
如果对该变量的引用(定义或使用)的动态计数很低,比如说,2(一个定义和一个使用),那么堆栈内存可能比使用调用保留寄存器稍微更有效。
首先是一个具有高动态使用计数的示例:

int sum = 0;
for ( int i = 0; i < len; i++ ) {
    sum += f(i);
}

字符串
这里我们可以估计sum有一个定义(int sum = 0;)加上一个读取和一个写入(sum += ...;)* 每次循环迭代 *。因为我们可能假设循环执行几次迭代,所以对寄存器的重复访问将是对存储器访问的改进-指示了调用保留寄存器。通过使用调用保留寄存器,与直接在循环内使用存储器相比,我们有效地将存储器读/写操作移动到序言/尾声,以保存这样的寄存器。
在另一个例子中,我们有递归的斐波那契(非常低效,但在这里使用迂腐)如下:

int fib(int n) {
    if (n<=1) return n;
    return fib(n-1) + fib(n-2);
}


这里有两个需要特别关注的变量,一个是n,另一个是一个未命名的临时变量,用于保存其中一个递归调用的中间结果。
n作为参数在函数入口时有效定义,使用三次(动态计算递归路径),但这些用法中只有一个在调用中实际上是活的。因此,我们自然会使用参数寄存器中的n,但可能在序言中或在第一次递归调用之前,我们将n移到调用幸存的存储器。由于使用计数较低(在调用之前定义,在调用之后使用一次),基于堆栈的存储器适合于此变量(只要我们尽可能长时间地从传入参数寄存器中使用它)。
临时寄存器保存第一次递归调用的返回值,它的使用次数也很低,在第二次递归调用之前定义,在第二次递归调用之后使用(通过加法运算符)。因此,它也是基于堆栈的内存比调用保留寄存器更好的候选寄存器。保留寄存器的开销只比直接使用堆栈内存的开销略高。
我们还要注意,返回地址是基于堆栈的内存的一个很好的候选,原因有二,即这是指令集call/ret的工作方式(x86),并且使用场景是返回地址通常在其他调用中有效,但只需要一次(动态计数)。不过,其它指令集在寄存器中提供返回地址,且这使其类似于需要相同的跨越调用的实时分析的寄存器参数。
这里有很多需要注意的事项;使用基于堆栈的内存需要分配堆栈空间,但是,让我们注意一下,可以一次性分配几个堆栈槽,这在一定程度上减轻了分配/解除分配开销此外,可能存在已经支付了用于呼叫保留寄存器的开销的情况,然而,寄存器在该点可用,并且可用于所考虑的目的。根据ISA,许多其他问题也会纳入分析中,例如x86-64与RISC V。例如,被分类为呼叫保留的可用寄存器的数量。

相关问题