用od分析_cdecl和_stdcall调用惯例的差异

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/tch3430493902/article/details/101366850

1.调用惯例简要介绍

调用惯例,是在函数调用时如何传递参数和返回值的约定。

具体关注四个问题:

  1. 使用什么方式传递参数?(寄存器?栈?or both)
  2. 参数以何种方式入栈?从右到左?抑或从左到右?(如果需要用到栈传递参数的话)
  3. 使用什么方式传递返回值?(寄存器?栈?or both)
  4. 谁负责清理栈上参数?(调用者函数?被调用者函数?)

针对这四个问题,不同的调用惯例有不同的实现,当今比较主流的调用惯例有cdecl、stdcall、fastcall。这里我们只分析cdecl和stdcall。

调用惯例 传参方式 入栈顺序 传返回值 清栈者
cdecl 栈传参 从右到左压栈 寄存器(EAX首选) 调用者函数
stdcall 栈传参 从右到左压栈 寄存器(EAX首选) 被调用者函数

可以看到,针对以上四个指标,这两种调用方式的差异只在清栈这一部分:cdecl约定由调用者函数负责清理栈上参数,stdcall则约定由被调用者函数清理栈。

那么这样看来我们的任务便很轻松了,不过抱着学习的心态,还是对前三种调用约定进行验证,就当拿od练手了。

2.[分析对象】—简单的main和sub函数

学习从简单的问题入手往往更有效率。
鉴于博主水平有限,这里也是先分析一下简单的main函数和sub函数的调用关系。后续学到新东西再继续跟进啦。
main函数:

int main(int argc, char *argv[]) {
	return sub(2, 1);
}

sub函数:

int __cdecl sub(int a, int b) {//cdecl调用惯例下的sub函数
	return  a - b;
}
int __stdcall sub(int a, int b) {//stdcall调用惯例下的sub函数
	return  a - b;
}

分别对两种调用惯例下的main(共用)和sub函数(区别所在)进行编译,生成二进制可执行文件,然后拖入od进行反汇编调试,观察main调用sub函数时栈状态的变化。注意,在编译源程序时,要取消优化选项,并采用debug模式(而不是release),这样我们才能看到更多细节。

3.用od分析两种调用惯例的差异

好了,到这一步我们就可以安心打开od开始看汇编码了。

cdecl调用惯例下:

  • main函数汇编码
    在这里插入图片描述

a.可以把这里理解为main函数执行时对栈的初始化,包括为自己申请栈空间、调整栈基址寄存器的值、保存寄存器等

00401050  |.  55            push ebp
00401051  |.  8BEC          mov ebp,esp
00401053  |.  83EC 40       sub esp,0x40 ;这里将栈顶指针减40,即为main函数自己保留四十个字节的栈空间,用于存储局部变量等
00401056  |.  53            push ebx
00401057  |.  56            push esi
00401058  |.  57            push edi

b.这里是main函数把传递给sub函数的参数压栈,准备调用sub了,注意看压栈顺序哦。

00401068  |.  6A 01         push 0x1
0040106A  |.  6A 02         push 0x2

c.这里call了sub函数,并且注意到语句”add esp,0x8“,这可是关键哦

0040106C  |.  E8 99FFFFFF   call test-cde.0040100A
00401071  |.  83C4 08       add esp,0x8
00401074  |.  5F            pop edi
00401075  |.  5E            pop esi
00401076  |.  5B            pop ebx

小知识:od以可执行文件名命名其中的用户函数,比如你的可执行文件是project.exe,那么od分析出来的用户函数命名基本上都是proje.XXXXXXA之类的形式,可不能帮你把名字都给原样搬出来,那简直就逆天了

  • sub函数汇编码:
    在这里插入图片描述

这里就是sub函数的代码逻辑所在了,先把参数一移到eax,然后用eax减去参数二。

00401038  |.  8B45 08       mov eax,[arg.1]
0040103B  |.  2B45 0C       sub eax,[arg.2]
  • 然后我们执行,注意查看栈状态。

参数入栈,先压入1,后压入2,即是以从右到左的顺序压进去的。
在这里插入图片描述
返回值自然是存储在eax里的了。
在这里插入图片描述
这里sub函数返回并没有清理栈上的参数。
在这里插入图片描述
这里把ESP的值加了两个字,main(调用者)函数将参数1和2弹出了栈,完成参数清理(调用者清理栈)。
在这里插入图片描述

stdcall调用惯例下:

  • main函数汇编码:
    在这里插入图片描述

  • sub函数汇编码:
    在这里插入图片描述
    这里我就不分析这两个函数的具体语句含义了,仔细对比一下前文的cdecl风格的函数汇编码,你能发现什么?

  • 谁在清理栈呢?

可以看到这里返回的时候是retn 8,意味着还要弹出8个字节,而从栈中可以看到,ESP+8之后恰好把参数1和2弹出,即在sub(被调用者)函数内部完成参数清理。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/tch3430493902/article/details/101366850