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 堆栈变化分析
一句一句的来说明:
-
push %ebp
将%ebp压入栈中,%ebp叫做基址指针寄存器,即每执行一个过程都会用一个ebp来记录stack底的地址,因为栈底是固定的,所以可以通过ebp偏移来寻址栈参数和变量(临时变量和局部变量)。每次当函数执行完毕之后我们都需要恢复到调用函数,ebp的值也需要变为调用函数的基址。所以,每次函数调用第一步都是讲ebp的值入栈,方便后面恢复。
如图所示,这里的地址是假设的:
-
mov %esp,%ebp
将%esp的值赋值给%ebp,因为每次入栈,出栈,esp的值都是自动变化的,所以1中的入栈操作,会改变esp的值,即esp内容会减4,就是新的栈顶,这个栈顶就是被调用函数的栈基址。所以需要把这个地址给ebp。
如图所示:
-
sub $0x4,%esp
将esp下移4个字节,这是为了预先给该函数使用的变量分配空间
如图所示:
-
movl $0x2,-0x4(%ebp)
将main函数中的a赋值为2,上一步中esp下移已经为变量a分配了空间,只需要将该2这个值存入该地址空间即可,因为ebp是固定不变的,所以通过ebp+偏移来找到对应的地址。
如图所示:
-
push $0x2
将func的参数存入栈中,这里是值传递,传递的参数是2,所以是把2这个值入栈。
如图所示:
-
call 0x80483db
调用func函数,call指令包含两步操作:1是call后面的指令地址入栈,即
0x080483fe <+20>: add $0x4,%esp
的0x080483fe这个地址入栈;2是跳转到call指令所指的地址,即call 0x80483db <func>
的0x080483db,这里跳转是修改eip的寄存器的值。如图所示:
-
push %ebp(这里是跳转到func函数的第一条指令)
call指令跳转后,进入func子函数,第一步就是记录调用函数的ebp,将ebp的值入栈,ebp的值就是main函数的栈基址。下图中我们也可以看到此时ebp却是保存着main函数的栈基址。
如图所示:
-
mov %esp,%ebp
保存完main函数的基址之后,就可以用ebp保存调用的子函数的基址了,这样我们就可以使用ebp,通过偏移存储子函数的变量了。
如图所示:
-
sub $0x4,%esp
为子函数的变量预分空间,因为只有一个int型的变量b,所以只需要分配4个字节即可。
如图所示:
-
mov 0x8(%ebp),%eax
在main函数中,调用了子函数func,并传递了参数a,他是一个值2,所以在main函数的栈中,保存了该参数,参照步骤5。从图中可以看到就是0x0FF8地址的值。此时ebp的值为0x0FF0,所以ebp+0x8,刚好就是0x0FF8
如图所示:
-
mov %eax,-0x4(%ebp)
这一步是func函数的赋值操作,将参数的值赋值给func函数的内部变量b。在进入func子函数之后,esp已经为内部变量分配了空间(步骤9),就是0x0FEC这个地址,即esp保存的地址,但是esp是动态改变的,所以这里我们用ebp的偏移来找到为变量分配的地址。
如图所示:
-
leave
leave包含两步操作:1.是将ebp的值赋值给esp,即恢复到进入func子函数的初始状态(步骤8);2.是将记录的main函数的基址重新赋值给ebp,在步骤7中,我们将main函数的基址入栈,ebp地址中保存的就是main函数的基址。使用pop可以同时将esp恢复到进入func函数之前的状态。这是的堆栈和步骤6跳转到func之前的状态是一样的。
如图所示:
-
ret
ret指令执行两步操作:1是将esp保存的地址的值出栈;2是跳转到这个值表示的地址。所以首先出栈即将esp保存的地址0x0FF4里存储的值0x080483fe出栈,并且跳转到这个地址即将eip指向0x080483fe这个值。这个地址保存的就是main函数调用func函数之后的指令
如图所示:
-
add $0x4,%esp
func函数调用完毕,传递的参数不在使用,所以把esp的指针上移4个字节,相当于释放了这个空间。这里不用pop是因为后续没有使用,不需要找一个寄存器保存这个值。
如图所示:
后续过程就不用再赘述了,leave和ret就和func函数一样的。