• 深入体会__cdecl与__stdcall



        在学习C++的过程中时常碰到WINAPI或者CALLBACK这样的调用约定,每每觉得十分迷惑。究竟这些东西有什么用?不用他们又会不会有问题?经过在网上的一番搜寻以及自己动手后,整理成以下的学习笔记。
    1.WINAPI与CALLBACK

        其实这两者在Windows下是相同的,在windef.h中定义如下:
    Code

        这里根据不同的系统版本选择不同的定义,Windows的话对应#elif (_MSC_VER >= 800) || defined(_STDCALL_SUPPORTED)后面的那一段,可以看得出都为__stdcall。至于为什么Windows要对应__stdcall可能与系统本身的内存处理方式有关。现在问题就在于__stdcall是什么?
    2.__stdcall与__cdecl
        在网上找到的资料如下:
        1)采用__cdecl约定时,函数参数按照从右到左的顺序入栈,并且由调用函数者把参数弹出栈以清理堆栈。因此,实现可变参数的函数只能使用该调用约定。由于每一个使用__cdecl约定的函数都要包含清理堆栈的代码,所以产生的可执行文件大小会比较大。
          2)采用__stdcall约定时,函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,函数参数个数固定。由于函数体本身知道传进来的参数个数,因此被调用的函数可以在返回前用一条ret n指令直接清理传递参数的堆栈。
      
     在看完这些描述后,有些问题必须解决的,首先什么是可变参数的函数,还有为什么__stdcall就不能调用可变参数的函数。
    3.汇编详解__stdcall与__cdecl
        1)准备  VS2008下查看程序反汇编代码的方法
        首先创建一个可运行的工程,接着在程序中设置一个断点,debug一下程序。在代码区域的任意地方右键-->"转到反汇编"即可看到程序的反汇编代码。
        2)VS2008一些默认的设置
        在VS2008下,全局函数的约定是__cdecl,类的成员函数的约定是__stdcall。还有一般的Win32 API函数都是__stdcall。一般来说可以用__stdcall的就不会去使用__cdecl,这样有更好的封装性,因为入栈和清空栈的代码在同一块地方。至于为什么全局函数会是__cdecl,我还没想出来。
        3)__cdecl约定的反汇编分析
    Code

        这里实现的是int Add(int a, int b),然后调用Add(1, 2)。从44行开始看,先push 2,再push 1,可见入栈是从右到左。接着call 41108Ch的内存,但在这段代码中是找不到这个内存的。在call处按F11,会看到如下代码:Add: 0041108C  jmp         Add (4113A0h) ,这里的4113A0h在上面代码就可以找到在第9行了。至于为什么要这样子?你可以认为VS在编译的时候把函数的地址放在一段内存中比较好管理。而事实上函数的地址也确实是这样子。为了更清楚,我把这段代码也展示一下。
    Code

        你会发现,在不知不觉中,编译器已经给你生成了这么多的函数,而Add仅仅在第26行出现过。看得出编译器对编程有多重要了。闲话少说,回到代码。在第二段代码展示的28行处的指令是ret,这里没有执行清栈。而在48行是add esp,8,这个操作将栈的指针修正到有参数入栈之前,这里的8就刚好是两个int的大小。
        4)__stdcall约定的反汇编分析
        这里只做一点点改动,将Add的定义改为int __stdcall Add(int a, int b)。在反汇编代码中不同的地方只有两处,一是add esp,8这条语句没有了,二是ret变为了ret 8。可见,清栈的工作变到了在函数里面。
        5)何为可变参数的函数
        我觉得必须先知道的是它的形式:type funcname(type para1, type para2, ...)。这里的"..."不是省略的意思,而是可变参数的函数必须这样声名。具体说明可参照http://hi.baidu.com/sunlit88/blog/item/272460da3f360f61d1164ea7.html。下面是我改了一些地方的代码。
    Code

        查看反汇编代码也可以看出清栈的操作是在Add(1, 2, 3, -1)后执行的。本来我想试试写成int __stdcall Add(int a, ...)会有什么后果的,谁知道VS在编译的时候硬是把__stdcall方式改成__cdecl方式,看来编译器也不笨啊,知道这种方式肯定会出问题,就把你的改过来了。不过这也是一个好的编译器所需要做的事情,有时候你会发现自己写的代码与实际运行会有点点差别,那可能就是编译器把自己觉得需要优化的东西优化后的结果。这时候我又想起了volatile这个关键字,它就是让编译器不要去优化的时候使用的。
        6)可变参函数为什么不能用__stdcall
        我觉得这个问题应该从编译时和运行时来说,因为函数的代码是在编译的时候就已经在内存中写好的,而当程序在编译的时候,可变参不能告知代码的ret n的n是多少。而add esp,n是在运行时执行的,所以知道n是多少。
    4.写在后面
        相对__cdecl和__stdcall还有很多约定,这里就不细说了。以前学汇编没细学,现在才发现只有从最最低层的代码才能看到程序的原貌。C++那层有时候还看不出问题,中间还有个编译器在搞鬼。
  • 相关阅读:
    #从零开始学Swift2.0# No.4 枚举, 元组, 数组和字典
    #从零开始学Swift2.0# No.3 基本数据类型
    #从零开始学Swift2.0# No.2 运算符和表达式
    #从零开始学Swift2.0# No.1 初识Swift
    MacOS下SVN的使用
    在Xcode中制作.a文件
    在Xcode中制作Framework
    Objective-C中的Runtime
    汉语字典或者词典的简单的ios小demo
    ios开发-UI进阶-核心动画-时钟动画小案例
  • 原文地址:https://www.cnblogs.com/sober/p/1558178.html
Copyright © 2020-2023  润新知