c语言堆栈基本代码入栈出栈_[原创]浅谈VC++中C语言函数入栈出栈的实现

本文探讨了C语言中函数调用的流程,包括参数的从右往左入栈顺序,以及C语言选择这种顺序的原因是为了支持可变长参数。文章还介绍了三种常见的函数调用约定(__Cdecl, __Stdcall, __Fastcall),并详细解释了每个约定的特点。此外,作者通过实例展示了函数入栈和出栈的过程,并讨论了安全检查和缓冲区溢出的概念。" 132704518,19673917,使用pandas将DataFrame列移动到第一列,"['Python', 'pandas', '数据处理']
摘要由CSDN通过智能技术生成

近日聆听了在科锐钱老师上C语言时所讲的函数调用约定,于是将课后笔记整理成此处男贴.虽力求完美,但难免因本人的学识浅薄而存在不足之处,望大神们批评指正...

--题记

1 函数调用流程

函数调用大家都不陌生,调用者向被调者传递一些参数,然后执行被调者的函数体代码,最后被调者向调用者返回结果.还有一句话是大家比较熟悉的,就是函数调用是在栈上发生的,那么C语言中的函数调用是如何实现的呢,下面我们一起分析分析...

1.1 C语言中函数参数的入栈顺序

为了能有个感官的认识,我们先通过一个小程序看看.

/*

*Copyright (c)2015,

*All rights reserved.

*文件名称:PushOrder.c

*作 者:韩逸

*完成日期:20161020

*版 本 号:Debug

*编译环境:Visual Studio 2012

*问题描述:C语言函数参数入栈的顺序

*/

#include

int Fun(int nNumA,int nNumB,int nNumC);

int main(int argc,char* argv[])

{

Fun(100,200,300);

return 0;

}

int Fun(int nNumA,int nNumB,int nNumC)

{

printf("nNumA = %d at [0x%p]\r\n",nNumA,&nNumA);

printf("nNumB = %d at [0x%p]\r\n",nNumB,&nNumB);

printf("nNumC = %d at [0x%p]\r\n",nNumC,&nNumC);

return 0;

}

运行结果

nNumA = 100 at [0x010FFDF0]

nNumB = 200 at [0x010FFDF4]

nNumC = 300 at [0x010FFDF8]

总结:

C语言中栈底为高地址,栈顶为低地址,因此可知上述例子函数入栈顺序是从右往左.通过钱老师的讲解以及百度所查到的文献,参数的入栈顺序是和编译器的调用约定相关的.比如,Pascal语言中参数就是从左往右入栈,而有些语言还可以定义修饰符进行置顶的传参顺序,如VC++就可以两种方式传参,那么C语言为什么要从右往左传参呢?

进一步发现Pascal语言不支持可变长参数,而C语言支持这种特色,正是因为这个原因使得C语言函数参数入栈顺序是从右往左,具体原因是C方式参数入栈顺序(从右往左)的好处是可以动态变化参数的个数,通过堆栈分析可知,最前面的参数被压在栈底,除非知道参数个数,否则无法通过栈指针的相对位移得到最左边的参数.这样左边的参数就不确定,正好和动态参数的个数方向相反.

显然,C语言函数参数采用自右向左的入栈顺序,主要原因是为了支持可变长参的形式.换句话说,如果C语言不支持这种特色,那么C语言完全和Pascal一样,采用自左向右的参数入栈.

1.2 函数调用约定

C语言中常用的调用方式有: __Cdecl , __Stdcall , __Fastcall

(VC编译器默认函数调用方式是__Cdecl,Windows API使用的是__Stdcall调用方式)

1.2.1 如何设置调用约定

本人所选的编译环境为Visual Studio 2012.有两种方法选择调用约定.

1.右键项目属性->配置属性->C/C++->所有选项->调用约定

2.在定义函数名的时候规定调用约定

int __cdecl Fun(int nNumA,int nNumB,int nNumC)

{

printf("nNumA = %d at [0x%p]\r\n",nNumA,&nNumA);

printf("nNumB = %d at [0x%p]\r\n",nNumB,&nNumB);

printf("nNumC = %d at [0x%p]\r\n",nNumC,&nNumC);

return 0;

}

或许您会奇怪为什么我们平时写代码的时候一般不写函数调用方式,因为VC++中C/C++缺省调用方式就是__cdecl函数约定,所以即使我们不写,编译器也会默认帮我们选择__cdecl调用方式.

1.2.2 调用方式详解

下面咱聊聊3种调用方式,分别分别是__Cdecl,__Stdcall,__Fastcall.

__Cdecl:

(1)压栈顺序:函数参数从右往左传递到栈顶

(2)返回值:返回值在寄存器

(3)参数空间谁释放:由调用方把参数弹出栈,调用方负责释放参数空间

备注:正因为如此,实现可变参数(如Printf)只能使用该调用约定

__Stdcall:

(1)压栈顺序:函数参数从右往左传递到栈顶

(2)返回值:返回值在寄存器

(3)参数空间谁释放:被调方负责释放参数空间(在退出时情况堆栈)

__Fastcall:

(1)压栈顺序:用ECX和EDX寄存器传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧

从右往左传递

(2)返回值:返回值在寄存器

(3)参数空间谁释放:被调方负责释放参数空间

(PS:如果返回值过大,寄存器放不下放栈顶)

2.函数的运行原理

很认真的灌水了一大篇,现在咱结合示例代码看看函数如何入栈与出栈吧.

当函数被调用时,指令指针的地制值增加1,使其指向函数调用后的下一条指令.该地址随后被放入栈中,作为函数返回时的返回地址.

( 程序中的所有代码都保存在程序代码区中,也就是将代码编译成二进制形式加载到内存中的程序代码区,其中每行源程序会编译成一条或若干条二进制指令,每行指令都有一个地址,指令指针就是用来存放即将要执行的下一条指令地址.)

第18行调用FunA函数,假设该指令地址是10086,指令指针先保存该地址,然后将该地址值+1,使其指向下一条指令,也就是第19行语句,该地址随后被放置在栈中,当FunA函数调用结束,将返回到第19行执行.

将下一条指令的地址保存到栈中是第一步,跳转到FunA函数定义处执行是第二步(跳到第22行去执行).程序又是如何知道应该跳到第22行去执行呢?

因为函数也有地址,该地址存在目标文件(多以.obj为扩展名)的符号表中(函数名是一个符号,该符号对应着一个标号,这个标号就是函数的地址,一般为相对地址,即函数第一条指令相对于程序代码区起始位置的偏移量),当调用某个函数时,就从符号表中提取该函数的地址,也就是该函数第1条指令地址,然后由寄存器中的指令指针来保存.接着系统将根据符号表中所秒速的函数返回值类型,在栈中开辟一块内存存储返回值,而栈顶的地址被记录下来保存在栈顶指针中.而从这时起,在函数返回之前进入栈的所有数据被视为局部变量.

调用者会按调用约定传入形参,返回地址,上级调用者栈底(__Cdecl自又往左的顺序将形参压入栈中,栈顶指针始终要指向栈顶,因此栈顶指针不断向上移动),当所有参数都压入栈后,才开始执行函数的第一条指令.

1.我们先分析分析Main函数的情况.(取Main函数中变量地址拖到内存窗口)

F10单步走一次

接下去就开始分析FunA函数的入栈情况了.F10单步,

可以看到此时形参和返回地址已经入栈,再单步一次应该会压入Main函数栈底地址:9CFAB8,并且为局部变量申请空间(依据局部变量的总大小抬高栈顶)(备注:如果是Debug编译选项组(/ZI+/OD),将局部变量全部初始化为0XCCCCCCCC),以及保存寄存器环境.F10单步

接下来就开始执行函数体

函数入栈就已经完成了.

当函数结束时,栈区中数据要进行清理,以释放内存,因此按照后进先出的原则,后进入的数据先弹出栈,每弹出一个栈,栈顶指针向下移,直到所有数据都弹出栈区.

函数体执行完后开始出栈,首先还原栈中保存的寄存器信息,然后更新当前栈顶为栈底,并恢复上级调用者栈底,取出栈顶内容,作为返回地址.按返回地址流程回到调用方.值得注意的是,函数出栈数据该还的还,该释放的释放,但不会清零,也没有必要清零,把栈顶降到相应的地址上,如果还有函数调用,再分配再覆盖就完事了.

总结:

纵欲知道为什么定义变量要养成初始化的习惯,因为函数入栈出栈只是把栈顶和栈底移位操作,并未将值清空,就像腾空转体360度弹鼻屎,也不知道谁会倒霉.

思维发散

突然想到,函数入栈的第二步是要传入返回地址,如果构造一个地址越界覆盖到返回地址,能不能执行代码呢...

就用上面的例子修改下吧...

/*

*Copyright (c)2015,

*All rights reserved.

*文件名称:FuncPush.cpp

*作 者:韩逸

*完成日期: 20161020

*版 本 号:Debug

*编译环境:Visual Studio 2012

*问题描述:观察函数调用流程

*/

#include

#include

int __cdecl FunA(int nFunANum);

void fnShellCode();

int __cdecl main(int argc,char* argv[])

{

int nMainNum=8;

FunA(nMainNum);

return 0;

}

int __cdecl FunA(int nFunANum)

{

nFunANum=16;

char szFunAName[18]="www.51asm.com";

*((DWORD*)(szFunAName+24))=(DWORD)fnShellCode;

return 0;

}

void fnShellCode()

{

system("calc");

exit(0);

}

编译提示失败,提示缓冲区即将溢出...

我猜测是因为编译器加入了安全机制,将函数返回地址备份,如果发现返回地址与备份地址不一致则...

但我只是想验证自己的想法是否正确,能否溢出覆盖返回地址然后执行fnShellCode()函数.

那把编译器的安全机制关闭吧,具体方法如下...

右键项目->属性->C/C++ –>代码生成 ->安全检查 ->否

在函数出桟的时候读取返回地址却跳到了我构造的函数中,那么之前的推断成立,函数入栈的时

候可以用局部变量越界覆盖返回地址执行自己想要的代码.(前提是要关闭安全检查,不然只能

自娱自乐...)

上传的附件:

2.png

(30.33kb,2次下载)

3.png

(80.20kb,10次下载)

4.png

(73.84kb,6次下载)

5.png

(75.48kb,6次下载)

6.png

(75.71kb,4次下载)

7.png

(68.95kb,1次下载)

8.png

(69.37kb,3次下载)

9.png

(41.86kb,6次下载)

10.png

(41.86kb,1次下载)

11.png

(44.84kb,2次下载)

12.png

(43.53kb,5次下载)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值