程序堆栈调用变化分析

0x01 源码分析


void func(int a)
{
    int b = a;
}

void main()
{
    int a = 2;
    func(2);
}


0x02 汇编代码


编译命令:gcc -fno-stack-protector -mpreferred-stack-boundary=2 -ggdb test.c -o test

汇编代码如下:

(gdb) disassemble main
Dump of assembler code for function main:
   0x080483ea <+0>:     push   %ebp
   0x080483eb <+1>:     mov    %esp,%ebp
   0x080483ed <+3>:     sub    $0x4,%esp
   0x080483f0 <+6>:     movl   $0x2,-0x4(%ebp)
   0x080483f7 <+13>:    push   $0x2
   0x080483f9 <+15>:    call   0x80483db <func>
   0x080483fe <+20>:    add    $0x4,%esp
   0x08048401 <+23>:    nop
   0x08048402 <+24>:    leave
   0x08048403 <+25>:    ret
End of assembler dump.


(gdb) disassemble func
Dump of assembler code for function func:
   0x080483db <+0>:     push   %ebp
   0x080483dc <+1>:     mov    %esp,%ebp
   0x080483de <+3>:     sub    $0x4,%esp
   0x080483e1 <+6>:     mov    0x8(%ebp),%eax
   0x080483e4 <+9>:     mov    %eax,-0x4(%ebp)
   0x080483e7 <+12>:    nop
   0x080483e8 <+13>:    leave
   0x080483e9 <+14>:    ret
End of assembler dump.


0x03 堆栈变化分析


一句一句的来说明:

  1. push %ebp

    将%ebp压入栈中,%ebp叫做基址指针寄存器,即每执行一个过程都会用一个ebp来记录stack底的地址,因为栈底是固定的,所以可以通过ebp偏移来寻址栈参数和变量(临时变量和局部变量)。每次当函数执行完毕之后我们都需要恢复到调用函数,ebp的值也需要变为调用函数的基址。所以,每次函数调用第一步都是讲ebp的值入栈,方便后面恢复。

    如图所示,这里的地址是假设的:

    1546432572114

  2. mov %esp,%ebp

    将%esp的值赋值给%ebp,因为每次入栈,出栈,esp的值都是自动变化的,所以1中的入栈操作,会改变esp的值,即esp内容会减4,就是新的栈顶,这个栈顶就是被调用函数的栈基址。所以需要把这个地址给ebp。

    如图所示:

    1546432598006

  3. sub $0x4,%esp

    将esp下移4个字节,这是为了预先给该函数使用的变量分配空间

    如图所示:

    1546432624969

  4. movl $0x2,-0x4(%ebp)

    将main函数中的a赋值为2,上一步中esp下移已经为变量a分配了空间,只需要将该2这个值存入该地址空间即可,因为ebp是固定不变的,所以通过ebp+偏移来找到对应的地址。

    如图所示:

    1546432642125

  5. push $0x2

    将func的参数存入栈中,这里是值传递,传递的参数是2,所以是把2这个值入栈。

    如图所示:

    1546432673329

  6. call 0x80483db

    调用func函数,call指令包含两步操作:1是call后面的指令地址入栈,即0x080483fe <+20>: add $0x4,%esp的0x080483fe这个地址入栈;2是跳转到call指令所指的地址,即call 0x80483db <func>的0x080483db,这里跳转是修改eip的寄存器的值。

    如图所示:

    1546485248233

  7. push %ebp(这里是跳转到func函数的第一条指令)

    call指令跳转后,进入func子函数,第一步就是记录调用函数的ebp,将ebp的值入栈,ebp的值就是main函数的栈基址。下图中我们也可以看到此时ebp却是保存着main函数的栈基址。

    如图所示:

    1546432698927

  8. mov %esp,%ebp

    保存完main函数的基址之后,就可以用ebp保存调用的子函数的基址了,这样我们就可以使用ebp,通过偏移存储子函数的变量了。

    如图所示:

    1546432712954

  9. sub $0x4,%esp
    为子函数的变量预分空间,因为只有一个int型的变量b,所以只需要分配4个字节即可。
    如图所示:

1546432723833

  1. mov 0x8(%ebp),%eax

    在main函数中,调用了子函数func,并传递了参数a,他是一个值2,所以在main函数的栈中,保存了该参数,参照步骤5。从图中可以看到就是0x0FF8地址的值。此时ebp的值为0x0FF0,所以ebp+0x8,刚好就是0x0FF8

    如图所示:

    1546432739768

  2. mov %eax,-0x4(%ebp)

    这一步是func函数的赋值操作,将参数的值赋值给func函数的内部变量b。在进入func子函数之后,esp已经为内部变量分配了空间(步骤9),就是0x0FEC这个地址,即esp保存的地址,但是esp是动态改变的,所以这里我们用ebp的偏移来找到为变量分配的地址。

    如图所示:

    1546432753271

  3. leave

    leave包含两步操作:1.是将ebp的值赋值给esp,即恢复到进入func子函数的初始状态(步骤8);2.是将记录的main函数的基址重新赋值给ebp,在步骤7中,我们将main函数的基址入栈,ebp地址中保存的就是main函数的基址。使用pop可以同时将esp恢复到进入func函数之前的状态。这是的堆栈和步骤6跳转到func之前的状态是一样的。

    如图所示:

    1546488097691

  4. ret

    ret指令执行两步操作:1是将esp保存的地址的值出栈;2是跳转到这个值表示的地址。所以首先出栈即将esp保存的地址0x0FF4里存储的值0x080483fe出栈,并且跳转到这个地址即将eip指向0x080483fe这个值。这个地址保存的就是main函数调用func函数之后的指令

    如图所示:

    1546432779743

  5. add $0x4,%esp

    func函数调用完毕,传递的参数不在使用,所以把esp的指针上移4个字节,相当于释放了这个空间。这里不用pop是因为后续没有使用,不需要找一个寄存器保存这个值。

    如图所示:

    1546489489653

后续过程就不用再赘述了,leave和ret就和func函数一样的。

猜你喜欢

转载自blog.csdn.net/yrx0619/article/details/85688871