非常简单的汇编介绍代码。似乎可以通过 gcc -o prog1 prog1.s
那么 ./prog1
只是跳过一行,什么都不显示,就像在等待一个代码不要求的输入。有什么问题吗?使用gcc(Debian 4.7.2-5)4.7.2在VMware.Code上运行的64位gNewSense。
/*
int nums[] = {10, -21, -30, 45};
int main() {
int i, *p;
for (i = 0, p = nums; i != 4; i++, p++)
printf("%d\n", *p);
return 0;
}
*/
.data
nums: .int 10, -21, -30, 45
Sf: .string "%d\n" # string de formato para printf
.text
.globl main
main:
/********************************************************/
/* mantenha este trecho aqui e nao mexa - prologo !!! */
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movq %rbx, -8(%rbp)
movq %r12, -16(%rbp)
/********************************************************/
movl $0, %ebx /* ebx = 0; */
movq $nums, %r12 /* r12 = &nums */
L1:
cmpl $4, %ebx /* if (ebx == 4) ? */
je L2 /* goto L2 */
movl (%r12), %eax /* eax = *r12 */
/*************************************************************/
/* este trecho imprime o valor de %eax (estraga %eax) */
movq $Sf, %rdi /* primeiro parametro (ponteiro)*/
movl %eax, %esi /* segundo parametro (inteiro) */
call printf /* chama a funcao da biblioteca */
/*************************************************************/
addl $1, %ebx /* ebx += 1; */
addq $4, %r12 /* r12 += 4; */
jmp L1 /* goto L1; */
L2:
/***************************************************************/
/* mantenha este trecho aqui e nao mexa - finalizacao!!!! */
movq $0, %rax /* rax = 0 (valor de retorno) */
movq -8(%rbp), %rbx
movq -16(%rbp), %r12
leave
ret
/***************************************************************/
tl;dr: 做 xorl %eax, %eax
之前 call printf
.
printf
是一个varargs函数。下面是System V AMD64 ABI对varargs函数的说明。
对于可能调用使用varargs或stdargs的函数的调用(无原型调用或调用声明中包含省略号(. . . )的函数)
%al
18 作为隐藏参数,用于指定使用的向量寄存器的数量。寄存器的内容是%al
不需要与寄存器的数量完全匹配,但必须是使用的向量寄存器数量的上限,并且是在0-8的范围内。
你打破了这个规则。你会发现,当你的代码第一次调用 printf
, %al
是10,比8的上界还大。在你的gNewSense系统上,这里是一个分解的开头的 printf
:
printf:
sub $0xd8,%rsp
movzbl %al,%eax # rax = al;
mov %rdx,0x30(%rsp)
lea 0x0(,%rax,4),%rdx # rdx = rax * 4;
lea after_movaps(%rip),%rax # rax = &&after_movaps;
mov %rsi,0x28(%rsp)
mov %rcx,0x38(%rsp)
mov %rdi,%rsi
sub %rdx,%rax # rax -= rdx;
lea 0xcf(%rsp),%rdx
mov %r8,0x40(%rsp)
mov %r9,0x48(%rsp)
jmpq *%rax # goto *rax;
movaps %xmm7,-0xf(%rdx)
movaps %xmm6,-0x1f(%rdx)
movaps %xmm5,-0x2f(%rdx)
movaps %xmm4,-0x3f(%rdx)
movaps %xmm3,-0x4f(%rdx)
movaps %xmm2,-0x5f(%rdx)
movaps %xmm1,-0x6f(%rdx)
movaps %xmm0,-0x7f(%rdx)
after_movaps:
# nothing past here is relevant for your problem
重要位的准C翻译是: goto *(&&after_movaps - al * 4);
. 为了提高效率,gcc和or glibc不想保存比你用的更多的向量寄存器,它也不想做一堆条件分支。每条保存向量寄存器的指令都是4个字节,所以它把向量寄存器保存指令的末尾,减去 al * 4
字节,并跳转到那里。这样的结果是,执行的指令刚好够用。由于你的指令超过了8条,结果跳得太靠后,落在了刚才的跳转指令之前,从而形成了一个无限循环。
至于为什么在现代系统上无法重现,下面是其开头的拆解。printf
:
printf:
sub $0xd8,%rsp
mov %rdi,%r10
mov %rsi,0x28(%rsp)
mov %rdx,0x30(%rsp)
mov %rcx,0x38(%rsp)
mov %r8,0x40(%rsp)
mov %r9,0x48(%rsp)
test %al,%al # if(!al)
je after_movaps # goto after_movaps;
movaps %xmm0,0x50(%rsp)
movaps %xmm1,0x60(%rsp)
movaps %xmm2,0x70(%rsp)
movaps %xmm3,0x80(%rsp)
movaps %xmm4,0x90(%rsp)
movaps %xmm5,0xa0(%rsp)
movaps %xmm6,0xb0(%rsp)
movaps %xmm7,0xc0(%rsp)
after_movaps:
# nothing past here is relevant for your problem
重要位的准C翻译是: if(!al) goto after_movaps;
. 为什么会有这种变化?我猜是Spectre。幽灵的缓解措施使间接跳跃变得非常缓慢,所以不再值得做这个技巧。 或者不值得;请看评论。 取而代之的是,他们做了一个更简单的检查:如果有任何矢量寄存器,那么就把它们全部保存起来。有了这段代码,你的坏值 al
并不是一场灾难,因为这只是意味着向量寄存器将被不必要地复制。