【问题标题】:What does subl do here?subl 在这里做什么?
【发布时间】:2011-01-28 17:51:31
【问题描述】:

所以...我正在使用 gcc -S -O2 -m32 编译成汇编程序:

void h(int y){int x; x=y+1; f(y); f(2); }

它给了我以下信息:

.file   "sample.c"
.text
.p2align 4,,15
.globl h
.type   h, @function
 h:
pushl   %ebp
movl    %esp, %ebp
subl    $24, %esp
movl    8(%ebp), %eax
movl    %eax, (%esp)
call    f
movl    $2, 8(%ebp)
leave
jmp f
.size   h, .-h
.ident  "GCC: (GNU) 4.4.3 20100127 (Red Hat 4.4.3-4)"
.section    .note.GNU-stack,"",@progbits

现在我知道什么是 pushl 和 movel:它们将当前帧指针存储到堆栈中,然后将帧指针寄存器的值设置为堆栈指针的值。

  1. 但我不知道 subl $24, %esp 是什么。我知道它将堆栈指针向下移动了 24 个字节。正确吗?
  2. 顺便说一下什么?
  3. 为什么 movl 8(%ebp), %eax 使用 8?是8个字节吗?这是为了适应 h 的返回值 + 参数 y 吗?还是我完全离开这里。所以这意味着从堆栈指针回溯 8 个字节?
  4. movl $2, 8(%ebp) 有什么作用?它将 contant 2 复制到帧指针前 8 个字节的位置。当我们调用 f 时,帧指针是否发生了变化?如果是,则 8(%ebp) 指向 f 的参数位置。
  5. 请假有什么用?它如何“删除”堆栈帧?我的意思是你不能只删除一段记忆。在文档中它说它确实 mov(esp, ebp), pop ebp

谢谢!

【问题讨论】:

  • 有趣的是,下面的答案被标记为已接受,尽管它实际上并没有对问题 1 做出解释。Here 是另一个对问题 1 进行解释的问题/答案。

标签: assembly


【解决方案1】:

要回答这些编号的问题:

1) subl $24,%esp

表示esp = esp - 24

GNU AS 使用 AT&T 语法,这与 Intel 语法相反。 AT&T 的目的地在右边,英特尔的目的地在左边。 AT&T 也明确说明了论点的大小。英特尔试图推断或强迫您明确表达。

堆栈在内存中向下增长,内存在和之后 esp 是堆栈内容,低于 esp 的地址是未使用的堆栈空间。 esp 指向最后一个压入堆栈的东西。

2) x86 指令编码主要允许以下内容:

movl rm,r   ' move value from register or memory to a register
movl r,rm   ' move a value from a register to a register or memory
movl imm,rm ' Move immediate value.

没有内存到内存的指令格式。 (严格来说,您可以使用movspush mempop mem 进行内存到内存的操作,但不能在同一条指令上使用两个内存操作数)

“立即”表示该值被直接编码到指令中。例如,将 15 存储在 ebx 中的地址:

movl $15,(%ebx)

15 是一个“立即”值。

括号使它使用寄存器作为指向内存的指针。

3) movl 8(%ebp),%eax

意思是,

  • 取ebp的值
  • 向其添加 8(但不修改 ebp),
  • 将其用作地址(括号),
  • 从该地址读取 32 位值,
  • 并将值存储在 eax 中

esp 是堆栈指针。 在 32 位模式下,堆栈上的每个 push 和 pop 都是 4 字节宽。通常,大多数变量无论如何都会占用 4 个字节。所以你可以说 8(%ebp) 的意思是,从栈顶开始,将值 2 (4 x 2 = 8) int 放入栈中。

通常,32 位代码使用 ebp 来指向函数中局部变量的开头。在 16 位 x86 代码中,没有办法将堆栈指针用作指针(很难相信,对吧?)。所以人们所做的就是将sp 复制到bp 并使用bp 作为本地帧指针。当 32 位模式(80386)出现时,这变得完全没有必要,它确实有办法直接使用堆栈指针。不幸的是,ebp 使调试更容易,因此我们最终继续在 32 位代码中使用 ebp(如果使用 ebp,则很容易进行堆栈转储)。

幸运的是,amd64 为我们提供了一个新的 ABI,它不使用 ebp 作为帧指针,64 位代码通常使用 esp 访问局部变量,ebp 可用于保存变量。

4)上面解释过

5) leave 是一条旧指令,它只执行 movl %ebp,%esppopl %ebp 并节省一些代码字节。它实际上所做的是撤消对堆栈的更改并恢复调用者的 ebp。被调用的函数必须在 x86 ABI 中保留ebp

在进入函数时,编译器执行 subl $24,%esp 来为局部变量腾出空间,有时还为没有足够寄存器容纳的临时存储空间。

在您的脑海中“想象”堆栈框架的最佳方式是将其视为位于堆栈上的结构。虚构结构的第一个成员是最近“推送”的值。因此,当您压入堆栈时,想象在结构的开头插入一个新成员,而其他成员都没有移动。当您从堆栈中“弹出”时,您将获得虚构结构的第一个成员的值,并且该结构的(第一)行从存在中消失。

堆栈帧操作主要只是移动堆栈指针,以便在我们称为堆栈帧的那个假想结构中腾出更多或更少的空间。从堆栈指针中减去只需一步将多个虚构成员放在结构的开头。添加到堆栈指针会使前面的这么多成员消失。

您发布的代码结尾不典型。 jmp 通常是 ret。编译器很聪明,做了一个“尾调用优化”,这意味着它只是清理它对堆栈所做的事情并跳转到f。当f(2) 返回时,它实际上会直接返回给调用者(而不是返回您发布的代码)

【讨论】:

    【解决方案2】:

    编译器在堆栈上为本地和它可能有的任何其他需求保留空间。我不确定为什么要保留 24 个字节(它似乎不需要或全部使用)。

    当调用函数f()时,它不是使用push指令将参数放入堆栈,而是使用简单的movl到它保留的最后一个位置:

    movl    8(%ebp), %eax    ; get the value of `y` passed in to `h()`
    movl    %eax, (%esp)     ; put that value on the stack for call to `f()`
    

    这里发生的一件更有趣(在我看来)的事情是编译器如何处理对f(2) 的调用:

    movl    $2, 8(%ebp)      ; store 2 in the `y` argument passed to `h()`
                             ;     since `h()` won't be using `y` anymore
    leave                    ; get rid of the stackframe for `h()`
    jmp f                    ; jump to `f()` instead of calling it - it'll return
                             ;     directly to whatever called `h()`
    

    回答您的问题,“顺便问一下?” - 这就是指令引用用来表明该值是在指令操作码中编码的,而不是在寄存器或内存位置等其他地方。

    【讨论】:

    • esp 寄存器中减去实际上是“保留”堆栈上的空间。就好像你已经将 24 个字节的东西(6 个 32 位值)推送到堆栈上——不同之处在于你不能依赖现在堆栈上的那些“推送”值是什么——你所知道的是您可以使用该内存而不必担心后续推送会覆盖它。
    • 它减去是因为堆栈在内存中“向下”增长。 'immed' 是一个即时值,很像你引用的那一行中的 $24。
    • 还要注意编译器做了两个优化: 对 x 的赋值被移除,因为值没有被使用。对 f 的第二次调用变成了 jmp 指令,因为它在调用完成时不需要返回到 h 函数。
    • 减法只是将堆栈指针向下移动,本质上是“推”堆栈上的东西。然后这会在堆栈中为局部变量创建空间,以作为帧指针的偏移量访问。
    • 回答您的问题 3 - 它从帧指针移动 8 个字节以获取函数的第一个参数。你是对的,它是 8,因为有 4 个字节用于保存在堆栈中的旧帧指针,4 个字节用于返回地址。该指令用于将参数放入寄存器。
    猜你喜欢
    • 1970-01-01
    • 2012-03-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多