【问题标题】:Help with understanding a very basic main() disassembly in GDB帮助理解 GDB 中一个非常基本的 main() 反汇编
【发布时间】:2011-06-12 16:44:28
【问题描述】:

嘿嘿,

我编写了这个非常基本的 main 函数来试验反汇编,并希望了解底层发生了什么:

int main() {
  return 6;
}

使用 gdb 来 disas main 会产生这样的结果:

0x08048374 <main+0>:    lea    0x4(%esp),%ecx
0x08048378 <main+4>:    and    $0xfffffff0,%esp
0x0804837b <main+7>:    pushl  -0x4(%ecx)
0x0804837e <main+10>:   push   %ebp
0x0804837f <main+11>:   mov    %esp,%ebp
0x08048381 <main+13>:   push   %ecx
0x08048382 <main+14>:   mov    $0x6,%eax
0x08048387 <main+19>:   pop    %ecx
0x08048388 <main+20>:   pop    %ebp
0x08048389 <main+21>:   lea    -0x4(%ecx),%esp
0x0804838c <main+24>:   ret  

这是我对正在发生的事情以及我需要逐行帮助的最佳猜测:

lea 0x4(%esp),%ecx

将esp + 4的地址加载到ecx中。 为什么要在 esp 上加 4?

我在某处读到这是命令行参数的地址。但是当我做x/d $ecx 时,我得到了 argc 的值。 实际的命令行参数值存储在哪里?

and $0xfffffff0,%esp

对齐堆栈

pushl -0x4(%ecx)

将 esp 最初所在的地址压入堆栈。 这样做的目的是什么?

push %ebp

将基指针压入堆栈

mov %esp,%ebp

将当前堆栈指针移动到基指针中

push %ecx

将原始 esp + 4 的地址压入堆栈。 为什么?

mov $0x6,%eax

我想在这里返回 6 所以我猜返回值存储在 eax 中?

pop %ecx

将 ecx 恢复为堆栈上的值。 当我们返回时,为什么我们希望 ecx 为 esp + 4?

pop %ebp

将 ebp 恢复为堆栈上的值

lea -0x4(%ecx),%esp

将esp恢复到原来的值

ret

在组装方面我是一个 n00b,所以任何帮助都会很棒!另外,如果您看到任何关于我认为发生的错误的陈述,请纠正我。

非常感谢! :]

【问题讨论】:

  • 投票结束过于广泛。请关注每个问题您不确定的一点。
  • 我不认为这篇文章太宽泛了。是的,它确实有多个问题,但关注的“单点”是理解基本的拆卸。你的提议是我单独提出每个问题。这对我来说没有意义,因为每个问题都没有上下文。我不会向每个关于一条装配线的问题发送一些垃圾邮件。另外,这篇文章已有 4 年历史了。
  • 别担心,太宽泛是很主观的,我们可能都是对的 :-) 干杯。
  • 对于一个有这么多有效和有趣答案的问题,我真的不认为结束是完全正确的。

标签: linux assembly gdb x86 disassembly


【解决方案1】:

堆栈帧

函数体开头的代码:

push  %ebp
mov   %esp, %ebp

是创建所谓的堆栈框架,它是引用程序本地参数和对象的“坚实基础”。 %ebp 寄存器(如其名称所示)用作基址指针,它指向过程内本地堆栈的基址(或底部)。

进入程序后,栈指针寄存器(%esp)指向调用指令存放在栈上的返回地址(即调用后指令的地址) .如果您现在只调用ret,则该地址将从堆栈弹出到%eip(指令指针),并且代码将从该地址(call 之后的下一条指令)进一步执行。但我们还没有回来,不是吗? ;-)

然后,您推送%ebp 寄存器以将其先前的值保存在某处而不会丢失,因为您很快就会使用它。 (顺便说一句,它通常包含调用函数的基指针,当您查看该值时,您会发现以前存储的%ebp,这将再次是更高一级函数的基指针,因此您可以跟踪这样调用堆栈。)当您保存%ebp时,您可以将当前的%esp(堆栈指针)存储在那里,这样%ebp将指向相同的地址:当前本地堆栈的基址。 %esp 将在过程中来回移动,当您将在堆栈上推送和弹出值或保留和释放局部变量时。但是%ebp 将保持不变,仍然指向本地堆栈帧的底部。

访问参数

调用者传递给过程的参数被“埋在地下”(也就是说,它们相对于基数有偏移,因为堆栈向下增长)。您在%ebp 中有本地堆栈的基地址,其中包含%ebp 的先前值。在它下面(即4(%ebp)是返回地址。所以第一个参数将在8(%ebp),第二个在12(%ebp)等等。

局部变量

并且局部变量可以分配在基数之上的堆栈上(也就是说,它们相对于基数会有偏移量)。只需将 N 减去 %esp 并且您刚刚在堆栈上为局部变量分配了 N 字节,方法是将堆栈顶部移动到该区域上方(或准确地说,下方):-) 您可以参考这个area by negative 相对于%ebp 的偏移量,即-4(%ebp) 是第一个单词,-8(%ebp) 是第二个单词等等。记住(%ebp) 指向本地堆栈的底部,其中前一个%ebp 值已保存。因此,请记住在过程结束时尝试通过pop %ebp 恢复%ebp 之前将堆栈恢复到以前的位置。您可以通过两种方式做到这一点:
1. 可以通过将N 添加回%esp(堆栈指针)来仅释放局部变量,也就是说,移动堆栈的顶部,就好像这些局部变量从未存在过一样。 (好吧,它们的值将保留在堆栈中,但它们将被视为“已释放”并且可能被后续推送覆盖,因此引用它们不再安全。它们是死尸;-J)
2. 您可以通过简单地将%esp 从之前已固定到堆栈底部的%ebp 恢复到地面并释放所有本地空间。在进入过程并将%esp 保存到%ebp 之后,它将把堆栈指针恢复到它的状态。这就像在你搞砸了一些东西时加载以前保存的游戏;-)

关闭帧指针

通过添加开关 -fomit-frame-pointer,可以减少来自 gcc -S 的杂乱组装。它告诉 GCC 不要组装任何代码来设置/重置堆栈帧,直到它确实需要某些东西。请记住,它可能会使调试器感到困惑,因为它们通常依赖于堆栈帧来跟踪调用堆栈。但是如果你不需要调试这个二进制文件,它不会破坏任何东西。它非常适合释放目标,并且可以节省一些空间。

调用帧信息

有时您会遇到一些奇怪的汇编指令,这些指令从 .cfi 开始与函数头交错。这就是所谓的调用帧信息。调试器使用它来跟踪函数调用。但它也用于高级语言中的异常处理,这需要堆栈展开和其他基于调用堆栈的操作。您也可以在程序集中将其关闭,方法是添加一个开关 -fno-dwarf2-cfi-asm。这告诉 GCC 使用普通的旧标签而不是那些奇怪的 .cfi 指令,并在程序集的末尾添加一个特殊的数据结构,引用这些标签。这不会关闭 CFI,只是将格式更改为更“透明”的格式:程序员可以看到 CFI 表。

【讨论】:

    【解决方案2】:

    你的解释做得很好。当一个函数被调用时,返回地址会自动压入栈中,这就是为什么第一个参数 argc 被压回了 4(%esp)。 argv 将从 8(%esp) 开始,每个参数都有一个指针,后跟一个空指针。此函数将 %esp 的旧值推入堆栈,以便它可以在返回时包含原始的未对齐值。返回时 %ecx 的值无关紧要,这就是为什么它被用作 %esp 引用的临时存储。除此之外,你对一切都是正确的。

    【讨论】:

    • 谢谢,这很有帮助!
    【解决方案3】:

    关于您的第一个问题(命令行参数存储在哪里),函数的参数就在ebp 之前。我必须说,你的“真正的”主要从&lt; main + 10 &gt; 开始,它推动ebp 并将esp 移动到ebp。我认为 gcc 将 leas 的所有内容都搞砸了,只是为了在函数调用之前和之后替换 esp 上的常规操作(加法和减法)。通常一个例程是这样的(我做了一个简单的函数作为例子):

       0x080483b4 <+0>:     push   %ebp     
       0x080483b5 <+1>:     mov    %esp,%ebp
       0x080483b7 <+3>:     sub    $0x10,%esp            # room for local variables
       0x080483ba <+6>:     mov    0xc(%ebp),%eax        # get arg2
       0x080483bd <+9>:     mov    0x8(%ebp),%edx        # and arg1
       0x080483c0 <+12>:    lea    (%edx,%eax,1),%eax    # just add them
       0x080483c3 <+15>:    mov    %eax,-0x4(%ebp)       # store in local var
       0x080483c6 <+18>:    mov    -0x4(%ebp),%eax       # and return the sum
       0x080483c9 <+21>:    leave
       0x080483ca <+22>:    ret 
    

    也许您启用了一些优化,这可能会使代码更加复杂。 最后是的,返回值存储在eax。无论如何,你的解释是完全正确的。

    【讨论】:

      【解决方案4】:

      我认为从您的原始问题中唯一突出的是为什么您的代码中存在以下语句:

      0x08048381 <main+13>:   push   %ecx
      0x08048382 <main+14>:   mov    $0x6,%eax
      0x08048387 <main+19>:   pop    %ecx
      

      %ecx 在&lt;main+13&gt;&lt;main+19&gt; 的推送和弹出似乎没有多大意义 - 在this 示例中它们并没有真正做任何事情,但考虑一下这种情况您的代码在哪里调用函数调用

      系统无法保证对其他函数的调用(将设置自己的堆栈激活帧)不会重置寄存器值。事实上,他们可能会。因此,代码在堆栈上设置了一个保存的寄存器部分,代码使用的任何寄存器(除了已经通过常规堆栈设置保存的 %esp 和 %ebp 之外)都存储在堆栈中可能会将控制权交给当前代码块的“肉”中的函数调用。

      当这些潜在调用返回时,系统会将值从堆栈中弹出以恢复调用前寄存器值。如果您直接编写汇编程序而不是编译,您将自己负责存储和检索这些寄存器值。

      但是,在您的示例代码的情况下,没有函数调用 - 只有一条指令在 &lt;main+14&gt; 处设置返回值,但编译器不知道这一点,并将其寄存器保留为平常。


      如果您在&lt;main+14&gt; 之后添加将其他值推入堆栈的 C 语句,看看这里会发生什么会很有趣。如果我认为这是堆栈的已保存寄存器部分是正确的,那么您会希望编译器在&lt;main+19&gt; 之前插入自动pop 语句以清除这些值。 p>

      【讨论】:

        猜你喜欢
        • 2011-07-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-09-13
        相关资源
        最近更新 更多