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
. push ;push parameter z
push ;push parameter y
push ;push parameter x
push ;push parameter w
call demo_cdecl ;call the function
. 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+], ; move parameter z to fourth position on stack
mov [esp+], ; move parameter y to third position on stack
mov [esp+], ; move parameter x to second position on stack
mov [esp], ; 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 ; 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 ; move parameter z to second position on stack
push ; move parameter y to tio position on stack
mov edx, ; move parameter x to edx
mov ecx, ; 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系统调用接受位于特定寄存器中的参数,有时候,如果可用寄存器无法存储所有的参数,它也接受位于内存中的参数。
x86函数调用约定的更多相关文章
- Windows x64汇编函数调用约定
最近在写一些字符串函数的优化,用到x64汇编,我也是第一次接触,故跟大家分享一下. x86:又名 x32 ,表示 Intel x86 架构,即 Intel 的32位 80386 汇编指令集. x64: ...
- X86调用约定 calling convention
http://zh.wikipedia.org/wiki/X86%E8%B0%83%E7%94%A8%E7%BA%A6%E5%AE%9A 这里描述了在x86芯片架构上的调用约定(calling con ...
- c++中的几种函数调用约定(转)
C++中的函数调用约定(调用惯例)主要针对三个问题: 1.参数传递的方式(是否采用寄存器传递参数.采用哪个寄存器传递参数.参数压桟的顺序等): 参数的传递方式,最常见的是通过栈传递.函数的调用方将参数 ...
- C语言函数调用约定
在C语言中,假设我们有这样的一个函数: int function(int a,int b) 调用时只要用result = function(1,2)这样的方式就可以使用这个函数.但是,当高级语言被编译 ...
- 【黑客免杀攻防】读书笔记7 - 软件逆向工程基础1(函数调用约定、Main函数查找)
0x1 准备工作 1.1.准备工具 IDA:交互式反汇编工具 OllyDbg:用户层调试工具 Visual Studio:微软开发工具 1.2.基础知识 C++开发 汇编语言 0x2 查找真正的mai ...
- 汇编 cdecl 函数调用约定,stdcall 函数调用约定
知识点: cdecl 函数调用约定 stdcall 函数调用约定 CALL堆栈平衡 配置属性--> c/c++ -->高级-->调用约定 一.cdecl调用约定 VC++ ...
- Microsoft函数调用约定
Microsoft函数调用约定 对于所有调用共有的约定:ebx.ebp.esi.edi都是calle-save,即由被调用的函数负责它们的保存(如果被调用函数用到了这些寄存器的话) 先看函数调用发生了 ...
- 关于函数调用约定-thiscall调用约定
函数调用约定描述了如何以正确的方式调用某些特定类型的函数.包括了函数参数在栈上的分配顺序.有哪些参数将通过寄存器传入,以及在函数返回时函数栈的回收方式等. 函数调用约定的几种类型 stdcall,cd ...
- C/C++函数调用约定与this指针
关于 C/C++ 函数调用约定,大多数时候并不会影响程序逻辑,但遇到跨语言编程时,了解一下还是有好处的. VC 中默认调用是 __cdecl 方式,Windows API 使用 __stdcall 调 ...
随机推荐
- H5表单基础知识(二)
表单新增属性 <!--<input type="text" class="name" />--> <!-- placeholder ...
- CoderForce 148D-Bag of mice (概率DP求概率)
题目大意:美女与野兽在玩画鸽子的游戏.鸽子在用黑布遮住的笼子里,白色的有w只,黑色的有b只,每次拿出一只作画,谁先画到白色的鸽子谁就赢.美女首先画,因为野兽太丑,它每次画的时候都会吓跑一只鸽子,所有出 ...
- SPFA单源最短路径算法
我们用数组d记录每个结点的最短路径估计值,而且用邻接表来存储图G.我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开 ...
- 装载问题(load)
装载问题(load) 问题描述: 有一批共n 个集装箱要装上艘载重量为c 的轮船,其中集装箱i 的重量为wi.找出一种最 优装载方案,将轮船尽可能装满,即在装载体积不受限制的情况下,将尽可能重的集装箱 ...
- Animation鱼眼效果
<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head> < ...
- SSH执行远程命令和传送数据
$ ssh user@host 'mkdir -p .ssh && cat >> .ssh/authorized_keys' < ~/.ssh/id_rsa.pub ...
- js获取当前点击元素的索引
以ul下的li元素为例:获取li的索引,代码如下: <ul id="list"> <li></li> <li></li> ...
- learning docker steps(4) ----- docker swarm 初次体验
参考:https://docs.docker.com/get-started/part4/ 了解 swarm 集群 swarm 是一组运行 Docker 并且已加入集群中的机器.执行此操作后,您可以继 ...
- 快速切题 sgu116. Index of super-prime bfs+树思想
116. Index of super-prime time limit per test: 0.25 sec. memory limit per test: 4096 KB Let P1, P2, ...
- C++11标准的类型别名
1.typedef 类型名 类型别名(类型别名列表):这是传统的C++类型别名声明. 2.C++11标准下,使用using,即using 类型别名=类型名.