如果你是编程新手,你确信对系统栈结构有所了解吗?

tangshuiling 2009-03-28 03:57:28

首先声明这篇文章是绝对的原创,希望对新手能起到抛砖引玉的作用。
你对系统栈了解多少?__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)),由于用的极少,
不在累赘,感兴趣的可以自己找资料看看。
好了就写这么多了,同时希望高手们能接续....

...全文
1197 73 打赏 收藏 转发到动态 举报
写回复
用AI写文章
73 条回复
切换为时间正序
请发表友善的回复…
发表回复
Guccang 2011-10-23
  • 打赏
  • 举报
回复
辛苦楼主了,这个你的这个教程使我写了第一个C++潜入汇编程序。
在网上看了许多win32反汇编的教程,但还是LZ的来的直接,真诚.
由衷的感谢和祝福你。
yongtian107 2010-11-16
  • 打赏
  • 举报
回复
小弟郁闷!!!确实看不怎么懂
yzq_xiaoxian 2010-11-10
  • 打赏
  • 举报
回复
学习!!!
lonerhythm 2009-03-31
  • 打赏
  • 举报
回复
good,
Paradin 2009-03-30
  • 打赏
  • 举报
回复
收藏。感谢楼主.
Mars_2009 2009-03-30
  • 打赏
  • 举报
回复
学习了
小童012 2009-03-30
  • 打赏
  • 举报
回复
写得不错,支持一下
danxuezx 2009-03-30
  • 打赏
  • 举报
回复
顶一个
foxpeter 2009-03-30
  • 打赏
  • 举报
回复
MARK一下
梅文海 2009-03-30
  • 打赏
  • 举报
回复
很多东西并不绝对,在写代码的时候常常用 jmp,ret 等指令来进行函数的调用,下面举个jmp调用函数的例子给大家看看:

#include <iostream>
using namespace std;
int _stdcall fun(int a,int b) //如果这里换成__stdcall又会怎么样?
{
cout<<"Show a value:"<<a<<" Show b value:"<<b<<endl;
return a;
//return a*b;
}

int main(int argc, char* argv[])
{
__asm
{
push 0x22222222 //__cdecl的调用约定,1入栈等同于b=1
push 2 //接着是2入栈,等同于a=2
lea eax,j
push eax
//call fun //能不能直接换成jmp fun呢?
jmp fun
add esp,4
j:
//add esp,8 //为何添加此条语句
}
cout<<"Thank you!"<<endl;
return 0;
}
tangshuiling 2009-03-30
  • 打赏
  • 举报
回复

接续......
如果,你对上面的简单分析已经吃透了,同时应55楼的要求,再深入一点点....
下面是代码段:
#include <iostream>
using namespace std;
int fun(int a,int b)
{
int address=0;
__asm
{
mov ecx,dword ptr[ebp+4];
mov dword ptr[address],ecx //这三条指令可直接用一条指令代替jmp dword ptr[ebp+4];
jmp dword ptr[address]
}
cout<<hex<<address<<endl; // 不会执行到此
}
int _tmain(int argc, _TCHAR* argv[])
{
int ESP=0;
__asm
{
xor eax,eax //eax寄存器清零
mov eax,esp
mov dword ptr[ESP],eax //存储esp的值到变量ESP中
push 1
push 2
call fun //前面已经讲过了,直接调用fun函数,不能用jmp指令
xor eax,eax
mov eax,dword ptr[ESP]
sub eax,esp //eax的值保存了两个函数(main,fun)栈顶的差值
add esp,eax //差值找到了,就能平衡栈了,add esp,8
}
cout<<"thank you!"<<endl;
return 0;
}

再次声明,编译环境是vs2005+xp
再机器上运行,你会发现fun函数根本没有运行cout<<hex<<address<<endl;语句,更别谈什么return语句啦,如果
函数的声明为__stdcall调用约定,运行立马崩溃!add esp,8也不灵了 why?
这段代码没有什么特殊的技巧,无非告诉你,函数之间的相互调用必须遵守一个基本的原则,系统栈必须得到恢复,在
vs2005的编译环境中有一个专门检测栈的函数叫checkesp,如果你对当前函数的栈顶没有进行必要的维护,chekesp会
抛出runtime check failure异常,在DEBUG的调试环境中,此函数是隐式的。
在看到了运行结果后,我们开始进入正题:
add esp,eax 为何不能换成add esp,8 ,就win32系统的调试环境来说,调用一个函数(包括main函数),其代码的
前期工作都有系统替你安排了,代码类似如下
push ebp
mov ebp,esp
sub esp,0D8h //注意看这里,esp减去了0D8
push ebx //同时又有不同的寄存器进行压栈操作
push esi
push edi
lea edi,[ebp-0D8h] //并对这段空间填入cccccccc的数值,这就是为何没有初始化的变量总是0xcccccccc
//的原因所在
mov ecx,36h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
上面这段代码,被win32系统强加在所有的函数头部,因此,如果想不通过return语句直接平衡堆栈,必须要计算到底esp
向下生长了多少,这就是为何add esp,eax的真正原因。
一些后话,如果对系统栈把握的很好,那对你Crack一些软件结构会有很大的帮助,请相信我!

liuwg9999 2009-03-30
  • 打赏
  • 举报
回复
学习
caitian6 2009-03-30
  • 打赏
  • 举报
回复
我顶你的肺!!!!!!!
blingpro 2009-03-30
  • 打赏
  • 举报
回复
mark
sagegz 2009-03-30
  • 打赏
  • 举报
回复
糖糖写的不错~!
greatmj001 2009-03-30
  • 打赏
  • 举报
回复
VC6.0加一句:
#include <tchar.h>
aaronlibra 2009-03-30
  • 打赏
  • 举报
回复
不错 获益匪浅
changhe325 2009-03-30
  • 打赏
  • 举报
回复
马克
sunnystone614 2009-03-30
  • 打赏
  • 举报
回复
标记,学习
wjb_yd 2009-03-30
  • 打赏
  • 举报
回复
厄...
我认为,这个栈好像跟那个数据结构没有多大的关系,这个是intel cpu为了方便程序员编写代码,而设置了ss,sp,至于你永不用他们,使用用的合理,就是另外一个事儿了
ss知道哪,哪就是栈

还有,lz可以再写一个,通过参数的地址,改变函数返回值,以达到跳过某些指令的目的的例子
加载更多回复(53)

64,648

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧