x86函数调用约定

以下摘自《IDA Pro》,貌似有一些细节之处没有交代清楚呢,需要进一步思考、实践。

了解栈帧的基本概念后,接下来详细介绍它们的结构。下面的例子涉及x86体系结构和与常见的x86编译器(如Microsoft Visual C/C++或GNU的gcc/g++)有关的行为。创建栈帧的最重要的步骤是,通过调用函数将函数存入栈中。调用函数必须存储被调用函数所需要的参数,否则可能导致严重的问题。各个函数会选择并遵照某一特定的调用约定,以表明他们希望以何种方式接收参数。

调用约定指定调用方放置函数所需参数的具体位置。调用预定可能需求将参数放置在特定的寄存器、程序栈、或者寄存器和栈中。同样重要的是,在传递参数时,程序栈还要决定:被调用函数完成其操作后,由谁负责从栈中删除这些参数。一些调用约定规定,由调用方负责删除它放置在栈中的参数,而另一些调用约定则要求被调用函数负责删除栈中的参数。遵照指定的调用约定对于维护程序栈指针的完整性尤为重要。

1.C调用约定

x86体系结构的许多C编译器使用默认调用约定叫做C调用约定。如果默认的调用约定被重写,则C/C++程序中常用的_cdecl修饰符会迫使编译器利用C调用约定。自现在开始,我们把这种调用约定叫做cdecl调用约定。cdecl调用约定规定:调用方按从右到左的顺序将函数参数放入栈中,在被调用的函数完成其操作时,调用方(而不是被调用方)负责从栈中清除参数。

从右到左在栈中放入参数的一个结果是,如果函数被调用,最左边的(第一个)参数将始终位于栈顶。这样无论该函数需要多少个参数,我们都可轻易找到第一个参数。因此,cdecl调用约定非常适用于那些参数数量可变的函数(如printf)。

要求调用函数从栈中删除参数,意味着你将经常看到:指令在由被调用的函数返回后,会立即对程序栈指针进行调整。如果函数能够接受数量可变的参数,则调用方非常适于进行这种调整,因为它清楚地知道,它像函数传递了多少个参数,因而能够轻松做出正确的调整。而被调用的函数事先无法知道自己会受到多少个参数,因而很难对栈做出必要的调整。

在下面的例子中,我们调用一个拥有以下原型的函数:

void demo_cdecl(int w, int x, int y, int z)

默认情况下,这个函数将使用cdecl调用预定,并希望你按从右到左的顺序压入4个参数,同时要求调用方清除栈中的参数,编译器可能会为这个函数的调用生成以下代码:

; demo_cdecl(1, 2, 3, 4);//programer calls demo_cdecl
1. push 4                ;push parameter z
   push 3                ;push parameter y
   push 2                ;push parameter x
   push 1                ;push parameter w
   call demo_cdecl       ;call the function
2. add  esp, 16          ;adjust esp to its former value

从1开始的4个push操作使程序栈指针(ESP发生)16个字节(在32位体系结构上为4*sizeof(int))的变化,从demo_cdecl返回后,它们在2处被撤销。如果demo_cdecl被调用50次,那么,每次调用之后,都会发生类似于2处的调整。下面的例子同样遵照cdecl调用约定,但是,在每次调用demo_cdecl后,调用方不需要删除栈中的参数。

; demo_cdecl(1, 2, 3, 4) //programemr calls demo_cdecl
  mov  [esp+12],     4   ; move parameter z to fourth position on stack
  mov  [esp+8],      3   ; move parameter y to third position on stack
  mov  [esp+4],      2   ; move parameter x to second position on stack
  mov  [esp],        1   ; move parameter w to top of stack
  call demo_cdecl        ; call the function

在这个例子中,在函数的“序言”阶段,编译器已经在栈顶为demo_cdecl的参数预先分配了存储空间。在demo_cdecl的参数放到栈上时,并不需要修改程序栈指针,因此,在调用demo_cdecl结束后,也就不需要调整栈指针。GNU编译器(gcc和g++)正是利用这种技巧将函数参数放到栈上的。注意,无论采用哪一种方法,在调用函数时,栈指针都会指向最左边的参数。

2.标准调用约定

这里的标准似乎有些用词不当,因为它是微软为自己的调用约定所起的名称。这种约定在函数声明中使用了修饰符_stdcall,如下所示:

void _stdcall demo_stdcall(int w, int x, int y);

为避免标准一词引起混淆,在本书的剩余部分,我们将这种调用约定称为stdcall调用约定。

和cdecl调用约定一样,stdcall调用约定按从右到左的顺序将函数参数放在程序栈上。使用stdcall调用约定的区别在于:函数结束执行时,应由被调用的函数负责删除栈中的函数参数。对被调用的函数而言,要完成这个任务,它必须清楚知道栈中有多少个参数,这只有在函数接受的参数数量固定不变时才有可能。因此,printf这种接受数量可变的参数的函数不能使用stdcall调用约定。例如,demo_stdcall函数需要3个整数参数,在栈上共占用12个字节(在32位体系结构上为3*sizeof(int))的空间。x86编译器能够使用RET指令的一种特殊形式,同时从栈顶提取返回地址,并给栈指针加上12,以清除函数参数。demo_stdcall可能会使用以下指令返回到调用方:

ret 12    ; return and clear 12 bytes from the stack

使用stdcall的主要优点在于,在每次函数调用之后,不需要通过代码从栈中清除参数,因而能够生成体积稍小、速度稍快的程序。根据惯例,微软对所有由共享库(DLL)文件输出的参数数量固定的函数使用stdcall约定。如果你正尝试为某个共享库组件生成函数原型或二进制兼容的替代者,请一定记住这一点。

3.x86 fastcall约定

fastcall约定是stdcall约定的一个变体,它向CPU寄存器(而非程序栈)最多传递两个参数。Microsoft Visual C/C++和GNU gcc/g++(3.4及更低(应该是更高吧?--shijianyujingshen)版本)编译器能够识别函数声明中的fastcall修饰符。如果指定使用fastcall约定,则传递给函数的前两个参数将分别位于ECX和EDX寄存器中。剩余的其他参数则以类似于stdcall约定的方式从右到左放入栈上。同样与stdcall约定类似的是,在返回其调用方时,fastcall函数负责从栈中删除参数。下面的声明中即使用了fastcall修饰符:

void fastcall demo_fastcall(int w, int x, int y, int z)

为调用demo_fastcall,编译器可能会生成以下代码:

; demo_fastcall(1, 2, 3, 4);   //programer calls demo fastcall
push  4                ; move parameter z to second position on stack
push  3                ; move parameter y to tio position on stack
mov   edx, 2           ; move parameter x to edx
mov   ecx, 1           ; move parameter w to ecx
call  demo_fastcall    ; call the function

注意,调用demo_fastcall返回后,并不需要调整栈,因为demo_fastcall负责在返回到调用方时从栈中清除参数y和z。由于有两个参数被传递到寄存器中,被调用的函数仅仅需要从栈中清除8字节,即使该函数拥有4个参数也是如此,理解这一点很重要。

4.C++调用约定

C++类中的非静态成员函数与标准函数不同,它们需要使用this指针,该指针指向用于调用函数的对象。用于调用函数的对象的地址必须由调用方提供,因此,它在调用非静态成员函数时作为参数提供。C++语言标准并未规定应如何向非静态成员函数传递this指针,因此,不同编译器使用不同的技巧来传递this指针,这点也就不足为奇。

Microsoft Visual C++提供thiscall调用约定,它将this指针传递到ECX寄存器,并且和在stdcall中一样,它要求非静态成员函数清除栈中的参数。GNU g++编译器将this看成是任何非静态成员函数的第一个隐含参数,而在所有其他方面与使用cdecl约定相同。因此,对使用g++编译的代码来说,在调用非静态成员函数之前,this被放置到栈顶,且调用方负责在函数返回时删除栈中参数(至少有一个参数)。已编译的C++代码的其他特性将在第8章中讨论。

5.其他调用约定

要完整地介绍现有的每一个调用约定,可能需要写一本书。调用约定通常是特定于语言、编译器和CPU的。如果遇到更少见的编译器生成的代码,可能需要你自己进行一番研究。但是,以下这些情况需要特别注意:优化代码、定制汇编语言代码和系统调用。

如果输出函数(如库函数)是为了供其他程序员使用,那么,它必须遵照主流的调用约定,以便程序员能够轻松调用这些函数。另外如果函数仅供内部程序使用,则该函数需要采用只有函数的程序才了解的调用约定。在这类情况下,优化编译器会选择使用备用的调用约定,以生成运行速度更快的代码。这样的例子包括:在Microsoft Visual C++中使用/GL选项,以及在GNU gcc/g++中使用regparm关键字

如果程序员不怕麻烦,使用了汇编语言,那么,他们就能够完全控制如何向他们创建的函数传递参数。除非他们希望创建供其他程序员使用的函数,否则,汇编语言程序员能够以任何他们认为适当的方式传递参数。因此,在分析自定义汇编代码时,请格外小心。在模糊例程(obfuscation routine)和shellcode中经常可以看到自定义汇编代码。

系统调用是一种特殊的函数调用,用于请求一项操作系统服务。通常,系统调用会造成状态转换,由用户模式进入内核模式,以便操作系统内核执行用户的请求。启动系统调用的方式因操作系统和CPU而异。例如,Linux x86系统调用使用int 0x80指令或sysenter指令启动,而其他x86操作系统可能只使用sysenter指令。在许多x86系统(Linux是一个例外)上,系统调用的参数位于运行时栈上,并在启动系统调用之前,在EAX寄存器中放入一个系统调用编号。Linux系统调用接受位于特定寄存器中的参数,有时候,如果可用寄存器无法存储所有的参数,它也接受位于内存中的参数。

时间: 2024-10-10 08:52:45

x86函数调用约定的相关文章

函数调用约定和堆栈

函数调用约定和堆栈 1 什么是堆栈 编译器一般使用堆栈实现函数调用.堆栈是存储器的一个区域,嵌入式环境有时需要程序员自己定义一个数组作为堆栈.Windows为每个线程自动维护一个堆栈,堆栈的大小可以设置.编译器使用堆栈来堆放每个函数的参数.局部变量等信息. 函数调用经常是嵌套的,在同一时刻,堆栈中会有多个函数的信息,每个函数占用一个连续的区域.一个函数占用的区域被称作帧(frame). 编译器从高地址开始使用堆栈. 假设我们定义一个数组a[1024]作为堆栈空间,一开始栈顶指针指向a[1023]

c++函数调用约定学习(一)

函数调用约定 常见的函数调用约定[5]:cdecl,stdcall,fastcall,thiscall,naked call MFC调用约定(VS6:Project Settings->C/C++ <Category:Code Generation> Calling convention:) 1, __cdecl(C调用约定.The C default calling convention)C/C++ 缺省调用方式 1)压栈顺序:函数参数从右到左 2)参数栈维护:由调用函数把参数弹出栈,

汇编 ? cdecl 函数调用约定,stdcall 函数调用约定

知识点: ? cdecl 函数调用约定 ? stdcall 函数调用约定 ? CALL堆栈平衡 配置属性--> c/c++ -->高级-->调用约定 一.cdecl调用约定 VC++默认约定__cdecl 1.源代码 int __cdecl add1(int a,int b) { return a+b; } 2.生成汇编代码 00401000 /$ 55 PUSH EBP 00401001 |. 8BEC MOV EBP,ESP 00401003 |. 8B45 08 MOV EAX,D

C/C++函数调用约定

C/C++函数调用约定 函数声明部分的extern "C"表示连接规范(Linkage Specification)采用C,而不是C++.如果不写的 话.默认采用C++,当然也可以写成extern "C++". 1.__cdecl: C和C++默认的函数调用约定,参数从右到左顺序压入堆栈,由函数负责清理堆栈,把参数弹出. 也正是因为用来传送参数的堆栈是由调用函数维护的,所以实现可变参数的函数只能使用这种函数调用约定.因为每一个调用它的函数都要包含清理堆栈的代码,所以

C语言函数调用约定

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

【系统篇】小议三种函数调用约定

小议三种函数调用约定 __cdecl.__stdcall.__fastcall是C/C++里中经常见到的三种函数调用方式.其中__cdecl是C/C++默认的调用方式,__stdcall是windows API函数的调用方式,只不过我们在头文件里查看这些API的声明的时候是用了WINAPI的宏进行代替了,而这个宏其实就是__stdcall了. 三种调用方式的区别相信大家应该有些了解,这篇文章主要从实例和汇编的角度阐述这些区别的表现形态,使其对它们的区别认识从理论向实际过渡. __cdecl: C

3 种关键函数调用约定

高级语言翻译成机器码后,计算机没有办法知道函数调用的参数个数.类型,也没有硬件可以保护这些参数. 另外,在C++中,因为重载的原因,所以对函数的命名方式和普通C语言并不一致,该方式称为名字改编. 函数调用者与函数之间,尤其是跨语言调用接口时,需要一个协议约定来传递参数——栈. 关键流程: 调用时,调用者依次把参数压栈,然后调用函数, 被调用函数,在堆栈中取得数据,并进行计算. 函数计算结束以后,或者调用者.或者函数本身修改堆栈,使堆栈恢复原装. 常见的函数调用约定: stdcall cdecl

__stdcall函数调用约定

__stdcall 被这个关键字修饰的函数,其参数都是从右向左通过堆栈传递的(__fastcall 的前面部分由ecx,edx传), 函数调用在返回前要由被调用者清理堆栈. 这个关键字主要见于Microsoft Visual C.C++.GNU的C.C++是另外一种修饰方式:__attribute__((stdcall)) 1函数调用约定 __stdcall是函数调用约定的一种,函数调用约定主要约束了两件事: 1.参数传递顺序 2.调用堆栈由谁(调用函数或被调用函数)清理 常见的函数调用约定:s

C++语言学习(十二)——C++语言常见函数调用约定

C++语言学习(十二)--C++语言常见函数调用约定 一.C++语言函数调用约定简介 C /C++开发中,程序编译没有问题,但链接的时候报告函数不存在,或程序编译和链接都没有错误,但只要调用库中的函数就会出现堆栈异常等现象.上述现象出现在C和C++的代码混合使用的情况下或在C++程序中使用第三方库(非C++语言开发)的情况下,原因是函数调用约定(Calling Convention)和函数名修饰(Decorated Name)规则导致的.函数调用约定决定函数参数入栈的顺序,以及由调用者函数还是被