精华内容
下载资源
问答
  • 函数入栈顺序

    千次阅读 2017-08-29 11:56:28
    (转载)函数入栈顺序 转载http://soft.chinabyte.com/database/138/12116138.shtml   栈,我想大家都知道。栈,是硬件。主要作用表现为一种数据结构,是只能在某一端插入和删除的特殊线性表。下面介绍...

    (转载)函数入栈顺序

    转载http://soft.chinabyte.com/database/138/12116138.shtml 

     栈,我想大家都知道。栈,是硬件。主要作用表现为一种数据结构,是只能在某一端插入和删除的特殊线性表。下面介绍C语言中函数参数的入栈顺序是怎样的。

      对技术执着的人,比如说我,往往对一些问题,不仅想做到"知其然",还想做到"知其所以然".C语言可谓博大精深,即使我已经有多年的开发经验,可还是有许多问题不知其所以然。某天某地某人问我,C语言中函数参数的入栈顺序如何?从右至左,我随口回答。为什么是从右至左呢?我终究没有给出合理的解释。于是,只好做了个作业,于是有了这篇小博文。

       

      运行结果:

      x = 100 at [BFE28760]

      y = 200 at [BFE28764]

      z = 300 at [BFE28768]

      C程序栈底为高地址,栈顶为低地址,因此上面的实例可以说明函数参数入栈顺序的确是从右至左的。可到底为什么呢?查了一直些文献得知,参数入栈顺序是和具体编译器实现相关的。比如,Pascal语言中参数就是从左到右入栈的,有些语言中还可以通过修饰符进行指定,如Visual C++.即然两种方式都可以,为什么C语言要选择从右至左呢?

      进一步发现,Pascal语言不支持可变长参数,而C语言支持这种特色,正是这个原因使得C语言函数参数入栈顺序为从右至左。具体原因为:C方式参数入栈顺序(从右至左)的好处就是可以动态变化参数个数。通过栈堆分析可知,自左向右的入栈方式,最前面的参数被压在栈底。除非知道参数个数,否则是无法通过栈指针的相对位移求得最左边的参数。这样就变成了左边参数的个数不确定,正好和动态参数个数的方向相反。

      因此,C语言函数参数采用自右向左的入栈顺序,主要原因是为了支持可变长参数形式。换句话说,如果不支持这个特色,C语言完全和Pascal一样,采用自左向右的参数入栈方式。

     

    展开全文
  • 最近在阅读《程序员的自我修养》,看到10.2节,又想起以前的入栈顺序,对此又深挖le

    最近在阅读《程序员的自我修养》,看到10.2节,又想起以前的入栈顺序,对此又深挖了一下:


    大家都知道:

    1.函数入栈顺序通常是:从右到左

    2.从右到左的好处是,第一个参数就在栈顶,我们很方便就定位到了第一个参数的位置

    3.网上一搜,大家都说,从右往左入栈的目的是方便的可变参数的使用,获得第一个参数的位置,


    好的,大部分的讨论终结于此,那么,为什么我们要获得第一个参数的位置呢?为什么获得第一个参数就方便可变参数的使用和实现呢?

    接下来,我们回想一下,可变参数的函数是如何使用的,不得不说到下面几个函数:

    void va_start(va_list ap, last);
    type va_arg(va_list ap, type);
    void va_end(va_list ap);
    void va_copy(va_list dest, va_list src);
    
    va_list是用于存放参数列表的数据结构。
    va_start函数根据初始化last来初始化参数列表。
    va_arg函数用于从参数列表中取出一个参数,参数类型由type指定。
    va_copy函数用于复制参数列表。
    va_end函数执行清理参数列表的工作。
    引用自http://www.jb51.net/article/43192.htm

    我们在使用可变参数的时候,一定会使用va_start这个函数,准确的说,它是一个宏:

    #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )

    也就是说,我们在使用可变参数的时候,必须要给出第一个参数,这样,va_start才能初始化后续的列表,而要定位第一个参数,直接用ebp-4即可(这里的减4是减去返回地址)


    回答最初的问题:

    1.为什么要获得第一个参数的位置:首先,C语言库使用可变参数函数时,就必须要求有第一个参数,获得第一个参数,可以定位后续的参数。这里又会想出一个问题,那么,为什么C语言库要至少有1个参数,我个人理解是,有一个参数可以方便用户处理后续的可变参数,因为如果你的代码中写死了后续参数的类型,你不如直接用固定参数,那么既然你使用了可变参数,就说明后续的类型个数,你可能是不确定的,你可以通过第一个参数来确定(当然,是否这样确定,如何确定由你的代码实现,参见printf的实现)


    2.如果没有第一个参数,会怎样?我想了下,没有第一个参数,那么你整个参数的类型,是没法确定的(排除有的脑袋进水,能确定类型还是用可变参数的人),当然,要确定也不是没有办法,比如,你可以把参数类型写入一个配置文件中,读取,即配置文件实现了第一个参数的功能。那么基于此,还不如有一个固定的参数,让用户能够方便的获取可变参数的类型和个数。


    3.基于1,2,可以知道为什么要有第一个参数,而不允许全部是可变参数?如果理解了1,2,既然C语言设计要求一定要有第一个参数,那么第一个参数就要方便获取,如果从右往左压栈,那么,第一个参数的地址直接ebp-4即可;如果从左往右压栈,由于可变参数的大小类型不知道,很难通过ebp去定位第一个参数的位置


    展开全文
  • C语言函数入栈顺序与可变参数函数 C语言真是太深奥了,越学越觉得很多东西都不会!前段时间面试栽倒的一个问题:C函数入栈顺序 下面先摘录一些: =============================...
    C语言函数入栈顺序与可变参数函数 C语言真是太深奥了,越学越觉得很多东西都不会!前段时间面试栽倒的一个问题:C函数入栈顺序 下面先摘录一些: ================================================ C/C++函数调用约定与函数名称修饰规则探讨 使用C/C++语言开发软件的程序员经常碰到这样的问题:有时候是程序编译没有 问题,但是链接的时候总是报告函数不存在(经典的LNK 2001错误),有时候是程序编译和链接都没有错误,但是只要调用库中的函数就会出现堆栈异常。这些现象通常是出现在C和C++的代码混合使用的情况下或 在C++程序中使用第三方的库的情况下(不是用C++语言开发的),其实这都是函数调用约定(Calling Convention)和函数名修饰(Decorated Name)规则惹的祸。函数调用方式决定了函数参数入栈的顺序,是由调用者函数还是被调用函数负责清除栈中的参数等问题,而函数名修饰规则决定了编译器使 用何种名字修饰方式来区分不同的函数,如果函数之间的调用约定不匹配或者名字修饰不匹配就会产生以上的问题。本文分别对C和C++这两种编程语言的函数调 用约定和函数名修饰规则进行详细的解释,比较了它们的异同之处,并举例说明了以上问题出现的原因。 函数调用约定(Calling Convention) 函数调用约定不仅决定了发生函数调用时函数参数的入栈顺序,还决定了是由调用者函数还是被调用函数负责清除栈中的参数,还原堆栈。函数调用约定有很多方 式,除了常见的__cdecl,__fastcall和__stdcall之外,C++的编译器还支持thiscall方式,不少C/C++编译器还支持 naked call方式。这么多函数调用约定常常令许多程序员很迷惑,到底它们是怎么回事,都是在什么情况下使用呢?下面就分别介绍这几种函数调用约定。 1.__cdecl 编译器的命令行参数是/Gd。__cdecl方式是C/C++编译器默认的函数调用约定,所有非C++成员函数和那些没有用__stdcall或__fastcall声明的函数都默认是__cdecl方式,它使用C函数调用方式,函数参数按照从右向左的顺序入栈,函数调用者负责清除栈中的参数, 由于每次函数调用都要由编译器产生清除(还原)堆栈的代码,所以使用__cdecl方式编译的程序比使用__stdcall方式编译的程序要大很多,但是 __cdecl调用方式是由函数调用者负责清除栈中的函数参数,所以这种方式支持可变参数,比如printf和windows的API wsprintf就是__cdecl调用方式。对于C函数,__cdecl方式的名字修饰约定是在函数名称前添加一个下划线;对于C++函数,除非特别使用extern "C",C++函数使用不同的名字修饰方式。 2.__fastcall 编译器的命令行参数是/Gr。__fastcall函数调用约定在可能的情况下使用寄存器传递参数,通常是前两个 DWORD类型的参数或较小的参数使用ECX和EDX寄存器传递,其余参数按照从右向左的顺序入栈,被调用函数在返回之前负责清除栈中的参数。编译器使用两个@修饰函数名字,后跟十进制数表示的函数参数列表大小,例如:@function_name@number。需要注意的是__fastcall函数调用约定在不同的编译器上可能有不同的实现,比如16位的编译器和32位的编译器,另外,在使用内嵌汇编代码时,还要注意不能和编译器使用的寄存器有冲突。 3.__stdcall 编译器的命令行参数是/Gz,__stdcall是Pascal程序的缺省调用方式,大多数Windows的API也是__stdcall调用约定。__stdcall函数调用约定将函数参数从右向左入栈,除非使用指针或引用类型的参数,所有参数采用传值方式传递,由被调用函数负责清除栈中的参数。对于C函数,__stdcall的名称修饰方式是在函数名字前添加下划线,在函数名字后添加@和函数参数的大小,例如:_functionname@number 4.thiscall thiscall只用在C++成员函数的调用,函数参数按照从右向左的顺序入栈,类实例的this指针通过ECX寄存器传递。需要注意的是thiscall不是C++的关键字,不能使用thiscall声明函数,它只能由编译器使用。 5.naked call 采用前面几种函数调用约定的函数,编译器会在必要的时候自动在函数开始添加保存ESI,EDI,EBX,EBP寄存器的代码,在退出函数时恢复这些寄存器 的内容,使用naked call方式声明的函数不会添加这样的代码,这也就是为什么称其为naked的原因吧。naked call不是类型修饰符,故必须和_declspec共同使用。 VC的编译环境默认是使用__cdecl调用约定,也可以在编译环境的Project Setting...菜单-》C/C++ =》Code Generation项选择设置函数调用约定。也可以直接在函数声明前添加关键字__stdcall、__cdecl或__fastcall等单独确定函 数的调用方式。在Windows系统上开发软件常用到WINAPI宏,它可以根据编译设置翻译成适当的函数调用约定,在WIN32中,它被定义为 __stdcall。 函数名字修饰(Decorated Name)方式 函数的名字修饰(Decorated Name)就是编译器在编译期间创建的一个字符串,用来指明函数的定义或原型。LINK程序或其他工具有时需要指定函数的名字修饰来定位函数的正确位置。 多数情况下程序员并不需要知道函数的名字修饰,LINK程序或其他工具会自动区分他们。当然,在某些情况下需要指定函数的名字修饰,例如在C++程序中, 为了让LINK程序或其他工具能够匹配到正确的函数名字,就必须为重载函数和一些特殊的函数(如构造函数和析构函数)指定名字装饰。另一种需要指定函数的 名字修饰的情况是在汇编程序中调用C或C++的函数。如果函数名字,调用约定,返回值类型或函数参数有任何改变,原来的名字修饰就不再有效,必须指定新的 名字修饰。C和C++程序的函数在内部使用不同的名字修饰方式,下面将分别介绍这两种方式。 1. C编译器的函数名修饰规则 对于__stdcall调用约定,编译器和链接器会在输出函数名前加上一个下划线前缀,函数名后面加上一个“@”符号和其参数的字节数,例如_functionname@number。__cdecl调用约定仅在输出函数名前加上一个下划线前缀,例如_functionname。__fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,例如@functionname@number。 2. C++编译器的函数名修饰规则 C++的函数名修饰规则有些复杂,但是信息更充分,通过分析修饰名不仅能够知道函数的调用方式,返回值类型,参数个数甚至参数类型。不管 __cdecl,__fastcall还是__stdcall调用方式,函数修饰都是以一个“?”开始,后面紧跟函数的名字,再后面是参数表的开始标识和 按照参数类型代号拼出的参数表。对于__stdcall方式,参数表的开始标识是“@@YG”,对于__cdecl方式则是“@@YA”,对于__fastcall方式则是“@@YI”。参数表的拼写代号如下所示: X--void D--char E--unsigned char F--short H--int I--unsigned int J--long K--unsigned long(DWORD) M--float N--double _N--bool U--struct .... 指针的方式有些特别,用PA表示指针,用PB表示const类型的指针。后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0” 代表一次重复。U表示结构类型,通常后跟结构体的类型名,用“@@”表示结构类型名的结束。函数的返回值不作特殊处理,它的描述方式和函数参数一样,紧跟 着参数表的开始标志,也就是说,函数参数表的第一项实际上是表示函数的返回值类型。参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。下面举两个例子,假如有以下函数声明: int Function1(char *var1,unsigned long); 其函数修饰名为“?Function1@@YGHPADK@Z”,而对于函数声明: void Function2(); 其函数修饰名则为“?Function2@@YGXXZ” 。 对于C++的类成员函数(其调用方式是thiscall),函数的名字修饰与非成员的C++函数稍有不同,首先就是在函数名字和参数表之间插入以“@”字符引导的类名;其次是参数表的开始标识不同,公有(public)成员函数的标识是“@@QAE”,保护(protected)成员函数的标识是“@@IAE”,私有(private)成员函数的标识是“@@AAE”,如果函数声明使用了const关键字,则相应的标识应分别为“@@QBE”,“@@IBE”和“@@ABE”。如果参数类型是类实例的引用,则使用“AAV1”,对于const类型的引用,则使用“ABV1”。下面就以类CTest为例说明C++成员函数的名字修饰规则: class CTest { ...... private: void Function(int); protected: void CopyInfo(const CTest &src); public: long DrawText(HDC hdc, long pos, const TCHAR* text, RGBQUAD color, BYTE bUnder, bool bSet); long InsightClass(DWORD dwClass) const; ...... }; 对于成员函数Function,其函数修饰名为“?Function@CTest@@AAEXH@Z”,字符串“@@AAE”表示这是一个私有函数。成员函数CopyInfo只有一个参数,是对类CTest的const引用参数,其函数修饰名为“?CopyInfo@CTest@@IAEXABV1@@Z”。 DrawText是一个比较复杂的函数声明,不仅有字符串参数,还有结构体参数和HDC句柄参数,需要指出的是HDC实际上是一个HDC__结构类型的指 针,这个参数的表示就是“PAUHDC__@@”,其完整的函数修饰名为“?DrawText@CTest@@QAEJPAUHDC__@@JPBDUtagRGBQUAD@@E_N@Z”。InsightClass是一个共有的const函数,它的成员函数标识是“@@QBE”,完整的修饰名就是“?InsightClass@CTest@@QBEJK@Z”。 无论是C函数名修饰方式还是C++函数名修饰方式均不改变输出函数名中的字符大小写,这和PASCAL调用约定不同,PASCAL约定输出的函数名无任何修饰且全部大写。 3.查看函数的名字修饰 有两种方式可以检查你的程序中的函数的名字修饰:使用编译输出列表或使用Dumpbin工具。使用/FAc,/FAs或/FAcs命令行参数可以让编译器 输出函数或变量名字列表。使用dumpbin.exe /SYMBOLS命令也可以获得obj文件或lib文件中的函数或变量名字列表。此外,还可以使用 undname.exe 将修饰名转换为未修饰形式。 函数调用约定和名字修饰规则不匹配引起的常见问题 函数调用时如果出现堆栈异常,十有八九是由于函数调用约定不匹配引起的。比如动态链接库a有以下导出函数: long MakeFun(long lFun); 动态库生成的时候采用的函数调用约定是__stdcall,所以编译生成的a.dll中函数MakeFun的调用约 定是_stdcall,也就是函数调用时参数从右向左入栈,函数返回时自己还原堆栈。现在某个程序模块b要引用a中的MakeFun,b和a一样使用 C++方式编译,只是b模块的函数调用方式是__cdecl,由于b包含了a提供的头文件中MakeFun函数声明,所以MakeFun在b模块中被其它 调用MakeFun的函数认为是__cdecl调用方式,b模块中的这些函数在调用完MakeFun当然要帮着恢复堆栈啦,可是MakeFun已经在结束 时自己恢复了堆栈,b模块中的函数这样多此一举就引起了栈指针错误,从而引发堆栈异常。宏观上的现象就是函数调用没有问题(因为参数传递顺序是一样 的),MakeFun也完成了自己的功能,只是函数返回后引发错误。解决的方法也很简单,只要保证两个模块的在编译时设置相同的函数调用约定就行了。 在了解了函数调用约定和函数的名修饰规则之后,再来看在C++程序中使用C语言编译的库时经常出现的LNK 2001错误就很简单了。还以上面例子的两个模块为例,这一次两个模块在编译的时候都采用__stdcall调用约定,但是a.dll使用C语言的语法编 译的(C语言方式),所以a.dll的载入库a.lib中MakeFun函数的名字修饰就是“_MakeFun@4”。b包含了a提供的头文件中MakeFun函数声明,但是由于b采用的是C++语言编译,所以MakeFun在b模块中被按照C++的名字修饰规则命名为“?MakeFun@@YGJJ@Z”,编译过程相安无事,链接程序时c++的链接器就到a.lib中去找“?MakeFun@@YGJJ@Z”,但是a.lib中只有“_MakeFun@4”,没有“?MakeFun@@YGJJ@Z”,于是链接器就报告: error LNK2001: unresolved external symbol ?MakeFun@@YGJJ@Z 解决的方法和简单,就是要让b模块知道这个函数是C语言编译的,extern "C"可以做到这一点。一个采用C语言编译的库应该考虑到使用这个库的程序可能是C++程序(使用C++编译器),所以在设计头文件时应该注意这一点。通常应该这样声明头文件: #ifdef _cplusplus extern "C" { #endif long MakeFun(long lFun); #ifdef _cplusplus } #endif 这样C++的编译器就知道MakeFun的修饰名是“_MakeFun@4”,就不会有链接错误了。 许多人不明白,为什么我使用的编译器都是VC的编译器还会产生“error LNK2001”错误?其实,VC的编译器会根据源文件的扩展名选择编译方式,如果文件的扩展名是“.C”,编译器会采用C的语法编译,如果扩展名是 “.cpp”,编译器会使用C++的语法编译程序,所以,最好的方法就是使用extern "C"。 ============================================== 大家会注意到,像gcc之类的编译器也是采用__cdecl方式,这样对于printf(“%d %d“,i++,i++)之类的输出结果的疑问就迎刃而解了。 另外引出的一个问题是C语言的变参数函数,对于这部分估计很少大学会涉及到,但这的确是C语言很常见的一个特性,比如printf等。 上面提到对于可变参数的支持,需要由函数调用者负责清除栈中的函数参数,但为什么非要这样才可以支持变参数呢? 函数调用所涉及到的参数传递是通过栈来实现的,子函数从栈中读取传递给它的参数,如果是从左向右压栈的话那么子函数的最后一个参数在栈顶,然后依次是倒数第二个参数......;如果是从右向左压栈的话那么子函数的第一个参数在栈顶,然后依次是第二个参数......,压栈的顺序问题决定了子函数读取其参数的位置。对于变参数的函数本身来讲,并不知道有几个参数,需要某些信息才能知道,对于printf来讲,就是从前面的格式化字符串fmt来分析出来,一共有几个参数。所以被调函数返回清理栈的方式,对于可变参数是无法实现的,因为被调函数不知道要弹出参数的数量。而对于函数调用着,自己传递给被调函数多少参数(通过把参数压栈)当然是一清二楚的,这样被调函数返回后的堆栈清理也就可以做到准确无误了(不会多也不会少地把参数清理干净)。(该色部分属于个人理解,如有错误,还望指点,以免误人子弟!谢谢!)
    展开全文
  • C语言函数入栈顺序

    2015-12-04 17:29:34
    2. 函数参数:大多数是参数是从右向左顺序入栈(大部分编译器,原因在于C语言支持可变参数个数,使最左边的参数保持在栈顶) 3. 局部变量 C语言栈底为高地址、栈顶为低地址。 静态变量不入栈

    1. 函数中的第一条可执行语句的地址

    2. 函数参数:大多数是参数是从右向左顺序入栈(大部分编译器,原因在于C语言支持可变参数个数,使最左边的参数保持在栈顶)

    3. 局部变量

    C语言栈底为高地址、栈顶为低地址。

    静态变量不入栈

    展开全文
  • 前段时间面试栽倒的一个问题:C函数入栈顺序 <br />下面先摘录一些: ================================================ C/C++函数调用约定与函数...
  •  栈,我想大家都知道。...下面介绍C语言中函数参数的入栈顺序是怎样的。  对技术执着的人,比如说我,往往对一些问题,不仅想做到"知其然",还想做到"知其所以然".C语言可谓博大精深,即使我已经有多年的开发经...
  • printf()函数入栈顺序

    2013-05-15 00:51:00
    (1)C语言入栈顺序从右向左,printf函数是确定不了参数的个数的,也不对类型进行检查。 (2)char、short类型的输出是以int进行的。 例子: #include <stdio.h> int main(int argc, char** argv) ...
  • 如果子类中有虚函数则先将子类的虚函数入栈,然后是父类的虚函数,如果子类重写了父类的虚函数,则入栈的是子类重写的函数,即重写的子类的函数替换对应的父类的虚函数。 如://A.h #ifndef __A_H #define __A_H ...
  •  如果子类中有虚函数则先将子类的虚函数入栈,然后是父类的虚函数,如果子类写重了父类的虚函数,则入栈的是子类写重的函数,即写重的子类的函数替换对应的父类的虚函数。  如://A.h  #ifndef __A_H #define _...
  • C语言函数参数入栈顺序从右到bai左是为了方便可变参数du函数。 一、在函数调用时,函数参数的传递,在C语言中是通过栈数据结构实现的。 在调用函数时,先根据调用函数使用的参数,自右向左依次压入栈中,然后调用...
  • 主要介绍了C语言中函数参数的入栈顺序详解及实例的相关资料,需要的朋友可以参考下
  • 对技术执着的人,比如说我,往往对一些问题,不仅想做到“知其然”,还想做到“知其所以然”。...某天某地某人问我,C语言中函数参数的入栈顺序如何?从右至左,我随口回答。为什么是从右至左呢?我终究没有给
  • C语言中函数参数的入栈顺序
  • printf函数参数入栈问题,从右向左入栈
  • 参数入栈顺序 c++提供了5种参数传递标准,除了main函数传递必须用_cdecl模式,其他函数可以自己在编译器设置,默认的是_cdecl模式,即从右到左入栈 参数计算顺序 参数的入栈和计算顺序并不是一回事,c++标准并未指定...
  • 判断函数参数入栈顺序的一个参数代码如下: void f(int i, int j, int k); int main(void) { int a = 1, b =2, c = 3;  f(a,b,c); return 0; } void f(int i, int j, int k) { int h; int g; printf("k...
  • (转)C语言中函数参数压栈顺序小结先看一个小程序:#include &lt;stdio.h&gt;int f(int i, int j, int k) { printf(&quot;%d at [%X]\n%d at [%X]\n%d at [%X]\n&quot;, i, &amp;i, j, &amp...
  • 一、已知入栈顺序求所有的出栈顺序 已知入栈顺序是{1,2,3,4,5},求所有的出栈顺序? 我的思路: 既然入栈顺序固定,我觉得可以使用递归来做。 先定义一个函数,比如说叫做help。 //伪代码 void help(vetcor<...
  • 函数参数入栈顺序,参数计算顺序以及可变参数的实现
  • C函数入栈顺序 下面先摘录一些: ================================================ C/C++函数调用约定与函数名称修饰规则探讨 使用C/C++语言开发软件的...
  • 先给出本文参考的链接: [C/C++函数参数读取顺序 ] [关于c语言和c++中的...[C++函数参数的入栈顺序]问题其实今天的学习始于这样一段代码的结果:#include <stdio.h>void foo( int x, int y, int z ){ printf( "%d
  • 由该文章知道计算机中栈的生长方向为由高到低,及栈底为高地址,栈顶为低地址,因此函数输入参数入栈顺序可以由栈地址大小判断,地址大的先入栈,地址小的后入栈 #include void Var_Order(int x, int y, int z) ...
  • 函数调用时入栈顺序

    2012-05-21 22:28:52
    函数调用过程: 1、将主函数中被调函数下一条指令地址入栈; 2、调用函数的参数按照从右到左的顺序入栈; 3、调用函数使用的寄存器、局部变量入栈

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 47,552
精华内容 19,020
关键字:

函数入栈顺序