首页 > 解决方案 > 程序集可执行文件不显示任何内容 (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    $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      
/***************************************************************/

标签: gccassemblyx86-64calling-conventionabi

解决方案


tl;博士:做xorl %eax, %eax之前call printf

printf是一个可变参数函数。以下是 System V AMD64 ABI 关于可变参数函数的说明:

对于可能调用使用 varargs 或 stdargs 的函数的调用(无原型调用或对声明中包含省略号 (...) 的函数的调用),%al18用作隐藏参数以指定使用的向量寄存器的数量。的内容%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 和/或 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并不是灾难,因为它只是意味着向量寄存器将被不必要地复制。


推荐阅读