原文:http://bbs.pediy.com/thread-133181.htm
(一) 内部机制
我见过的最严谨的C++框架就是Qt框架,Qt将C++带入了一个新的高度。Qt引入的信号(signal)和槽(slot)技术很有创意,其中一点就是,一个对象可以不要声明就可以调用其它对象的方法。为了运作信号和槽,Qt采用了动态化机制(dynamism)。这种动态化机制可以由Qt框架自动实现,也可以由开发人员通过QMetaObject类手动实现。有关信号和槽的内容可以参考http://doc.trolltech.com/4.7/signalsandslots.html。
我们看一个简单的信号和槽的例子:
// sas.h
#include
class Counter : public QObject
{
Q_OBJECT
public:
Counter() { m_value = 0; };
int value() const { return m_value; };
public slots:
void setValue(int value)
{
if (value != m_value)
{
m_value = value;
emit valueChanged(value);
}
};
signals:
void valueChanged(int newValue);
private:
int m_value;
};
// main.cpp
#include "sas.h"
int main(int argc, char *argv[])
{
Counter a, b;
QObject::connect(&a, SIGNAL(valueChanged(int)),
&b, SLOT(setValue(int)));
a.setValue(12); // a.value() == 12, b.value() == 12
b.setValue(48); // a.value() == 12, b.value() == 48
return 0;
}
SIGNAL和SLOT宏将括号中的内容封装成一个字符串,同时还附加一个ID号,如下所示:
#define SLOT(a) "1"#a
#define SIGNAL(a) "2"#a
所以,也可以直接这么写connect函数:
QObject::connect(&a, "2valueChanged(int)", &b, "1setValue(int)");
signals和slots是Qt关键词,可以在头文件中找到,只用于Qt的元编译器(moc)。
# if defined(QT_NO_KEYWORDS)
# define QT_NO_EMIT
# else
# define slots
# define signals protected
# endif
# define Q_SLOTS
# define Q_SIGNALS protected
# define Q_PRIVATE_SLOT(d, signature)
# define Q_EMIT
#ifndef QT_NO_EMIT
# define emit
#endif
emit宏没什么需要解释的,signals宏有点不同,它限定Qt信号为protected方法,而slots宏可以是任意类型。
我们接下来看看Q_OBJECT宏:
/* tmake ignore Q_OBJECT */
#define Q_OBJECT_CHECK \
template
const \
{ int i = qYouForgotTheQ_OBJECT_Macro(this, &_q_argument); i = i; }
template
inline int qYouForgotTheQ_OBJECT_Macro(T, T) { return 0; }
template
inline void qYouForgotTheQ_OBJECT_Macro(T1, T2) {}
#endif // QT_NO_MEMBER_TEMPLATES
/* tmake ignore Q_OBJECT */
#define Q_OBJECT \
public: \
Q_OBJECT_CHECK \
static const QMetaObject staticMetaObject; \
virtual const QMetaObject *metaObject() const; \
virtual void *qt_metacast(const char *); \
QT_TR_FUNCTIONS \
virtual int qt_metacall(QMetaObject::Call, int, void **); \
private:
staticMetaObject是 QMetaObject对象,因为需要给属于同一类的全部实例共享,所以它是静态的。metaObject方法仅仅返回staticMetaObject。QT_TR_FUNCTIONS是一个用于所有tr函数的宏,用来实现多语言支持。qt_metacast用于按照类名或它的某个基类名进行动态转换(dynamic cast)【Qt显然不依赖运行时类型检查(RTTI)】。qt_metacall通过索引调用内部信号和槽。以下是QMetaObject的声明:
struct Q_CORE_EXPORT QMetaObject
{
const char *className() const;
const QMetaObject *superClass() const;
QObject *cast(QObject *obj) const;
#ifndef QT_NO_TRANSLATION
// ### Qt 4: Merge overloads
QString tr(const char *s, const char *c) const;
QString trUtf8(const char *s, const char *c) const;
QString tr(const char *s, const char *c, int n) const;
QString trUtf8(const char *s, const char *c, int n) const;
#endif // QT_NO_TRANSLATION
int methodOffset() const;
int enumeratorOffset() const;
int propertyOffset() const;
int classInfoOffset() const;
int methodCount() const;
int enumeratorCount() const;
int propertyCount() const;
int classInfoCount() const;
int indexOfMethod(const char *method) const;
int indexOfSignal(const char *signal) const;
int indexOfSlot(const char *slot) const;
int indexOfEnumerator(const char *name) const;
int indexOfProperty(const char *name) const;
int indexOfClassInfo(const char *name) const;
QMetaMethod method(int index) const;
QMetaEnum enumerator(int index) const;
QMetaProperty property(int index) const;
QMetaClassInfo classInfo(int index) const;
QMetaProperty userProperty() const;
static bool checkConnectArgs(const char *signal, const char *method);
static QByteArray normalizedSignature(const char *method);
static QByteArray normalizedType(const char *type);
// internal index-based connect
static bool connect(const QObject *sender, int signal_index,
const QObject *receiver, int method_index,
int type = 0, int *types = 0);
// internal index-based disconnect
static bool disconnect(const QObject *sender, int signal_index,
const QObject *receiver, int method_index);
// internal slot-name based connect
static void connectSlotsByName(QObject *o);
// internal index-based signal activation
static void activate(QObject *sender, int signal_index, void **argv);
static void activate(QObject *sender, int from_signal_index, int to_signal_index, void **argv);
static void activate(QObject *sender, const QMetaObject *, int local_signal_index, void **argv);
static void activate(QObject *sender, const QMetaObject *, int from_local_signal_index,
int to_local_signal_index, void
**argv);
// internal guarded pointers
static void addGuard(QObject **ptr);
static void removeGuard(QObject **ptr);
static void changeGuard(QObject **ptr, QObject *o);
static bool invokeMethod(QObject *obj, const char *member,
Qt::ConnectionType,
QGenericReturnArgument ret,
QGenericArgument val0 = QGenericArgument(0),
QGenericArgument val1 = QGenericArgument(),
QGenericArgument val2 = QGenericArgument(),
QGenericArgument val3 = QGenericArgument(),
QGenericArgument val4 = QGenericArgument(),
QGenericArgument val5 = QGenericArgument(),
QGenericArgument val6 = QGenericArgument(),
QGenericArgument val7 = QGenericArgument(),
QGenericArgument val8 = QGenericArgument(),
QGenericArgument val9 = QGenericArgument());
// [ ... several invokeMethod overloads ...]
enum Call {
InvokeMetaMethod,
ReadProperty,
WriteProperty,
ResetProperty,
QueryPropertyDesignable,
QueryPropertyScriptable,
QueryPropertyStored,
QueryPropertyEditable,
QueryPropertyUser
};
#ifdef QT3_SUPPORT
QT3_SUPPORT const char *superClassName() const;
#endif
struct { // private data
const QMetaObject *superdata;
const char *stringdata;
const uint *data;
const QMetaObject **extradata;
} d;
};
需要注意的是它的d结构。d结构的第一个成员是一个QMetaObject类指针,指向父Qt元数据类。用户设计的类可以从多个类派生,但只能拥有一个QObject(或从它派生)基类,这同时也是超类。看一个Qt对话框的例子,它经常从多个类派生:
class ConvDialog : public QDialog, private Ui::ConvDialog
{
Q_OBJECT
Moc将产生以下代码:
const QMetaObject ConvDialog::staticMetaObject = {
{ &QDialog::staticMetaObject, qt_meta_stringdata_ConvDialog,
qt_meta_data_ConvDialog, 0 }
};
如果在QDialog前先继承Ui::ConvDialog,moc将会生成:
const QMetaObject ConvDialog::staticMetaObject = {
{ &Ui::ConvDialog::staticMetaObject, qt_meta_stringdata_ConvDialog,
qt_meta_data_ConvDialog, 0 }
};
这是错误的,因为Ui::ConvDialog不是QObject的一个派生类,由此不拥有staticMetaObject成员,这样做只会导致一个编译错误。
d结构的第二个成员是一个字符数值,表示类的字面元数据。第三个成员是一个无符号整型数组,这个数组是一个表,包含所有元数据的偏移、特征等等。所以,如果你想枚举一个类的信号和槽,那就应该遍历这个表,通过偏移量从stringdata数组中获得方法名。它也涉及到属性(properties)和枚举类型(enums)。第四个成员是以null结尾的QMetaObject类数组,用来保存附加类的元数据信息,它一般由QMetaObject_findMetaObject函数引用。
static const QMetaObject *QMetaObject_findMetaObject(const QMetaObject *self, const char *name)
{
while (self) {
if (strcmp(self->d.stringdata, name) == 0)
return self;
if (self->d.extradata) {
const QMetaObject **e = self->d.extradata;
while (*e) {
if (const QMetaObject *m =QMetaObject_findMetaObject((*e), name))
return m;
++e;
}
}
self = self->d.superdata;
}
return self;
}
这个函数只被property方法调用,property又被propertyCount, propertyOffset 和 indexOfProperty调用。
下面是我们的Counter 类被moc生成的代码:
/****************************************************************************
** Meta object code from reading C++ file 'sas.h'
**
** Created: Mon 3. Nov 15:20:11 2008
** by: The Qt Meta Object Compiler version 59 (Qt 4.4.3)
**
** WARNING! All changes made in this file will be lost!
*****************************************************************************/
#include "../sas.h"
#if !defined(Q_MOC_OUTPUT_REVISION)
#error "The header file 'sas.h' doesn't include
#elif Q_MOC_OUTPUT_REVISION != 59
#error "This file was generated using the moc from 4.4.3. It"
#error "cannot be used with the include files from this version of Qt."
#error "(The moc has changed too much.)"
#endif
QT_BEGIN_MOC_NAMESPACE
static const uint qt_meta_data_Counter[] = {
// content:
1, // revision
0, // classname
0, 0, // classinfo
2, 10, // methods
0, 0, // properties
0, 0, // enums/sets
// signals: signature, parameters, type, tag, flags
18, 9, 8, 8, 0x05,
// slots: signature, parameters, type, tag, flags
42, 36, 8, 8, 0x0a,
0 // eod
};
static const char qt_meta_stringdata_Counter[] = {
"Counter\0\0newValue\0valueChanged(int)\0"
"value\0setValue(int)\0"
};
const QMetaObject Counter::staticMetaObject = {
{ &QObject::staticMetaObject, qt_meta_stringdata_Counter,
qt_meta_data_Counter, 0 }
};
const QMetaObject *Counter::metaObject() const
{
return &staticMetaObject;
}
void *Counter::qt_metacast(const char *_clname)
{
if (!_clname) return 0;
if (!strcmp(_clname, qt_meta_stringdata_Counter))
return static_cast
return QObject::qt_metacast(_clname);
}
int Counter::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
_id = QObject::qt_metacall(_c, _id, _a);
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
switch (_id) {
case 0: valueChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
case 1: setValue((*reinterpret_cast< int(*)>(_a[1]))); break;
}
_id -= 2;
}
return _id;
}
// SIGNAL 0
void Counter::valueChanged(int _t1)
{
void *_a[] = { 0, const_cast
QMetaObject::activate(this, &staticMetaObject, 0, _a);
}
QT_END_MOC_NAMESPACE
qt_metacall方法通过索引调用其它内部方法。Qt动态机制不采用指针,而由索引实现。实际调用方法的工作由编译器实现。这使得信号和槽的机制执行效率比较高。
参数由一个指向指针数组的指针进行传递,并在调用方法时进行适当的转换。当然,使用指针是将不同类型的参数放在一个数组的唯一办法。参数索引从1开始,因为0号代表函数返回值。我们这个例子中信号和槽被声明为void,所以没有值返回。如果需要返回数据,那么包含在switch中的代码将与下面代码类似:
if (_c == QMetaObject::InvokeMetaMethod) {
switch (_id) {
case 0: valueChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
case 1: setValue((*reinterpret_cast< int(*)>(_a[1]))); break;
case 2: { int _r = exampleMethod((*reinterpret_cast< int(*)>(_a[1])));
if (_a[0]) *reinterpret_cast< int*>(_a[0]) = _r; } break;
}
另外一个需要关注的是valueChanged,它将调用QMetaObject的activate方法。
void QMetaObject::activate(QObject *sender, int from_signal_index, int to_signal_index, void **argv)
{
// [... other code ...]
// emit signals in the following order: from_signal_index <= signals <= to_signal_index, signal < 0
for (int signal = from_signal_index;
(signal >= from_signal_index && signal <= to_signal_index) || (signal == -2);
(signal == to_signal_index ? signal = -2 : ++signal))
{
if (signal >= connectionLists->count()) {
signal = to_signal_index;
continue;
}
const QObjectPrivate::ConnectionList &connectionList = connectionLists->at(signal);
int count = connectionList.count();
for (int i = 0; i < count; ++i) {
const QObjectPrivate::Connection *c = &connectionList[i];
if (!c->receiver)
continue;
QObject * const receiver = c->receiver;
// determine if this connection should be sent immediately or
// put into the event queue
if ((c->connectionType == Qt::AutoConnection
&& (currentThreadData != sender->d_func()->threadData
|| receiver->d_func()->threadData != sender->d_func()->threadData))
|| (c->connectionType == Qt::QueuedConnection)) {
queued_activate(sender, signal, *c, argv);
continue;
} else if (c->connectionType == Qt::BlockingQueuedConnection) {
blocking_activate(sender, signal, *c, argv);
continue;
}
const int method = c->method;
QObjectPrivate::Sender currentSender;
currentSender.sender = sender;
currentSender.signal = signal < 0 ? from_signal_index : signal;
QObjectPrivate::Sender * const previousSender =
QObjectPrivate::setCurrentSender(receiver, ¤tSender);
locker.unlock();
if (qt_signal_spy_callback_set.slot_begin_callback != 0) {
qt_signal_spy_callback_set.slot_begin_callback(receiver,
method,
argv ? argv : empty_argv);
}
#if defined(QT_NO_EXCEPTIONS)
receiver->qt_metacall(QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv);
#else
try {
receiver->qt_metacall(QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv);
} catch (...) {
locker.relock();
QObjectPrivate::resetCurrentSender(receiver, ¤tSender, previousSender);
--connectionLists->inUse;
Q_ASSERT(connectionLists->inUse >= 0);
if (connectionLists->orphaned && !connectionLists->inUse)
delete connectionLists;
throw;
}
#endif
locker.relock();
if (qt_signal_spy_callback_set.slot_end_callback != 0)
qt_signal_spy_callback_set.slot_end_callback(receiver, method);
QObjectPrivate::resetCurrentSender(receiver, ¤tSender, previousSender);
if (connectionLists->orphaned)
break;
}
if (connectionLists->orphaned)
break;
}
--connectionLists->inUse;
Q_ASSERT(connectionLists->inUse >= 0);
if (connectionLists->orphaned && !connectionLists->inUse)
delete connectionLists;
locker.unlock();
if (qt_signal_spy_callback_set.signal_end_callback != 0)
qt_signal_spy_callback_set.signal_end_callback(sender, from_signal_index);
}
这个方法需要做很多工作,包括检查当前的事件是需要立即处理还是先放进事件序列中,并分别调用不同的activate方法,而后继续处理ConnectionList中的下一个事件。如果事件需要立即处理,首先从事件中获得将要调用的方法的ID,从而调用接收方的qt_metacall。过程如下:
const QObjectPrivate::ConnectionList &connectionList = connectionLists->at(signal);
int count = connectionList.count();
for (int i = 0; i < count; ++i) {
const QObjectPrivate::Connection *c = &connectionList[i];
QObject * const receiver = c->receiver;
const int method = c->method;
receiver->qt_metacall(QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv);
从中我们可以明白信号和槽的内部机制:当调用connect函数时,信号和槽的签名转换成ID,然后储存在Connection类中。每次一个信号发出后,与此信号ID相关的事件即被确定,同时对应的槽被调用。
至于动态调用(dynamic invokes),主要通过QMetaObject类提供的invokeMethod方法实现。这个方法不同于信号和槽,它需要从它的返回类型、名字和参数类型构造一个签名,然后查找元数据来得到其ID,最后调用对象的qt_metacall方法。
bool QMetaObject::invokeMethod(QObject *obj, const char *member, Qt::ConnectionType type,
QGenericReturnArgument ret,
QGenericArgument val0,
QGenericArgument val1,
QGenericArgument val2,
QGenericArgument val3,
QGenericArgument val4,
QGenericArgument val5,
QGenericArgument val6,
QGenericArgument val7,
QGenericArgument val8,
QGenericArgument val9)
{
if (!obj)
return false;
QVarLengthArray
int len = qstrlen(member);
if (len <= 0)
return false;
sig.append(member, len);
sig.append('(');
enum { MaximumParamCount = 11 };
const char *typeNames[] = {ret.name(), val0.name(), val1.name(), val2.name(), val3.name(),
val4.name(), val5.name(), val6.name(), val7.name(), val8.name(),
val9.name()};
int paramCount;
for (paramCount = 1; paramCount < MaximumParamCount; ++paramCount) {
len = qstrlen(typeNames[paramCount]);
if (len <= 0)
break;
sig.append(typeNames[paramCount], len);
sig.append(',');
}
if (paramCount == 1)
sig.append(')'); // no parameters
else
sig[sig.size() - 1] = ')';
sig.append('\0');
int idx = obj->metaObject()->indexOfMethod(sig.constData());
if (idx < 0) {
QByteArray norm = QMetaObject::normalizedSignature(sig.constData());
idx = obj->metaObject()->indexOfMethod(norm.constData());
}
if (idx < 0)
return false;
// check return type
if (ret.data()) {
const char *retType = obj->metaObject()->method(idx).typeName();
if (qstrcmp(ret.name(), retType) != 0) {
// normalize the return value as well
// the trick here is to make a function signature out of the return type
// so that we can call normalizedSignature() and avoid duplicating code
QByteArray unnormalized;
int len = qstrlen(ret.name());
unnormalized.reserve(len + 3);
unnormalized = "_("; // the function is called "_"
unnormalized.append(ret.name());
unnormalized.append(')');
QByteArray normalized = QMetaObject::normalizedSignature(unnormalized.constData());
normalized.truncate(normalized.length() - 1); // drop the ending ')'
if (qstrcmp(normalized.constData() + 2, retType) != 0)
return false;
}
}
void *param[] = {ret.data(), val0.data(), val1.data(), val2.data(), val3.data(), val4.data(),
val5.data(), val6.data(), val7.data(), val8.data(), val9.data()};
if (type == Qt::AutoConnection) {
type = QThread::currentThread() == obj->thread()
? Qt::DirectConnection
: Qt::QueuedConnection;
}
if (type == Qt::DirectConnection) {
return obj->qt_metacall(QMetaObject::InvokeMetaMethod, idx, param) < 0;
} else {
if (ret.data()) {
qWarning("QMetaObject::invokeMethod: Unable to invoke methods with return values in queued "
"connections");
return false;
}
int nargs = 1; // include return type
void **args = (void **) qMalloc(paramCount * sizeof(void *));
int *types = (int *) qMalloc(paramCount * sizeof(int));
types[0] = 0; // return type
args[0] = 0;
for (int i = 1; i < paramCount; ++i) {
types[i] = QMetaType::type(typeNames[i]);
if (types[i]) {
args[i] = QMetaType::construct(types[i], param[i]);
++nargs;
} else if (param[i]) {
qWarning("QMetaObject::invokeMethod: Unable to handle unregistered datatype '%s'",
typeNames[i]);
for (int x = 1; x < i; ++x) {
if (types[x] && args[x])
QMetaType::destroy(types[x], args[x]);
}
qFree(types);
qFree(args);
return false;
}
}
if (type == Qt::QueuedConnection) {
QCoreApplication::postEvent(obj, new QMetaCallEvent(idx, 0, -1, nargs, types, args));
} else {
if (QThread::currentThread() == obj->thread()) {
qWarning("QMetaObject::invokeMethod: Dead lock detected in BlockingQueuedConnection: "
"Receiver is %s(%p)",
obj->metaObject()->className(), obj);
}
// blocking queued connection
#ifdef QT_NO_THREAD
QCoreApplication::postEvent(obj, new QMetaCallEvent(idx, 0, -1, nargs, types, args));
#else
QSemaphore semaphore;
QCoreApplication::postEvent(obj, new QMetaCallEvent(idx, 0, -1, nargs, types, args, &semaphore));
semaphore.acquire();
#endif // QT_NO_THREAD
}
}
return true;
}
方法的ID由indexOfMethod返回。如果方法的签名不能创建,invokeMethod返回false。
(二) 逆向
逆向时我们需要使用Qt提供的元数据。先看看元数据表:
QT_BEGIN_MOC_NAMESPACE
static const uint qt_meta_data_Counter[] = {
// content:
1, // revision
0, // classname
0, 0, // classinfo
2, 10, // methods
0, 0, // properties
0, 0, // enums/sets
// signals: signature, parameters, type, tag, flags
18, 9, 8, 8, 0x05,
// slots: signature, parameters, type, tag, flags
42, 36, 8, 8, 0x0a,
0 // eod
};
这个表不仅仅告诉了我们方法的数量,同时也给出了方法的偏移。下面是此结构的C++头:
struct QMetaObjectPrivate
{
int revision;
int className;
int classInfoCount, classInfoData;
int methodCount, methodData;
int propertyCount, propertyData;
int enumeratorCount, enumeratorData;
};
我们已经知道了方法的数量和偏移,接下来就是分析方法本身的数据。这个数据可以分为五个整数,其意义分别是:签名、参数、类型、标签、特征。签名(signature)段是一个字符串数据中的偏移,对应valueChanged(int)方法的部分声明(不包含返回类型)。参数(parameters)段是对应参数名的偏移。我们例子中的名字就是'newValue'。名字用逗号分开,所以,如果我们的槽有两个参数,那么会显示为'newValue1,newValue2'。类型(type)段对应方法的返回类型。如果像我们例子中一样为空字符串,那么可以确定方法是void类型。标签(tag)段暂时不用(Tags are special macros recognized by moc that make it possible to add extra information about a method. For the moment, moc doesn't support any special tags.)。最后一个段特征(flags)不是一个偏移量。特征值有:
enum MethodFlags {
AccessPrivate = 0x00,
AccessProtected = 0x01,
AccessPublic = 0x02,
AccessMask = 0x03, //mask
MethodMethod = 0x00,
MethodSignal = 0x04,
MethodSlot = 0x08,
MethodTypeMask = 0x0c,
MethodCompatibility = 0x10,
MethodCloned = 0x20,
MethodScriptable = 0x40
};
关于方法的知识就这些,下面我们研究枚举和属性。我们先在代码中加入这两类代码:
class Counter : public QObject
{
Q_OBJECT
Q_PROPERTY(Priority priority READ priority WRITE setPriority)
Q_ENUMS(Priority)
public:
Counter() { m_value = 0; };
enum Priority { High, Low, VeryHigh, VeryLow };
void setPriority(Priority priority) { m_priority = priority; };
Priority priority() const { return m_priority; };
int value() const { return m_value; };
public slots:
void setValue(int value)
{
if (value != m_value)
{
m_value = value;
emit valueChanged(value);
}
};
signals:
void valueChanged(int newValue);
private:
int m_value;
Priority m_priority;
};
这时moc会生成:
static const uint qt_meta_data_Counter[] = {
// content:
1, // revision
0, // classname
0, 0, // classinfo
2, 10, // methods
1, 20, // properties
1, 23, // enums/sets
// signals: signature, parameters, type, tag, flags
18, 9, 8, 8, 0x05,
// slots: signature, parameters, type, tag, flags
42, 36, 8, 8, 0x0a,
// properties: name, type, flags
65, 56, 0x0009510b,
// enums: name, flags, count, data
56, 0x0, 4, 27,
// enum data: key, value
74, uint(Counter::High),
79, uint(Counter::Low),
83, uint(Counter::VeryHigh),
92, uint(Counter::VeryLow),
0 // eod
};
static const char qt_meta_stringdata_Counter[] = {
"Counter\0\0newValue\0valueChanged(int)\0"
"value\0setValue(int)\0Priority\0priority\0"
"High\0Low\0VeryHigh\0VeryLow\0"
};
同样,我们也可以得到属性和枚举的数量和偏移。对属性而言,包含3个整数:名字(name)、类型(type)和特征(flags)。类型对应的是属性的类型,我们例子中就是Priority。特征值可分为:
enum PropertyFlags {
Invalid = 0x00000000,
Readable = 0x00000001,
Writable = 0x00000002,
Resettable = 0x00000004,
EnumOrFlag = 0x00000008,
StdCppSet = 0x00000100,
// Override = 0x00000200,
Designable = 0x00001000,
ResolveDesignable = 0x00002000,
Scriptable = 0x00004000,
ResolveScriptable = 0x00008000,
Stored = 0x00010000,
ResolveStored = 0x00020000,
Editable = 0x00040000,
ResolveEditable = 0x00080000,
User = 0x00100000,
ResolveUser = 0x00200000
};
对于枚举,有名字(name)、特征(flags)、数量(count)和数据(data)。特征段没有使用。数量段表示枚举中条目的数量。数据段是一个元数据表的偏移,指向枚举中的条目。每个条目由两个整数表示:key和value。Key指向当前条目的名字,value是此条目的实际值。
为了获得二进制文件的元数据信息,作者特意用Python写了一个IDA脚本,可以从一个Q_OBJECT类中提取出方法、属性和枚举。代码如下:
# ---------------------------------------------
# MetaData Parser Class
# ---------------------------------------------
# change for 64 bit exes
b64bit = False
# i'm assuming that the exe is Little Endian
# the external methods used by this class are Byte(addr) and Dword(addr)
def AddressSize():
if b64bit == True:
return 8
return 4
def ReadAddress(addr):
if b64bit == True:
return (Dword(addr+4) << 32) | Dword(addr)
return Dword(addr)
class MetaParser:
def __init__(self, stringsaddr, tableaddr):
self.MetaStrings = stringsaddr
self.MetaTable = tableaddr
def ReadString(self, addr):
c = addr
b = Byte(c)
str = ""
while b != 0:
# set a limit, just in case
if (c - addr) > 1000:
return "error"
str += chr(b)
c += 1
b = Byte(c)
return str
# read metadata
"""
struct QMetaObjectPrivate
{
int revision;
int className;
int classInfoCount, classInfoData;
int methodCount, methodData;
int propertyCount, propertyData;
int enumeratorCount, enumeratorData;
};
"""
# ---------------------------------------------
# enums (quick way to convert them to python)
# ---------------------------------------------
class Enum:
def __init__(self, **entries): self.__dict__.update(entries)
MethodFlags = Enum( \
AccessPrivate = 0x00, \
AccessProtected = 0x01, \
AccessPublic = 0x02, \
AccessMask = 0x03, \
MethodMethod = 0x00, \
MethodSignal = 0x04, \
MethodSlot = 0x08, \
MethodTypeMask = 0x0c, \
MethodCompatibility = 0x10, \
MethodCloned = 0x20, \
MethodScriptable = 0x40)
PropertyFlags = Enum( \
Invalid = 0x00000000, \
Readable = 0x00000001, \
Writable = 0x00000002, \
Resettable = 0x00000004, \
EnumOrFlag = 0x00000008, \
StdCppSet = 0x00000100, \
Designable = 0x00001000, \
ResolveDesignable = 0x00002000, \
Scriptable = 0x00004000, \
ResolveScriptable = 0x00008000, \
Stored = 0x00010000, \
ResolveStored = 0x00020000, \
Editable = 0x00040000, \
ResolveEditable = 0x00080000, \
User = 0x00100000, \
ResolveUser = 0x00200000)
# ---------------------------------------------
# methods
# ---------------------------------------------
def GetClassName(self):
stringaddr = Dword(self.MetaTable + 4) + self.MetaStrings
return self.ReadString(stringaddr)
def GetMethodNumber(self):
return Dword(self.MetaTable + 16)
def GetMethodSignature(self, method_index):
if method_index >= self.GetMethodNumber():
return "error: method index out of range"
method_offset = (Dword(self.MetaTable + 20) * 4) + (method_index * (5 * 4))
# get accessibility
access_flags = self.GetMethodAccess(method_index)
access_type = "private: "
if access_flags == self.MethodFlags.AccessProtected:
access_type = "protected: "
elif access_flags == self.MethodFlags.AccessPublic:
access_type = "public: "
# read return type
rettype = self.ReadString(Dword(self.MetaTable + method_offset + 8) + self.MetaStrings)
if rettype == "":
rettype = "void"
# read partial signature
psign = self.ReadString(Dword(self.MetaTable + method_offset) + self.MetaStrings)
# retrieve argument types
par_index = psign.find("(")
arg_types = psign[(par_index + 1):(len(psign) - 1)].split(",")
# read argument names
arg_names = self.ReadString(Dword(self.MetaTable + method_offset + 4) \
+ self.MetaStrings).split(",")
# if argument types and names are not the same number,
# then show signature without argument names
if len(arg_types) != len(arg_names):
return access_type + rettype + " " + psign
# build signatrue with argument names
ntypes = len(arg_types)
x = 0
args = ""
while x < ntypes:
if x != 0:
args += ", "
if arg_types[x] == "":
args += arg_names[x]
elif arg_names[x] == "":
args += arg_types[x]
else:
args += (arg_types[x] + " " + arg_names[x])
# increment loop
x += 1
return access_type + rettype + " " + psign[0:(par_index + 1)] + args + ")"
def GetMethodFlags(self, method_index):
if method_index >= self.GetMethodNumber():
return -1
method_offset = (Dword(self.MetaTable + 20) * 4) + (method_index * (5 * 4))
return Dword(self.MetaTable + method_offset + 16)
def GetMethodType(self, method_index):
return self.GetMethodFlags(method_index) & self.MethodFlags.MethodTypeMask
def GetMethodAccess(self, method_index):
return self.GetMethodFlags(method_index) & self.MethodFlags.AccessMask
def GetPropertyNumber(self):
return Dword(self.MetaTable + 24)
def GetPropertyDecl(self, property_index):
if property_index >= self.GetPropertyNumber():
return "error: property index out of range"
property_offset = (Dword(self.MetaTable + 28) * 4) + (property_index * (3 * 4))
# read name
pr_name = self.ReadString(Dword(self.MetaTable + property_offset) + self.MetaStrings)
# read type
pr_type = self.ReadString(Dword(self.MetaTable + property_offset + 4) + self.MetaStrings)
return pr_type + " " + pr_name
def GetPropertyFlags(self, property_index):
if property_index >= self.GetPropertyNumber():
return -1
property_offset = (Dword(self.MetaTable + 28) * 4) + (property_index * (3 * 4))
return Dword(self.MetaTable + property_offset + 8)
def PropertyFlagsToString(self, flags):
if flags == 0:
return "Invalid"
fstr = ""
if flags & self.PropertyFlags.Readable:
fstr += " | Readable"
if flags & self.PropertyFlags.Writable:
fstr += " | Writable"
if flags & self.PropertyFlags.Resettable:
fstr += " | Resettable"
if flags & self.PropertyFlags.EnumOrFlag:
fstr += " | EnumOrFlag"
if flags & self.PropertyFlags.StdCppSet:
fstr += " | StdCppSet"
if flags & self.PropertyFlags.Designable:
fstr += " | Designable"
if flags & self.PropertyFlags.ResolveDesignable:
fstr += " | ResolveDesignable"
if flags & self.PropertyFlags.Scriptable:
fstr += " | Scriptable"
if flags & self.PropertyFlags.ResolveScriptable:
fstr += " | ResolveScriptable"
if flags & self.PropertyFlags.Stored:
fstr += " | Stored"
if flags & self.PropertyFlags.ResolveStored:
fstr += " | ResolveStored"
if flags & self.PropertyFlags.Editable:
fstr += " | Editable"
if flags & self.PropertyFlags.ResolveEditable:
fstr += " | ResolveEditable"
if flags & self.PropertyFlags.User:
fstr += " | User"
if flags & self.PropertyFlags.ResolveUser:
fstr += " | ResolveUser"
return fstr[3:]
def GetEnumNumber(self):
return Dword(self.MetaTable + 32)
def GetEnumDecl(self, enum_index):
if enum_index >= self.GetPropertyNumber():
return "error: property index out of range"
enum_offset = (Dword(self.MetaTable + 36) * 4) + (enum_index * (4 * 4))
# read name
enum_name = self.ReadString(Dword(self.MetaTable + enum_offset) + self.MetaStrings)
# read number of items
items_num = Dword(self.MetaTable + enum_offset + 8)
# items addr
items_addr = (Dword(self.MetaTable + enum_offset + 12) * 4) + self.MetaTable
decl = "enum " + enum_name + "\n{\n"
# add items
x = 0
while x < items_num:
# read item name
item_name = self.ReadString(Dword(items_addr) + self.MetaStrings)
# read data
item_data = "0x%X" % Dword(items_addr + 4)
# add
decl += " " + item_name + " = " + item_data + ",\n"
# inc loop
x += 1
items_addr += 8
decl += "\n};"
return decl
# ---------------------------------------------
# Display MetaData
# ---------------------------------------------
def DisplayMethod(parser, method_index):
print(str(method_index) + " - " + parser.GetMethodSignature(method_index))
def DisplayProperty(parser, property_index):
print(str(property_index) + " - " + parser.GetPropertyDecl(property_index))
flags = parser.GetPropertyFlags(property_index)
print(" flags: " + parser.PropertyFlagsToString(flags))
def DisplayEnum(parser, enum_index):
print("[" + str(enum_index) + "]\n" + parser.GetEnumDecl(enum_index) + "\n")
def DisplayMetaData(stringsaddr, tableaddr):
parser = MetaParser(stringsaddr, tableaddr)
print("\n-------------------------------------------------")
print("--- " + "Qt MetaData Displayer by Daniel Pistelli")
print("--- " + "metadata of the class: " + parser.GetClassName() + "\n")
num_methods = parser.GetMethodNumber()
num_properties = parser.GetPropertyNumber()
num_enums = parser.GetEnumNumber()
# ---------------------------------------------
# methods
# ---------------------------------------------
# signals
print("--- Signals:\n")
x = 0
while x < num_methods:
# print if it's a signal
if parser.GetMethodType(x) == parser.MethodFlags.MethodSignal:
DisplayMethod(parser, x)
# increment loop
x += 1
# slots
print("\n--- Slots:\n")
x = 0
while x < num_methods:
# print if it's a slot
if parser.GetMethodType(x) == parser.MethodFlags.MethodSlot:
DisplayMethod(parser, x)
# increment loop
x += 1
# other methods
print("\n--- Other Methods:\n")
x = 0
while x < num_methods:
# print if it's a slot
if parser.GetMethodType(x) == parser.MethodFlags.MethodMethod:
DisplayMethod(parser, x)
# increment loop
x += 1
# ---------------------------------------------
# properties
# ---------------------------------------------
print("\n--- Properties:\n")
x = 0
while x < num_properties:
DisplayProperty(parser, x)
# increment loop
x += 1
# ---------------------------------------------
# enums
# ---------------------------------------------
print("\n--- Enums:\n")
x = 0
while x < num_enums:
DisplayEnum(parser, x)
# increment loop
x += 1
print("-------------------------------------------------\n")
# ---------------------------------------------
# Main
# ---------------------------------------------
addrtoparse = ScreenEA()
if addrtoparse != 0:
stringsaddr = ReadAddress(addrtoparse + AddressSize())
tableaddr = ReadAddress(addrtoparse + AddressSize() * 2)
if stringsaddr != 0 or tableaddr != 0:
DisplayMetaData(stringsaddr, tableaddr)
DisplayMetaData函数接收两个参数:元数据表地址和元数据字符串地址。这是因为有时类的结构要在运行时才能确定,就像下面的VC++例子:
.text:00401D60 sub_401D60 proc near ; DATA XREF: .rdata:00402108
.text:00401D60 push ebp
.text:00401D61 mov ebp, esp
.text:00401D63 mov eax, ds:?staticMetaObject@QObject@@2UQMetaObject@@B
; QMetaObject const QObject::staticMetaObject
.text:00401D68 mov dword_403070, eax
.text:00401D6D mov dword_403074, offset Str2 ; "Counter"
.text:00401D77 mov dword_403078, offset unk_4021A8
.text:00401D81 mov dword_40307C, 0
.text:00401D8B pop ebp
.text:00401D8C retn
.text:00401D8C sub_401D60 endp
但有时候,类结构可以静态确定:
.data:00406000 dd 0 ; SuperData
.data:00406004 dd offset MetaStrings ; "Counter"
.data:00406008 dd offset MetaTable
这时,脚本可以直接在SuperData地址上执行,这是QMetaObject中的第一个d结构。Counter类执行脚本后的输出是:
-------------------------------------------------
--- Qt MetaData Displayer by Daniel Pistelli
--- metadata of the class: Counter
--- Signals:
0 - protected: void valueChanged(int newValue)
--- Slots:
1 - public: void setValue(int value)
--- Other Methods:
--- Properties:
0 - Priority priority
flags: Readable | Writable | EnumOrFlag | StdCppSet | Designable | Scriptable | Stored | ResolveEditable
--- Enums:
[0]
enum Priority
{
High = 0x0,
Low = 0x1,
VeryHigh = 0x2,
VeryLow = 0x3,
};
-------------------------------------------------
是不是很爽?方法的签名甚至包括了参数的名字(当可用时)。
(作者还测试了QWidget类,这里略去数百字)……
如何找到类的元数据呢?让我们看Counter类的结构:
.text:004012F2 mov eax, ds:_ZN7QObjectC2EPS_
.text:004012F7 mov [esp+88h+var_84], ecx
.text:004012FB mov [ebp+var_3C], 2
.text:00401302 call eax ; _ZN7QObjectC2EPS_
.text:00401304 mov eax, [ebp+var_4C]
.text:00401307 mov dword ptr [eax], offset virtual_ptrs
最后一个汇编指令设置了虚函数表指针。Q_OBJECT类的虚函数表类似于:
.rdata:00402158 off_402158 dd offset metaObject
.rdata:0040215C dd offset qt_metacast
.rdata:00402160 dd offset qt_metacall
metaObject方法可以获得元数据表:
.text:00401430 metaObject proc near
.text:00401430 push ebp
.text:00401431 mov eax, offset dword_406000 ; QMetaObject class layout
.text:00401436 mov ebp, esp
.text:00401438 pop ebp
.text:00401439 retn
.text:00401439 metaObject endp
dword_406000对应QMetaObject类的结构,我们需要它来执行脚本。
获得类的元数据以后,下一步就是将方法(属性)的名字和实际反汇编代码联系起来。脚本输出了每个方法和属性的索引。要想从索引得到方法的地址,必须考虑moc生成的qt_metacall方法:
int Counter::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
_id = QObject::qt_metacall(_c, _id, _a);
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
switch (_id) {
case 0: valueChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
case 1: setValue((*reinterpret_cast< int(*)>(_a[1]))); break;
}
_id -= 2;
}
#ifndef QT_NO_PROPERTIES
else if (_c == QMetaObject::ReadProperty) {
void *_v = _a[0];
switch (_id) {
case 0: *reinterpret_cast< Priority*>(_v) = priority(); break;
}
_id -= 1;
} else if (_c == QMetaObject::WriteProperty) {
void *_v = _a[0];
switch (_id) {
case 0: setPriority(*reinterpret_cast< Priority*>(_v)); break;
}
_id -= 1;
} else if (_c == QMetaObject::ResetProperty) {
_id -= 1;
} else if (_c == QMetaObject::QueryPropertyDesignable) {
_id -= 1;
} else if (_c == QMetaObject::QueryPropertyScriptable) {
_id -= 1;
} else if (_c == QMetaObject::QueryPropertyStored) {
_id -= 1;
} else if (_c == QMetaObject::QueryPropertyEditable) {
_id -= 1;
} else if (_c == QMetaObject::QueryPropertyUser) {
_id -= 1;
}
#endif // QT_NO_PROPERTIES
return _id;
}
这个方法可以解析方法名和属性get/set方法名。
qt_metacall的地址可以从虚函数表获得。在分析此函数代码前,我们先看这个枚举:
enum Call {
InvokeMetaMethod, // 0
ReadProperty, // 1
WriteProperty, // 2
ResetProperty, // 3
QueryPropertyDesignable, // 4
QueryPropertyScriptable, // 5
QueryPropertyStored, // 6
QueryPropertyEditable, // 7
QueryPropertyUser // 8
};
然后看反汇编:
.text:004014D0 qt_metacall proc near
.text:004014D0
.text:004014D0 var_28 = dword ptr -28h
.text:004014D0 var_24 = dword ptr -24h
.text:004014D0 var_20 = dword ptr -20h
.text:004014D0 var_1C = dword ptr -1Ch
.text:004014D0 var_10 = dword ptr -10h
.text:004014D0 var_C = dword ptr -0Ch
.text:004014D0 var_8 = dword ptr -8
.text:004014D0 var_4 = dword ptr -4
.text:004014D0 arg_0 = dword ptr 8
.text:004014D0 arg_4 = dword ptr 0Ch
.text:004014D0 arg_8 = dword ptr 10h
.text:004014D0 arg_C = dword ptr 14h
.text:004014D0
.text:004014D0 push ebp
.text:004014D1 mov ebp, esp
.text:004014D3 sub esp, 28h
.text:004014D6 mov [ebp+var_C], ebx
.text:004014D9 mov eax, [ebp+arg_0]
.text:004014DC mov ebx, [ebp+arg_8]
.text:004014DF mov [ebp+var_8], esi
.text:004014E2 mov esi, [ebp+arg_4] ; esi = _c
.text:004014E5 mov [ebp+var_4], edi
.text:004014E8 mov edi, [ebp+arg_C]
.text:004014EB mov [ebp+var_10], eax
.text:004014EE mov [esp+28h+var_20], ebx
.text:004014F2 mov [esp+28h+var_1C], edi
.text:004014F6 mov [esp+28h+var_24], esi
.text:004014FA mov [esp+28h+var_28], eax
.text:004014FD call _ZN7QObject11qt_metacallEN11QMetaObject4CallEiPPv
.text:00401502 test eax, eax ; eax = _id
.text:00401504 mov ebx, eax
.text:00401506 js short loc_40151C ; _id < 0 ?
.text:00401508 test esi, esi
.text:0040150A jnz short loc_401530 ; _c != InvokeMetaMethod
.text:0040150C test eax, eax ; _id == 0 ?
.text:0040150E jz loc_4015B8
.text:00401514 cmp eax, 1 ; _id == 1 ?
.text:00401517 jz short loc_401590
假如esi不等于0,那么就是一个InvokeMetaMethod。考虑"_id == 0"的情况:
.text:004015B8 loc_4015B8:
.text:004015B8 mov eax, [edi+4]
.text:004015BB mov edx, [ebp+var_10]
.text:004015BE mov eax, [eax]
.text:004015C0 mov [esp+28h+var_28], edx
.text:004015C3 mov [esp+28h+var_24], eax
.text:004015C7 call sub_401490
由此我们很快就可以确定sub_401490就是valueChanged信号:
.text:00401490 ; void __cdecl signal_valueChanged(int this, int newValue)
.text:00401490 signal_valueChanged proc near
.text:00401490
.text:00401490 var_18 = dword ptr -18h
.text:00401490 var_14 = dword ptr -14h
.text:00401490 var_10 = dword ptr -10h
.text:00401490 var_C = dword ptr -0Ch
.text:00401490 var_8 = dword ptr -8
.text:00401490 var_4 = dword ptr -4
.text:00401490 this = dword ptr 8
.text:00401490 newValue = dword ptr 0Ch
.text:00401490
.text:00401490 push ebp
.text:00401491 xor ecx, ecx
.text:00401493 mov ebp, esp
.text:00401495 lea eax, [ebp+newValue]
.text:00401498 sub esp, 18h
.text:0040149B mov [ebp+var_8], 0
.text:004014A2 mov edx, offset dword_406000
.text:004014A7 mov [ebp+var_4], eax
.text:004014AA lea eax, [ebp+var_8]
.text:004014AD mov [esp+18h+var_C], eax
.text:004014B1 mov eax, [ebp+this]
.text:004014B4 mov [esp+18h+var_10], ecx
.text:004014B8 mov [esp+18h+var_14], edx
.text:004014BC mov [esp+18h+var_18], eax
.text:004014BF call ds:_ZN11QMetaObject8activateEP7QObjectPKS_iPPv
.text:004014C5 leave
.text:004014C6 retn
接下来就无需解释了。同样的方法也可用来解决属性的get/set。对于一个复杂文件,可以结合前面的元数据脚本,编写一个解析器自动解析switch块中的函数。但是要注意的是:switch块是平台和编译器相关的,因此脚本需要频繁调整。此外,一些小的方法可能被内联(inline)进qt_metacall方法中。