【问题标题】:Understanding the un-optimized asm for C that dereferences an uninitialized pointer, causing a segmentation fault了解取消引用未初始化指针的 C 的未优化 asm,从而导致分段错误
【发布时间】:2018-11-04 08:05:39
【问题描述】:

我设置了一个实验,看看它是否有效

int *p;
p[0] = 3;

我的想法是编译器给p一个随机值,我可以把它当作一个数组。

但结果是分段错误,我看不懂汇编代码。

   0x0000000000401530 <+0>: push   %rbp
   0x0000000000401531 <+1>: mov    %rsp,%rbp
   0x0000000000401534 <+4>: sub    $0x30,%rsp
   0x0000000000401538 <+8>: mov    %ecx,0x10(%rbp)
   0x000000000040153b <+11>:    mov    %rdx,0x18(%rbp)
   0x000000000040153f <+15>:    callq  0x402170 <__main>
   0x0000000000401544 <+20>:    mov    -0x8(%rbp),%rax
=> 0x0000000000401548 <+24>:    movl   $0x3,(%rax)
   0x000000000040154e <+30>:    mov    $0x0,%eax
   0x0000000000401553 <+35>:    add    $0x30,%rsp
   0x0000000000401557 <+39>:    pop    %rbp
   0x0000000000401558 <+40>:    retq    

我在 google 上搜索,mov 是 Intel 风格,movl 是 AT&T 风格。这两种风格怎么会融合在一起?

在这一行:

mov    -0x8(%rbp),%rax

似乎将地址 rbp-0x8 的值移动到注册 rax,对吧? 这个“-0x8(%rbp)”是随机值到p的吗?

我认为 %rax 不是 p,因为在下一行 CPU 将 $0x3 给 %rax。似乎 %rax 是数组的第一个内存。

如何解释这个汇编代码?谢谢。

【问题讨论】:

  • 谢谢。为什么将 32 位值 3 移动到寄存器 rax 是分段错误?
  • 因为它试图将立即值移动到您的程序无权访问的某个地址。
  • @iBug:AT&T 语法仅在不明确时才需要操作数大小的后缀(例如直接源和内存目标)。 mov $0, %eax 是 32 位操作数大小,由 EAX 寄存器隐含。普通的mov 绝不意味着16 位。这种反汇编只在不明确的地方使用后缀,不像objdump -d 在每条指令上都使用后缀。 (@Andy:整个列表都是纯 AT&T 语法,总是在右边显示目的地)。
  • @iBug:不,这将是一个错误:将其放入bar.S 并与gcc -c bar.S 组装,您将收到此汇编错误消息bar.S:1: Error: no instruction mnemonic suffix given and no register operands; can't size instruction。即操作数大小不明确,因此汇编器拒绝汇编它。
  • @iBug mov 不一定是16位操作! movw 是 16 位 mov,不带后缀的 mov 移动其操作数所需的位数。

标签: c pointers gcc assembly


【解决方案1】:

我不是汇编专家,但代码看起来很清楚,所以我会尝试解释一下。


   0x0000000000401530 <+0>: push   %rbp
   0x0000000000401531 <+1>: mov    %rsp,%rbp

(上)这是编译器在未开启优化时生成的一些“过程”代码。它保存了 RSP(64 位堆栈指针)的状态。

   0x0000000000401534 <+4>: sub    $0x30,%rsp

这会在堆栈上为局部变量保留额外空间。

   0x0000000000401538 <+8>: mov    %ecx,0x10(%rbp)
   0x000000000040153b <+11>:    mov    %rdx,0x18(%rbp)

这会将 argc 和 argv 保存到堆栈中,因为您进行了调试构建,因此所有变量都必须在内存中。 (它使用的空间在返回地址上方,main 的调用者保留空间。这称为影子空间,是 Windows x64 调用约定的一个特性。)

   0x000000000040153f <+15>:    callq  0x402170 <__main>

这会调用某种早期初始化函数。它可能使用 argc 和 argv(仍在寄存器中),也可能不使用;我们无法从代码中看出。

   0x0000000000401544 <+20>:    mov    -0x8(%rbp),%rax

这会将未初始化的堆栈内存加载为int *p 的值。自动变量放在堆栈上。你读了p而没有先写它,编译器只是读取它为int *p;选择的堆栈槽中已经存在的任何垃圾或零

=> 0x0000000000401548 <+24>:    movl   $0x3,(%rax)

这一行将rax指向的地址设置为立即数3。

p 的值在rax 中,所以这是你的p[0] = 3;,取消引用p 持有的任何垃圾。你崩溃是因为它没有碰巧指向可写内存。 (覆盖内存中的一些随机 dword 几乎不会更好,但至少你的代码不会在 here 崩溃,只是可能在稍后的某个时间点,如果 p 的垃圾值恰好是有效的指针。)

   0x000000000040154e <+30>:    mov    $0x0,%eax

这会将寄存器eax 设置为零,effectively setting rax to zero, too。 Windows x64(像每个标准调用约定一样)使用 RAX 作为返回值,因此这是在 main 底部实现隐式 return 0;

   0x0000000000401553 <+35>:    add    $0x30,%rsp
   0x0000000000401557 <+39>:    pop    %rbp
   0x0000000000401558 <+40>:    retq

将指针状态恢复到函数调用之前。

【讨论】:

  • call __main 之前的指令并没有“准备”它,它们只是将main 的函数参数保存在堆栈中。 (其中 x64 Windows 调用约定在 RCX=argc, RDX=argv 中传递)。实际上它只是将它们溢出到函数入口(进入返回地址上方的阴影空间),因为它是一个调试版本;它以后不会重新加载它们。我们无法判断__main 是否真的接受任何参数,因为main 的参数在被调用时仍在参数传递寄存器中。我们无法判断__main 是否在看他们。
  • 我不想花时间写我自己的答案,所以我对你的答案做了实质性的修改。这些解释现在已经得到专家的认可。 :) 你以前很接近,包括正确解决问题的确切原因,但我将call __main 之前的代码分成更多单独的块,因为它们不相关。
  • @PeterCordes 谢谢,组装专家。我想我也有了更好的理解。
  • @iBug 嗨。您能否告诉我这个问题的哪一部分可以在不编辑上下文的前提下得到改进?我想删除反对票。
猜你喜欢
  • 2013-07-26
  • 1970-01-01
  • 2021-08-13
  • 2020-11-10
  • 1970-01-01
  • 2019-10-04
  • 2019-03-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多