【问题标题】:Assembly executable doesn't show anything (x64)程序集可执行文件不显示任何内容 (x64)
【发布时间】:2020-05-05 20:26:13
【问题描述】:

非常简单的汇编介绍代码。
似乎可以通过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      
/***************************************************************/

【问题讨论】:

  • 如果您可以将 cmets 翻译成英文并解释您期望的输出类型(我想与您上面列出的 C 程序的输出相同),事情会变得容易得多。
  • 对我来说,它就像您评论中的 C 代码一样工作。你确定你正在编译和运行你认为的东西吗?
  • 您应该在 call printf 之前将 %al 归零,因为您不使用任何 SSE 寄存器作为参数。不过,这不太可能导致这个问题。您可以尝试通过strace 运行程序,当然也可以使用调试器。
  • 等等,我刚刚在 gNewSense 4 VM 中尝试过,我可以在那里重现问题。毕竟我也许能弄清楚这一点。
  • @Jester 关于需要将%al 归零是正确的。这样做,它的工作原理。完整的答案和解释很快就会到来。

标签: gcc assembly x86-64 calling-convention abi


【解决方案1】:

tl;dr:在call printf 之前执行xorl %eax, %eax

printf 是一个可变参数函数。以下是 System V AMD64 ABI 对可变参数函数的看法:

对于可能调用使用可变参数或标准参数的函数的调用(无原型 调用或调用在声明中包含省略号 (...) 的函数)%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 的缓解措施使间接跳跃非常缓慢,因此不再值得这样做。 或者不这样做;请参阅 cmets。 相反,它们会进行更简单的检查:如果有任何向量寄存器,则将它们全部保存。使用此代码,al 的错误值并不是灾难,因为它只是意味着将不必要地复制向量寄存器。

【讨论】:

  • Spectre 的缓解措施使间接跳转变得非常缓慢 - 只有当你使用 lfence 或其他东西来装备它们时才会变慢,而 GCC 通常默认情况下不会这样做。我认为这种变化早于 Spectre。可能只是因为间接分支更难预测,而且当你有一个 FP arg 并没有太多成本时,FP printf 比转储额外的寄存器要少得多。 (特别是在具有良好 OoO exec 和大型存储缓冲区的现代 CPU 上。)有趣的发现;我不知道 gcc variadic code-gen 除了检查 AL!=0 之外还做了什么。
  • @Ajna 因为问题是它跳跃得很厉害,值不是 10,所以它可能最终会跳转到某个指令的中间,而该指令恰好不是其他有效指令,因此收到SIGILL非法指令。
  • @Ajna 不,这不是 gNewSense 的问题。这是您的代码的问题。您的代码违反了 ABI 的规则之一,而恰好新系统比旧系统对您违反的规则更宽容(即,在较新的系统上,它只是稍微慢一点,而不是完全破坏)。
  • @Ajna:错误的 asm 代码意外/碰巧工作并不罕见。其他 ABI 违规(例如修改调用保留寄存器)通常也不会导致简单调用者出现问题,但会破坏其他代码。将代码扔在墙上,看看在 asm 中的效果比在其他语言中更差。不要依赖于反复试验。 (虽然它可以找到绝对不起作用的东西,例如,像这里它在一个测试系统上中断的地方。)
  • @Ajna:问题中违反 ABI 的代码是从哪里来的?你直到现在才这么说,但我想这解释了为什么你一直认为它一定是 gNewSense 中的一个错误,即使在解释了该代码中的错误之后。即使您知道自己在做什么而只是忘记了某些事情,错误也会偶然发生。出于同样的原因,故意试错是不安全的,在碰巧工作的系统上进行测试时,很容易错过此类错误。从 C 编译器输出开始或与 C 编译器输出进行比较通常是一个好主意;编译器在遵循调用约定时不会出错。
猜你喜欢
  • 2013-06-11
  • 2016-01-26
  • 1970-01-01
  • 2012-12-30
  • 1970-01-01
  • 1970-01-01
  • 2021-09-30
  • 1970-01-01
  • 2018-11-08
相关资源
最近更新 更多