考虑一下这样一个函数,我们要调用它:
someType f(int para1, char[] str2)
要调用一个函数,我们首先需要考虑如何传参是吧。怎么传参呢?那就把要传的参数到栈里面。等会要访问参数,就访问栈里的这些东西。
有多少种传参的办法呢?
- 如果操作数刚好是四个字节或者两个字节的,我们调用push指令就可以了。
push %eax
push $0x12345678
2. 如果我们想放很多东西进去呢?比如,我想把整个Hello worldn0都放进栈里?那我们可以学习push的内部实现,也就是esp自减,然后用偏移地址填数。
sub %esp, 0x8
mov 0x34333231, %esp
mov 0x38373635, %esp+4
上面这些指令执行完了之后,栈就有了这些变化
3. 我要传指针怎么办?
那就把地址push了呗。我们假设0x804a104里面存着一个字符串0x00333231,然后edx寄存器里面存放着0x804a100这个值。那么就用以下的办法把字符串首地址传进去:
lea 0x4(%edx), %eax
push %eax
要记住使用lea,否则你就会把0x00333231给传进eax。
好了,知道了怎么传参,我们要按什么顺序把para1和str2传进去呢?答案是反着顺序push:
; 把字符串入栈
mov 字符串str2首地址, %eax
push %eax
; 把数字入栈
push 整数para1
这样做是图个什么呢?因为等会我们要在栈空间的下方去执行调用的函数,那么在取参数的时候我们就可以按顺序取参数。
好了,我们做好了参数的准备工作,执行call指令吧。
call指令的实现方式有很多种。从指令上来说,可以直接call某个由立即数确定的相对地址,也可以call某个modrm字节确定的地址。而这个地址的确定,有的是直接靠填好的信息,有的是留出待重定向的字节,然后等着在链接成可执行文件的时候把这些字节补上去。
比如我们在链接前一般会看到这样的指令:
e8 fc ff ff ff
重定向替换的计算公式如下:
Addr(被调用函数的地址) - (Addr(.text节起始位置) + 需要重定位部分的地址 - 修正量)
其中就是e8字节所在地址+1,修正量为0xfcffffff (即-4)。
call指令干了什么事情呢?很简单:
push %eip
add 相对位移, %eip
也就是它把eip寄存器的值放入了栈中,留待以后返回,然后把PC移动到要执行的代码。一般我们想缓冲区溢出也是可以通过修改这个返回地址来执行我们自己代码啥的。
现在栈成这样了:
然后就是调用每个函数都需要进行的规定动作:
push %ebp
mov %esp, %ebp
这是在干啥呢?这相当于先保存了原有的栈帧,然后把再把当前的栈顶作为新栈帧的栈底。
然后你就知道怎么访问para1了吧?那就是0x8(%ebp)了。
如果要访问str2呢?比如我想把str2的第二个字节改成0x39?那么就会有如下过程:
; 取出地址
movl 0xc(%ebp), %ecx
; *(str2 + 1) = 0x39
movb 0x39,0x1(%eax)
好了,经过一通操作,这个函数全都执行完毕,我们怎么将值传回去,并且返回刚才的函数继续执行呢?
一般你会在被调用函数的末尾看到这么两个语句:
leave
ret
首先,leave干了什么事情?它把esp恢复,然后把ebp恢复:
mov %ebp, %esp ; 有没有想起开头的mov esp, ebp?
pop %ebp ; 把栈顶元素弹出,送入ebp
然后栈就变成了这样:
然后,ret干了什么事情?它把eip恢复:
pop %eip
等等,返回值去哪了?
其实返回值在leave之前,会有一条代码将返回值送入eax。然后,回到刚才的函数的时候,就可以通过操作eax来处理返回值:
call f
mov %eax, 0xc(%ebp) ; 存到当前的某个变量里头
如果返回值是一个int,那么就直接把返回值送入eax。如果返回值是一个自定义的结构体之类的,则似乎会把待接收值的变量的地址传过去?暂时不是很清楚。
似乎,就这么完了吗?别急,我们的栈现在是这样的:
这意味着我们还要让刚才push进来的参数也释放掉。一般来说,可以一个接一个的pop,但为了节省指令数量,我们一般通过直接加esp的方式来完成任务:
add $0x8, %esp
至此,一个函数就算调用完毕了。