前言:
   __cdecl:C/C++函数默认调用约定,参数依次从右向左传递,并压入堆栈,最后由调用函数清空堆栈,这种方式适用于传递参数个数可变的被调用函数,只有被调用函数才知道它传递了多少个参数给被调用函数,比如printf();

__stdcall:参数由右向左传递,并压入堆栈,由被调用函数清空堆栈,当函数有可变参数个数时,函数调用约定自动转换成__cdecl调用约定;

__thiscall:C++非静态成员函数默认调用约定,不能使用个数可变参数,调用非静态成员函数时,this指针直接保存在ecx寄存器中,不入栈,其他方面同__stdcall;

__fastcall:凡是接口函数都必须指明其调用规范,除非接口函数是类的非静态成员函数;

1).简洁的编程模型抽象

由于是简单而本质的抽象,因此我们不考虑分页机制、MMU(memory management unit)之类的。正是如此,它们本来对于我们就是透明的。

所以内存就被考虑为一个从编号(地址)0开始、以编号(地址)0xffff ffff结束的字节序列。每一个字节都被顺序地编号。编号就是字节的地址。
在32位FLAT模式汇编中,本来就是如此。

在程序加载入内存后,程序的指令和数据都按某种方式存放在内存里面。要访问和执行他们,只需要知道他们的地址就可以了。

最重要的东西登场,它就是eip,指令指针寄存器,或称程序计数器。eip中的值程序员无法修改(嗯,可是汇编程序员呢?汇编程序员也无法修改它的值吗?废话,汇编程序员也是程序员啊!),它的值就是下一条即将执行的指令的地址。就是说eip永远指向下一条指令。

然后就是esp,它指向栈的栈顶。当向栈压入数据或从栈弹出数据时,esp的值不断变化,但无论如何变化,它都指向栈顶。

最后就是ebp,它用来把栈中的某个地址作为基址(基本地址,这样理解就是了),它用来标识栈中的某个固定位置,因此可以通过它访问这个固定位置附近的数据。

80X86的栈是向下增长的。也就是说,当向栈压入4个字节的数据时,esp = esp - 4; 当从栈中弹出4个字节时,esp = esp + 4。

以上!多么幸福的事情啊,32位汇编只需要在意这3个寄存器就可以了!

栈图1

这你妹,解释下上图代表什么意思...纯粹照顾完全的新手。
首先,那一排格子代表内存空间中的一小段,每个格子代表4个字节。右边的十六位数值代表方格的地址。格子中间的“...”代表格子的内容。
图中地址是从下往上增长的。
esp永远指向栈顶。一开始它指向地址为0x0063 fff4的字节。然后向栈压入4个字节。
对80X86来说,指令就是push ...;
数据压入后,esp指向0x0063 fff0。这是新的栈顶。

弹出数据跟上面的过程相反。esp中的值会增加。

...这你妹,我画这干嘛,多此一举...好像画了图也没能说的更明白或者表达更深层次的意思啊。总之就是这样了,esp永远指向栈顶,记住就OK。

关于80X86 32位CPU汇编模型就讲上面这些了。之所以讲这么少,因为这就是最基本的和最本质的内容,讲多了反而把重点搞没了。
总结就是记住3个寄存器。eip, esp, ebp。记住他们的意义就可以了。

2).栈帧与C函数调用

关于这个其实没有什么好讲的。

关于计算机,最重要的三个抽象是什么?答案是虚拟地址空间、进程、文件。

一个进程就是一个运行中的程序,或者被加载到内存中的程序。现代操作系统使进程看上去独占了所有的系统资源,但实际上系统中运行着多个进程。

所以从一个进程的视角看去,它独占了系统中的所有内存资源和CPU资源。对于32位系统虚拟地址空间被抽象为编号0~0xffff ffff的字节序列,它是平坦的,线性的,被系统抽象了的,所以叫它平坦地址或线性地址、虚拟地址。

对于Linux来说,保留高1G为系统使用。0-3G空间被应用程序也就是进程独占。

对于一个被加载了的程序也就是进程,其在内存中的分布为:

共享内存段
自由存储区(堆)
BSS段
数据段
只读数据段
代码段

栈向下增长。

每一个函数调用,都是一个栈帧(stack frame)。
以下代码:
int add(int x, int y)
{
    int z;
    z = x + y;
    return z;
}
int main(int argc, char* argv[])
{
    add(3, 5);
    return 0;
}
那么main函数是一个栈帧,add是一个栈帧。
当程序运行时,main函数栈帧先被建立,这个栈帧在高地址。然后调用add函数。此时add函数栈帧被建立,在低地址。当程序执行流进入add函数时,add函数内的局部变量在add函数栈帧中被建立。然后add返回。当add函数返回,此时add函数栈帧被销毁,同时add函数内的局部变量也被销毁。所以,C编程原则告诉我们:永远不要返回一个指向局部对象的指针。也就是说如下代码是错误的:
int* getNumber(void)
{
    int a = 3;
    return &a;
}

那么运行时的栈是什么样子的呢?它是一个随着运行,不断增长(进入新的函数调用)和缩短(函数返回)的动态影像。

OK,关于C栈帧就说到这里,完毕。

3).函数调用的ASM级解释以及栈图

首先复习两个80X86汇编指令,call和ret.

先来一段汇编代码。很简单,有注释。
请注意。不同的汇编编译器使用不同的文法。MASM、NASM、gcc后端汇编编译器,它们的文法几乎完全不一样。尤其是gcc后端,他妹的那文法那个汗。
这里使用的是MASM.学习汇编的话用MASM还是NASM都没关系,学了之后用什么都一样,因为那只是文法方面的东西。指令助记符一般也不会有太多改变。如果真的写汇编代码的话,我想我倾向于使用NASM.
汇编语句分为指令(instruction)、指示性语句(directive)、和宏(macro).
只有指令是真正的机器代码。指示性语句是编译器处理的东西。宏是一堆指令性语句或指示性语句。
以下代码使用MASM。

.386 ;386系统
.MODEL FLAT ;32位平坦地址模式

Exit PROTO NEAR32 stdcall, dwPara:DWORD ;退出函数原型
                                                ;Exit是函数名,dwPara是函数参数

.STACK 4096 ;保留4096字节栈空间

.DATA ;数据段,定义全局变量
number1 DWORD 11111111h ;定义变量number1,大小4字节
number2 DWORD 22222222h ;定义变量number2, 大小4字节

.CODE ;程序代码
Init PROTO NEAR32 ;定义函数Init
        mov number1, 0 ;假设该指令地址为0x0040 0000

mov number2, 0
        ret ;函数Init返回
Init ENDP ;函数Init结束

_start: ;相当于main函数
        call Init ;调用函数Init,此指令地址为0x0040 000f
         ...... ;该处指令地址为0x0040 0014
       
        INVOKE Exit, 0 ;调用Exit退出

PUBLIC _start ;公开入口点

END ;程序结束

其实代码不用看的...
假设程序被加载入内存,这时esp被初始化,然后esp指向栈顶。设此时栈顶地址为0x0063 00f8.一切为了说明方便哈。总之程序加载后,栈被初始化,也就是esp被初始化,esp会指向内存中的某个地址,并以这个地址作为栈的起始。
eip始终指向执行流,也就是“下一条指令”。

这里说明一下。程序一旦加载,所有的指令、全局变量都被载入内存并有了确切的内存地址(程序加载前,或者说程序没有运行时,只是硬盘上的一个可执行文件对吧。程序运行前有一个系统加载动作,这个加载由操作系统完成)。这个我的另一篇BLOG《程序员的基本概念》里面略提过。清楚加载细节的是操作系统开发者,同时涉及到编译器和链接器。要更明白这个问题请参照《Linker and Loader》。

那么程序加载。栈初始化了。数据区域在内存中开辟出来了,全局变量被给予确切地址(这里是虚拟地址,因为这是一个进程,它的地址只管在虚拟地址空间中给就可以了,虚拟地址到物理地址的映射由操作系统和MMU完成)。代码段(也就是要执行的指令)也被放入内存中并给予确切地址。eip指向代码段的开始,并开始执行程序...

所以eip只管指向某个内存地址,这个内存地址存储着程序员编写的指令,然后CPU把指令取出来执行就是了。所以计算机叫做“顺序存储控制机”。对不起我啰嗦了。

好的。我们假设了,在程序加载后,esp被初始化为0x0063 00f8,并假设了mov number1, 0这个指令的地址在0x0040 0000,根据这个假设的地址和每个指令码的长度(这些指令都放在代码段,而且一个一个指令就是挨着放的),推断出call指令的地址是0x0040 000f,call指令的下一条指令的地址是0x0040 0014(因为这个call指令的长度占用5个字节,0x0040 000f + 5 = 0x0040 0014)。这里不算我对指令长度的计算错误,总之假设我的地址计算是正确的。

OK开始了。程序已经加载。那么开始程序执行。eip首先指向call指令,因为_start开始那里就是call指令。嗯,eip就是一个32位寄存器,这个寄存器里面的值永远是即将执行的指令的内存地址,这时eip里面的值是0x0040 000f。

call指令执行!该指令首先将下一条指令的地址压入栈,也就是说,call指令的第一个动作是将0x0040 0014(call指令的下一条指令地址)压入栈。esp此时变化,其值变为0x0063 00f4。为什么?因为esp被初始化为0x0063 00f8,一个地址4个字节入栈之后,esp = esp - 4。然后call指令转去调用Init过程代码。eip变化为0x0040 0000,为什么?因为Init过程的第一个指令地址就是0x0040 0000.这个过程是由CPU自动完成的,也就是说,call指令,让CPU自动完成这一系列动作。

然后Init过程执行到ret指令。
ret指令干什么?它将栈内数据弹出,并用该数据填充eip。栈内数据是什么?就是0x0040 0014,它就是call指令的下一条指令的地址!同时esp = esp + 4.也就是说,ret指令执行后,eip值变为0x0040 0014, esp的值变回0x0063 00f8.这个过程由CPU自动完成。ret指令让CPU自动完成这一系列动作。

整理:执行call,call指令首先将下一条指令地址入栈,然后跑去执行过程代码;过程代码中执行ret,ret首先从栈中将下一条指令地址弹回eip,这样程序就开始执行call指令后的指令。一句话:eip始终指向下一条指令地址。

以上!就是汇编函数调用和返回的过程。就是一个call和一个ret.eip在这个执行过程中通过栈来保存。

接下来,让我们开始考察C语言的过程调用和返回,也就是C语言函数的参数压栈和参数访问过程。

先看一个汇编调用压参和参数访问过程。

假设有一个add过程,这个过程的工作是将两个整型值(每个整型值4字节)相加,并将相加的和返回eax寄存器。
如果通过把参数压入堆栈来传递参数调用过程,那么调用方(caller)代码如下:
    push var1 ;第一个变量值
    push var2 ;第二个变量值
    call add ;调用add过程
    add esp, 8 ;从栈移除参数

而被调用过程(callee)add的代码如下:
add PROC NEAR32 ;add过程,该过程将两个整型值相加
    push ebp ;保存基栈指针
    mov ebp, esp ;建立栈
    mov eax, [ebp + 8] ;复制第二个参数值(var2)
    mov eax, [ebp + 12] ;加上第一个参数值(var1)
    pop ebp ;恢复ebp寄存器
    ret ;过程返回
add ENDP ;过程结束

我们将根据这段代码建立栈图。

先把调用栈图发上来...
解释再说

栈图2

左边是caller(调用者)栈,右边是callee(被调用者)栈(是同一个栈,分别是压参前、call指令执行后的状态。caller和callee的视图)。
图中画的内存地址是向上增长的。

首先,esp是栈顶,直接从caller栈顶看起。也就是,在调用前,esp指向某个内存地址。
在调用函数前将参数压入栈中。
push var1
push var2
这两行代码使esp - 8. 然后压参完毕,图中即为压参完毕esp.
然后调用函数:
call add
嗯,之前复习call指令时说什么了?call指令执行时,首先将返回地址压入栈。
也就是将add esp, 8 这条指令的地址压入栈。
如左图所示。

然后call指令执行过程调用,eip指向add函数内第一条指令的地址:
push ebp ;将ebp保存到栈中,同时esp - 4(说过了80X86的栈是向低地址方向增长的).
此时ebp原值被保存入栈中。参看右图,蓝色部分是ebp原值。
然后:
mov ebp, esp
此时以ebp为基准的栈建立了。此时ebp和esp都指向栈顶(ebp原值被栈保存起来了哦)。
为什么要这么做?
因为esp是随时变动的,只要有压栈和出栈的操作,esp的值就随着压栈和出栈的操作变化(随着push和pop操作变化,甚或,程序员直接改动esp的值)。
而ebp却不会随着push和pop操作变化。程序员在callee中不会修改ebp的值,而是使用ebp作为基准访问参数。

那么接下来就很好理解了,第二个参数的地址是ebp + 8, 第一个参数的地址是ebp + 12.
所以
mov eax, [ebp + 8] ;复制第二个参数值(var2)到eax
mov eax, [ebp + 12] ;加上第一个参数值(var1)
就不难理解了。

在过程把实现代码处理完毕的最后,pop ebp将ebp原值从栈中弹出恢复。
然后ret返回指令将返回地址弹出并赋给eip(请注意,返回地址弹出后,esp + 4, 这时esp正好指向调用者压参完毕的位置),...
回到调用者的地方并继续执行。

那么调用处的add esp, 8 ;从栈移除参数
是干什么用的?注释已经说得很清楚了。
调用者将var1和var2压到栈中,由于调用者的压栈,esp被往下移动了8;那么这个esp的原始位置也就是caller的栈顶应该在过程调用后恢复,add esp, 8就是恢复esp的。

ok。基本上就是如此了!

对于C语言的过程调用,比如,在main函数里面调用add
int main(int argc, char* argv[])
{
    ...
    add(x, y);
    ...
}
实际上,这里add(x, y)(调用者处)被编译器编译成如下汇编代码:
push y
push x
call add
add esp, 8

以上,这就是C过程调用的汇编解释。

接下来给出一般过程的入口代码和出口代码。

不难猜测,所有的过程(被调用函数)都有一样的入口代码和出口代码:

所有的C函数,在被编译器编译成汇编代码之后,
函数开始的几行汇编代码总是这样的,所以我们称这它为入口代码(entry code):
push ebp ;保存基址
mov ebp, esp ;建立ebp偏移基准
sub esp, n ;n个字节的局部变量参数
push ... ;保存过程中会用到的通用寄存器
...
pushf ;保存标识寄存器,也就是保存标志位

而结尾的几行总是这样的,所以称其为出口代码:
popf ;恢复标识寄存器
pop ... ;恢复寄存器
...
mov esp, ebp ;恢复callee esp
pop ebp ;恢复ebp
ret ;返回

4). stdcall和cdcel

既然已经了解了上述内容,那么调用惯例就很容易理解了。
cdcel和stdcall是约定俗成的调用惯例,它们的区别在于由谁来恢复esp。

cdcel是由调用者恢复esp的调用惯例,
也就是说
push var1
push var2
call add
add esp, 8
这是cdcel调用惯例

而stdcall则是由callee恢复esp的调用惯例
stdcall会在callee里面将ret这样写:
ret 8
意思是返回的同时esp + 8.

这两种调用惯例,stdcall的好处是不用每次都在调用过程后写add esp, 8这样就减小了代码量,减小了目标文件的体积。
而stdcall的缺陷更明显,那就是callee有时候无法推断参数的个数和长度,这样的话esp只能由调用者恢复(比如变参数函数,这种函数callee是无法推断参数个数的,也就无法知道应该在ret后面加多少偏移量)。

转载于:https://www.cnblogs.com/foohack/p/3582228.html

函数call相关[ASM]相关推荐

  1. R语言使用pwr包的pwr.r.test函数对相关信息分析进行效用分析(power analysis)、在已知效应量(effect size)、显著性水平、效用值的情况下计算需要的样本量

    R语言使用pwr包的pwr.r.test函数对相关信息分析(Correlations)进行效用分析(power analysis).在已知效应量(effect size).显著性水平(sig).效用值 ...

  2. R语言使用pwr包的pwr.r.test函数对相关信息分析(Correlations)进行效用分析(power analysis)的语法

    R语言使用pwr包的pwr.r.test函数对相关信息分析(Correlations)进行效用分析(power analysis)的语法 目录

  3. 【错误记录】Visual Studio 中配置 NDK 头文件路径 ( NDK 的三个头文件路径 | 与 CPU 架构相关 asm 头文件路径选择 )

    文章目录 一.报错信息 二.解决方案 1.NDK 的三个头文件路径 2.与 CPU 架构相关 asm 头文件路径选择 一.报错信息 参考 [Android 逆向]Android 进程注入工具开发 ( ...

  4. C语言algorithm主函数,C语言中主函数中相关有关问题?

    C语言中主函数中相关问题??? #include #include #include #include #include #include #include #include #include usi ...

  5. python列表内置函数_Python-列表总结(操作符、方法、内置函数、相关模块)

    目录 上篇文章思考题 简介 创建与赋值 操作符 访问与更新(序列操作符切片) 判断元素是否存在(成员关系操作in,not in) 拼接列表(连接操作符+) 重复(重复操作符*) 删除 方法 添加 删除 ...

  6. QT(C++)DeviceIoControl()函数的相关使用

    Microsoft官网中有这个函数的介绍,对,仅仅就是介绍,有时候官网的查询结果也就只能看看-- 在我写过的一个软件中,我曾经多次使用该函数获取相关结果,现将我的使用经验分享给大家! DeviceIo ...

  7. C++游戏梦 | EasyX详解 | ②:EasyX函数-设备相关

    <<<上一篇-①:安装及基本概念 系列文章 ①:安装及基本概念 ②:EasyX函数-设备相关 ③:EasyX函数-颜色模型 ④:EasyX函数-图形样式 ⑤:EasyX函数-绘图相关 ...

  8. C语言网络编程函数与相关结构汇总

    持续更新中- 服务器和客户端的一般流程 服务器端:socket()-->bind( )-->listen()-->accept()-->read()/write()---> ...

  9. 关于strcmp与strcpy函数的相关用法

    strcpy的相关讲解:若字符串char b[100]赋值给字符串char a[100],若strlen(a)>length(b),a字符串的'\0'后也确实赋值了,但是输出的话,遇到第一个'\ ...

最新文章

  1. goland/go语言项目--本地包的导入(将项目添加至GOPATH中)(基于macOS)
  2. 在JavaScript中使用正好两位小数格式化数字
  3. 【设计模式】建造者模式 ( 简介 | 适用场景 | 优缺点 | 代码示例 )
  4. 【Flutter】Image 组件 ( Image 组件简介 | Image 构造函数 | Image.network 构造函数 | Image.asset 构造函数 )
  5. VS2005 解决应用程序配置不正确,程序无法启动问题
  6. Node.js 的http.serverRequest 或http.IncomingMessage
  7. java开发简介_Java Web开发介绍
  8. 一个关于if else容易迷惑的问题(转自鸟哥公众号)
  9. 五大领域总目标指南_幼儿教师这样读《指南》事半功倍
  10. 大数据技术的发展方向
  11. 一次数据库优化的对话
  12. arduino动态刷新显示_Arduino驱动TFT彩色触摸屏-有没有更好的方法?
  13. scratch python的区别ev3_机器人编程和少儿编程,傻傻分不清—乐高EV3入门感想
  14. 心电信号去噪(part1)--心电信号简介
  15. Qt +ffmpeg(vp8) 记录视频每一帧并生成webm文件格式
  16. oracle diagnosticdest,Oracle 11g自动诊断信息库(Automatic Diagnostic Repository,ADR)概述
  17. 小白 0-1 学习 app 开发,从配置到 hello world
  18. 【路径优化】基于帝企鹅算法求解TSP问题(Matlab代码实现)
  19. oracle数据库按中文拼音排序
  20. JAVA pdf中插入自定义图片

热门文章

  1. java 改变文件路径_在C#中改变文件路径
  2. php随机数字不重复使等式成立_Schur补与矩阵打洞,SMW求逆公式,分块矩阵与行列式(不)等式...
  3. VB 字符串MD5加密函数
  4. SCPPO(四):框架的学习
  5. 软考(一):迎战软考
  6. 机房收费系统合作版(一):开始团队合作之旅
  7. 干货 | 如何写一个更好的Python函数?
  8. DOTA 2血虐人类的OpenAI,原来靠的是作弊?
  9. 史上最严重数据车祸:100+车厂机密全曝光,通用丰田特斯拉统统中招
  10. 李飞飞最新研究成果!斯坦福正在用算法判断政治倾向