深入理解函数调用堆栈

堆栈是C语言程序运行时一个必须的记录函数调用路径和参数的空间。堆栈提供函数调用框架,具有传递参数,保存函数返回地址,提供局部变量空间等功能。了解堆栈存在的意义和编译器对堆栈使用的规则是深入理解操作系统核心代码的基础。

1.堆栈寄存器和堆栈操作

  1. 与堆栈相关的寄存器有两个:esp和ebp。

    1. esp,堆栈指针,指向栈顶
    2. ebp,基址指针,指向栈底
  2. 堆栈操作
    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
    
    1. ret
      弹出原来保存在栈顶的原eip中的值,放入eip。

2.函数的堆栈框架

每一个函数都会维持一个堆栈。堆栈框架会包裹函数执行体。如下如所示。


函数调用堆栈框架.png

如上图所示,每一个被调用函数在执行体年后都有:

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函数建议一个新的空的堆栈。


1.png

(2)

subl    $32, %esp
movb    $97, -13(%ebp)
movl    $1, -12(%ebp)
movl    $2, -8(%ebp)

第一条指令的目的是在栈中开辟32字节的空间,存储局部变量。编译器会事先扫描函数中局部变量个数和大小,预分配给一个空间,供存储局部变量和参数调用。
剩下三条指令为将局部变量压栈。


2.png

(3)

movsbl  -13(%ebp),%eax
movl    %eax, (%esp)

将参数c=‘a’压栈

3.png

(4)

call    p1

相当于:

 pushl %eip
 movl p1, %eip
4.png

(5)p1

pushl   %ebp
movl    %esp, %ebp
subl    $12, %esp
5.png

(6)

movl    8(%ebp), %eax
movb    %al, -4(%ebp)
movsbl  -4(%ebp),%eax
movl    %eax, 4(%esp)
movl    $.LC0, (%esp)
6.png

(7)

call    printf
leave
ret

leave指令相当于:

movl %ebp, %esp
pop %ebp

ret相当于:

popl %eip(*)
7.png

(8)mian

movl    -8(%ebp), %eax
movl    %eax, 4(%esp)
movl    -12(%ebp), %eax
movl    %eax, (%esp)

以上指令就是参数调用的过程


8.png

(9)p2
进入p2函数,过程和p1函数调用过程相似。

以上就是函数调用堆栈的过程。

你可能感兴趣的:(深入理解函数调用堆栈)