异常处理与MiniDump详解(2) 智能指针与C++异常
write by九天雁翎(JTianLing) – www.jtianling.com**
讨论新闻组及文件
一、 综述
《异常处理与MiniDump详解(1) C++异常》稍微回顾了下C++异常的语法及其类似于函数参数传递的抛出异常对象的copy,引用语义,但是有个问题没有详细讲,那就是C++异常的绝佳搭档,智能指针。在没有智能指针的时候会感觉C++的异常少了一个用于释放资源的finally语法,但是C++没有这样的语法是有理由的,因为C++的智能指针。假如不用智能指针仅仅使用异常,那就像是吃一道没有放肉的辣椒炒肉一样。。。。。。。。。。。
智能指针对于C++的重要性很多人可能并没有认识到,看看C++相关的书籍吧,几乎每本都有其痕迹,从《C++ Primer》,TCPL到《C++沉思录》《C++编程艺术》《C++Templates》,Meyes的《Effective C++》中讲过,《More Effective C++》再讲,无论是概念,用法,实现都是一本一本反复提起,这样反复提起,反复讲解的知识,自然是有其作用的,其作用也很简单,为了弥补C++对于内存管理的不足。众所周知,C++对于内存管理的方式称为手动管理,说白了就是C++本身不管理,都由程序员管理,事实上导致的问题一片一片,不记得多少次反复的在公司的服务器程序中去查内存泄露了,几乎随着每次大规模功能的更新,都会有新的内存泄露出现。。。。。C++相信程序员的原则告诉我们,程序员总是对的,那么即使是内存泄露了,也是有他的理由。。。。。。他的理由就是下一次可以进行内存泄露检查的工作,以浪费一天又一天的时间。随着BoundsChecker这样的工具出现,虽然简化了一部分明显的内存泄露检查,但是实际上复杂情况下的内存泄露还是只能靠自己完成,BoundsChecker误报的本事太强了,并且根本无法正常运行公司的地图服务器并退出,可能因为随后的内存泄露正常报告+误报超出了BoundsChecker的整数上限,总是会将BoundsChecker拉入无响应的状态。
事实上,我工作中也用的相对较少,毕竟C++标准库中的auto_ptr是个比较扭曲的东西,不仅使用语义奇怪,碰到稍微复杂的应用就根本胜任不了了,这是很无奈的事情。这一点可以参考我很久以前的文章《C++可怜的内存管理机制漫谈及奇怪补救auto_ptr介绍》,文中可以看到,在加入了类,异常机制,并且延续着C语言中手动管理内存方式的C++中,auto_ptr其实最多也就算种奇异的补丁机制。
但是我们可以求助于boost的智能指针库,那里丰富的资源改变了很多事情,但是我工作中是不允许使用boost库的。。。。又一次的无奈。
二、 智能指针
1. 什么是智能指针
要知道什么是智能指针,首先了解什么称为 “资源分配即初始化”这个翻译的异常扭曲的名词。RAII—Resource Acquisition Is Initialization,外国人也真有意思,用一个完整的句子来表示一个应该用名词表示的概念,我们有更有意思了,直接翻译过来,相当扭曲。。。。。
在《C++ Primer》这样解释的,“通过定义一个类来封装资源的分配和释放,可以保证正确释放资源”
而智能指针就是这种技术的实现,《More Effective C++》中这样描述的:“Smart pointers are objects that are designed to look,act,and feel like build-in pointers,but to offer greater functionality.They have a variety of applications, including resource management.”
《Effective C++》给出的关键特点是:
1. 资源分配后立即由资源管理对象接管。
2. 资源管理对象用析构函数来确保资源被释放。
基本上,这就是智能指针的核心概念了,至于智能指针实现上的特点,比如所有权转移,所有权独占,引用计数等,都是次要的东西了。
目前我见过关于各种智能指针分类,介绍,使用方法说明最详细的应该是《Beyond the C++ Standard Library: An Introduction to Boost》一书,此书第一章第一个专题库就是关于智能指针的,除了对标准库中已有的auto_ptr没有介绍(因为本书是讲Boost的嘛),对Boost库中的智能指针进行了较为详细的描述,推荐想了解的都去看看。
文中论及的智能指针包括
scoped_ptr,scoped_array:所有权限制实现
shared_ptr,shared_array:引用计数实现
intrusive_ptr:引用计数插入式实现
weak_ptr:无所有权实现
关于智能指针的实现及原理,本人看过最详细的介绍是在More Effective C++ Items 28,29
这里仅仅介绍最广泛使用的智能指针shared_ptr,加上以前写过的auto_ptr(《C++可怜的内存管理机制漫谈及奇怪补救auto_ptr介绍》)给出智能指针的一些用法示例,其他的智能指针因为实现上的区别导致使用上也有一些区别,但是核心概念是一样的,都是上面提及的两条关键特点。
2. shared_ptr介绍
shared_ptr是通过引用计数计数实现的智能指针,应用也最为广泛,也是早在TR1就已经确认会进入下一版C++标准的东西,现在我还会因为标准库中没有,boost库不准用而遗憾,过几年,总有一天,我们就能自由使用类似shared_ptr的指针了。
原型如下:
namespace boost {
template<typename T> class shared_ptr {
public:
template <class Y> explicit shared_ptr(Y* p);
template <class Y,class D> shared_ptr(Y* p,D d);
~shared_ptr();
shared_ptr(const shared_ptr & r);
template <class Y> explicit
shared_ptr(const weak_ptr<Y>& r);
template <class Y> explicit shared_ptr(std::auto_ptr<Y>& r);
shared_ptr& operator=(const shared_ptr& r);
void reset();
T& operator*() const;
T* operator->() const;
T* get() const;
bool unique() const;
long use_count() const;
operator unspecified_bool_type() const;
void swap(shared_ptr<T>& b);
};
template <class T,class U>
shared_ptr<T> static_pointer_cast(const shared_ptr<U>& r);
}
首先,为了查看资源分配方便,引入一个方便查看资源转移,拷贝情况的类:
#include <string>
#include <iostream>
template<class T>
class CResourceObserver
{
public:
CResourceObserver()
{
cout <<typeid(T).name() <<" Construct." <<endl;
}
CResourceObserver(const CResourceObserver& orig)
{
cout <<typeid(T).name() <<" Copy Construct." <<endl;
}
operator=(const CResourceObserver& orig)
{
cout <<typeid(T).name() <<" operator = " <<endl;
}
virtual ~CResourceObserver(void)
{
cout <<typeid(T).name() <<" Deconstruct." <<endl;
}
};
这个类,利用了运行时类型识别及模板,这样发生与资源有关的操作时,都能通过输出恰当的反映出来。
shared_ptr的最简单应用
这里看个最简单的shared_ptr使用的例子,顺便看看CResourceObserver的使用。在最简单的一次性使用上,shared_ptr几乎没有区别。
例一:
#include <boost/smart_ptr.hpp>
#include "ResourceObserver.h"
using namespace std;
using namespace boost;
class MyClass : public CResourceObserver<MyClass>
{
};
void Fun()
{
shared_ptr<MyClass> sp(new MyClass);
}
int _tmain(int argc, _TCHAR* argv[])
{
cout <<"Fun called." <<endl;
Fun();
cout <<"Fun ended." <<endl;
return 0;
}
输出如下:
Fun called.
class MyClass Construct.
class MyClass Deconstruct.
Fun ended.
我们只new,并没有显式的delete,但是MyClass很显然也是析构了的。
这里将shared_ptr替换成auto_ptr也是完全可以的,效果也一样。
shared_ptr的与auto_ptr的区别
shared_ptr与auto_ptr的区别在于所有权的控制上。如下例:
例二:
#include <boost/smart_ptr.hpp>
#include <memory>
#include "ResourceObserver.h"
using namespace std;
using namespace boost;
class MyClass : public CResourceObserver<MyClass>
{
public:
MyClass() : CResourceObserver<MyClass>()
{
mstr = typeid(MyClass).name();
}
void print()
{
cout <<mstr <<" print" <<endl;
}
std::string mstr;
};
typedef shared_ptr<MyClass> spclass_t;
//typedef auto_ptr<MyClass> spclass_t;
void Fun2(spclass_t& asp)
{
spclass_t sp3(asp);
cout <<asp.use_count() <<endl;
asp->print();
return;
}
void Fun()
{
spclass_t sp(new MyClass);
cout <<sp.use_count() <<endl;
spclass_t sp2(sp);
cout <<sp.use_count() <<endl;
Fun2(sp);
cout <<sp.use_count() <<endl;
}
int _tmain(int argc, _TCHAR* argv[])
{
cout <<"Fun called." <<endl;
Fun();
cout <<"Fun ended." <<endl;
return 0;
}
输出:
Fun called.
class MyClass Construct.
1
2
3
class MyClass print
2
class MyClass Deconstruct.
Fun ended.
此例中将shared_ptr分配的资源复制了3份(实际是管理权的复制,资源明显没有复制),每一个shared_ptr结束其生命周期时释放一份管理权。每一个都有同等的使用权限。输出的引用计数数量显式了这一切。在这里,可以尝试替换shared_ptr到auto_ptr,这个程序没有办法正确运行。
shared_ptr的引用计数共享所有权
因为没有拷贝构造及operator=的操作,我们可以知道,对象没有被复制,为了证实其使用的都是同一个资源,这里再用一个例子证明一下:
例3:
#include <boost/smart_ptr.hpp>
#include <memory>
#include "ResourceObserver.h"
using namespace std;
using namespace boost;
class MyClass : public CResourceObserver<MyClass>
{
public:
MyClass() : CResourceObserver<MyClass>()
{
mstr = typeid(MyClass).name();
}
void set(const char* asz)
{
mstr = asz;
}
void print()
{
cout <<mstr <<" print" <<endl;
}
std::string mstr;
};
typedef shared_ptr<MyClass> spclass_t;
void Fun2(spclass_t& asp)
{
spclass_t sp3(asp);
sp3->set("New Name");
return;
}
void Fun()
{
spclass_t sp(new MyClass);
spclass_t sp2(sp);
Fun2(sp);
sp->print();
sp2->print();
}
int _tmain(int argc, _TCHAR* argv[])
{
cout <<"Fun called." <<endl;
Fun();
cout <<"Fun ended." <<endl;
return 0;
}
输出:
Fun called.
class MyClass Construct.
New Name print
New Name print
class MyClass Deconstruct.
Fun ended.
shared_ptr与标准库容器
在标准库容器中存入普通指针来实现某个动态绑定的实现是很普遍的事情,但是实际上每次都得记住资源的释放,这也是BoundsChecker误报的最多的地方。
例4:
#include <boost/smart_ptr.hpp>
#include <vector>
#include "ResourceObserver.h"
using namespace std;
using namespace boost;
class MyClass : public CResourceObserver<MyClass>
{
public:
MyClass() : CResourceObserver<MyClass>()
{
mstr = typeid(MyClass).name();
}
void set(const char* asz)
{
mstr = asz;
}
void print()
{
cout <<mstr <<" print" <<endl;
}
std::string mstr;
};
typedef shared_ptr<MyClass> spclass_t;
typedef vector< shared_ptr<MyClass> > spclassVec_t;
void Fun()
{
spclassVec_t spVec;
spclass_t sp(new MyClass);
spclass_t sp2(sp);
cout <<sp.use_count() <<endl;
cout <<sp2.use_count() <<endl;
spVec.push_back(sp);
spVec.push_back(sp2);
cout <<sp.use_count() <<endl;
cout <<sp2.use_count() <<endl;
sp2->set("New Name");
sp->print();
sp2->print();
spVec.pop_back();
cout <<sp.use_count() <<endl;
cout <<sp2.use_count() <<endl;
sp->print();
sp2->print();
}
int _tmain(int argc, _TCHAR* argv[])
{
cout <<"Fun called." <<endl;
Fun();
cout <<"Fun ended." <<endl;
return 0;
}
输出:
Fun called.
class MyClass Construct.
2
2
4
4
New Name print
New Name print
3
3
New Name print
New Name print
class MyClass Deconstruct.
Fun ended.
当在标准库容器中保存的是shared_ptr时,几乎就可以不考虑资源释放的问题了,该释放的时候自然就释放了,当一个资源从一个容器辗转传递几个地方的时候,常常会搞不清楚在哪个地方统一释放合适,用了shared_ptr后,这个问题就可以不管了,每次的容器Item的添加增加计数,容器Item的减少就减少计数,恰当的时候,就释放了。。。。方便不可言喻。
3. 智能指针的高级应用:
已经说的够多了,再说下去几乎就要脱离讲解智能指针与异常的本意了,一些很有用的应用就留待大家自己去查看资料吧。
1. 定制删除器,shared_ptr允许通过定制删除器的方式将其用于其它资源的管理,几乎只要是通过分配,释放形式分配的资源都可以纳入shared_ptr的管理范围,比如文件的打开关闭,目录的打开关闭等自然不在话下,甚至连临界区,互斥对象这样的复杂对象,一样可以纳入shared_ptr的管理。
2. 从this创建shared_ptr 。
以上两点内容在《Beyond the C++ Standard Library: An Introduction to Boost》智能指针的专题中讲解了一些,但是稍感不够详细,但是我也没有看到更为详细的资料,聊胜于无吧。
3. Pimpl:
《Beyond the C++ Standard Library: An Introduction to Boost》中将智能指针的时候有提及,在《C++ Coding Standards: 101 Rules, Guidelines, and Best Practices》 第43 条Pimpl judiciously中对其使用的好处,坏处,方式等都有较为详细的讲解,大家可以去参考一下,我就不在此继续献丑了。
但是,事实上,很多时候并不是只能使用智能指针(比如pimpl),只是,假如真的你合理的使用了智能指针,那么将会更加安全,更加简洁。
以下是《Beyond the C++ Standard Library: An Introduction to Boost》对shared_ptr使用的建议:
1.当有多个使用者使用同一个对象,而没有一个明显的拥有者时
2.当要把指针存入标准库容器时
3.当要传送对象到库或从库获取对象,而没有明确的所有权时
4.当管理一些需要特殊清除方式的资源时
三、 智能指针与C++异常
因为考虑到大家可能对智能指针不够熟悉,所以讲到这里的时候对智能指针进行了较多的讲解,几乎就脱离主题了,在这里开始进入正题。
一开始我就讲了智能指针对于C++的异常机制非常重要,到底重要在哪里呢?这里看两个C++异常的例子,一个使用了没有使用智能指针,一个使用了智能指针。
void Fun()
{
MyClass* lp1 = NULL;
MyClass* lp2 = NULL;
MyClass* lp3 = NULL;
lp1 = new MyClass;
try
{
lp2 = new MyClass;
}
catch(bad_alloc)
{
delete lp1;
}
try
{
lp3 = new MyClass;
}
catch(bad_alloc)
{
delete lp1;
delete lp2;
}
// Do Something.....
delete lp1;
delete lp2;
delete lp3;
}
void Fun2()
{
try
{
spclass_t sp1(new MyClass);
spclass_t sp2(new MyClass);
spclass_t sp3(new MyClass);
}
catch(bad_alloc)
{
// No need to delete anything
}
// Do Something
// No need to delete anything
}
区别,好处,明眼人不用看第二眼。这里为了简单,用内存的作为示例,虽然现在内存分配的情况很少见了,但是其他资源原理上是一样的,个人经验最深的地方实在Python的C API使用上,哪怕一个简单的C++,Python函数相互调用,都会有N个PyObject*出来,一个一个又一个,直到头昏脑胀,使用了智能指针后,简化的不止一半代码。
其实从本质上来讲,异常属于增加了程序从函数退出的路径,而C++原来的内存管理机制要求每个分支都需要手动的释放每个分配了的资源,这是本质的复杂度,在用于普通return返回的时候,还有一些hack技巧,见《do…while(0)的妙用》,但是异常发生的时候,能够依赖的就只有手动和智能指针两种选择了。
在没有智能指针的光使用异常的时候,甚至会抱怨因为异常增加了函数的出口,导致代码的膨胀,说智能指针是C++异常处理的绝佳搭档就在于其弥补的此缺点。
另外,其实很多语言还有个finally的异常语法,JAVA,Python都有,SEH也有,其与使用了智能指针的C++异常比较在刘未鹏关于异常处理的文章《错误处理(Error-Handling):为何、何时、如何(rev#2)》中也有详细描述,我就不在此多费口舌了,将来讲SEH的时候自然还会碰到。个人感觉是,有也不错。。。。毕竟,不是人人都有机会在每个地方都用上智能指针。
四、 参考资料
1.C++ Primer,中文版第4版,Stanley B.Lippman, Josee lajoie, Barbara E.Moo著 人民邮电出版社
2.Effective C++,Third Edition 英文版,Chapter 3 Resource Management, Scott Meyes著,电子工业出版社
3.More Effective C++(英文版),Scott Meyes著,Items 28,29,机械工业出版社
4.Beyond the C++ Standard Library: An Introduction to Boost,By Björn Karlsson著,Part 1,Library 1,Addison Wesley Professional
5.C++ Coding Standards: 101 Rules, Guidelines, and Best Practices
Herb Sutter, Andrei Alexandrescu著, Addison Wesley Professional
write by九天雁翎(JTianLing) – www.jtianling.com
阅读全文....
异常处理与 MiniDump详解(1) C++异常
write by 九天雁翎(JTianLing) – www.jtianling.com
讨论新闻组及文件
一、 综述
我很少敢为自己写的东西弄个详解的标题,之所以这次敢于这样,自然还算是有点底气的。并且也以此为动力,督促自己好好的将这两个东西研究透。
当年刚开始工作的时候,第一个工作就是学习breakpad的源代码,然后了解其原理,为公司写一个ExceptionHandle的库,以处理服务器及客户端的未处理异常(unhandle exception),并打下dump,以便事后分析,当年这个功能在有breakpad的示例在前时,实现难度并不大,无非就是调用了SetUnhandledExceptionFilter等函数,让windows在出现未处理异常时让自己的回调函数接管操作,然后利用其struct _EXCEPTION_POINTERS* ExceptionInfo的指针,通过MiniDumpWriteDump API将Dump写下来。但是仍记得,那时看到《Windows 核心编程》第五部分关于结构化异常处理的描述时那种因为得到新鲜知识时的兴奋感,那是我第一次这样接近Windows系统的底层机制,如同以前很多次说过的,那以后我很投入的捧着读完了《Windows 核心编程》,至今受益匪浅。当时也有一系列一边看源代码一边写下心得的时候,想想,都已经一年以前的事情了。
《读windows核心编程,结构化异常部分,理解摘要》
《Breakpad在进程中完成dump的流程描述》
《Breakpad 使用方法理解文档》**
直到最近,为了控制服务器在出现异常时不崩溃,(以前是崩溃的时候打Dump),对SEH(windows结构化异常)又进行了进一步的学习,做到了在服务器出现了异常情况(例如空指针的访问)时,服务器打下Dump,并继续运行,并不崩溃,结合以前也是我写的监控系统,通知监控客户端报警,然后就可以去服务器上取回dump,并分析错误,这对服务器的稳定性有很大的帮助,不管我们对服务器的稳定性进行了多少工作,作为C++程序,偶尔的空指针访问,几乎没有办法避免。。。。。。但是,这个工作,对这样的情况起到了很好的缓冲作用。在这上面工作许久,有点心得,写下来,供大家分享,同时也是给很久以后的自己分享。
二、 为什么需要异常
《Windows核心编程》第4版第13章开头部分描述了一个美好世界,即所编写的代码永远不会执行失败,总是有足够的内存,不存在无效的指针。。。。但是,那是不存在的世界,于是,我们需要有一种异常的处理措施,在C语言中最常用的(其实C++中目前最常用的还是)是利用错误代码(Error Code)的形式。
这里也为了更好的说明,也展示一下Error Code的示例代码:
Error Code常用方式:
1.最常用的就是通过返回值判断了:
比如C Runtime Library中的fopen接口,一旦返回NULL,Win32 API中的CreateFiley一旦返回INVALID_HANDLE_VALUE,就表示执行失败了。
2.当返回值不够用(或者携带具体错误信息不够的)时候,C语言中也常常通过一个全局的错误变量来表示错误。
比如C Runtime Library中的errno 全局变量,Win32 API中的GetLastError, WinSock中的 WSAGetLastError函数就是这种实现。
既然Error Code在这么久的时间中都是可用的,好用的,为什么我们还需要其他东西呢?
这里可以参考一篇比较浅的文章。《错误处理和异常处理,你用哪一个 》,然后本人比较钦佩的pongba还有一篇比较深的文章:《错误处理(Error-Handling):为何、何时、如何(rev#2)》, 看了后你一定会大有收获。当pongba列出了16条使用异常的好处后,我都感觉不到我还有必要再去告诉你为什么我们要使用异常了。
但是,这里在其无法使用异常的意外情况下,(实际是《C++ Coding Standards: 101 Rules, Guidelines, and Best Practices》一书中所写)
一, 用异常没有带来明显的好处的时候:比如所有的错 误都会在立即调用端解决掉或者在非常接近立即调用端的地方解决掉。
二, 在实际作了测定之后发现异常的抛出和捕获导致了显著的时间开销:这通常只有两种情 况,要么是在内层循环里面,要么是因为被抛出的异常根本不对应于一个错误。
很明显,文中列举的都是完全理论上理想的情况,受制于国内的开发环境,无论多么好的东西也不一定实用,你能说国内多少地方真的用上了敏捷开发的实践经验?这里作为现实考虑,补充几个没有办法使用异常的情况:
一. 所在的项目组中没有合理的使用RAII的习惯及其机制,比如无法使用足够多的smart_ptr时,最好不要使用异常,因为异常和RAII的用异常不用RAII就像吃菜不放盐一样。这一点在后面论述一下。
二. 当项目组中没有使用并捕获异常的习惯时,当项目组中认为使用异常是奇技淫巧时不要使用异常。不然,你自认为很好的代码,会在别人眼里不可理解并且作为异类,接受现实。
三、 基础篇
先回顾一下标准C++的异常用法
1. C++标准异常
只有一种语法,格式类似:
try
{
}
catch()
{
}
经常简写为try-catch,当然,也许还要算上throw。格式足够的简单。
以下是一个完整的例子:
MyException:
#include <string>
#include <iostream>
using namespace std;
class MyException : public exception
{
public:
MyException(const char* astrDesc)
{
mstrDesc = astrDesc;
}
string mstrDesc;
};
int _tmain(int argc, _TCHAR* argv[])
{
try
{
throw MyException("A My Exception");
}
catch(MyException e)
{
cout <<e.mstrDesc <<endl;
}
return 0;
}
这里可以体现几个异常的优势,比如自己的异常继承体系,携带足够多的信息等等。另外,虽然在基础篇,这里也讲讲C++中异常的语义,
如下例子中,
throwException:
#include <string>
#include <iostream>
using namespace std;
class MyException : public exception
{
public:
MyException(const char* astrDesc)
{
mstrDesc = astrDesc;
}
MyException(const MyException& aoOrig)
{
cout <<"Copy Constructor MyException" <<endl;
mstrDesc = aoOrig.mstrDesc;
}
MyException& operator=(const MyException& aoOrig)
{
cout <<"Copy Operator MyException" <<endl;
if(&aoOrig == this)
{
return *this;
}
mstrDesc = aoOrig.mstrDesc;
return *this;
}
~MyException()
{
cout <<"~MyException" <<endl;
}
string mstrDesc;
};
void exceptionFun()
{
try
{
throw MyException("A My Exception");
}
catch(MyException e)
{
cout <<e.mstrDesc <<" In exceptionFun." <<endl;
e.mstrDesc = "Changed exception.";
throw;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
try
{
exceptionFun();
}
catch(MyException e)
{
cout <<e.mstrDesc <<" Out exceptionFun." <<endl;
throw;
}
return 0;
}
输出:
Copy Constructor MyException
A My Exception In exceptionFun.
~MyException
Copy Constructor MyException
A My Exception Out exceptionFun.
~MyException
可以看出当抛出C++异常的copy语义,抛出异常后调用了Copy Constructor,用新建的异常对象传入catch中处理,所以在函数中改变了此异常对象后,再次抛出原异常,并不改变原有异常。
这里我们经过一点小小的更改,看看会发生什么:
throwAnotherException
#include <string>
#include <iostream>
using namespace std;
class MyException : public exception
{
public:
MyException(const char* astrDesc)
{
mstrDesc = astrDesc;
}
MyException(const MyException& aoOrig)
{
cout <<"Copy Constructor MyException" <<endl;
mstrDesc = aoOrig.mstrDesc;
}
MyException& operator=(const MyException& aoOrig)
{
cout <<"Copy Operator MyException" <<endl;
if(&aoOrig == this)
{
return *this;
}
mstrDesc = aoOrig.mstrDesc;
return *this;
}
~MyException()
{
cout <<"~MyException" <<endl;
}
string mstrDesc;
};
void exceptionFun()
{
try
{
throw MyException("A My Exception");
}
catch(MyException e)
{
cout <<e.mstrDesc <<" In exceptionFun." <<endl;
e.mstrDesc = "Changed exception.";
throw e;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
try
{
exceptionFun();
}
catch(MyException e)
{
cout <<e.mstrDesc <<" Out exceptionFun." <<endl;
throw;
}
return 0;
}
这里和throwException程序的唯一区别就在于不是抛出原有异常,而是抛出改变后的异常,输出如下:
Copy Constructor MyException
A My Exception In exceptionFun.
Copy Constructor MyException
Copy Constructor MyException
~MyException
~MyException
Changed exception. Out exceptionFun.
~MyException
你会发现连续的两次Copy Constructor都是改变后的异常对象,这点很不可理解。。。。。。。因为事实上一次就够了。但是理解C++的Copy异常处理语义就好理解了,一次是用于传入下一次的catch语句中的,还有一次是留下来,当在外层catch再次throw时,已经抛出的是改变过的异常对象了,我用以下例子来验证这点:
throwTwiceException
#include <string>
#include <iostream>
using namespace std;
class MyException : public exception
{
public:
MyException(const char* astrDesc)
{
mstrDesc = astrDesc;
}
MyException(const MyException& aoOrig)
{
cout <<"Copy Constructor MyException" <<endl;
mstrDesc = aoOrig.mstrDesc;
}
MyException& operator=(const MyException& aoOrig)
{
cout <<"Copy Operator MyException" <<endl;
if(&aoOrig == this)
{
return *this;
}
mstrDesc = aoOrig.mstrDesc;
return *this;
}
~MyException()
{
cout <<"~MyException" <<endl;
}
string mstrDesc;
};
void exceptionFun()
{
try
{
throw MyException("A My Exception");
}
catch(MyException e)
{
cout <<e.mstrDesc <<" In exceptionFun." <<endl;
e.mstrDesc = "Changed exception.";
throw e;
}
}
void exceptionFun2()
{
try
{
exceptionFun();
}
catch(MyException e)
{
cout <<e.mstrDesc <<" In exceptionFun2." <<endl;
throw;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
try
{
exceptionFun2();
}
catch(MyException e)
{
cout <<e.mstrDesc <<" Out exceptionFuns." <<endl;
throw;
}
return 0;
}
输出如下,印证了我上面的说明。
Copy Constructor MyException
A My Exception In exceptionFun.
Copy Constructor MyException
Copy Constructor MyException
~MyException
~MyException
Changed exception. In exceptionFun2.
~MyException
Copy Constructor MyException
Changed exception. Out exceptionFuns.
上面像语言律师一样的讨论着C++本来已经足够简单的异常语法,其实简而言之,C++总是保持着一个上次抛出的异常用于用户再次抛出,并copy一份在catch中给用户使用。
但是,实际上,会发现,其实原有的异常对象是一直向上传递的,只要你不再次抛出其他异常,真正发生复制的地方在于你catch异常的时候,这样,当catch时使用引用方式,那么就可以避免这样的复制。
referenceCatch
#include <string>
#include <iostream>
using namespace std;
class MyException : public exception
{
public:
MyException(const char* astrDesc)
{
mstrDesc = astrDesc;
}
MyException(const MyException& aoOrig)
{
cout <<"Copy Constructor MyException: " <<aoOrig.mstrDesc <<endl;
mstrDesc = aoOrig.mstrDesc;
}
MyException& operator=(const MyException& aoOrig)
{
cout <<"Copy Operator MyException:" <<aoOrig.mstrDesc <<endl;
if(&aoOrig == this)
{
return *this;
}
mstrDesc = aoOrig.mstrDesc;
return *this;
}
~MyException()
{
cout <<"~MyException" <<endl;
}
string mstrDesc;
};
void exceptionFun()
{
try
{
throw MyException("A My Exception");
}
catch(MyException& e)
{
cout <<e.mstrDesc <<" In exceptionFun." <<endl;
e.mstrDesc = "Changed exception.";
throw;
}
}
void exceptionFun2()
{
try
{
exceptionFun();
}
catch(MyException e)
{
cout <<e.mstrDesc <<" In exceptionFun2." <<endl;
throw;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
try
{
exceptionFun2();
}
catch(MyException e)
{
cout <<e.mstrDesc <<" Out exceptionFuns." <<endl;
throw;
}
return 0;
}
上例中,使用引用方式来捕获异常,输出如下:
A My Exception In exceptionFun.
Copy Constructor MyException: Changed exception.
Changed exception. In exceptionFun2.
~MyException
Copy Constructor MyException: Changed exception.
Changed exception. Out exceptionFuns.
~MyException
完全符合C++的引用语义。
基本可以发现,做了很多无用功,因为try-catch无非是一层迷雾,其实这里复制和引用都还是遵循着原来的C++简单的复制,引用语义,仅仅这一层迷雾,让我们看不清楚原来的东西。所以,很容易理解一个地方throw一个对象,另外一个地方catch一个对象一定是同一个对象,其实不然,是否是原来那个对象在于你传递的方式,这就像这是个参数,通过catch函数传递进来一样,你用的是传值方式,自然是通过了复制,通过传址方式,自然是原有对象,仅此而已。
另外,最终总结一下,《C++ Coding Standards》73条建议Throw by value,catch by reference就是因为本文描述的C++的异常特性如此,所以才有此建议,并且,其补上了一句,重复提交异常的时候用throw;
四、 参考资料
1. Windows核心编程(Programming Applications for Microsoft Windows),第4版,Jeffrey Richter著,黄陇,李虎译,机械工业出版社
2. MSDN—Visual Studio 2005 附带版,Microsoft
3. 错误处理和异常处理,你用哪一个, apollolegend
4. 错误处理(Error-Handling):为何、何时、如何(rev#2), 刘未鹏
write by九天雁翎(JTianLing) – www.jtianling.com
阅读全文....
平时用igoogle,有个子曰的小widget,很有意思,会随机的抽出论语的某句话,一般没有什么问题,正常效果示意如图:

今天出现的问题就很恶搞了。。。。。我看着吓了一跳,孔子果然语出惊人,如图:

原来,孔子外语学的还不错。。。。。。我汗颜
阅读全文....
PyQt(2) 对话框
write by 九天雁翎(JTianLing) – www.jtianling.com**
**
讨论新闻组及文件
综述
对话框是比较简单的一种GUI形式,说其简单,不仅仅是处于开发的角度,从用户使用的角度来说也是如此,对话框可以说是一般图形界面程序的基本元素,即便是复杂的应用程序,往往也需要对话框来完成一些与用户交互的工程,并且,往往来说,创建基于对话框的程序也是比较简单的。也因为这样,在公司开发的众多工具当中,除了数据校验工具使用了MFC的文档视图(属于密集信息输出型),其他工具清一色的都是对话框,只不过,加了比较多的tab页,同样的强大。
最最简单的Win32汇编程序是一个MessageBox,这是对话框的一种简单形式,Windows中很多好用的公用对话框,也是极大的简化了开发。RAD工具的出现也几乎是为了对话框而设置的,当拖放控件的潮流出现后,很多人就开始设想下一步不用敲代码完成程序的编写了:)
基本上,开发对话框程序的难易,在一定程度上决定着一个GUI库的使用难易程度。下面介绍手动创建一个简单的对话框PyQt程序。
最简单形式的对话框:
这里就像是PyQt(1)中的例子quit.pyw一样,那个例子是一个单纯的按钮。。。。。。。。实际上又没有办法突然显示个按钮,所以Qt自动为其加上了一个边框,真正的程序应该是这样的:
simplestdialog.pyw
import sys
from PyQt4 import QtCore, QtGui
app = QtGui.QApplication(sys.argv)
dialog = QtGui.QDialog()
quit = QtGui.QPushButton("Quit",dialog)
QtCore.QObject.connect(quit, QtCore.SIGNAL("clicked()"),
app, QtCore.SLOT("quit()"))
dialog.show()
sys.exit(app.exec_())
这样两个程序其实差不多,但是仔细看还是有一定区别的。如图:

默认的Qt对话框没有最大化和最小化按钮,而自动为button生成的有,然后button那里button占据了整个用户区域,而对话框这里由其自动生成的大小并没有。
从面向过程到面向对象
到目前为止,PyQt(1)中所有的程序都还是用面向过程的方式。大概如PyQt(1)layout.pyw例子中的形式。
在面向过程到面向对象的比较上原来奇趣的一个教程有很有示范性的对比。(reference 3)
比如,面向过程是这样的形式:
#!/usr/bin/env python
# PyQt tutorial 3
import sys
from PyQt4 import QtCore, QtGui
app = QtGui.QApplication(sys.argv)
window = QtGui.QWidget()
window.resize(200, 120)
quit = QtGui.QPushButton("Quit", window)
quit.setFont(QtGui.QFont("Times", 18, QtGui.QFont.Bold))
quit.setGeometry(10, 40, 180, 40)
QtCore.QObject.connect(quit, QtCore.SIGNAL("clicked()"),
app, QtCore.SLOT("quit()"))
window.show()
sys.exit(app.exec_())
这是同样程序的面向对象版本:
#!/usr/bin/env python
# PyQt tutorial 4
import sys
from PyQt4 import QtCore, QtGui
**class** MyWidget(QtGui.QWidget):
**def** __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.setFixedSize(200, 120)
self.quit = QtGui.QPushButton("Quit", self)
self.quit.setGeometry(62, 40, 75, 30)
self.quit.setFont(QtGui.QFont("Times", 18, QtGui.QFont.Bold))
self.connect(self.quit, QtCore.SIGNAL("clicked()"),
QtGui.qApp, QtCore.SLOT("quit()"))
app = QtGui.QApplication(sys.argv)
widget = MyWidget()
widget.show()
sys.exit(app.exec_())
以上是两个几乎完全一样的程序,除了实现方式上的不同。
在实现上:
面向过程的方式大概的思路就像是生成一个默认的widget,然后通过函数调用,通过子widget按钮的创建去修饰这个widget,然后显示。
面向对象的方式大概思路就像是先描述我们想要的widget是有一个子widget按钮的,大小是多少。。。。。然后直接创建一个我们描述过的想要的widget,将其显示。
有人说,编程的方式取决于你看世界的方式-_-!其实这点用于对于面向过程和面向对象是比较合适的。
至于面向过程和面向对象的好处就不在这里费太多口舌了,也超出了本文的讨论范围。但是,即使是在这样简单的一个GUI程序中,不去讨论什么封装,数据隐藏等东西,也可以得出面向对象版本会更加容易复用一点。虽然N多书籍强调了使用面向对象技术最主要的原因不在于提高代码复用率,但是这一点确实是很有用并且最容易通过对比讲解的。比如,假如我们需要两个一样的这样的对话框显示不同标题,同时显示在不同位置的情况。
面向对象版本可以这样实现:
#!/usr/bin/env python
# PyQt tutorial 4 ext objWindow
import sys
from PyQt4 import QtCore, QtGui
**class** MyWidget(QtGui.QWidget):
**def** __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.setFixedSize(200, 120)
self.quit = QtGui.QPushButton("Quit", self)
self.quit.setGeometry(62, 40, 75, 30)
self.quit.setFont(QtGui.QFont("Times", 18, QtGui.QFont.Bold))
self.connect(self.quit, QtCore.SIGNAL("clicked()"),
QtGui.qApp, QtCore.SLOT("quit()"))
app = QtGui.QApplication(sys.argv)
widget = MyWidget()
widget.move(350,50)
widget.setWindowTitle("A Widget")
widget.show()
anotherWidget = MyWidget()
anotherWidget.move(50,50)
anotherWidget.setWindowTitle("Another Widget")
anotherWidget.show()
sys.exit(app.exec_())
尝试想象,面向过程的版本会怎么样?只能将所有的代码全部再复制一次,这面机械的操作我甚至都不想给出例子了。程序员的天性应该讨厌复制!
其实,这个面向对象版本的程序还有点问题,因为原来的程序中将对话框与应用程序混在一起了,所以连接按钮到qApp的退出槽上了,这样当你点击其中一个对话框的时候,两个对话框都关闭了。更加面向对象的做法是将其与对话框连接在一起。将那行代码改为:
self.connect(self.quit, QtCore.SIGNAL(“clicked()”),
self, QtCore.SLOT(“close()”))
这样就让两个对话框可以分开控制了,呵呵,关于类的职责问题一直是面向对象书籍讨论的重点,在这个简单的演示例子里面奇趣的教程也就没有考虑这么多了,所以才会出现这样的情况。
面向对象的对话框
前面的例子中,我们的widget是由QWidget继承而来,那么就是一个普通的widget,假如从dialog继承而来,那么就是一个dialog了,这样两者还是有一定区别的。
比如将上述的例子改成对话框。
import sys
from PyQt4 import QtCore, QtGui
**class** MyDialog(QtGui.QDialog):
**def** __init__(self, parent=None):
QtGui.QDialog.__init__(self, parent)
self.setFixedSize(200, 120)
self.quit = QtGui.QPushButton("Quit", self)
self.quit.setGeometry(62, 40, 75, 30)
self.quit.setFont(QtGui.QFont("Times", 18, QtGui.QFont.Bold))
self.connect(self.quit, QtCore.SIGNAL("clicked()"),
self, QtCore.SLOT("close()"))
app = QtGui.QApplication(sys.argv)
dialog = MyDialog()
dialog.show()
sys.exit(app.exec_())
显示的效果对比与前面的对比很相似:

这里我没有说明哪个是对话框,那个是widget,不过看过前面的对比也很明显了吧。这里有点要说明的是,在Qt中对话框与普通widget的区别与MFC中对话框与文档视图的区别要小多了,在上面的例子中我们也基本可以确定,当我们仅仅是想要显示一个button的时候,实际上Qt是为我们默认生成的是一个widget。
更复杂的对话框
《C++ GUI Qt4编程(第二版)》中第2章实现了一个比较复杂的对话框程序,我用Qt Creater尝试了其C++版本,好多代码要敲啊,所以反而丧失了再次敲代码去实现PyQt版本的意思。。。。。。。呵呵,不过,其实知道了大概流程和含义,看到C++版本的Qt程序,再实现一个PyQt版本的程序是非常省事的事情,都差不多。
不过,作为学习,我还是实现一个简化版本的PyQt对话框程序吧,不然private slots,Q_OBJECT,tr在PyQt中对应的东西我还真不知道。
complicateDialog.pyw
import sys
from PyQt4 import QtCore, QtGui
**class** MyDialog(QtGui.QDialog):
**def** __init__(self, parent=None):
QtGui.QDialog.__init__(self, parent)
self.quit = QtGui.QPushButton("Quit")
self.change = QtGui.QPushButton("Change")
self.change.setEnabled(False)
# funny widget
self.lcd = QtGui.QLCDNumber(2)
self.slider = QtGui.QSlider(QtCore.Qt.Horizontal)
self.slider.setRange(0, 99)
self.slider.setValue(0)
self.lineEdit = QtGui.QLineEdit()
self.connect(self.quit, QtCore.SIGNAL("clicked()"),
QtGui.qApp, QtCore.SLOT("quit()"))
self.connect(self.lineEdit, QtCore.SIGNAL("textChanged(const QString&)"),
self.enableChangeButton)
self.connect(self.slider, QtCore.SIGNAL("valueChanged(int)"),
self.SliderChange)
self.connect(self.change, QtCore.SIGNAL("clicked()"),
self.Change)
self.rightLayout = QtGui.QVBoxLayout()
self.rightLayout.addWidget(self.lineEdit)
self.rightLayout.addWidget(self.change)
self.leftLayout = QtGui.QVBoxLayout()
self.leftLayout.addWidget(self.lcd)
self.leftLayout.addWidget(self.slider)
self.layout = QtGui.QHBoxLayout()
self.layout.addWidget(self.quit)
self.layout.addLayout(self.leftLayout)
self.layout.addLayout(self.rightLayout)
self.setLayout(self.layout);
**def** enableChangeButton(self, text):
self.change.setEnabled(text.isEmpty() == False)
**def** Change(self):
value = int(self.lineEdit.text())
self.lcd.display(value)
self.slider.setValue(value)
**def** SliderChange(self):
value = self.slider.value()
self.lcd.display(value)
self.lineEdit.setText(str(value))
app = QtGui.QApplication(sys.argv)
dialog = MyDialog()
dialog.show()
sys.exit(app.exec_())
我曾经怎么说来着?来个简化版?-_-!最后写着写着,为了试验新特性,结果说是复杂版也不为过了。
效果如图:

这个程序演示了几个Qt的特性:
- 很有意思的lcd效果的数字,这种效果竟然原生在Qt的Widget中,很有意思。
-
其中演示了4个widget的交互,分别是lcd,slider,change,lineEdit其中三个是可以控制的,lcd被动显示,每改动一个,都能在其他三个(两个)中反映出来。控制的方式是通过connect某个singal到对话框类的某个方法上。其中我发现PyQt的connect消息方式很搞笑,为什么这样说呢?因为在Python中,竟然出现了”textChanged(const QString&)”这样的字符串-_-!再强大的东西,毕竟是通过C++来实现的。。。呵呵,估计非C++背景的PyQt使用者会感到这样的语法比较困惑吧。。。。。。我也比较奇怪,PyQt为啥不干脆一次性将这样的Singal的字符串也改成Python的语法呢?呵呵,也许工作量会更大吧。当然,slot响应的方式倒是简化了一些,当时我还在想,以什么形式将slot的参数用字符串表示出来呢。。。。结果发现,完全不用表示。。。。如源代码所示。这方面有个文章讲的比较详细:Connecting with signals and slots
- layout的嵌套,实现了比较复杂的dialog,并且,在代码中我没有实际的通过任何坐标来控制它们,它们是自动放在了我希望它们在的地方,在这一点上,Qt对手工编写代码的友好性胜过MFC又何止百倍啊。
但是,其实上述代码的问题还是有的,比如,lineEdit widget可以输入任何形式的字符,而不仅仅是数字,这在我们这个例子中应该是不允许的,在Qt中也提供了一种远胜MFC的限制方式,不用继承并实现一个自己的lineEdit widget就能实现非常 复杂的限制功能,这和STL中泛型的算法思维有点类似。这就是Qt 中的Validator,功能强大到你甚至可以很简单的就使用正则表达式去限制lineEdit。。。。呵呵,强大。。。这里我根据需要,使用QIntValidator就足够了。
在上例中加入如下代码:
intValidator = QtGui.QIntValidator(0,99, self)
self.lineEdit = QtGui.QLineEdit()
self.lineEdit.setValidator(intValidator)
加在那个位置就不用我教了吧,就是定义self.lineEdit的上下位置。再试试效果,除了0,99的整数,什么都输不进入了。
题外话:
在进入使用designer之前,我决定先看看一些关于PyQt的资料,(以前就是看 reference1那本书。单纯这样从C++转换到Python,光靠自己的理解还是难免会有些问题,虽然摸索着上面那样的程序也能写出来:)。但是,就我的经验来说,在使用图形化的设计工具前不将手工代码的原理都弄清楚,以后可能会不再想回过头来彻底弄清楚了,(因为图形化设计会简单的多),但是,碰到稍微有点例外的情况,却会手足无措(使用MFC的经验)。
reference
- C++ GUI Qt4编程(第二版) C++ GUI Programming with Qt4, Second Edition。Jasmin Blanchette【加拿大】,Mark Summerfield【英】著,闫锋欣,曾泉人,张志强译,电子工业出版社。
- GUI Programming with Python: Qt Edition
- Qt tutorial
- PyQt GPL v4.4.4 for Python v2.6 examples from riverbankcomputing
write by 九天雁翎(JTianLing) – www.jtianling.com
阅读全文....
PyQt的学习(1) 入门
write by 九天雁翎(JTianLing) – www.jtianling.com**
**
讨论新闻组及文件
动机:
刚学习完Python,借鉴以前学习C++时的经验,当时用MFC巩固了C++的学习,并且可以做出实际的程序,给了我继续学习的动力,学习Qt也是出于类似的目的。
为什么我要写,为了学习。不仅仅为了将来有个可以用的,好用的跨平台GUI库,也为了用Python快速开发时也能有个GUI库,但是又不喜欢TK,那么,综合考虑,Qt也就是不错的考虑了。
综述
这些文章基本遵循《C++ GUI Qt 4编程》第2版(C++ GUI Programming with Qt4, Second Edition)的流程,文字可能会很少,除非自己想出的例子,不然例子就是书中C++例子的Python版本,**基本上,可以以此书为蓝本对照这些文章中的例子来看。
**
虽然用途不算太大,但是考虑到PyQt的好教材是如此的稀少,这样也不算完全没有价值吧,毕竟还有只明白Python不懂C++的人。
这里有一本PyQt的书《GUI Programming with Python: Qt Edition》,但是老到基本上是属于Qt2时代的东西了。
至于PyQt 嘛,你不会不知道到google上搜索一下,取得其下载地址吧?安装方式如此简单,点击下一步以后,就可以在Python中通过import PyQt4来使用这个库了。
HelloWorld
自从K&R;后,好像所有的程序语言讲解都是以HelloWorld开始,我们也不免俗吧,我们从HelloQT开始。并且,这个例子也可以作为一个测试程序,测试一下看看安装PyQt是否成功。
HelloQt.pyw
1 import sys
2 from PyQt4 import QtGui
3
4
5 app = QtGui.QApplication(sys.argv)
6 label = QtGui.QLabel("Hello Qt!")
7 label.show()
8
9 sys.exit(app.exec_())
这样,就是一个现实”Hello Qt”的窗口,利用了QLabel部件。–说明一下,Qt中将MFC中常常称作控件(control)的东西称作部件(Widget),其实一回事。
这里也可以看出Qt的足够简洁,我很欣赏,而其对象使用的风格和方式也是比较好的,不然,尝试用VS生成一个类似的MFC程序试试?^^
其实这里还不足以见证Qt的强大,看下面的例子,QLabel竟然至此HTML样式。。。。。-_-!
MoreHelloQt.pyw
1 import sys
2 from PyQt4 import QtGui
3
4
5 app = QtGui.QApplication(sys.argv)
6 label = QtGui.QLabel("
## _Hello_ QT!
") #这里我没有办法让CSDN不将其解释为HTML。。。所以,参考界面的源代码看看是什么吧
7 label.show()
8
9 sys.exit(app.exec_())
强大吧:)
Qt(C++) VS PyQt
这里,顺便比较一下PyQt与普通Qt(C++)生成程序的区别。一般而言,两者速度没有可比性,但是,速度在这里不是主要问题,原因在于PyQt的核心也就是Qt库,那是用C++写的,这样,一般而言不会占用太多时间的逻辑代码速度慢点,不会成为瓶颈,使得PyQt的程序可以跑得足够的快。但是,使用方式上,却没有失去Python的优雅语法,快速开发的能力,也结合了Qt的强大,呵呵,广告用语。。。。。。。。。。。来点实在的。
左边的是用C++开发出来的Qt程序,右边是PyQt开发出来的程序,由于都是使用了同一个库,看不出两者的区别。基本上,多懂了一种语言,就多了一种选择。。。比如说这个时候的Python。
建立连接
quit.pyw
1 import sys
2 from PyQt4 import QtCore, QtGui
3
4 app = QtGui.QApplication(sys.argv)
5
6 quit = QtGui.QPushButton("Quit")
7
8 QtCore.QObject.connect(quit, QtCore.SIGNAL("clicked()"),
9 app, QtCore.SLOT("quit()"))
10
11 quit.show()
12 sys.exit(app.exec_())
这里展示了Qt的信槽模式,有点怪异的是,在Python中,信号,槽,都是用字符串来表示-_-!这点似乎有点奇怪。
我还不懂Qt的原理,也没有看过Qt的源代码,但是总是感觉这里奇怪,于是我翻看了一下Qt的SIGNAL,SLOT宏,于是一切也就没有那么奇怪了。
# define SLOT(a) qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a) qFlagLocation("2"#a QLOCATION)
本来,他们就是以字符串来表示的。。。。。。。。我原本还以为在Qt中这些都是通过回调函数的形式出现的呢。。。。。。。汗-_-!真那样,与一般的WxWidget原理有啥不同啊。。。呵呵,看来,Qt的原理还得好好理解理解。其实就现在的信息看,无非也就是Observer模式的一种扩展,再难也难不到哪儿去。
在PyQt的安装包中,有个tutorial,展示了更复杂一点的button使用方法,可以参考参考
morequit.pyw
1 import sys
2 from PyQt4 import QtCore, QtGui
3
4
5 app = QtGui.QApplication(sys.argv)
6
7 quit = QtGui.QPushButton("Quit")
8 quit.resize(75, 30)
9 quit.setFont(QtGui.QFont("Times", 18, QtGui.QFont.Bold))
10
11 QtCore.QObject.connect(quit, QtCore.SIGNAL("clicked()"),
12 app, QtCore.SLOT("quit()"))
13
14 quit.show()
15 sys.exit(app.exec_())
分别是设置Button的大小和按钮文字的字体和样式,这个阶段就不深抠细节了,了解概念和Qt大概的风格就好。
窗口部件的布局
用过MFC的人都知道在MFC中创建一个随窗口动态改变控件大小的程序的困难。。。。。。需要每次OnMove,OnSize的时候去重新计算控件的大小,为了保持布局合理,最好还得将所有控件的位置用百分比来计算,痛苦不言而喻,可以参考我写的正则表达式测试程序0.3版(要么就是更老的版本)。但是在Qt中好像就要简单的比较多。看看示例:
layout.pyw
1 import sys
2 from PyQt4 import QtCore, QtGui
3
4 app = QtGui.QApplication(sys.argv)
5
6 window = QtGui.QWidget()
7
8 spinBox = QtGui.QSpinBox()
9 slider = QtGui.QSlider(QtCore.Qt.Horizontal)
10 spinBox.setRange(0, 130)
11 slider.setRange(0, 130)
12
13 QtCore.QObject.connect(spinBox, QtCore.SIGNAL("valueChanged(int)"),
14 slider, QtCore.SLOT("setValue(int)"))
15 QtCore.QObject.connect(slider, QtCore.SIGNAL("valueChanged(int)"),
16 spinBox, QtCore.SLOT("setValue(int)"))
17 spinBox.setValue(35)
18
19 layout = QtGui.QHBoxLayout()
20 layout.addWidget(spinBox)
21 layout.addWidget(slider)
22 window.setLayout(layout)
23
24 window.show()
25 sys.exit(app.exec_())
行了,基本上PyQt的程序是怎么样子的,大概有个了解了:),下面就开始慢慢来了。我也需要睡觉去了。
write by 九天雁翎(JTianLing) – www.jtianling.com
阅读全文....
通过纯静态分析来还原算法,获取《加密与解密》第2章的TraceMe的注册机
write by 九天雁翎(JTianLing) – www.jtianling.com
其实原书有源代码,并且也介绍了一种通过内存断点来获取注册码的方法。但是这里我是为了检验今天看静态分析这一章的成果,纯粹靠IDA Pro静态分析不带调试信息Release版本,也不动态调试的方法来分析算法。并还原算法,写出注册机。这里并没有鼓励或提倡以此方式来分析程序的意思。呵呵,书中好像也鼓励大家试试写出一个注册机来,作为购买了正版书籍的读者,自然要响应作者号召了。
另外,我虽然最近老是发一些奇怪的文章,但是我只是个C++程序员,是以开发游戏为生的,最近研究这些东西并不是准备将来以此牟利,仅仅是为了学习,最近的工作是反外挂,所以才学习这些东西。这方面我是纯粹的新手,发点文章属于记录学习过程和个人爱好,有不对或者稚嫩的地方请高手指教。
这里仅仅只看我注释的片段,全部源代码即完整程序请去下载《加密与解密》(第3版)的配套光盘第2章部分,或者购买原书。
关键函数如下:调用环境其实可知lpString1为用户输入的key,lpString2为用户输入的用户名,aiLen为长度
.text:00401340 ; int __cdecl KeyGenAndCompare(LPCSTR lpString1, LPSTR lpString2, int aiLen)
.text:00401340 KeyGenAndCompare proc near ; CODE XREF: DialogFunc+115p
.text:00401340
.text:00401340 lpString1 = dword ptr 4
.text:00401340 lpString2 = dword ptr 8
.text:00401340 aiLen = dword ptr 0Ch
.text:00401340
.text:00401340 000 push ebp
.text:00401341 004 mov ebp, [esp+4+lpString2]
.text:00401345 004 push esi
.text:00401346 008 push edi
.text:00401347 00C mov edi, [esp+0Ch+aiLen] ; 总是通过esp来操作,有点混乱,其实就是将长度保存到edi
.text:0040134B 00C mov ecx, 3
.text:00401350 00C xor esi, esi ; 清空esi
.text:00401352 00C xor eax, eax ; 清空eax
.text:00401354 00C cmp edi, ecx ; Compare Two Operands
.text:00401356 00C jle short loc_401379 ; 判断当长度小于等于3时,不进行计算了,因为肯定是错的
.text:00401358 00C push ebx
.text:00401359
.text:00401359 KeyGenLoop: ; CODE XREF: KeyGenAndCompare+36j
.text:00401359 010 cmp eax, 7 ; three below: if ( eax > 7 ) eax = 0;
.text:0040135C 010 jle short loc_401360 ; Jump if Less or Equal (ZF=1 | SF!=OF)
.text:0040135E 010 xor eax, eax ; Logical Exclusive OR
.text:00401360
.text:00401360 loc_401360: ; CODE XREF: KeyGenAndCompare+1Cj
.text:00401360 010 xor edx, edx ; Logical Exclusive OR
.text:00401362 010 xor ebx, ebx ; Logical Exclusive OR
.text:00401364 010 mov dl, [ecx+ebp] ; 这里可以知道是从第3个元素才开始比较的
.text:00401367 010 mov bl, gacCode[eax] ; 这里用eax从0增长,等于7时清零来循环遍历一个数组
.text:0040136D 010 imul edx, ebx ; Signed Multiply
.text:00401370 010 add esi, edx ; Add
.text:00401372 010 inc ecx ; Increment by 1
.text:00401373 010 inc eax ; Increment by 1
.text:00401374 010 cmp ecx, edi ; Compare Two Operands
.text:00401376 010 jl short KeyGenLoop ; Jump if Less (SF!=OF)
.text:00401378 010 pop ebx
.text:00401379
.text:00401379 loc_401379: ; CODE XREF: KeyGenAndCompare+16j
.text:00401379 00C push esi
.text:0040137A 010 push offset aLd ; "%ld"
.text:0040137F 014 push ebp ; LPSTR
.text:00401380 018 call ds:wsprintfA ; Indirect Call Near Procedure
.text:00401386 018 mov eax, [esp+18h+lpString1]
.text:0040138A 018 add esp, 0Ch ; Add
.text:0040138D 00C push ebp ; lpString2
.text:0040138E 010 push eax ; lpString1
.text:0040138F 014 call ds:lstrcmpA ; Indirect Call Near Procedure
.text:00401395 00C neg eax ; Two's Complement Negation
.text:00401397 00C sbb eax, eax ; Integer Subtraction with Borrow
.text:00401399 00C pop edi
.text:0040139A 008 pop esi
.text:0040139B 004 inc eax ; Increment by 1
.text:0040139C 004 pop ebp
.text:0040139D 000 retn ; Return Near from Procedure
.text:0040139D KeyGenAndCompare endp
还原的算法源代码如下:
1 #include
2 #include
3 **int** gacCode[] = {0x0c, 0x0A, 0x13, 0x9, 0x0c, 0x0b, 0x0a, 0x08};
4
5 **int** __cdecl KeyGen(**char** * lpName, **int** aiLen)
6 {
7 **int** liTemp = 0; // means esi
8 **int** i = 3; //just because using VC6,know 3 from ecx == 3
9 **int** j = 0;
10 **for**(; i
11 {
12 **if**(j > 7)
13 {
14 j = 0;
15 }
16 liTemp += lpName[i] * gacCode[j];
17 }
18
19 **return** liTemp;
20 }
21
22
23 **int** main()
24 {
25 **char** lszName[] = "abcdefg";
26 **int** liKey = KeyGen(lszName, strlen(lszName));
27
28 printf("%i/n",liKey);
29 **return** 0;
30 }
31
write by 九天雁翎(JTianLing) – www.jtianling.com
阅读全文....
程序员手中的利器(2)–文本编辑工具
write by 九天雁翎(JTianLing) – www.jtianling.com
不管有多少人告诉过你vim简直就不是设计给人用的,不管你曾经抱着怀疑的态度试过多少次并且放弃,我还是得说,vim绝对值得花时间去学习!并且,按某些人的原话,当你真的熟悉了vim的思维后,没有vim你甚至都觉得完全不自在!的确是这样!
首先得说一些其他理由,作为程序员,不说做的最多的,但是很经常需要做的可能就是输入代码了,输入代码时一个好的编辑器是非常需要的!这是节省你生命的最佳方式。
并且,Vim还有它随处可用的特性,不仅Windows下有GVim可用,X下也有GVim可用,并且就算是一个Linux服务器,没有安装图形界面,一样有vim可以用,哪怕它是最小安装,你起码都能有个vi可以用!这些都不是EditPlus,UE等工具可比的。还有更夸张的事情!因为Vim是如此的好用,导致没有Vim很多程序员都会觉得不自然,活不下去!所以,很多程序员都试图将Vim变得在任何时候都可以用!比如Visual Studio中有VimEmu插件,Eclipse中有VimPlugin和viPlugin,使得你不脱离你熟悉的IDE开发环境,你一样可以使用Vim。一次学习,随时随地的使用到其强大的功能,除了Vim我不知道还有哪个工具可以!最后Vim的语法着色支持的语言很多,也是的你在进行各种语言的时候都可以依赖它,比如我无论写lua,bash,python,C/C++的时候,我都只是使用vim。
至于Vim的模式编辑方式,和众多好用的快捷键,我这里就不想多讲了,这些讲多了也没有用,只能等学习的人自己去熟悉去体会,我只想说,学习Vim投入的时间将远远少于它为你节省的生命。
很多人对工具很不屑,总是说无论什么工具都能写好的代码,这里我得说,我从来不认为这些工具能够使你写的代码的更好,但是,实实在在的节省的是打字时间,也就是你的生命。学习这些不是为了炫耀什么,仅仅是不想把时间都浪费在打字上面,剩余的时间我可以用来想怎么把代码写好……….
这里引用
http://www.wanglianghome.org/blog/2006/04/emacs-show-motivation.html
一文中的一个观点,虽然此文是支持Emacs的,但是动机是一致的,而且文中对于编辑器应该有的功能的分析我也是比较赞同。但是因为我并不是纯GNU环境的程序员,所以一直没有需要转到Emacs的使用上去。文中提到:
“记得当初关于”软件蓝领”的争论焦点就是写程序是体力活还是脑力活,是否仅仅等于敲敲键盘。虽然到头来双方仍是自说自话,但似乎都同意”敲键盘是体力活”。”
既然如此,为什么我们不用一个好的工具来使这个活更轻松一些呢?
既然谈到了Emacs。。。。。m我想说说两个有意思的事情,这也应该属于我平时比较感兴趣的程序员文化。
- 有人说:世界上的程序员分三种,一种使用Emacs,一种使用vim,剩余的是其它。
- Emacs使用者说Vim的模式导致编辑的时候都不知道自己在什么模式。Vim使用者说,Emacs的组合快捷键如此复杂,以至于使用者都希望能有个脚踏板。。。。。
其实,一旦在讲Vim的时候谈到了Emacs都会让我觉得很有意思,有人说这是一个伴随着计算机发展的圣战。。。。。。
可以参看:
http://blog.csdn.net/oyd/archive/2007/02/16/1511128.aspx
这里面也有一个很有意思的对白:
Greg Menke发出了第一个置顶帖。使用vi?那该多么麻烦呀?他把自己的步骤贴了出来:
1.获得一条任意品种的鱼,长度大于12英寸就行
2.冰冻一晚上
3.把它放到电脑前,运行vi
4.抓住鱼的尾巴不停的往你头上砸,直到你决定用EMACS。因为鱼鳞在你周围飞溅并且你的头开始痛
Menke,当然,他是在暗示,顽固的不仅是vi,vi的用户更甚,他们顽固到拒绝承认他们虐待自己,就像拿冰冻的鱼砸自己的头一样。
没过多久,vi的拥护者们开始回击,其中一个建议Menke把这一行加到他的步骤中去:在第1步之前键入emacs,这样在第4步结束时emacs才可能加载完成。
他是在说EMACS编辑器慢吗?
最后,Emacs是一个附带有文本编辑功能的操作系统的话也很出名:)
Ultra Edit(UE)也是个不错的文本编辑工具,也有列编辑功能(vim也有),也有16进制编辑功能,一开始我都是用UE来解决很多问题。但是当我习惯了vim以后,UE很多时候仅仅作为一个功能强大点的记事本了。
write by 九天雁翎(JTianLing) – www.jtianling.com
阅读全文....
多鼠标是博客中发表文章中大家最感兴趣并且联系我最多的一个内容,因为国内的资料相对较少,并且当时的确为了写论文花了一定的时间进行研究,(一年前的事情了,其实现在忘的差不多了)但是今天发现一个很有意思的事情(见下表)。。。。。。呵呵,英国有个兄弟缺少多鼠标的资料。。。。甚至利用google的翻译功能,将我的文章翻译成E文观看。。。。可怜的人哪。。。。不过这方面国内的资料少还可以理解,国外的资料其实还是有一些的,至于这样吗?-_-!呵呵,感觉比较有意思,我们痛苦的读者E文的资料的时候。。。。还有同样痛苦的兄弟啊。。。。
基本信息 | 这是此用户第 1 次访问九天雁翎博客, [ 查看他浏览的所有网页 ]
英国, 英语 - 美国 (en-us), 位于1 时区
客户端信息 | Windows XP, MSIE 7.0, 1280×800, 232
色, 未装 Alexa 工具条
来路 | http://translate.google.com/translate?hl=en&sl;=zh-CN&u;=http://blog.csd
他查看的详细路径,懂E文的兄弟也可以看看google的机器翻译效果。。。。也可以体会一下此英国兄弟的感受-_-!
http://translate.google.com/translate?hl=en&sl;=zh-CN&u;=http://www.jtianling.com/archive/2008/04/23/2316676.aspx&ei;=g-kxSp2lE4KQjAf92_mXCg&sa;=X&oi;=translate&resnum;=8&ct;=result&prev;=/search%3Fq%3DRegisterRawInputDevices%2Bvb%26hl%3Den%26sa%3DN%26start%3
阅读全文....
在VS中debug时,将未初始化变量都赋值为CC的顿悟
write by 九天雁翎(JTianLing) – www.jtianling.com**
**
讨论新闻组及文件
一直以来,我都不是太理解这种方式,在 C++函数调用原理理解中,我仅仅是简单的认为,那么做,可能是因为CC平时用的少,而且好看:)所以初始化这样一个不怎么常用的变量,可以让人很快发现。。。。事实上,的确有这样的效果,当Debug时,我看一个变量为CC时的确第一时间就能反应过来,我又犯了一个不可饶恕的低级错误,又忘了初始化了,这点在变量为指针类型的时候更加严重。
但是,在学习过反汇编这么久后,今天在看《C缺陷与陷阱》时,突然顿悟了CC的意义。。。。。至于为什么是看这本和这件事完全没有关系的时候突然想到,我也不知道,反正就是那样发生了。
CC在汇编代码中表示为int 3,实际表示一个中断,在与硬件中断(CPU中加入的DR寄存器指示)做区别的时候也叫软中断。。。。几乎所有的调试工具在调试时,都是靠int 3来完成任务的。。。。。。这些我知道时间不短了。。。。但是今天才将其与VS在debug时的初始化联系起来。。。。。这样的话,假如有异常的跳转,程序运行到了不应该运行的地方。。。。那么,就会触发中断,让调试程序获得控制,这样可以更早的发现问题,而不是当运行了一堆莫名其妙的代码后才出现问题。。。。。。
至于VS在debug时的初始化,可以用debug方式编译任何程序,你都能看到
比如在C++函数调用原理理解例子中如下:
0041136C lea edi,[ebp-0C0h] ;读入[ebp-0C0h]有效地址,即原esp-0C0h,正好是为该函数留出的临时存储区的最低位
00411372 mov ecx,30h ;ecx = 30h(48),30h*4 = 0C0h
00411377 mov eax,0CCCCCCCCh ;eax = 0CCCCCCCCh;
0041137C rep stos dword ptr es:[edi] ;重复在es:[edi]存入30个;0CCCCCCCCh? Debug模式下把Stack上的变量初始化为0xcc,检查未初始化的问题
write by 九天雁翎(JTianLing) – www.jtianling.com
阅读全文....
在C++中内嵌汇编代码分析
write by九天雁翎(JTianLing) – www.jtianling.com
用JAVA或者Python的人常常会告诉你,现在的硬件已经太快了,以至于你可以完全不再考虑性能,快速的开发才是最最重要的。这是个速食者的年代,什么都是,甚至是编程。
但是。。。。。。永远有但是,任何C/C++程序员在工作一段时间后都会发现,不关心性能的C/C++程序员是干不下去的。。。。。C语言的程序员犹甚,C++的程序员也许还可以光靠着MFC等类库混口饭吃。。。
C/C++程序员常常会发现自己只有有限的内存,要占用更小的CPU资源(或者仅仅只有速度非常有限的嵌入式CPU可用),但是却得完成非常复杂的任务,这时候能够利用的工具却非常有限。。。唯一可能有用的就是自己的大脑。。。呵呵,改进算法永远是最重要的,但是一个非常优秀的算法也没有办法满足要求的时候,你再剩下的东西可能就是无限的优化原有的C/C++代码,接着就是汇编了。
这里从内嵌汇编将起,然后讲讲用MASM独立编译,然后链接的方法,都以VS2005为例子,其他编译器请参考。
内嵌汇编:
内嵌汇编的使用是很简单并且方便的,在VS中以的形式就可以简单的使用。对于此语法我没有必要再多说了,各类资料都有介绍,可以参考参考《加密与解密》(第3版)附录2.这里想举几个的函数的调用例子来说明一下,因为在函数调用时需要特别注意调用的约定,各类调用约定有不同的规矩需要留意,因为一旦使用了汇编,出现问题没有东西可以保护你,唯一可以看到的就是程序崩溃。
对于各类调用约定也可以参考《加密与解密》(第3版),其中的分类还比较详细。
我在《 反汇编时的函数识别及各函数调用约定的汇编代码分析》,《 C++中通过指针,引用方式做返回值的汇编代码分析》中也有一些实例的分析,但是不能代替完整的说明,仅供参考。
http://www.jtianling.com/archive/2009/01/20/3844768.aspx
http://www.jtianling.com/archive/2009/01/20/3844520.aspx
以下形式就是最方便的内嵌汇编用法:
例1:
// 仅仅返回参数
int __cdecl GetArgument(int ai)
{
int li = 0;
__asm
{
mov eax, ai;
mov li, eax
}
return li;
}
以上程序就是一个仅仅返回参数的函数,用__cdecl调用约定,需要注意的是,mov li, ai的形式是不允许的。。。。与在普通汇编中不允许从内存mov到内存的限制一致。
其实上面的例子中,程序可以优化为如下形式:
例2:
// 仅仅返回参数
int __cdecl GetArgument(int ai)
{
__asm
{
mov eax, ai
}
}
在函数中没有指定返回值时,eax就是返回值了。。。这和普通的汇编代码一致。。。。顺便看看生成的代码:
printf("%d/n",GetArgument(100));
0041142E push 64h
00411430 call GetArgument (41100Ah)
00411435 add esp,4
足够人性化的,栈由编译器自动的平衡了,不需要我们额外操心,这是很愉快的事情。
再看例三:
// 仅仅返回参数
int __stdcall GetArgument(int ai)
{
__asm
{
mov eax, ai
}
}
这次改成了__stdcall,那么编译器还会帮我们吗?答案还是肯定的。所以也可以放心用。
调用时:
0041142E push 64h
00411430 call GetArgument (4111DBh)
00411435 mov esi,esp
原函数:
// 仅仅返回参数
int __stdcall GetArgument(int ai)
{
004113C0 push ebp
004113C1 mov ebp,esp
004113C3 sub esp,0C0h
004113C9 push ebx
004113CA push esi
004113CB push edi
004113CC lea edi,[ebp-0C0h]
004113D2 mov ecx,30h
004113D7 mov eax,0CCCCCCCCh
004113DC rep stos dword ptr es:[edi]
__asm
{
mov eax, ai
004113DE mov eax,dword ptr [ai]
}
}
004113E1 pop edi
004113E2 pop esi
004113E3 pop ebx
004113E4 add esp,0C0h
004113EA cmp ebp,esp
004113EC call @ILT+330(__RTC_CheckEsp) (41114Fh)
004113F1 mov esp,ebp
004113F3 pop ebp
004113F4 ret 4
其实只关心外部调用者没有维持栈平衡,而函数内部最后的ret 4就够了,完全符合__stdcall的定义。
再接下来呢?thiscall?
int GetArgument(int ai)
{
00411450 push ebp
00411451 mov ebp,esp
00411453 sub esp,0CCh
00411459 push ebx
0041145A push esi
0041145B push edi
0041145C push ecx
0041145D lea edi,[ebp-0CCh]
00411463 mov ecx,33h
00411468 mov eax,0CCCCCCCCh
0041146D rep stos dword ptr es:[edi]
0041146F pop ecx
00411470 mov dword ptr [ebp-8],ecx
__asm
{
mov eax, ai
00411473 mov eax,dword ptr [ai]
add eax, [ecx]
00411476 add eax,dword ptr [ecx]
}
}
00411478 pop edi
00411479 pop esi
0041147A pop ebx
0041147B add esp,0CCh
00411481 cmp ebp,esp
00411483 call @ILT+330(__RTC_CheckEsp) (41114Fh)
00411488 mov esp,ebp
0041148A pop ebp
0041148B ret 4
最后的结果也是正确的,也符合ecx为this指针,并且函数内部维护栈的约定,这个例子也许得将测试程序也贴出来:
int _tmain(int argc, _TCHAR* argv[])
{
CTestThisCall lo(20);
printf("%d/n",lo.GetArgument(10));
system("PAUSE");
return 0;
}
得出的结果是30,结果是正确的。
然后呢?fastcall?
见例4:
int __fastcall GetArgument(int ai)
{
__asm
{
mov eax, ecx
}
}
int _tmain(int argc, _TCHAR* argv[])
{
printf("%d/n",GetArgument(10));
system("PAUSE");
return 0;
}
结果也是正确的,fastcall的规则在VC中是已知的,但是在很多编译器中实现并不一样,所以很多书籍推荐最好不要使用fastcall来内嵌汇编,因为你不知道到底fastcall到底会使用哪些寄存器来传递参数。
MSDN中有如下描述:
In general, you should not assume that a register will have a given value when an __asm block begins. Register values are not guaranteed to be preserved across separate __asm blocks. If you end a block of inline code and begin another, you cannot rely on the registers in the second block to retain their values from the first block. An __asm block inherits whatever register values result from the normal flow of control.
If you use the __fastcall calling convention, the compiler passes function arguments in registers instead of on the stack. This can create problems in functions with __asm blocks because a function has no way to tell which parameter is in which register. If the function happens to receive a parameter in EAX and immediately stores something else in EAX, the original parameter is lost. In addition, you must preserve the ECX register in any function declared with __fastcall.
To avoid such register conflicts, don’t use the __fastcall convention for functions that contain an __asm block. If you specify the __fastcall convention globally with the /Gr compiler option, declare every function containing an __asm block with __cdecl or __stdcall. (The __cdecl attribute tells the compiler to use the C calling convention for that function.) If you are not compiling with /Gr, avoid declaring the function with the __fastcall attribute.
在《加密与解密》中有适当的翻译。。。。我就不翻译了,应该能看懂吧。其实大意就是最好别用fastcall,原因主要是不知道哪个寄存器用来传递参数,假如一个函数中你需要使用ecx一定要记得将其还原。。。呵呵,这个道理很简单,因为一个参数的时候fastcall必定是由ecx来传递参数的。两个参数就是ecx,edx,起码在VS2005中是这样。虽然个人没有感觉用起来有多大的风险,但是文中甚至提及可能通过eax传递参数。。(应该是在优化的时候),所以假如真的用了fastcall,请看汇编代码确认一下,以防万一。
再见一个更详细的例5:
int __fastcall GetArgument(int ai1, int ai2, int ai3)
{
__asm
{
mov eax, ecx
add eax, edx
add eax, [ebp+8]
}
}
int _tmain(int argc, _TCHAR* argv[])
{
printf("%d/n",GetArgument(10, 20, 30));
system("PAUSE");
return 0;
}
反汇编:
printf("%d/n",GetArgument(10, 20, 30));
00411ACE push 1Eh
00411AD0 mov edx,14h
00411AD5 mov ecx,0Ah
00411ADA call GetArgument (4111F4h)
int __fastcall GetArgument(int ai1, int ai2, int ai3)
{
004130B0 push ebp
004130B1 mov ebp,esp
004130B3 sub esp,0D8h
004130B9 push ebx
004130BA push esi
004130BB push edi
004130BC push ecx
004130BD lea edi,[ebp-0D8h]
004130C3 mov ecx,36h
004130C8 mov eax,0CCCCCCCCh
004130CD rep stos dword ptr es:[edi]
004130CF pop ecx
004130D0 mov dword ptr [ebp-14h],edx
004130D3 mov dword ptr [ebp-8],ecx
__asm
{
mov eax, ecx
004130D6 mov eax,ecx
add eax, edx
004130D8 add eax,edx
add eax, [ebp+8]
004130DA add eax,dword ptr [ai3]
}
}
004130DD pop edi
004130DE pop esi
004130DF pop ebx
004130E0 add esp,0D8h
004130E6 cmp ebp,esp
004130E8 call @ILT+330(__RTC_CheckEsp) (41114Fh)
004130ED mov esp,ebp
004130EF pop ebp
004130F0 ret 4
说实话,ebp+8的计算我原来是搞错了,我当时直接用了esp+8,但是后来才发现在debug没有优化的时候,此处是会采用先保存esp,然后再用ebp的标准用法的。这里没有任何可健壮和移植性可言,仅仅是为了说明thiscall其实也是有规则并且符合约定的,比如上述程序在VS2005最大速度优化时也能正确运行,反汇编代码如下:
int _tmain(int argc, _TCHAR* argv[])
{
printf("%d/n",GetArgument(10, 20, 30));
00401010 mov edx,14h
00401015 push 1Eh
00401017 lea ecx,[edx-0Ah] ;注意此处,是编译器相当强程序优化的 成果,用此句替代了mov ecx,0Ah,原因说白了就很简单,因为此句才3个字节,而用mov的需要5个字节。。。。。
0040101A call GetArgument (401000h)
0040101F push eax
00401020 push offset string "%d/n" (4020F4h)
00401025 call dword ptr [__imp__printf (4020A4h)]
system("PAUSE");
0040102B push offset string "PAUSE" (4020F8h)
00401030 call dword ptr [__imp__system (40209Ch)]
00401036 add esp,0Ch
return 0;
00401039 xor eax,eax
}
int __fastcall GetArgument(int ai1, int ai2, int ai3)
{
00401000 55 push ebp
00401001 8B EC mov ebp,esp
__asm
{
mov eax, ecx
00401003 8B C1 mov eax,ecx
add eax, edx
00401005 03 C2 add eax,edx
add eax, [ebp+8]
00401007 03 45 08 add eax,dword ptr [ai3]
}
}
0040100A 5D pop ebp
0040100B C2 04 00 ret 4
最后,在VS2005中还有一个专门为内嵌汇编准备的函数调用方式,naked。。。裸露的调用方式?-_-!呵呵,其实应该表示是无保护的。
例6:
int __declspec(naked) __stdcall GetArgument(int ai)
{
__asm
{
mov eax, [esp+04H]
ret 4
}
}
int _tmain(int argc, _TCHAR* argv[])
{
printf("%d/n",GetArgument(10));
system("PAUSE");
return 0;
}
反汇编代码:
int _tmain(int argc, _TCHAR* argv[])
{
printf("%d/n",GetArgument(10));
00401010 push 0Ah
00401012 call GetArgument (401000h)
00401017 push eax
00401018 push offset string "%d/n" (4020F4h)
0040101D call dword ptr [__imp__printf (4020A4h)]
system("PAUSE");
00401023 push offset string "PAUSE" (4020F8h)
00401028 call dword ptr [__imp__system (40209Ch)]
0040102E add esp,0Ch
return 0;
00401031 xor eax,eax
}
int __declspec(naked) __stdcall GetArgument(int ai)
{
__asm
{
mov eax, [esp+04H]
00401000 mov eax,dword ptr [esp+4]
ret 4
00401004 ret 4
}
}
哈哈,在debug下naked函数都是如此简洁,因为我们对其有完完全全的控制,非常完全!以至于。。。在这里你想用mov eax,ai的形式都是不可以的。。。只能完全遵循普通汇编的规则来走。
但是无论此函数再怎么naked,VS也不是完全不管的,见下例7:
int __declspec(naked) __cdecl GetArgument(int ai)
{
__asm
{
mov eax, [esp+04H]
ret
}
}
int _tmain(int argc, _TCHAR* argv[])
{
printf("%d/n",GetArgument(10));
system("PAUSE");
return 0;
}
假如VS完全不管GetArgument函数,因为GetArgument函数内部无法维护栈平衡,那么程序崩溃是必然的,还好VS管理了这个问题:
printf("%d/n",GetArgument(10));
00411ACE push 0Ah
00411AD0 call GetArgument (4111FEh)
00411AD5 add esp,4
这里需要说明的是,当一个函数naked的时候,完全的指挥权都在程序员手中,包括栈的平衡,函数的返回都需要程序员来做,ret是必不可少的,这里想说明的是,一旦你使用了内嵌汇编,你就开始走在了悬崖边上,因为C++强健的编译时期类型检查完全帮助不了你,能够帮助你的仅仅是你的大脑和你对汇编的理解了。呵呵,既然是用汇编写代码,其实都没有反汇编的概念可言,源代码就在哪儿,看仔细了debug比什么都重要,ollydbg,softICE,windbg你爱用什么就用什么吧。
另外,当函数不是naked的时候,从理论上讲你也是可以在汇编中ret的,只不过,你冒的风险也更大了,因为VS好像还不够智能化,它无法预见你的ret,或者从设计上来讲,不是naked的时候你不应该自己处理ret的,所以,即使是你在内嵌汇编中已经有了ret,它还是会原封不动的用于函数返回的代码。。。。
见下例8:
int __cdecl GetArgument(int ai)
{
__asm
{
mov eax, ai
mov esp,ebp
pop ecx
ret
}
}
int _tmain(int argc, _TCHAR* argv[])
{
printf("%d/n",GetArgument(10));
system("PAUSE");
return 0;
}
上例在VS2005中最大速度优化,内联选项为仅仅在有inline声明才内联时可以运行正确,见汇编代码:
int _tmain(int argc, _TCHAR* argv[])
{
printf("%d/n",GetArgument(10));
00401020 call GetArgument (401000h)
00401025 push eax
00401026 push offset string "%d/n" (4020F4h)
0040102B call dword ptr [__imp__printf (4020A4h)]
system("PAUSE");
00401031 push offset string "PAUSE" (4020F8h)
00401036 call dword ptr [__imp__system (40209Ch)]
0040103C add esp,0Ch
return 0;
0040103F xor eax,eax
}
int __cdecl GetArgument(int ai)
{
00401000 push ebp
00401001 mov ebp,esp
00401003 push ecx
00401004 mov dword ptr [ai],0Ah
__asm
{
mov eax, ai
0040100B mov eax,dword ptr [ai]
mov esp,ebp
0040100E mov esp,ebp
pop ecx
00401010 pop ecx
ret
00401011 ret
}
}
00401012 mov esp,ebp
00401014 pop ebp
00401015 ret
当你在不应该ret的时候ret,带来的往往是灾难性的后果,所以此处你必须的预见(或者先反汇编看到)原程序会做的所有操作,并且完整的重复它们,不然你在一个非naked的函数中用ret,结果几乎肯定是程序的崩溃。无论如何,除非你有特殊原因,还是按规矩办事比较好。
其实这样的代码对于反汇编来说还挺有意思的:IDA中可以看到
.text:00401000 ; ===========================================================================
.text:00401000
.text:00401000 ; Segment type: Pure code
.text:00401000 ; Segment permissions: Read/Execute
.text:00401000 _text segment para public 'CODE' use32
.text:00401000 assume cs:_text
.text:00401000 ;org 401000h
.text:00401000 assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing
.text:00401000
.text:00401000 ; =============== S U B R O U T I N E =======================================
.text:00401000
.text:00401000 ; Attributes: bp-based frame
.text:00401000
.text:00401000 GetArgument proc near ; CODE XREF: _mainp
.text:00401000
.text:00401000 var_4 = dword ptr -4
.text:00401000
.text:00401000 55 push ebp
.text:00401001 8B EC mov ebp, esp
.text:00401003 51 push ecx
.text:00401004 C7 45 FC 0A 00 00 00 mov [ebp+var_4], 0Ah
.text:0040100B 8B 45 FC mov eax, [ebp+var_4]
.text:0040100E 8B E5 mov esp, ebp
.text:00401010 59 pop ecx
.text:00401011 C3 retn
.text:00401011 GetArgument endp
.text:00401011
.text:00401012 ; ---------------------------------------------------------------------------
.text:00401012 8B E5 mov esp, ebp
.text:00401014 5D pop ebp
.text:00401015 C3 retn
多余的代码IDA人性的分割掉了,这在没有源代码的人那里过去了就过去了,要是真深究的话,可能还会以为因为花指令的影响导致代码反汇编错了。。。呵呵,不然也不会有3行代码无缘无故的被分割在那里,一看也不像数据段。。。。。
另外push ecx,pop ecx的处理也是编译器极端优化的一个例子,这里本来可以用sub,add修改esp的做法的,但是那样的话一条指令就是5个字节。。。这样的话才1个字节!真是极端的优化啊。。。。。
参考资料:
1. MSDN
2. 《加密与解密》(第3版)附录2
write by九天雁翎(JTianLing) – www.jtianling.com
阅读全文....