如果你是编程新手,你确信对系统栈结构有所了解吗?
首先声明这篇文章是绝对的原创,希望对新手能起到抛砖引玉的作用。
你对系统栈了解多少?__cdecl,__stdcall,__thiscall与栈有什么直接的联系?
汇编对你的工作兴许没什么帮助,但我还是请求你看完下面的示例,看看下面的分析,你会从中看懂你应该懂得的东西。
代码段一、
#include <iostream>
using namespace std;
int __cdecl fun(int a,int b) //如果这里换成__stdcall又会怎么样?
{
cout<<"Show a value:"<<a<<" Show b value:"<<b<<endl;
return a;
}
int _tmain(int argc, _TCHAR* argv[])
{
__asm
{
push 1 //__cdecl的调用约定,1入栈等同于b=1
push 2 //接着是2入栈,等同于a=2
call fun //能不能直接换成jmp fun呢?
add esp,8 //为何添加此条语句
}
cout<<"Thank you!"<<endl;
return 0;
}
一段典型的c/c++内嵌汇编的代码,为了分析更直观,先请有编译器的新手们在机器上运行一下,当然运行的结果并不会令你惊奇!程序输出了2和 1,thank you! 我们并不能把脚步停留在这表面的东西,还有很多的东西需要我们来挖掘。带着我代码中的疑问,go on...
换成__stdcall的调用约定,代码显示2和1后就直接崩溃了,why?这是因为,被__stdcall修饰的函数自行会调整栈结构,等到函数fun return返回时,esp所指的位置已经还原成main函数起初的位置,而再次对其施加指令add esp,8无非是画蛇添足了,这就叫"我用的我还原"; 而_cdecl对栈结构的维护不同,叫"我用你还原",因此,上述代码_stdcall和__cdecl修饰的fun函数最大的差别在于,在内嵌的汇编代码中写不写add esp,8这句代码。可能有的新手会故意把这条代码写成add esp,4 可是一运行发现不管用的是__stdcall还是__cdecl,代码都会崩掉,why? add esp,8 不是随便写的,内存的汇编代码中不是有push 1 ,push 2,两句代码嘛。这表示程序运行的栈结构中多了8个字节,一个是1,一个是2。说到这里,可能有的新手还是不死心,于是写下了sub esp ,8 对不起代码还是崩了。why?为何是加上8而不是减8呢,如果你假设的win32系统栈的成长方向是由低到高,恭喜你sub esp,8是正确的,可偏偏是win32系统栈的成长方向是由高到低。因此,最先压栈的东西占领了更高的地址位,讲到这里或许都认为问题已经明了化了,no,no...我们还得go on...
call fun指令能不能换成jmp fun呢?从功能上来说,call 是由一系列的push指令和一条jmp指令组成的因此,无论如何就指令的运行效率来说,jmp肯定相对call更高效。但是这里用jmp替换call指令是绝不可以的,答案很简单,call指令会对栈进行必要的维护其中必有的隐含指令是push eip ,看到没有call指令被调用后,必须把指令指针压入栈中,call在给自己留后路(必须记得是谁调用了我,我还得回家). jmp是那种绝不做任何停留勇往直前,也绝不回头。因此,在换成jmp指令后,根本就看不到thank you的输出了。
另外,如果你看到这里还没有睡着的话,那我们就讲讲_cdecl和__stdcall另外的一些区别,__stdcall函数一般不允许函数以变参声明,像这样 int __stdcall fun(int a,...) ,这种声明是__cdecl独有的,是c/c++语言独有的,以__stdcall声明不定参数的函数形式必定给栈结构带来毁灭性的灾难!
最后谈谈__thiscall,这是c/c++类中成员函数的调用约定,他在形参个数固定的情况下等同于__stdcall,个数不定是等同于__cdecl,还有几个特殊的调用约定(__fastcall,__declspec(naked)),由于用的极少,不在累赘,感兴趣的可以自己找资料看看。
好了就写这么多了,同时希望高手们能接续....