从底层分析C语言中的参数传递与返回值

使用到的工具

  • VC6.0(观察寄存器变化)
  • Excel(画堆栈图)

函数定义

函数定义的格式如下:

返回类型 函数名(参数列表)
{
	功能

	return;
}

例子:

int plus(int,x int,y)
{

	return x+y;
}

int代表的是字节宽度,除int外,还有两个常使用的变量类型

变量类型 数据宽度
int 4个字节
short 2个字节
char 1个字节

画堆栈图

int plus(x,y)
{
	return x+y;
}

void main()		//程序入口
{
	plus(1,2);	//函数调用

	return;		//执行结束
}

就从上面这个程序来说,它究竟是怎么执行的呢?我们下个断点,追一下程序内存的变化,就像这样:
在这里插入图片描述
这里一定要记住两个寄存器的变化,一个是ESP栈顶寄存器,一个是EBP栈底寄存器,我们现在还没有运行,先记录一下栈顶:

在这里插入图片描述
接着看一下汇编指令,在这里可以看到,参数的传递有两个特征:第一是从右向左传递,第二是使用push指令在汇编中实现。

push 2

在这里插入图片描述
接着画图,这行指令执行完堆栈会发生什么变化呢?push 2,向堆栈中压入参数2,栈顶指针ESP的值-4,如图:
在这里插入图片描述
在这里插入图片描述

接着执行,push 1向堆栈压入参数1,esp的值-4。

push 1

在这里插入图片描述在这里插入图片描述
接着往下走,这里push(1,2);对应这call,也就是说call指令就代表着函数调用,哪这个call指令执行以后要不要改堆栈?要,它会修改esp的的值-4,并将它下一行的地址压入栈顶。
在这里插入图片描述

接着F11跟进去看一下堆栈的变化,是不是与我们画的图返回的结果相同?
在这里插入图片描述接着往下走,这里的这个jmp是VC6自己生成的,是它的特点,并不是直接跳到函数的指令那里,而是通过一个jmp来跳转,我们知道jmp是无条件跳转,不影响堆栈,所以跳就行了。

jmp plus(00101020)

在这里插入图片描述
接着往下看,这里它push了一个ebp,那么运行后堆栈的变化就是,向堆栈中压入ebp的值,栈顶指针esp的值-4,我们接着画一下。

push ebp

在这里插入图片描述
运行看一下结果是不是跟我们画的相同。
在这里插入图片描述接着往下走,这里使用mov ebp,esp来提升堆栈,进行ebp寻址。

mov ebp,esp

在这里插入图片描述

在堆栈中应该是这样反应的。
在这里插入图片描述
我们运行一下看看结果:
在这里插入图片描述接着往下走,这里esp的值要-40来提升堆栈,为这个函数的运行腾出空间,这里提升了40个堆栈,我们的堆栈一格是4个字节,这里需要进制转换,40转换为10进制是64,64÷4是16,也就是说我们要提升16个格,在堆栈中应该是这样显示:

sub esp,40h

在这里插入图片描述
单步一下看看结果:
在这里插入图片描述接着往下看,这里push了三个寄存器,这其实可以理解为“备份”,因为程序后续运行可能会覆盖掉寄存器的值,但又会用到它原先的值,这样被覆盖了程序就出错了,我们画一下它的运行后的堆栈图。

push ebx
push esi
push edi

在这里插入图片描述
单步一下看看结果是不是跟我们画的一样。
在这里插入图片描述

缓冲区

在本程序中,从0012FF20到0012FEE4这一块内存,就是程序的缓冲区。

当前的函数在执行过程中,它需要用内存,那么它就会提升堆栈,自己给自己分配一块内存,这块内存,就是所谓的缓冲区。
在这里插入图片描述
接着往下走,可以看到这4行指令

lea         edi,[ebp-40h]
mov         ecx,10h
mov         eax,0CCCCCCCCh
rep stos    dword ptr [edi]

我们一行一行的分析,首先第一行,lea指令的意思就是,将源操作数给出的有效地址传送到指定的的寄存器中。

也就是说,这一行的意思就是,把ebp地址减去40的结果赋给edi,在堆栈中就是这样显示:
在这里插入图片描述
第二、三行的意思不必多说,把10赋给ecx,把CCCCCCCC赋给eax,第四行,stos就是把eax的值放到edi的位置,结合rep指令重复执行,这里重复执行多少次看的是ecx的值,这里重复10次换算过来刚好是16次。总结一下就是,把缓冲区的值,全部换成4个CC。

这里的CC可以理解为下的断点,程序一遇到CC它就会停止运行,这样就避免程序自己运行时的溢缓冲区出了。

看一下它在堆栈中的结果:

在这里插入图片描述
我们运行一下程序看看结果是否相同:
在这里插入图片描述

进行计算

接着往下看,这里的两行指令就是进行1+2的运算了

mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]

第一行指令:把ebp+8的值放到eax里
第二行指令:令eax与ebp+c的值相加,结果返回给eax
那么此时eax里边的值应该是3

看看堆栈图:
在这里插入图片描述运行一下程序看看结果:
在这里插入图片描述

恢复堆栈

接着是三个出栈操作:

pop edi //把栈顶的值取出赋给edi。esp+4
pop esi //把栈顶的值取出赋给esi,esp+4
pop ebx //把栈顶的值取出赋给ebx,esp+4

这三个操作刚好对应了之前的压栈操作,这时候esp的值应该+c,我们看一下它在堆栈中的结果:
在这里插入图片描述运行程序看看结果:
在这里插入图片描述继续往下走,这里它把ebp的值赋给了esp

mov esp,ebp

这行代码执行完就意味着,esp=ebp,我们画一下堆栈图:
在这里插入图片描述运行一下程序看看结果:
在这里插入图片描述继续往下走,又是一个出栈操作,把当前栈顶的值赋给ebp,栈顶指针esp的值+4

pop ebp

我们画一下程序运行后的堆栈图:
在这里插入图片描述
运行一下程序看看结果是否相同:
在这里插入图片描述

程序结束

最开始运行前程序的操作是提升堆栈,现在程序即将执行完毕它就开始慢慢的恢复堆栈,这一波操作就是汇编中的堆栈平衡

接着程序执行完毕,ret这一行指令的意思是:把ESP当前堆栈中的值赋给EIP,所以ret也等于pop eip

ret

画一下它在堆栈中的变化:
在这里插入图片描述
运行一下程序,所有的变化都跟我们分析的一摸一样:
在这里插入图片描述最后的这一行指令是为了保持堆栈平衡,也就是函数执行前堆栈是什么样子,执行后堆栈就恢复到什么样子,堆栈平衡有内平栈与外平栈两种方法,这里就是采用的外平栈。

add esp,8 //esp的值+8,结果返回到esp中

画一下堆栈图:
在这里插入图片描述
运行一下程序,完全一致:
在这里插入图片描述
到这里,整个程序就执行完毕了,这里有两点变化:

  1. EAX的值发生了变化,储存了程序计算的结果。
  2. 堆栈中多了很多“垃圾”,这些如果是有价值的信息,是非常值得黑客去挖掘的。

好了,到这里我们就分析完毕了,剩下的指令就是程序刚开始main()函数生成的代码,这里我们就没必要再去跟了,因为我们已经分析了整个函数从传参、调用、执行、结束的全部过程了。

总结

  • C语言中的参数传递:堆栈传参,从右到左。
  • C语言中,返回值存储在寄存器EAX中。
  • C语言中,参数传递用PUSH,函数调用用CALL。
发布了60 篇原创文章 · 获赞 68 · 访问量 8408

猜你喜欢

转载自blog.csdn.net/qq_43573676/article/details/104695648