编译器通常是否将寄存器用于“预期”目的?

时间:2017-04-06 16:35:24

标签: assembly compilation compiler-construction x86 x86-64

我一直在学习汇编,我读过四个主要的x86通用寄存器(eax,ebx,ecx和edx)都有预期或建议的目的。例如,eax是累加器寄存器,ecx用作循环的计数器,依此类推。大多数编译器是否尝试将寄存器用于建议的目的,或者是否忽略了寄存器“应该”的用途,只是将值分配给下一个可用的寄存器?

另外,在查看x64寄存器时,我注意到添加了额外的8个通用寄存器,如果忽略rbp,rsp,rsi和rdi,则会将gp寄存器的总数增加到12个(因为它们没有通用用途),如果包括它们,则为16。在普通用户程序(即浏览器,文字处理器等,而不是需要大量寄存器的加密程序)中,这些寄存器中有多少通常在任何给定时间使用?像Firefox这样的程序是否常常同时使用所有12/16普通寄存器,或者它们只使用一个子集,因为它们没有足够的变量来填充它们?我将通过拆解二进制文件来了解这一点,看看一般情况是什么,但我希望得到比我更有知识的人的回答。

此外,编译器通常使用半gp寄存器(rsi,rdi,rsp和rbp)作为通用目的,如果它们目前尚未用于非通用应用程序吗?我很好奇,因为我看到这些寄存器被列为“通用”,但即便如此我也可以想到这些寄存器不能用于一般存储的例子(例如,你不想存储变量到rbp和rsp,然后将值推送到堆栈!)。编译器是否可以尝试使用这些寄存器? x86和x64编译之间是否存在差异,因为x64处理器有更多可用的寄存器,因此不必将变量填充到任何可用的寄存器中?

2 个答案:

答案 0 :(得分:4)

所有GP寄存器都是通用的 只有在执行特定的(通常是遗留的)指令时,它们才具有特殊含义。

例如四元组rsirdirbprsp只有后者有特殊目的,这是由call等指令引起的,retpush等 如果你不使用它们,即使是隐含的(不太可能的情况),你可以将它用作累加器。

这个原则是通用的,编译器会利用它。

考虑这个人为的例子 [1]

void maxArray(int* x, int* y, int*z, short* w) {
    for (int i = 0; i < 65536; i++)
    {
        int a = y[i]*z[i];
        int b = z[i]*z[i];
        int c = y[i]*x[i]-w[i];
        int d = w[i]+x[i]-y[i];
        int e = y[i+1]*w[i+2];
        int f = w[i]*w[i];

        x[i] = a*a-b+d; 
        y[i] = b-c*d/f+e;
        z[i] = (e+f)*2-4*a*d;
        w[i] = a*b-c*d+e*f;
    }
}

由GCC汇编到此列表中

maxArray(int*, int*, int*, short*):
        push    r13
        push    r12
        xor     r8d, r8d
        push    rbp
        push    rbx
        mov     r12, rdx
.L2:
        mov     edx, DWORD PTR [rsi+r8*2]    
        mov     ebp, DWORD PTR [r12+r8*2]
        movsx   r11d, WORD PTR [rcx+r8]
        mov     eax, DWORD PTR [rdi+r8*2]
        movsx   ebx, WORD PTR [rcx+4+r8]
        mov     r9d, edx
        mov     r13d, edx
        imul    r9d, ebp
        imul    r13d, eax
        lea     r10d, [rax+r11]
        imul    ebx, DWORD PTR [rsi+4+r8*2]
        mov     eax, r9d
        sub     r10d, edx
        imul    ebp, ebp
        sub     r13d, r11d
        imul    eax, r9d
        imul    r11d, r11d
        sub     eax, ebp
        add     eax, r10d
        mov     DWORD PTR [rdi+r8*2], eax
        mov     eax, r13d
        imul    eax, r10d
        cdq
        idiv    r11d
        mov     edx, ebp
        sub     edx, eax
        mov     eax, edx
        lea     edx, [0+r9*4]
        add     eax, ebx
        mov     DWORD PTR [rsi+r8*2], eax
        lea     eax, [rbx+r11]
        imul    r9d, ebp
        imul    r11d, ebx
        add     eax, eax
        imul    edx, r10d
        add     r9d, r11d
        imul    r10d, r13d
        sub     eax, edx
        sub     r9d, r10d
        mov     DWORD PTR [r12+r8*2], eax
        mov     WORD PTR [rcx+r8], r9w
        add     r8, 2
        cmp     r8, 131072
        jne     .L2
        pop     rbx
        pop     rbp
        pop     r12
        pop     r13
        ret

您可以看到使用了大多数GP寄存器(我没有计算过它们),包括rbprsirdi
寄存器的用途均不限于其规范形式。

注意在此示例中,rsirdi用于加载和读取(每个寄存器)一个数组,这是巧合。
这些寄存器用于传递前两个整数/指针参数。

int sum(int a, int b, int c, int d)
{
    return a+b+c+d;
}

sum(int, int, int, int):
        lea     eax, [rdi+rsi]
        add     eax, edx
        add     eax, ecx
        ret

答案 1 :(得分:2)

最初(如在16位8086中),寄存器的功能比后来的x86处理器更受限制。只有BX,BP,SI和DI可用于寻址存储器,并且使用CISC样式的指令更常见,这些指令通过一条指令执行了许多操作。

例如,LOOP指令递减CX,将其与零进行比较,如果仍为正则跳转。如果你看一下为当前系统生成的代码,你不太可能看到它,而是DEC和JNE。后者需要更多的代码空间,但允许您使用任何寄存器。

80386和32位模式解除了寻址的大部分限制,允许所有寄存器用作指针。此外,更复杂的指令已经过时了,我认为这与处理器本身中增加的乱序执行和其他优化技术有关。

因此,在大多数情况下,没有什么理由可以区别对待寄存器。当然,ESP / RSP仍然是堆栈指针。