subl在这里做什么?
所以...我编译成汇编器,用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是什么:它们将当前帧指针存储在堆栈中,然后将帧指针寄存器的值设置为堆栈指针的值。
谢谢!
编译器为本地人保留堆栈空间以及其它任何需要的内容。 我不确定它为什么保留了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()`
为了回答你的问题,“马上行动?” - 指令引用用来指示该值在指令操作码中编码,而不是像寄存器或内存位置那样到达其他地方。
要回答这些编号问题:
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.
没有存储器到存储器的指令格式。 (严格地说,您可以使用movs
或push mem
, pop mem
进行内存到内存的操作,但在同一条指令中不会占用两个内存操作数)
“立即”意味着该值被编码到指令中。 例如,要在ebx的地址中存储15个:
movl $15,(%ebx)
15是“即时”值。
括号使它使用寄存器作为指向内存的指针。
3) movl 8(%ebp),%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,%esp
和popl %ebp
并保存少量代码字节的旧指令。 它实际上做的是撤消对堆栈的更改并恢复调用者的ebp。 被调用函数必须在x86 ABI中保留ebp
。
在进入函数时,编译器执行subl $ 24,%esp来为本地变量腾出空间,有时候临时存储器没有足够的寄存器来保存。
在你脑海中“想象”堆栈框架的最好方法是将它看作坐在堆栈上的结构。 想象结构的第一个成员是最近“推动”的价值观。 所以当你推入堆栈时,想象一下在结构的开始处插入一个新成员,而其他成员没有移动。 当你从堆栈中“弹出”时,你会得到虚构结构的第一个成员的值,并且该结构的第一行会从存在中消失。
堆栈帧操作通常只是移动堆栈指针,以便在我们称为堆栈帧的虚构结构中创建更多或更少的空间。 从堆栈指针中减去只是在结构的起始处一步放入多个虚构成员。 添加到堆栈指针使得第一个这么多的成员消失。
您发布的代码的结尾并不典型。 该jmp
通常是一个ret
。 编译器很聪明,并做了“尾部调用优化”,这意味着它只是清理它对堆栈做了什么并跳转到f
。 当f(2)
返回时,它实际上会直接返回给调用者(而不是返回到您发布的代码)