C语言函数的学习

0x01.函数

这节就先讲函数吧,函数大致分为四种类型

1、无参数、无返回值的函数格式

void 函数名()
{
	//代码段
}
void Hello()
{
	printf("Hello World!");
}

2、有参数,无返回值的函数格式

void 函数名(参数类型 参数名,参数类型 参数名)
{
	//代码段
}
void add(int a,int b)
{
	int c = a + b;
	printf("当前的值:%d",%c);
}

3、无参数,有返回值的函数格式

返回值类型 函数名()
{
	//代码段
	return 具体的值,需要和返回值类型 匹配
}
int sub()
{
	int c = a - b;
	return c;
}

4、有参数、有返回值的函数格式

返回值类型 函数名(参数类型 参数名,参数类型,参数名)
{
	//代码段
	return 具体的值,必须与返回值类型匹配
}
int ret(int b,int c)
{
	int c = b * c;
	return c;
}

0x02. 函数的反汇编

a.空函数

#include <stdio.h>

void add()
{

}

int main()
{
	add();
	return 0;
}

运行窗口之后,进入反汇编窗口

可以看到这边call了 add函数,这个函数的地址是0x0DF135Ch,这时候我们再f11进去

这边下一步又跳到了jmp(jmp指令是无条件跳转)这边,可以看到它是在调用add的参数了,进行执行了

跳到了 0x0DF3840

终于到了函数内部,这时候我们脑里想象出一个图,

这时候可以发现call的时候会跳到 jmp那边,一跳完后才是真正的函数内部

1)开辟空间

我们先看前三条

00DF3840  push        ebp  
00DF3841  mov         ebp,esp  
00DF3843  sub         esp,0C0h  

看不懂没关系,我们可以把随便一个东西拉到od中,然后调一下

栈窗口在这边,我们看一下怎么回事

(补充:push是压栈,把数据压进去、pop是出栈,把东西拿出来。拿出来的话是按照先进后出,后进先出的顺序,之后汇编部分会仔细讲。esp寄存器是栈顶,ebp是栈低)

可以看出现在esp是 00D3FAA8,ebp是00D3FAB4

这时候我们把刚刚在visual studio的那段反汇编写进去试试

这边是把ebp栈低压入栈中

压入之后栈顶和栈低就一样了,这时候再把栈顶(esp)的地址,给栈低(ebp),由于一样,就没啥变化

然后在esp减掉 c0

我们计算一下,栈低减栈顶等于c0,说明正确。其实这边就是在栈中开辟一块空间,方便存储数据

2)保护数据恢复数据

00DF3840  push        ebp        开辟空间
00DF3841  mov         ebp,esp  
00DF3843  sub         esp,0C0h  

00DF3849  push        ebx 	     保护数据
00DF384A  push        esi 
00DF384B  push        edi 
...
...
00DF385E  pop         edi  		 恢复数据
00DF385F  pop         esi  		
00DF3860  pop         ebx  		

保护和恢复其实是一个对应的,我就写出来

就类似我们这种web狗,一般打了站要试头像能不能拿shell,就会先把这个管理员的头像保存一下,然后随便搞。之后恢复一下现场

可以看出先压入的数据在下面,最后压入的数据在上面

恢复数据的时候用pop,然后先把edi弹出去,就是后进先出的意思

3)缓冲区填充数据

00DF384C  lea         edi,[ebp-0C0h]  				
00DF3852  mov         ecx,30h  				
00DF3857  mov         eax,0CCCCCCCCh  				
00DF385C  rep stos    dword ptr es:[edi]  				

我们单步走一下,发现ebp-c0的地址给edi了,发现这个地址其实就是栈低的值

(补充:lea是传送地址,mov是传送数据)

ECX 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。

然后把30h给了ecx,这边的30是十六进制的,不是10进制的,最后把CCCCCCCC给eax

rep指令的目的是重复其上面的指令.ECX的值是重复的次数.
STOS指令的作用是将eax中的值拷贝到ES:EDI指向的地址.

最后发现其实就是把其他地址的值变为8个C而已

最后再恢复数据,恢复数据和保存数据差不多,遵守后进先出即可

4)恢复堆栈

00DF3861     mov         esp,ebp  
00DF3863     pop         ebp  	

瞬间回到了之前,最后再把原本的栈低恢复

回到了之前的样子了,执行完函数后就用ret返回

b.简单的函数功能

我们再看一下函数内有数据又会是怎么样?

#include <stdio.h>

void add()
{
	int c = 10 + 10;
}

int main()
{
	add();
	return 0;
}

发现就多了一行,因为我们代码中就已经把值写死了,我们传个有参数看看怎么样

#include <stdio.h>

void add(int a,int b)
{
	int c = a + b;
}

int main()
{
	add(2,3);
	return 0;
}

这时候我们可以发现这边先是把3压入栈中,再把2压入栈中,接下来就和上面空函数一样,jmp完之后就是真正的函数内部地址了

00333840  push        ebp  
00333841  mov         ebp,esp  
00333843  sub         esp,0CCh  

00333849  push        ebx  
0033384A  push        esi  
0033384B  push        edi  

0033384C  lea         edi,[ebp-0CCh]  
00333852  mov         ecx,33h  
00333857  mov         eax,0CCCCCCCCh  
0033385C  rep stos    dword ptr es:[edi]  

	int c = a + b;
0033385E  mov         eax,dword ptr [a]  
00333861  add         eax,dword ptr [b]  
00333864  mov         dword ptr [c],eax  
}

00333867  pop         edi  
00333868  pop         esi  
00333869  pop         ebx  

0033386A  mov         esp,ebp  
0033386C  pop         ebp  
0033386D  ret  

每一个部分我都加了个换行,这时候可以发现是在填充数据之后,再进行加减法

先是把a的值给eax,然后使用add指令,给eax寄存器加个b的值

(补充:add exp1,exp2

add指令:把exp2的值给exp1

最后我们是定义了用c接受传参,所以它这边把算完后结果的eax给c

0x03.局部变量与全局变量

#include <stdio.h>

int Request = 10;
void add()
{
	int b = 8;
	int c = b + Request;
	printf("%d", c);
}

int main()
{
	add();
	return 0;
}


这时候我们反汇编看一下request的定义

再看一下把变量放到函数内部是什么样子的

0025385E mov dword ptr [Request],0Ah
把0a放到request地址上面,也就是赋值给request

这时候可以总结全局变量的特点了:
1、全局变量在程序编译完成后地址就已经确定下来了,只要程序启动,全局变量就已经存在了,启动后里面是否有值取决于声明时是否给定了初始值,如果没有,默认为0
2、全局变量的值可以被所有函数所修改,里面存储的是最后一次修改的值.
3、全局变量所占内存会一直存在,直到整个进程结束.
4、全局变量的反汇编识别:
MOV 寄存器,byte/word/dword ptr ds:[0x12345678]
通过寄存器的宽度,或者byte/word/dword 来判断全局变量的宽度

局部变量特点:

1、局部变量在程序编译完成后并没有分配固定的地址.
2、在所属的方法没有被调用时,局部变量并不会分配内存地址,只有当所属的程序被调用了,才会在堆栈中分配内存.
3、当局部变量所属的方法执行完毕后,局部变量所占用的内存将变成垃圾数据.局部变量消失.
4、局部变量只能在方法内部使用,函数A无法使用函数B的局部变量.
5、局部变量的反汇编识别:
[ebp-4]
[ebp-8]
[ebp-0xc]

0x04.函数参数分析

先看一下调用约定

void __cdecl Function1(int x, int y, int z)
{
	g_r = x + y + z;
}
void __stdcall Function2(int x, int y, int z)
{
	g_r = x + y + z;
}
void __fastcall Function3(int x, int y, int z)
{
	g_r = x + y + z;
}

然后这边我们要判断看是否存在几个参数,用Function2来试,因为一般默认都是使用stdcall

断点下到函数这个地方,然后我们直接去反汇编窗口看

可以看到这边是压入3个参数,分别是从右往左压入

这时候就和前面讲的差不多了,也看的懂了。通过push压入的值来判断函数参数几个,我们也可以仔细看一下ret的值,也可以得到参数值
或者看下main函数里面的堆栈平衡也可以判断参数,由于都是int类型,4字节,在汇编中是dword类型。0ch其实就是十进制的12。
12 / 4 = 3。这时候也可以算出参数

所以判断函数参数数量有三种方法(补充,main函数也是函数,也是需要开辟空间,保存数据等...)
1、查看push入栈的参数

00D142BE  push        3  
00D142C0  push        2  
00D142C2  push        1  
00D142C4  call        Function2 (0D1139Dh)  

2、查看堆栈平衡

3、进入函数查看返回值


查看函数内部返回值的值

posted @ 2020-09-28 15:30  0X7e  阅读(337)  评论(0编辑  收藏  举报