堆栈是C语言程序运行时一个必须的记录函数调用路径和参数的空间。堆栈提供函数调用框架,具有传递参数,保存函数返回地址,提供局部变量空间等功能。了解堆栈存在的意义和编译器对堆栈使用的规则是深入理解操作系统核心代码的基础。
1.堆栈寄存器和堆栈操作
-
与堆栈相关的寄存器有两个:esp和ebp。
- esp,堆栈指针,指向栈顶
- ebp,基址指针,指向栈底
-
堆栈操作
1.push 进栈
栈顶指针减少4个字节(栈是由高地址向低地址增长的),例如 pushl %eax相当于:subl $4, %esp movl %eax, (%esp)
2.pop 出栈
栈顶地址增加4个字节,例如,popl %eax相当于:movl (%esp), %eax add $4, %esp
3.call
eip,程序计数器,总是指向下一条指令的位置。在进行函数调用时,会执行call指令,将eip中内容指向对应函数的位置。call具体执行过程如下:
首先将eip的值压如栈顶,然后将eip执行被调用函数的入口地址。具体指令如下:pushl %eip movl 0x12345, %eip
- ret
弹出原来保存在栈顶的原eip中的值,放入eip。
- ret
2.函数的堆栈框架
每一个函数都会维持一个堆栈。堆栈框架会包裹函数执行体。如下如所示。
如上图所示,每一个被调用函数在执行体年后都有:
pushl %ebp
movl %esp, %ebp
movl %ebp,%esp
popl %ebp
ret
以上指令就构成了函数调用框架。
3.分析一段小程序
源文件test.c
#include
#include
void p1(char c)
{
printf(%c\n",c);
}
int p2(int x,int y)
{
return x + y;
}
int main()
{
char c = 'a';
int x,y,z;
x = 1;
y = 2;
p1(c);
z = p2(x,y);
printf("%d = %d + %d\n",z,x,y);
}
首先使用gcc -g 生成test.c的可执行文件test
然后使用 objdump -S 获得test的反汇编文件test.s
可以使用gcc -S test.c -mpreferred-stack-boundary=2 -o test.s 为了方便我们理解我们这里设置gcc采用4字节对齐。(gcc默认是16字节对齐,这是因为CPU提供一个SSE功能(Streaming SIMD Extensions)。SSE 加入新的 8 个128Bit寄存器(XMM0~XMM7),正好每个寄存器大小为16字节。往XMM0~XMM7里存放数据,只能一次16字节一次16字节,所以呢,内存中数据当然要采用16字节对齐,为使用SSE作准备啦。)
.LC0:
.string "%c\n"
.text
p1:
pushl %ebp
movl %esp, %ebp
subl $12, %esp
movl 8(%ebp), %eax
movb %al, -4(%ebp)
movsbl -4(%ebp),%eax
movl %eax, 4(%esp)
movl $.LC0, (%esp)
call printf
leave
ret
p2:
pushl %ebp
movl %esp, %ebp
movl 12(%ebp), %eax
addl 8(%ebp), %eax
popl %ebp
ret
.LC1:
.string "%d = %d + %d\n"
.text
main:
pushl %ebp
movl %esp, %ebp
subl $32, %esp
movb $97, -13(%ebp)
movl $1, -12(%ebp)
movl $2, -8(%ebp)
movsbl -13(%ebp),%eax
movl %eax, (%esp)
call p1
movl -8(%ebp), %eax
movl %eax, 4(%esp)
movl -12(%ebp), %eax
movl %eax, (%esp)
call p2
movl %eax, -4(%ebp)
movl -8(%ebp), %eax
movl %eax, 12(%esp)
movl -12(%ebp), %eax
movl %eax, 8(%esp)
movl -4(%ebp), %eax
movl %eax, 4(%esp)
movl $.LC1, (%esp)
call printf
leave
ret
接下来,我们分析一下堆栈是如何传递参数,调用函数的。
3.1 main
(1)
pushl %ebp
movl %esp, %ebp
以上两条指令属于函数调用框架,作用就是为main函数建议一个新的空的堆栈。
(2)
subl $32, %esp
movb $97, -13(%ebp)
movl $1, -12(%ebp)
movl $2, -8(%ebp)
第一条指令的目的是在栈中开辟32字节的空间,存储局部变量。编译器会事先扫描函数中局部变量个数和大小,预分配给一个空间,供存储局部变量和参数调用。
剩下三条指令为将局部变量压栈。
(3)
movsbl -13(%ebp),%eax
movl %eax, (%esp)
将参数c=‘a’压栈
(4)
call p1
相当于:
pushl %eip
movl p1, %eip
(5)p1
pushl %ebp
movl %esp, %ebp
subl $12, %esp
(6)
movl 8(%ebp), %eax
movb %al, -4(%ebp)
movsbl -4(%ebp),%eax
movl %eax, 4(%esp)
movl $.LC0, (%esp)
(7)
call printf
leave
ret
leave指令相当于:
movl %ebp, %esp
pop %ebp
ret相当于:
popl %eip(*)
(8)mian
movl -8(%ebp), %eax
movl %eax, 4(%esp)
movl -12(%ebp), %eax
movl %eax, (%esp)
以上指令就是参数调用的过程
(9)p2
进入p2函数,过程和p1函数调用过程相似。
以上就是函数调用堆栈的过程。