程序集可执行文件不显示任何内容 (x64)

Assembly executable doesn't show anything (x64)

非常简单的汇编介绍代码。
似乎通过 gcc -o prog1 prog1.s 编译正常,然后 ./prog1 只是跳过一行并且什么都不显示,就像等待代码不询问的输入一样。怎么了?
在 VMware 上的 64 位 gNewSense 运行 中使用 gcc (Debian 4.7.2-5) 4.7.2。 代码:

/*
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    , %rsp
  movq    %rbx, -8(%rbp)
  movq    %r12, -16(%rbp)
/********************************************************/

  movl  [=10=], %ebx  /* ebx = 0; */
  movq  $nums, %r12  /* r12 = &nums */

L1:
  cmpl  , %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  , %ebx  /* ebx += 1; */
  addq  , %r12  /* r12 += 4; */
  jmp  L1         /* goto L1; */

L2:  
/***************************************************************/
/* mantenha este trecho aqui e nao mexa - finalizacao!!!!      */
  movq  [=10=], %rax  /* rax = 0  (valor de retorno) */
  movq  -8(%rbp), %rbx
  movq  -16(%rbp), %r12
  leave
  ret      
/***************************************************************/

tl;dr:在 call printf.

之前执行 xorl %eax, %eax

printf 是可变参数函数。以下是 System V AMD64 ABI 对可变参数函数的说明:

For calls that may call functions that use varargs or stdargs (prototype-less calls or calls to functions containing ellipsis (. . . ) in the declaration) %al18 is used as hidden argument to specify the number of vector registers used. The contents of %al do not need to match exactly the number of registers, but must be an upper bound on the number of vector registers used and is in the range 0–8 inclusive.

你违反了那个规则。你会看到你的代码第一次调用 printf 时,%al 是 10,这超过了 8 的上限。在你的 gNewSense 系统上,这是 [=14 开头的反汇编=]:

printf:
   sub    [=10=]xd8,%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 and/or glibc 不想保存比你使用的更多的向量寄存器,它也不想做一堆条件分支。每条保存向量寄存器的指令都是 4 个字节,所以它需要向量寄存器保存指令的末尾,减去 al * 4 字节,然后跳转到那里。这导致刚好足够的指令执行。由于你有超过 8 个,它最终跳得太远,并在它刚刚执行的跳转指令之前着陆,从而形成了一个无限循环。

至于为什么它不能在现代系统上重现,这里是他们 printf:

开头的反汇编
printf:
   sub    [=11=]xd8,%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 的错误值并不是灾难,因为它只是意味着向量寄存器将被不必要地复制。