subl在这做什么?

时间:2010-03-17 19:27:31

标签: assembly

所以...我正在使用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个字节吗?这是为了适应返回值+参数 y 到h?或者我完全离开这里。那么这意味着从堆栈指针回看8个字节?
  4. movl $ 2,8(%ebp)做什么?它将第2个副本复制到帧指针前8个字节的位置。当我们调用f时,帧指针是否改变了?如果是 - 那么8(%ebp)指向f。
  5. 的参数位置
  6. 休假做什么?它如何“移除”堆栈框架?我的意思是你不能删除一块记忆。它在文档中说它确实 mov(esp,ebp),pop ebp
  7. 谢谢!

2 个答案:

答案 0 :(得分:5)

回答这些有问题的问题:

1) subl $24,%esp

表示esp = esp - 24

GNU AS使用AT& T语法,这与英特尔语法相反。 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执行内存到内存操作,但不能在同一条指令上执行两个内存操作数)

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

movl $15,(%ebx)

15是“即时”值。

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

3) movl 8(%ebp),%eax

手段,

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

esp是堆栈指针。 在32位模式下,堆栈上的每个推送和弹出都是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)返回时,它实际上将直接返回给调用者(而不是返回到您发布的代码)

答案 1 :(得分:4)

编译器正在为堆栈保留本地空间以及它可能具有的任何其他需求。我不确定为什么它会保留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()`

回答你的问题,“顺便说一下?” - 这就是指令引用用于指示值在指令操作码中编码而不是像寄存器或存储器位置那样在其他地方编码。