目录
前言:
本文的很多地方可能会有超纲的内容,听不懂没关系,因为我们主要也不是讲这些东西的,主要讲的是堆栈图的绘制,这样以后遇到C语言代码,就可以直接转到反汇编去看看底层是如何去实现的,所以能看懂的地方就看,看不懂的就跳过,知道是这么回事就行了。当然能看懂更好,毕竟百度一大堆东西嘛。
1、函数定义
返回类型 函数名(参数列表)
{
return ; -- 分号必须是英文的
}
例子:
这个函数想表达什么东西呢?
返回值类型
首先函数的返回值类型是int,int类型占四个字节,它是用来说明返回值的数据类型是什么
int 4个字节
short 2个字节
char 1个字节
long 8个字节
等等,因为这些都是整型,所以long类型的函数能返回char类型的不足为奇,但是char类型不能返回long类型的。
函数名
对于函数名,这个是你自己想怎么取就怎么取的,前提是要遵守函数名格式的规则,函数名的规则就是:
1、必须字母、数字、下划线组成;
2、数字不能开头
参数列表
参数列表分为形参和实参,它们的区别如下:
形参出现在 函数定义 中,在整个函数体内都可以使用, 离开该函数则不能使用。. 实参出现在 主调函数中,进入被调函数后,实参变量也不能使用 。
可能概念会有些模糊
形参:
实参:
打印返回值:
函数返回
我们学汇编的时候知道,函数调用了之后,函数中是必须设有RETN做返回的,不然会导致函数运行一下就直接乱了,找不到刚刚执行的地方了,所以在C语言也是,函数最好还是做一下返回,并且这里的函数返回后面会有一个值,叫做返回值,返回值的数据类型必须和函数名前边的类型保持一致。
long可以返回char,因为他们都是整型
char不能返回long,因为char是一个字节,long八个字节放在一个字节中,可能会丢失数据。
那是不是当返回值类型和返回值的数据宽度一样,就可以返回了?毕竟放得下嘛
不是
了解即可
指针类型在32位程序中占4个字节,在64位程序中占八个字节。但是我们的int(4个字节)类型是没有办法返回一个指针的。
2、查看反汇编
待解析的函数代码如下:
int plus(int x,int y)
{ // 括号内是函数体,可以写你想要实现的东西,不是只能写返回
return x+y;
}
int main() // 入口程序 程序开始执行的地方
{
plus(1,2); // 调用函数
return 0; // 执行结束
}
首先我们需要转到反汇编模式,然后再一步一步调试
设置断点,然后运行,然后ALT+8转到反汇编
但是有的时候,当你转到反汇编的时候,没有黄色的箭头,这个时候我们只需要F10执行一下,然后屏幕会自动追踪到黄色箭头的位置。BUT,万一你的断点设置在程最关键的一步了,但是这个时候你的屏幕上没有黄色的箭头,难道你要F10执行吗?
这里教大家一个小技巧,你可以在你想要设置断点的上面加一行"没有作用的汇编指令",然后你把断点设置在这个指令上就行了。
如下:
像这样,加一行没用的指令,把断点设置在这条指令上就行了,那么当屏幕上没有出现黄色的箭头的时候也可以放心的使用F10了。
__asm是内嵌汇编的关键字,之前讲过。
3、环境配置
我们既然要画堆栈图,那么就要准备画图的工具啊,所以我一般会使用excel表格去进行绘制,并且这里如果大家使用的是vs2010的话,应该把寄存器窗口和内存窗口调出来,既然我们是分析底层分析汇编的,那么寄存器和内存肯定不能少啊。
创建excel
win11:右键桌面->新建->xls工作表
双击进入excel表,点击试图,取消网格线
随便选中一列,右键
上面这一列就是我们用来模拟堆栈的地方
vs2010窗口
首先我们运行调出反汇编
内存尽量设置成四个字节四个字节的显示
至此我们的环境就配置好了,下面我们来逐步分析函数代码底层是如何操作的。
4、逐步分析汇编并绘制堆栈图
首先我们的寄存器中的值,先写在堆栈图上
下面是我的,栈顶esp栈底ebp,每个人的不一样
绘制堆栈图:
查看指令
F11执行,esp发生了改变
堆栈图变化:
查看内存,验证:
指令:
执行:
堆栈图:
验证:
通过上面两行代码我们知道了函数的传参是从右往左传
指令,这里call指令必须F11执行,不然会直接跳过。
执行:
堆栈应该会存放call指令的下一个地址
验证:
指令:
执行,这里JMP会跳到后面的地址,所以EIP会存放这个地址
堆栈无变化
指令:
执行:
堆栈:
验证:
指令:
执行:
堆栈:
无需验证,这部操作就是ebp寻址的第一步。
指令:
执行,esp减0C0H,H是十六进制的标志,所以就是减去C0
C0等于十进制的192,然后一行显示四个字节,所以192/4=48行
所以堆栈往上提上48行
验证:
堆栈:
至此,堆栈提升完成。
指令:
执行:
堆栈:
验证:
指令:
执行:
excel:
验证:
指令:
执行:
excel:
缓冲区
从31F608到31F6C8就是我们常说的缓冲区,关于缓冲区的知识以后再讲
验证:
上面这三行指令的作用就是将需要用到的寄存器进行备份。
指令:
执行:将[ebp-C0]这个“地址” 存入edi寄存器
excel:
无需验证
指令:
执行,往ecx里存放十六进制的C0
堆栈无变化,无需验证
指令:
执行:
堆栈无变化
指令:
rep重复执行指令,重复的值就是ecx的值30,换成十进制就是48
所以这里重复执行48次将eax的值(dword)存放到edi指向的位置,我们上面edi指向了31F608,所以这里也就是将我们刚刚提升堆栈中的48行全部设置为0xCCCCCCCC。
执行:
堆栈:
无需验证。
上面几行指令,就是将缓冲区填满CC,CC就是汇编指令INT 3,当程序运行时遇到值为CC的地址就会理解停下来,这里填充CC有很大的争议,这是vs编译器默认的做法,我们只需要知道0xCC就是断点就行了。
指令:
解释一下,这里中括号里边的"x"是编译器做的优化,实际应该是ebp+8,也就是第一个参数(第二次传进来的参数)。所以这行指令应该是 mov eax,dword ptr [ebp+8]
执行:
无需查看堆栈。
指令:
add eax,dword ptr [ebp+CH]
执行:
无需查看堆栈
下面这几行指令就是将寄存器原来的值还给寄存器,我就不演示了
对比一下原来的值一切正常
执行ret,函数调用完毕
此时的堆栈大概如下:
所以下面这行指令就是平衡堆栈的,这里用的是外平栈
执行之后的堆栈如下:
下面的指令已经没必要再跟了,xor eax,eax就是将eax寄存器置零的
xor相同为0,不同为1,所以这条指令后的eax肯定为0
总结
通过上面的逐步观察,我们可以发现,调用函数无非就是call指令进入函数,然后提升堆栈,创建缓冲区,之后将需要用到的寄存器拷贝一份,然后运算,完成后将寄存器的值还原,平衡堆栈。就是这些步骤完成了一个函数的调用。
关于函数调用底层是如何运行的,就讲到这里,如果有错误的地方,希望大佬指正。我个人是新手,关于写文章讲解方面有什么建议的话可以私信我的博客,谢谢大家!