2.函数原型: int function(int i)
现在有了参数了,也有了返回值了,相对来说更比较复杂了。这里就得引入%esp寄存器值的变化了,不然就难以把问题分析清楚了,如果想形象一点地描述那就画图,自己画个图根据我的数据变化一起分析吧。看看一段简单的C代码:
// C Code
int function(int i)
{
return 2 * i;
}
int main(void)
{
int j = function(10);
return 0;
}
之所以些这么简单只是为了我们分析问题的方便,懂得个原理就算是复杂的其实稍微再分析一下也就懂了。我们从main开始分析吧:
main:
pushl %ebp
movl %esp, %ebp
subl $24, %esp
andl $-16, %esp
movl $0, %eax
addl $15, %eax
addl $15, %eax
shrl $4, %eax
sall $4, %eax
subl %eax, %esp #到这里其实和前面的例子基本一样,就不分析了
movl $10, (%esp)
call function
movl %eax, -4(%ebp)
movl $0, %eax
leave
ret
看看上面的汇编代码,和前面一样的不分析。但是其中有句不一样:subl $24, %esp ; 因为主函数里有两个临时变量i, j;这里为了有足够的空间留给临时变量所以干脆在堆栈里腾出24个字节空间。在看看下面的代码:
movl $10, (%esp) #====> %esp = 800, (800) = 10
,其中800是我们假设的地址值,(800)表示地址800的内容这里的(%esp)指的是%esp地址里的内容,
刚才我们假设这时候%esp的值是800, 那么地址为800的内容就是10了。执行函数调用了,注意在调用函数前其实是先把函数调用指令
call之后的地址压栈,也就是call之后那条指令的IP值压栈,所以这时候 %esp =
796;这里要弄明白为什么要把下条指令地址压栈,假设如果不把IP值压栈,那么当函数调用完毕后怎么能找到函数调用时的地址呢?也就是说如果没把IP压
栈,那么函数调用完之后就回不到原来的执行地址了,就会造成程序执行顺序的错误!
下面列出函数function的汇编代码:
function:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl %eax, %eax
popl %ebp
ret
pushl %ebp; 经过这条指令后 %esp值减4,所以这时候%esp值是792。下面这句:
movl %esp, %ebp #==============> %ebp = 792, %esp = 792, (792) = %ebp ;其中(792)表示地址792的内容
movl 8(%ebp), %eax #========> %eax = 10
上面这句很多人可能不明白了,8(%ebp)指的是什么?8(%ebp)等于 : (%ebp + 8) ,这里注意,%ebp + 8
是表示一个地址值,加上括号表示存储在该地址上的内容。
所以8(%ebp)其实就是地址为800的值,看前面地址800的值刚好是10!所以这句其实是把10复制给%eax寄存器.
addl %eax, %eax #======> %eax = 20
相当于2 * %eax, %eax这时候等于20了,刚好是实现了C代码中的 (2 * i);
popl %ebp #=========> 恢复%ebp寄存器的值, %esp这时候等于796
ret #=========> 函数调用完毕返回,这句其实是把刚才压栈的IP值弹出栈,执行这条指令后 %esp = 800
# 800!想想我们在调用函数的时候%esp也是800啊!这就是实现了“清栈”了,就是把调用函数所在的栈清除了!
好了,函数 function的汇编代码分析完了,现在回头继续看看main函数里的下一条指令了。接下来是这句:
movl %eax, -4(%ebp)
%eax寄存器存放的是什么?看function函数的代码,可以知道其实就是(2 *
i)的值,所以返回值其实是通过%eax来传递的!传递到-4(%ebp)里去了,-4(%ebp) = (%ebp - 4);
-4(%ebp)到底是什么呢?看看C代码,返回值传给变量j,那么-4(%ebp)会不会就是j呢?答案是肯定的!我们先看看%ebp的值是什么。看看
main函数的汇编代码,可以得出,%ebp其实指向了main函数的栈底部,但记不记得前面说的subl $24,
%esp是为临时变量而留出的空间?没错,-4(%ebp) 就是存储在临时变量区域!也就是变量 j 了。