call 指令与 retn 指令

首先我们得了解 CALL 和 RETN 指令的作用,才能更好地理解调用规则,这也是先决条件。

实际上,CALL 指令就是先将下一条指令的 EIP 压栈,然后 JMP 跳转到对应的函数的首地址,当执行完函数体后,通过 RETN 指令从堆栈中弹出 EIP,程序就可以继续执行 CALL 的下一条指令。

__cdecl 与 __stdcall 调用规则

C/C++ 中不同的函数调用规则会生成不同的机器代码,产生不同的微观效果,接下来让我们一起来浅析四种调用规则的原理和它们各自的异同。首先我们通过一段 C 语言代码来引导我们的浅析过程。

这里我们编写了三个函数,它们的功能都是返回两个参数的相加结果,只是每个函数都有不一样的调用规则。

我们使用 printf 函数主要是为了在 OllyDBG 中能够快速下断点,以确定后边调用三个函数的位置,便于分析。在这里我给每个函数都用了内联的 NOP 指令来分隔开,图中也用红框标明,这样可以便于区分每个函数的调用过程。通过一些简单的步骤,我们用 OllyDBG 查看了编译后代码的“真面目”。代码中有 4 个 CALL,第一个是 printf,我们不关心这个。后面三个分别是具有 __cdecl,__stdcall,__fastcall 调用规则的函数 CALL(这里我已经做了注释)。

在这里为了循序渐进,我们先介绍 __cdecl 与 __stdcall 调用规则,后面我们会接着浅析 __fastcall 调用规则。

首先,我们得明白一个教条(其实也是自己概括的),那就是 —— 调用规则的区别产生其实就是由于调用者与被调用者之间的“责任分配”问题。

代码段中的第 2 个就是 __cdecl 调用规则的 CALL。__cdecl 是 C/C++、MFC 默认的调用规则。我们可以看到,在执行 CALL 之前,程序会将参数按照从右到左的方式压栈,这里是两个整型参数,每压栈一个 ESP 都会减 4,这样下来 ESP 会减少 8,然后 CALL 这个函数。常规地,我们可以看到,这个 CALL 里面参数的处理和通常情况下一致,先将 EBP 压栈保存现场,然后使 EBP 重合于 ESP,再通过 EBP + 偏移地址来取得两个参数值,赋值再累加到 EAX 中,EAX 将作为返回值给调用者使用,还原 EBP 现场,调用 RETN 返回到调用者。最后,使得 ESP 加 8。哎!这刚好和开头对称嘛!为了堆栈平衡,ESP 最终又被拉回到了 CALL 之前的位置。我们暂且可以小结一下,实际上在 __cdecl 调用规则中,需要调用者来负责清栈操作(由调用者将 ESP 拉高以维持堆栈平衡)。

代码段中的第 3 个是 __stdcall 调用规则的 CALL。__stdcall 调用规则在 Win32 API 函数中用的比较多。跟 __cdecl 一样,在执行 CALL 之前,程序会先将参数从右到左依次压栈,我们跟进 CALL 里面,可以看到以下的反汇编代码,我们很容易发现,除了最后一条指令,其他的指令与 __cdecl 调用规则是基本一样的。最后一条指令是“RETN 0x8”,这是什么意思呢?实际上呢,就相当于先执行“ADD ESP, 0x8”再执行“POP EIP” 。换言之,就是将 ESP 加 8,然后正常 RETN 返回到调用者。

我们不难发现,__stdcall 调用规则使得被调用者来执行清栈操作(由被调用者函数自身将 ESP 拉高以维持堆栈平衡),这也是 __stdcall 与 __cdecl 调用规则的最根本的区别。

__cdecl 偏向于把责任分配给调用者,动脑筋想想,我们的程序在 CALL __cdecl 调用规则的函数之前,把参数从右到左依次压栈,CALL 返回后,剩下的清栈操作都交给调用者处理,调用者负责拉高 ESP。再回来想想 __stdcall,在 CALL 中将调用者的 EBP 压栈以保存现场,然后使 EBP 对齐于 ESP,然后通过 EBP + 偏移地址取得参数,并且经过加法得到 EAX 返回值,从堆栈弹出 EBP 恢复现场,但是最后不一样的地方,程序将执行 “RETN 0x8” 将 ESP 拉回之前的 ESP + 8 的位置,换言之,被调用者将负责清栈操作。这就是之前所谓的“责任分配”的区别。

__fastcall 调用规则

不难揣测 fastcall 的英文意思貌似是“快速调用”,这一点与它的调用规则息息相关,它的快速是有原因的,让我们继续来看看之前那张反汇编的截图,代码段中的第 4 个就是 __fastcall 调用规则的 CALL。进 CALL 前,出乎意料地,程序将两个参数从右到左分别传给了 EDX,ECX 寄存器,讲到这里,学过计算机系统相关知识的人很容易理解为什么这叫“快速调用”了,寄存器比内存快很多很多倍,可以认为传参给寄存器,要比在内存中更快得多,效率更高。

由于参数是直接传递给了寄存器,堆栈并未发生改变,在 CALL 中,EBP 压栈,EBP 和 ESP 对齐之后,ESP 减 8,这个操作有点像对局部变量分配堆栈空间(这里有我之前一篇博客,对局部变量的存放规则做了浅析),然后程序将 EDX,ECX 分别赋值给 EBP – 8 与 EBP – 4 这两个地址,这个过程相当于用寄存器给局部变量赋值,接下来运算结果将保存在 EAX 中,ESP 归位,EBP 恢复现场,最后 RETN 返回调用者领空。

本例只传送了两个整数型参数。其实呢,对于 __fastcall 调用规则,左边开始的两个不大于4字节(int)的参数分别放在ECX和EDX寄存器,其余的参数仍旧自右向左压栈传送。并且,__fastcall 调用规则使得被调用者负责清理栈的操作(由被调用者函数自身将 ESP 拉高以维持堆栈平衡),这一点和 __stdcall 一样。

__pascal 调用规则

__pascal 是用于 Pascal / Delphi 编程语言的调用规则,C/C++ 中也可以使用这种调用规则。简单地说,__pascal 调用规则与 __stdcall 不同的地方就是压栈顺序恰恰相反,前面讲到的三种调用规则的压栈顺序都是从右到左依次入栈,__pascal 则是从左到右依次入栈。并且,被调用者(函数自身)将自行完成清栈操作,这和 __stdcall,__fastcall 一样。由于比较简单,我就没有做出示例。

小结

做个表格来小结一下,很直观就能看出这四种调用规则的异同:

调用规则

入栈顺序

清栈责任

__cdecl

从右到左

调用者

__stdcall

从右到左

被调用者

__fastcall

从右到左(先 EDX、ECX,再到堆栈)

被调用者

__pascal

从左到右

被调用者

__cdecl、__stdcall、__fastcall 与 __pascal 浅析的更多相关文章

  1. __cdecl,__stdcall,__fastcall,__pascal,__thiscall 的区别

    关于函数的调用规则(调用约定),大多数时候是不需要了解的,但是如果需要跨语言的编程,比如VC写的dll要delphi调用,则需要了解. microsoft的vc默认的是__cdecl方式,而windo ...

  2. 函数的调用规则(__cdecl,__stdcall,__fastcall,__pascal)

    关于函数的调用规则(调用约定),大多数时候是不需要了解的,但是如果需要跨语言的编程,比如VC写的dll要delphi调用,则需要了解. microsoft的vc默认的是__cdecl方式,而windo ...

  3. __cdecl __stdcall __fastcall之函数调用约定讲解

    首先讲解一下栈帧的概念: 从逻辑上讲,栈帧就是一个函数执行的环境:函数参数.函数的局部变量.函数执行完后返回到哪里等等. 实现上有硬件方式和软件方式(有些体系不支持硬件栈) 首先应该明白,栈是从高地址 ...

  4. C调用约定__cdecl、__stdcall、__fastcall、__pascal分析

    参考原文地址:https://www.cnblogs.com/yenyuloong/p/9626658.html C/C++ 中不同的函数调用规则会生成不同的机器代码,产生不同的微观效果,接下来让我们 ...

  5. 函数调用方式--__thiscall调用方式和__cdecl,__stdcall有什么区别

    函数调用方式--__thiscall调用方式和__cdecl,__stdcall有什么区别 首先,__thiscall是关于类的一种调用方式,它与其他调用方式的最大区别是:    __thiscall ...

  6. __cdecl 、__fastcall、__stdcall

    调用约定: __cdecl __fastcall与 __stdcall,三者都是调用约定(Calling convention),它决定以下内容:1)函数参数的压栈顺序,2)由调用者还是被调用者把参数 ...

  7. __declspec,__cdecl,__stdcall区别和作用

    _cdecl和__stdcall都是函数调用规范(还有一个__fastcall),规定了参数出入栈的 顺序和方法,如果只用VC编程的话可以不用关心,但是要在C++和Pascal等其他语言通信的时候就要 ...

  8. __declspec,__cdecl,__stdcall都是什么意思?有什么作用?

    __cdecl和__stdcall都是函数调用规范(还有一个__fastcall),规定了参数出入栈的顺序和方法,如果只用VC编程的话可以不用关心,但是要在C++和Pascal等其他语言通信的时候就要 ...

  9. __cdecl & __stdcall calling conventions

    (一) __cdecl: c declaration C语言默认的函数调用方法:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈.C/C++默认的调用方式,可用于函数参数不确定的情况下. ...

随机推荐

  1. BestCoder Round #56/hdu5463 Clarke and minecraft 水题

    Clarke and minecraft 问题描述 克拉克是一名人格分裂患者.某一天,克拉克分裂成了一个游戏玩家,玩起了minecraft.渐渐地,克拉克建起了一座城堡. 有一天,克拉克为了让更多的人 ...

  2. luogu2827 蚯蚓

    题目大意 本题中,我们将用符号[c]表示对c向下取整,例如:[3.0」= [3.1」= [3.9」=3. 蛐蛐国最近蚯蚓成灾了!隔壁跳蚤国的跳蚤也拿蚯蚓们没办法,蛐蛐国王只好去请神刀手来帮他们消灭蚯蚓 ...

  3. Codeforces Round #395 (Div. 1)

    比赛链接:http://codeforces.com/contest/763 A题: #include <iostream> #include <cstdio> #includ ...

  4. POJ1743 Musical Theme 最长重复子串 利用后缀数组

    POJ1743 题目意思是求不重叠的最长相同变化的子串,输出该长度 比如1 2 3 4 5 6 7 8 9 10,最长长度为5,因为子串1 2 3 4 5 和 6 7 8 9 10变化都一样的 思路: ...

  5. JeePlus:API工具

    ylbtech-JeePlus:API工具 1.返回顶部   2.返回顶部   3.返回顶部   4.返回顶部   5.返回顶部 0. http://wiki.jeeplus.org/docs/sho ...

  6. 【174】C#添加非默认字体

    参考:C# WinForm程序安装字体或直接调用非注册字体 参考:百度知道 在Debug文件夹下面新建一个font的文件夹,然后将字体的文件复制到里面,使用的时候,直接调用字体文件! private ...

  7. P2700逐个击破(并查集/树形dp)

    P2700 逐个击破 题目背景 三大战役的平津战场上,傅作义集团在以北平.天津为中心,东起唐山西至张家口的铁路线上摆起子一字长蛇阵,并企图在溃败时从海上南逃或向西逃窜.为了就地歼敌不让其逃走,老毛同志 ...

  8. oracle-数据库泵EXPDP导出用户下所有

    1登录sys用户 2创建目录 create directory [dirname] as ‘[dirpath]’; dirname:取的名字 dirpath:dmp文件导出路径 示例:create d ...

  9. Jsp四个作用域page、request、session和application的区别

    1.简单说 page指当前页面.在一个jsp页面里有效  2.request 指从http请求到服务器处理结束,返回响应的整个过程.在这个过程中使用forward方式跳转多个jsp.在这些页面里你都可 ...

  10. ActiveMQ命令行工具

    命令行工具 命令行工具 n        activemq——运行activemq代理 n activemq-admin——管理代理的实例 在5.0之前activemq-admin被分成多个脚本,例如 ...