所以......我正在使用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:它们将当前帧指针存储到堆栈中,然后将帧指针寄存器的值设置为堆栈指针的值 .
-
但我不知道 subl $24, %esp 是什么 . 我明白它会将堆栈指针向下移动24个字节 . 正确?
-
方式是什么?
-
为什么 movl 8(%ebp), %eax 使用8?是8个字节吗?这是否适应返回值参数 y 到h?或者我完全离开这里 . 那么这意味着从堆栈指针中回顾8个字节?
-
movl $2, 8(%ebp) 做什么?它将第2个副本复制到帧指针之前的8个字节的位置 . 当我们调用f时,帧指针是否改变了?如果是 - 那么8(%ebp)指向f的参数位置 .
-
什么离开呢?怎么能"remove"堆叠帧?我的意思是你不能删除一块记忆 . 在文档中它说它确实 mov(esp, ebp), pop ebp .
谢谢!
2 回答
编译器正在为堆栈保留本地空间以及它可能具有的任何其他需求 . 我不确定为什么它保留了24个字节(它似乎不需要或者全部使用它) .
当调用函数
f()
时,它不是使用push指令将参数放在堆栈上,而是使用一个简单的movl
到它保留的最后一个位置:一个更有趣的(在我看来)这里发生的事情是编译器如何处理对
f(2)
的调用:要回答你的问题,“顺便说一下?” - 这就是指令引用用来指示值在指令操作码中编码而不是像寄存器或存储器位置那样到达其他地方 .
要回答这些有问题的问题:
1)
subl $24,%esp
意味着
esp = esp - 24
GNU AS使用AT&T语法,这与Intel语法相反 . AT&T的目的地在右侧,英特尔的目的地在左侧 . AT&T也明确说明了参数的大小 . 英特尔试图推断它或迫使你明确 .
堆栈在内存中增长,esp中和esp之后的内存是堆栈内容,低于esp的地址是未使用的堆栈空间 . esp指向推入堆栈的最后一件事 .
2) x86指令编码主要允许以下内容:
没有内存到内存的指令格式 . (严格来说,您可以使用
movs
或push mem
,pop mem
执行内存到内存操作,但不能在同一指令上占用两个内存操作数)“立即”表示该值被编码到指令中 . 例如,要在ebx中的地址存储15:
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,%esp
和popl %ebp
并保存一些代码字节 . 它实际上做的是撤消对堆栈的更改并恢复调用者的ebp . 被调用的函数必须在x86 ABI中保留ebp
.在进入函数时,编译器执行了$ 24,%esp以便为局部变量腾出空间,有时甚至临时存储它没有足够的寄存器来保存 .
The best way to "imagine" the stack frame 在你的脑海中将它看作是一个坐在堆栈上的结构 . 虚构结构的第一个成员是最近的"pushed"值 . 因此,当您推送到堆栈时,想象一下在结构的开头插入一个新成员,而其他成员都没有移动 . 当您从堆栈中获得"pop"时,您将获得虚构结构的第一个成员的值,并且该结构的(第一)行将从存在中消失 .
堆栈框架操作主要是移动堆栈指针,以便在我们称之为堆栈帧的虚构结构中产生更多或更少的空间 . 从堆栈指针中减去只需将多个虚构成员放在结构的开头一步 . 添加到堆栈指针使得第一个成员消失 .
您发布的代码的结尾并不典型 .
jmp
通常是ret
. 编译器很聪明,并且做了一个"tail call optimization",这意味着它只是清理它对堆栈做了什么并跳转到f
. 当f(2)
返回时,它实际上将直接返回给调用者(而不是返回到您发布的代码)