`
wanjianfei
  • 浏览: 308019 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

论调用约定

阅读更多

在C语言中,假设我们有这样的一个函数:

int function(int a,int b)

调用时只要用result = function(1,2)这样的方式就可以使用这个函数。但是,当高级语言被编译成计算机可以识别的机器码时,有一个问题就凸现出来:在CPU中,计算机没有办法知道一个函数调用需要多少个、什么样的参数,也没有硬件可以保存这些参数。也就是说,计算机不知道怎么给这个函数传递参数,传递参数的工作必须由函数调用者和函数本身来协调。为此,计算机提供了一种被称为栈的数据结构来支持参数传递。

栈是一种先进后出的数据结构,栈有一个存储区、一个栈顶指针。栈顶指针指向堆栈中第一个可用的数据项(被称为栈顶)。用户可以在栈顶上方向栈中加入数据,这个操作被称为压栈(Push),压栈以后,栈顶自动变成新加入数据项的位置,栈顶指针也随之修改。用户也可以从堆栈中取走栈顶,称为弹出栈(pop),弹出栈后,栈顶下的一个元素变成栈顶,栈顶指针随之修改。

函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在堆栈中取得数据,并进行计算。函数计算结束以后,或者调用者、或者函数本身修改堆栈,使堆栈恢复原装。

在参数传递中,有两个很重要的问题必须得到明确说明:

  • 当参数个数多于一个时,按照什么顺序把参数压入堆栈
  • 函数调用后,由谁来把堆栈恢复原装

在高级语言中,通过函数调用约定来说明这两个问题。常见的调用约定有:

  • stdcall
  • cdecl
  • fastcall
  • thiscall
  • naked call

stdcall调用约定

stdcall很多时候被称为pascal调用约定,因为pascal是早期很常见的一种教学用计算机程序设计语言,其语法严谨,使用的函数调用约定就是stdcall。在Microsoft C++系列的C/C++编译器中,常常用PASCAL宏来声明这个调用约定,类似的宏还有WINAPI和CALLBACK。

stdcall调用约定声明的语法为(以前文的那个函数为例):

int __stdcall function(int a,int b)

stdcall的调用约定意味着:1)参数从右向左压入堆栈,2)函数自身修改堆栈 3)函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸

以上述这个函数为例,参数b首先被压栈,然后是参数a,函数调用function(1,2)调用处翻译成汇编语言将变成:

push 2 第二个参数入栈 push 1 第一个参数入栈 call function 调用参数,注意此时自动把cs:eip入栈

而对于函数自身,则可以翻译为:

push ebp 保存ebp寄存器,该寄存器将用来保存堆栈的栈顶指针,可以在函数退出时恢复 mov ebp,esp 保存堆栈指针 mov eax,[ebp + 8H] 堆栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a add eax,[ebp + 0CH] 堆栈中ebp + 12处保存了b mov esp,ebp 恢复esp pop ebp ret 8

而在编译时,这个函数的名字被翻译成_function@8

注意不同编译器会插入自己的汇编代码以提供编译的通用性,但是大体代码如此。其中在函数开始处保留esp到ebp中,在函数结束恢复是编译器常用的方法。

从函数调用看,2和1依次被push进堆栈,而在函数中又通过相对于ebp(即刚进函数时的堆栈指针)的偏移量存取参数。函数结束后,ret 8表示清理8个字节的堆栈,函数自己恢复了堆栈。

cdecl调用约定

cdecl调用约定又称为C调用约定,是C语言缺省的调用约定,它的定义语法是:

int function (int a ,int b) //不加修饰就是C调用约定 int __cdecl function(int a,int b)//明确指出C调用约定

在写本文时,出乎我的意料,发现cdecl调用约定的参数压栈顺序是和stdcall是一样的,参数首先由有向左压入堆栈。所不同的是,函数本身不清理堆栈,调用者负责清理堆栈。由于这种变化,C调用约定允许函数的参数的个数是不固定的,这也是C语言的一大特色。对于前面的function函数,使用cdecl后的汇编码变成:

调用处 push 1 push 2 call function add esp,8 注意:这里调用者在恢复堆栈 被调用函数_function处 push ebp 保存ebp寄存器,该寄存器将用来保存堆栈的栈顶指针,可以在函数退出时恢复 mov ebp,esp 保存堆栈指针 mov eax,[ebp + 8H] 堆栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a add eax,[ebp + 0CH] 堆栈中ebp + 12处保存了b mov esp,ebp 恢复esp pop ebp ret 注意,这里没有修改堆栈

MSDN中说,该修饰自动在函数名前加前导的下划线,因此函数名在符号表中被记录为_function,但是我在编译时似乎没有看到这种变化。

由于参数按照从右向左顺序压栈,因此最开始的参数在最接近栈顶的位置,因此当采用不定个数参数时,第一个参数在栈中的位置肯定能知道,只要不定的参数个数能够根据第一个后者后续的明确的参数确定下来,就可以使用不定参数,例如对于CRT中的sprintf函数,定义为:

int sprintf(char* buffer,const char* format,...)

由于所有的不定参数都可以通过format确定,因此使用不定个数的参数是没有问题的。

fastcall

fastcall调用约定和stdcall类似,它意味着:

  • 函数的第一个和第二个DWORD参数(或者尺寸更小的)通过ecx和edx传递,其他参数通过从右向左的顺序压栈
  • 被调用函数清理堆栈
  • 函数名修改规则同stdcall

其声明语法为:int fastcall function(int a,int b)

thiscall

thiscall是唯一一个不能明确指明的函数修饰,因为thiscall不是关键字。它是C++类成员函数缺省的调用约定。由于成员函数调用还有一个this指针,因此必须特殊处理,thiscall意味着:

  • 参数从右向左入栈
  • 如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入堆栈。
  • 对参数个数不定的,调用者清理堆栈,否则函数自己清理堆栈

为了说明这个调用约定,定义如下类和使用代码:

class A
{
public:
 int function1(int a,int b);
 int function2(int a,...);
};
int A::function1 (int a,int b)
{
 return a+b;
}
#include <stdarg></stdarg>
int A::function2(int a,...)
{
 va_list ap;
 va_start(ap,a);
 int i;
 int result = 0;
 for(i = 0 ; i < a ; i ++)
 {
  result += va_arg(ap,int);
 }
 return result;
}
void callee()
{
 A a;
 a.function1 (1,2);
 a.function2(3,1,2,3);
}

callee函数被翻译成汇编后就变成:

//函数function1调用 0401C1D push 2 00401C1F push 1 00401C21 lea ecx,[ebp-8] 00401C24 call function1 注意,这里this没有被入栈 //函数function2调用 00401C29 push 3 00401C2B push 2 00401C2D push 1 00401C2F push 3 00401C31 lea eax,[ebp-8] 这里引入this指针 00401C34 push eax 00401C35 call function2 00401C3A add esp,14h

可见,对于参数个数固定情况下,它类似于stdcall,不定时则类似cdecl

naked call

这是一个很少见的调用约定,一般程序设计者建议不要使用。编译器不会给这种函数增加初始化和清理代码,更特殊的是,你不能用return返回返回值,只能用插入汇编返回结果。这一般用于实模式驱动程序设计,假设定义一个求和的加法程序,可以定义为:

__declspec(naked) int  add(int a,int b)
{
   __asm mov eax,a
   __asm add eax,b
   __asm ret 
}

注意,这个函数没有显式的return返回值,返回通过修改eax寄存器实现,而且连退出函数的ret指令都必须显式插入。上面代码被翻译成汇编以后变成:

mov eax,[ebp+8] add eax,[ebp+12] ret 8

注意这个修饰是和__stdcall及cdecl结合使用的,前面是它和cdecl结合使用的代码,对于和stdcall结合的代码,则变成:

__declspec(naked) int __stdcall function(int a,int b)
{
    __asm mov eax,a
    __asm add eax,b
    __asm ret 8        //注意后面的8
}

至于这种函数被调用,则和普通的cdecl及stdcall调用函数一致。

函数调用约定导致的常见问题

如果定义的约定和使用的约定不一致,则将导致堆栈被破坏,导致严重问题,下面是两种常见的问题:

  1. 函数原型声明和函数体定义不一致
  2. DLL导入函数时声明了不同的函数约定

以后者为例,假设我们在dll种声明了一种函数为:

__declspec(dllexport) int func(int a,int b);//注意,这里没有stdcall,使用的是cdecl

使用时代码为:

      typedef int (*WINAPI DLLFUNC)func(int a,int b);
      hLib = LoadLibrary(...);
      DLLFUNC func = (DLLFUNC)GetProcAddress(...)//这里修改了调用约定
      result = func(1,2);//导致错误

由于调用者没有理解WINAPI的含义错误的增加了这个修饰,上述代码必然导致堆栈被破坏,MFC在编译时插入的checkesp函数将告诉你,堆栈被破坏了。

分享到:
评论

相关推荐

    rdtsc-checkvirt-poc:通过推测执行PoC和论文进行虚拟化检测

    概要 使用推测执行的虚拟机... 但是,它在有限的分布上进行了测试,因此在某些发行版上可能存在编译和调用约定的问题。 作者 该漏洞是由Innokentiy Sennovskiy发现的。 特别感谢Alexander Salikov对测试和PoC的帮助。

    浅谈Turbo C过程调用汇编 (2012年)

    通过了解Turbo C和汇编兼容使用IBMPC系列机存贮系统的方法,遵循Turbo C调用汇编的各项约定,可将汇编语言程序当作C语言的一个过程来处理,并通过实例阐释如何实现Turbo C对汇编语言程序的调用。

    RayTracing:Coleman 2011年论文的Python实现

    所有向量都将按照(r,theta,phi)=(径向,极地,方位角)的ISO约定在球坐标中存储和使用示踪剂包含用于跟踪广义射线的核心代码的文件。输入项已知参数:射线的起点和终点电离层模型:在给定坐标系中空间位置的...

    二进制翻译中的库函数识别技术研究 (2006年)

    体系结构的不断发展给软件开发者带来了巨大的风险,造成了新体系结构推广的困难,而二进制翻译技术使得...基于IA-64体系结构的调用约定及实例分析证明,该方法是一种简单实用的库函数识别方法,经I2A系统验证是有效的。

    r2-learner:代码支持“关于递归表示模型的某些限制”论文

    实现遵循 scikit-learn 约定,但请注意,使用set_params调用的方法可能存在一些问题。 请记住,这只是研究实施。 再现结果 为了重现结果,您需要拟合所有模型。 更改n_jobs参数以加快计算速度。 拟合 SVM、线性 ...

    lua 程序设计学习.doc 版

    1.3 词法约定 1.4 命令行方式 第2章 类型和值 2.1 Nil 2.2 Booleans 2.3 Numbers 2.4 Strings 2.5 Functions 2.6 Userdata and Threads 第3章 表达式 3.1 算术运算符 3.2 关系运算符 3.3 逻辑运算符 3.4 连接运算符 ...

    AtX:elf将Arm32转换为X86_64

    将ARM转换为X86 我试图回答将ARM32 elf二进制文件转换为X86_64 elf二进制文件有多困难。...基于ABI的函数调用约定,我决定了寄存器的映射: ARM32 X86_64 00 电子数据交换 R1 ESI R2 EDX R3 ECX R4 EAX

    餐厅营业查看系统Java源码-elements-of-unit-testing-style:单元测试风格的元素

    餐厅营业查看系统Java源码单元测试风格的元素 许多论文都是关于编写单元测试的过程,包括使用测试优先...和其他项目中的约定,让新手很容易有宾至如归的感觉。 但是,我们不会调用单元测试中的方法。 它们不需要代表请

    TCPIP协议详解卷2:实现

    1.2.2 印刷约定 2 1.3 历史 2 1.4 应用编程接口 3 1.5 程序示例 4 1.6 系统调用和库函数 6 1.7 网络实现概述 6 1.8 描述符 7 1.9 mbuf与输出处理 11 1.9.1 包含插口地址结构的mbuf 11 1.9.2 包含数据的mbuf 12 1.9.3...

    Java毕业设计-基于springboot开发的网上订餐系统-毕业论文(附毕设源代码).rar

    本项目采用Spring Boot框架构建,充分利用了Spring Boot的“约定优于配置”的特性,极大地简化了项目的搭建和开发流程。同时,Spring Boot与众多开源组件的无缝集成,使得项目在安全性、性能、可维护性等方面都达到...

    校园网设计方案-王传奇.doc

    因为不同类型 的计算机通信需要遵循共同的规则和约定,就像我们讲不同语言的人进行对话需要一种 标准语言才能沟通,在计算机网络中双方需共同遵守的规则和约定就叫计算机网络协议 ,由它解释、协调和管理计算机之间...

    windows驱动开发技术详解-part2

     3.1.1 调用约定  3.1.2 函数的导出名  3.1.3 运行时函数的调用  3.2 用DDK编译环境编译驱动程序  3.2.1 编译版本  3.2.2 nmake工具  3.2.3 build工具  3.2.4 makefile文件  3.2.5 dirs文件  ...

    Windows驱动开发技术详解的光盘-part1

     3.1.1 调用约定  3.1.2 函数的导出名  3.1.3 运行时函数的调用  3.2 用DDK编译环境编译驱动程序  3.2.1 编译版本  3.2.2 nmake工具  3.2.3 build工具  3.2.4 makefile文件  3.2.5 dirs文件  ...

    Lua中文教程(pdf版)

    1.3 词法约定...7 1.4 命令行方式.................7 第2章类型和值9 2.1 Nil..............9 2.2 Booleans....9 2.3 Numbers...10 2.4 Strings......10 2.5 Functions.12 2.6 Userdata and Threads.12 第3...

    c#学习笔记.txt

    看完了前面几段,我的朋友提出了不同的意见:C#不是Java的Clone,它只是长得有些像Java而已,其实面向对象、中间语言什么的也不是什么新玩意儿,非Sun独创,有文为证:华山论剑:C#对Java。另外他对我上一集中说...

    gcc/g++下C/C++和汇编的接口* (2004年)

    汇编语言是低级语言,与硬件和操作系统紧密联系。它能够完成许多其他语言所不能完成的功能。笔者较详细地分析了在Linux平台的gcc/g++开发环境下,汇编程序与调用它的C/C++程序的接口约定。

    asp.net知识库

    也论该不该在项目中使用存储过程代替SQL语句 如何使数据库中的表更有弹性,更易于扩展 存储过程——天使还是魔鬼 如何获取MSSQLServer,Oracel,Access中的数据字典信息 C#中利用GetOleDbSchemaTable获取数据库内表信息...

Global site tag (gtag.js) - Google Analytics