printf在x86-64 Linux上使用较旧的gcc时,在AL = 10时陷入无限循环

e0bqpujr  于 2022-11-30  发布在  Linux
关注(0)|答案(1)|浏览(143)

非常简单的程序集介绍代码。
看起来通过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      
/***************************************************************/
nwwlzxa7

nwwlzxa71#

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

printf是一个varargs函数。以下是System V AMD 64 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和/或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。Spectre的缓解措施使间接跳转变得非常慢,所以不再值得使用这个技巧了。* 或者不需要;* 相反,他们做了一个简单得多的检查:如果有任何向量寄存器,则保存它们。对于这段代码,错误的al值并不是灾难,因为它只是意味着向量寄存器将被不必要地复制。

相关问题