最终真是团团转,真可以说是好事做尽,坏事做绝,

然而想想写点东西既有助于记忆,又有利于他人参考,所以还是决定抽点时间草书此文

以前在有关破解的博文中也稍微提到这个问题,现在就深入一点去考究它吧

狭义的编译一般指的是将程序语言代码转为CPU能执行的机器码,比如C++(VC++)

VB6的主程序也是切实编译的,然而大部分却类似java,生成了中间代码,由虚拟机在运行时解释为机器码

这一点跟脚本很类似,只是中间代码是二进制的,不容易为人所理解,脚本则更直观

对于.NET(VB,C#等)则是纯粹的生成中间代码(微软中间语),因而这些语言生成的程序可以很容易的"反编译"并任意转换语言

生成中间代码,广义上也算是编译.

我们今天要说的主要是狭义的编译,而且主要以VC6为例子,考究函数调用的那些细节,其实我还是比较关注细节的

VC中常用的函数调用有以下几种:

1、_stdcall
2、__cdec(默认)
3、__fastcall
4、thiscall(隐式)
5、naked(裸函数)

其实naked不是一种调用约定,而是函数修饰符,是面向编译的,它允许程序员自由的控制函数的堆栈.

编译以后可以与thiscall以外所有调用方式相同.我们写个小demo来分别看看这些函数都是怎么调用的.

// call.h ...
#ifndef __CALL_H_
#define __CALL_H_#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000//#ifdef __cplusplus
//extern "C" {
//#endifclass CCall
{
public:CCall();~CCall();int Call(int arg1, short arg2, char arg3, void *arg4);
protected:int m_Var1;
};//#ifdef __cplusplus
//}
//#endif#endif

处于种种目的, 我还是把函数体写在类外面:

// call.cpp ...
#include "call.h"CCall::CCall()
{m_Var1 = 18;
}CCall::~CCall()
{
}int CCall::Call(int arg1, short arg2, char arg3, void *arg4)
{int var1;short var2;char var3;int *p;var1 = arg1;var2 = arg2;var3 = arg3;p = (int *)arg4;*p = m_Var1;return 0;
}

还有入口和全局函数:

// main.cpp ...
#include <windows.h>
#include "call.h"int g_var1;void fnVoid(int arg1, short arg2, char arg3)
{int var1;short var2;char var3;var1 = arg1;var2 = arg2;var3 = arg3;arg1 = -1;g_var1 = 111;return;
}int fnDefaultCall(int arg1, short arg2, char arg3, void *arg4)
{int var1;short var2;char var3;int *p;var1 = arg1;var2 = arg2;var3 = arg3;p = (int *)arg4;*p = 7;return 0;
}int __stdcall fnStandardCall(int arg1, short arg2, char arg3, void *arg4)
{int var1;short var2;char var3;int *p;var1 = arg1;var2 = arg2;var3 = arg3;p = (int *)arg4;*p = 11;return 0;
}int __fastcall fnFastCall(int arg1, short arg2, char arg3, void *arg4)
{int var1;short var2;char var3;int *p;var1 = arg1;var2 = arg2;var3 = arg3;p = (int *)arg4;*p = 14;return 0;
}__declspec(naked) int __cdecl fnNakedCall(int arg1, short arg2, char arg3, void *arg4)
{// 1. 到这里所有寄存器的值与调用前一样// 2. 用变量名引用任何局部变量等同于引用主调函数变量或参数// 3. 必须负责寄存器的维护, 这里函数作为__cdecl__asm{push        ebp                 ; prolog beginmov           ebp, espsub         esp, 50hpush        ebxpush     esipush     edilea          edi, [ebp-50h]mov           ecx, 14hmov         eax, 0CCCCCCCChrep stos dword ptr [edi]     ; prolog end// var1 = arg1;mov         eax, dword ptr [ebp + 8]       ; [esp + 8]mov         dword ptr [ebp-4], eax          ; [esp - 4]// var2 = arg2;mov          cx, word ptr [ebp + 0Ch]mov         word ptr [ebp - 8], cx// var3 = arg3;mov          dl, byte ptr [ebp + 10h]mov            byte ptr [ebp - 0Ch], dl// p = (int *)arg4;mov         eax, dword ptr [ebp + 14h]mov          dword ptr [ebp - 10h], eax// *p = -1;mov         ecx, dword ptr [ebp - 10h]mov         dword ptr [ecx], 0FFFFFFFFh// return 22;mov         eax, 16h            ; 0x16 = 22pop         edi                 ; epilog beginpop           esipop          ebxmov          esp, ebppop         ebp                 ; epilog end// return to caller function(do not use ret 10h)ret}
}int main(int argc, char **argv)
{CCall *pCall;int var1;int ret;fnVoid(1, 2, 3);ret = fnDefaultCall(4, 5, 6, &var1);ret = fnStandardCall(8, 9, 10, &var1);ret = fnFastCall(11, 12, 13, &var1);pCall = new CCall();ret = pCall->Call(15, 16, 17, &var1);delete pCall; // pCall = NULL;ret = fnNakedCall(19, 20, 21, &var1);return 0;
}

下面在DEBUG下看看调用过程,注意如果是VS.NET,VC编译时会在每个变量前后都加一个DWORD,目的是检测缓冲区溢出

首先是调用无返回值的void函数,默认是__cdecl调用:

120:      fnVoid(1, 2, 3);
0040135D   push        3
0040135F   push        2
00401361   push        1
00401363   call        @ILT+5(fnVoid) (0040100a)
00401368   add         esp,0Ch
121:

可以看出,参数被从右向左压入堆栈,而后call函数地址,然后add esp,清理堆栈

注:

堆栈是从高地址向低地址延伸的,比如第一个push之前esp(栈顶指针)=0x0012FF04,那么push 3之后esp=0x0012FF00

以此类推,push 2,esp=0x0012FEFC; push 1,esp=0x0012FEF8

接着是call指令,这个指令将返回地址,即下一条指令位置(eip,指令指针)压入堆栈,比如

call之前eip=0x00401363(下一条eip=0x00401368)

call之后eip=0x0040100A,esp=0x0012FEF4

然后调用结束,__cdecl约定函数最后的ret指令会pop 栈顶给eip指针

eip=0x00401368 ESP=0x0012FEF8

而后add esp,0xc,这里0xC=12即3个DWORD就是前面push的数量(pop要弹出给某个寄存器,add直接修改栈顶位置,减少堆栈大小)

到此,堆栈和eip恢复调用前的状态.

接着,我们进入函数内部,看看它都做了什么见不得人的勾当:

7:    void fnVoid(int arg1, short arg2, char arg3)
8:    {
00401140   push        ebp
00401141   mov         ebp,esp
00401143   sub         esp,4Ch
00401146   push        ebx
00401147   push        esi
00401148   push        edi
00401149   lea         edi,[ebp-4Ch]
0040114C   mov         ecx,13h
00401151   mov         eax,0CCCCCCCCh
00401156   rep stos    dword ptr [edi]
9:        int var1;
10:       short var2;
11:       char var3;
12:       var1 = arg1;
00401158   mov         eax,dword ptr [ebp+8]
0040115B   mov         dword ptr [ebp-4],eax
13:       var2 = arg2;
0040115E   mov         cx,word ptr [ebp+0Ch]
00401162   mov         word ptr [ebp-8],cx
14:       var3 = arg3;
00401166   mov         dl,byte ptr [ebp+10h]
00401169   mov         byte ptr [ebp-0Ch],dl
15:
16:       arg1 = -1;
0040116C   mov         dword ptr [ebp+8],0FFFFFFFFh
17:       g_var1 = 111;
00401173   mov         dword ptr [g_var1 (0042ae74)],6Fh
18:       return;
19:   }
0040117D   pop         edi
0040117E   pop         esi
0040117F   pop         ebx
00401180   mov         esp,ebp
00401182   pop         ebp
00401183   ret
--- No source file  --------------------------------------------------------------
00401184   int         3

首先ebp是栈底指针,是高地址(比esp高),函数的堆栈应在esp到ebp之间,不应该读写高于ebp的堆栈内存

注意,不应该不是不可以,黑客所用的缓冲区溢出攻击就是利用这一点,当你的程序不小心写入了这些地方的时候他们就可以执行任意代码

包括添加管理员帐户等等,这种通常是strcpy之类的函数,比如char szText[256],但是源字符串超出256字节

push ebp是保存栈底的值,这个栈底是调用之前的,然后

mov ebp, esp把栈顶赋值给栈底,相当于调用前的栈顶作为现在的栈底,再接着

sub esp, 4Ch栈顶减小4C=76(19个DWORD),相当于堆栈大小是76字节,这样就创建了一个当前函数所使用的堆栈

接下来

push ebx将基址寄存器入栈,编译器是很机械的,其实到现在为止,并不需要基址寄存器,当然不需要暂存它的值,不过编译器并不是人,它不管这个

接着push esi和edi是串操作的原指针和目的指针,了解汇编语言的就知道,这小子开始批量处理了

lea edi,[ebp-4Ch]其实ebp-4Ch就是esp就是栈顶,栈顶地址作为目的(内存地址较低)

mov  ecx,13h数量0x13=19,还记得刚刚说的19个DWORD吗?

mov  eax,0CCCCCCCCh,串操作的值,0xCCCCCCCC

rep stos dword ptr [edi],向edi指向的dword写入eax的值,即0xcccccccc,如果ecx不为零,edi递增一个dword继续写入

知道为什么VC变量为什么默认值总是0xCC了吧,局部变量都保存在堆栈上,现在整个堆栈都是这个值

其实还有一个用处,等下函数返回时我们再说.

现在"春田花花同学会"正式开始,

// var1 = arg1;

mov eax,dword ptr [ebp+8]

mov dword ptr [ebp-4],eax

ebp是新的栈底指针,也就是原来的栈顶,前面调用的时候说过,call会push返回地址(指令地址不是返回值地址),

也就是说现在ebp指向的是返回地址?错!注意开始的push ebp,它又压入了一个DWORD,因此此时ebp指向的是原来的ebp

堆栈向低地址扩展,那么ebp+4就是函数的返回地址,顺序倒过来,ebp+8就是最后一个push压入的参数,也就是第一个参数!

堆栈向低地址扩展,那么ebp-4就是第一个局部变量了,有人问为什么要mov到eax,再从eax放到第一个局部变量?狄春说:这不是多次一举吗

元芳说:mov指令两个参数不能都是存储器,也就是内存,这就是为什么叫寄存器的原因,英文为REGISTER是登记的意思,既是名词也是动词

想通了这一点,后面的就好理解了,只不过用低字,低字节来转移而已

接着我们修改参数的值,其实也好理解了,因为调用后直接add esp,xx参数直接丢弃,因而并不改变什么,除了临时废弃的堆栈

接着是赋值全局变量,将一个立即数传送给全局变量的内存地址,也好理解了

没有返回值单函数,函数结尾return没有任何意义,如果在上面return会生成一条jmp指令,跳到这里来

最后,清理现场,最先push的最后pop恢复他们之前的值,恢复原来栈顶的值,pop恢复原来的栈底

最后一条ret指令,在函数调用时我们已经说了,这里说一下的是,如果此时堆栈中的返回地址(恢复后的栈顶esp指向的地址)被修改了,会有什么情况发生呢?

比如指向了ShellExecute这个API的地址,参数是cmd /c net user admin1 123456 /add

这个就留给大家思考吧, 还记得刚刚说0xCC的另一个用处吗,如果此时没有ret,执行到后面就是0xCC这个机器码对应的是int 3中断

在debug,比如OllyDebug等会在断点处插入0xCC,调试者继续运行才恢复这个字节原来的值再继续执行

所以,不经意间的缓冲区,往往造成的是内存禁止访问,或者中断,而有些人却对此十分敏感,就像有个美女裙子被吹起来,

阿弥陀佛,罪过!罪过!

文章好像很长了,我先int3一下,下文继续吧

关于编译型语言函数的调用(一)相关推荐

  1. Go 学习笔记(16)— 函数(02)[函数签名、有名函数、匿名函数、调用匿名函数、匿名函数赋值给变量、匿名函数做回调函数]

    1. 函数签名 函数类型也叫做函数签名,可以使用 fmt.Printf("%T") 格式化参数打印函数类型. package mainimport "fmt"f ...

  2. DllMain中不当操作导致死锁问题的分析--进程对DllMain函数的调用规律的研究和分析

    不知道大家是否思考过一个过程:系统试图运行我们写的程序,它是怎么知道程序起始位置的?很多同学想到,我们在编写程序时有个函数,类似Main这样的名字.是的!这就是系统给我们提供的控制程序最开始的地方(注 ...

  3. python函数+定义+调用+多返回值+匿名函数+lambda+高级函数(reduce、map、filter)

    python函数+定义+调用+多返回值+匿名函数+lambda+高级函数(reduce.map.filter) Python 中函数的应用非常广泛,比如 input() .print().range( ...

  4. golang 相互引用_golang go run undefined 同一个package中函数互相调用的问题

    golang中同一个package中函数互相调用的问题 同一个packge中(test) a.go package main func main(){ Test() } b.go package ma ...

  5. python调用自定义函数返回值的类型_生成dll文件以及python对DLL中函数的调用(参数类型以及返回值)...

    工具:VS2010    python2.7 (若使用的python是64位的,生成的dll也要使用x64) 系统:win7pro 64bit 首先,dll工程的创建以及dll文件的生成: new p ...

  6. C++对象模型8——构造函数和析构函数中对虚函数的调用、全局对象构造和析构、局部static数组的内存分配

    一.构造函数和析构函数中对虚函数的调用 仍然以https://blog.csdn.net/Master_Cui/article/details/109957302中的代码为例 base3构造函数和析构 ...

  7. Direct3D Draw函数 异步调用原理解析

    概述 在D3D10中,一个基本的渲染流程可分为以下步骤: 清理帧缓存: 执行若干次的绘制: 通过Device API创建所需Buffer: 通过Map/Unmap填充数据到Buffer中: 将Buff ...

  8. Swift2.0语言教程之函数嵌套调用形式

    Swift2.0语言教程之函数嵌套调用形式 Swift2.0语言函数嵌套调用形式 在Swift中,在函数中还可以调用函数,从而形成嵌套调用.嵌套调用的形式往往有两种:一种是在一个函数中调用其他函数:另 ...

  9. 空函数有参函数调用参数的注意事项Swift 1.1语言

    函数有参函数调用参数的注意事项Swift 1.1语言 空函数有参函数调用参数的注意事项Swift 1.1语言 空函数 函数有参函数调用参数的注意事项Swift 1.1语言空函数是函数中最简单的形式.在 ...

最新文章

  1. Tensorflow加载多个模型
  2. CRF++命名实体识别(NER)初步试探
  3. Java-GUI编程实战之管理系统 Day2【Swing(组件介绍、布局管理器、事件类及监听器类)、基础组件按钮和输入框的用法】
  4. Elasticsearch2.x Breaking changes
  5. 转:json与map互转
  6. dijkstra算法学习
  7. 奖励名单表格模板_“我用一套表格,解决了孩子的拖延症,一路用到小学高年级!”...
  8. [hdu2089]不要62(数位dp)
  9. ES6中Number中的扩展
  10. sersync实现多台服务器实时同步文件
  11. js读取txt文件中的内容
  12. VB中对数据库进行增、删、改操作
  13. 使用pn532将全加密卡复制到手环上 NFC校园门禁卡模拟教程
  14. IE9打开的html文件打印不了,IE9无法查看打印预览的2个解决方法
  15. How to debug Windows bugcheck 0x9F, parameter 3
  16. Electron-Vue中操作本地数据库NeDB
  17. 价格行为交易策略:锤子十字线,Fakey,内部日烛线
  18. BLAST中的E值的理解
  19. sip pbx_PBX免费CRM
  20. 操作系统理论的探索: (之三)

热门文章

  1. 浪迹天涯,总在落叶的季节里
  2. 面向对象—多态、鸭子类型(Day21)
  3. 安卓判断APP是在前台还是在后台
  4. 2021-11-09 2.bea生命周期和回调及其它注解
  5. 用Python做个打飞机小游戏超详细教程
  6. xsl是什么文件 html 样式表单,XSL 指扩展样式表语言
  7. 图书管理系统 jsp + servlet + mysql (2023)
  8. 实验心得html,心得体会 html实训心得.doc
  9. labview入门范例 哈哈
  10. linux下lds链接脚本详解