我一直在学习汇编,我读过四个主要的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处理器有更多可用的寄存器,因此不必将变量填充到任何可用的寄存器中?
答案 0 :(得分:4)
所有GP寄存器都是通用的 只有在执行特定的(通常是遗留的)指令时,它们才具有特殊含义。
例如四元组rsi
,rdi
,rbp
,rsp
只有后者有特殊目的,这是由call
等指令引起的,ret
,push
等
如果你不使用它们,即使是隐含的(不太可能的情况),你可以将它用作累加器。
这个原则是通用的,编译器会利用它。
考虑这个人为的例子 [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寄存器(我没有计算过它们),包括rbp
,rsi
和rdi
。
寄存器的用途均不限于其规范形式。
注意在此示例中,rsi
和rdi
用于加载和读取(每个寄存器)一个数组,这是巧合。
这些寄存器用于传递前两个整数/指针参数。
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仍然是堆栈指针。