65.9K
CodeProject 正在变化。 阅读更多。
Home

Qt 内部和逆向工程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (27投票s)

2008 年 11 月 28 日

CPOL

11分钟阅读

viewsIcon

72712

downloadIcon

702

本文的第一部分展示了 Qt 框架的动态内部机制,这些机制实现了信号和槽机制。第二部分侧重于如何使用 IDAPython 脚本从二进制文件中检索 Qt moc 生成的元数据信息,以及如何在反汇编中使用它。

引言

本文的一半内容来自我更长的论文“动态 C++ 提案”。我决定将有关 Qt 内部机制的部分单独作为一个文章,并加上逆向工程部分。由于其性质,这并非我通常撰写的文章类型。事实上,我不到一天就写完了逆向工程部分。所以,这是一篇非常容易的文章。不过,我认为它对于那些需要逆向 Qt 应用程序的人很有用,而且他们肯定不会考虑阅读我关于动态 C++ 的其他论文,因为那听起来不像一篇关于 Qt 的论文,事实上它也不是一篇关于 Qt 的论文:关于 Qt 的段落只是其中的许多段落之一。此外,我没有看到过关于这个主题的严肃文章。

逆向 Qt 应用程序时需要考虑的第一件事是 Qt 为 C++ 语言带来了什么。事件(在 Qt 框架内)只是虚函数,所以这没什么新东西。这不是 C++ 逆向工程指南。Qt 的新之处在于信号和槽,它们依赖于 Qt 框架的动态性。

因此,我将要展示的第一件事是这种动态性是如何工作的。第二部分侧重于逆向工程,届时,我将展示如何在反汇编一个“Q_OBJECT”类时获得所有需要的元数据。

内部

我所见过的唯一严肃的 C++ 框架是 Qt 框架。Qt 将 C++ 推向了一个新的水平。虽然 Qt 引入的信号和槽技术非常新颖,但整个想法真正有趣之处在于,一个对象可以调用其他对象的成员函数,而无需考虑对象的声明。为了使信号和槽工作,动态性被引入了 C++。当然,当仅使用信号和槽时,开发人员不会直接注意到这种行为,一切都由框架处理。但是,开发人员也可以通过 QMetaObject 类来使用这种动态性。

我不会解释信号和槽的基础知识。读者可以查阅 Qt 文档页面。我将做的是简要介绍 Qt 动态性的内部工作原理。在我撰写本文时,Qt 框架的当前版本是 4.4.3。

让我们来看一个简单的信号和槽示例

// sas.h

#include <QObject>

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;
}

SIGNALSLOT 宏用方括号将它们的内容括起来,使其成为一个字符串。嗯,它们还做了另一件事。它们在字符串中放入了一个标识号。

#define SLOT(a)          "1"#a
#define SIGNAL(a)        "2"#a

所以,也可以这样写

QObject::connect(&a, "2valueChanged(int)", &b, "1setValue(int)");

Qt 关键字“signals”和“slots”,可以在类头文件中找到,仅对 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)成员函数,而槽可以是任何类型。第一个有趣的部分是“Q_OBJECT”宏。

/* tmake ignore Q_OBJECT */
#define Q_OBJECT_CHECK \
    template <typename T> inline 
             void qt_check_for_QOBJECT_macro(const T &_q_argument) 

const \
    { int i = qYouForgotTheQ_OBJECT_Macro(this, &_q_argument); i = i; }

template <typename T>
inline int qYouForgotTheQ_OBJECT_Macro(T, T) { return 0; }

template <typename T1, typename T2>
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:

staticMetaObjectQMetaObject,它是静态的,因为作为元数据类,它被同一个类的所有实例共享。metaObject 方法只是返回 staticMetaObjectQT_TR_FUNCTIONS 是一个宏,它定义了所有用于多语言支持的“tr”函数。qt_metacast 在给定类名或其基类之一的名称时执行动态转换(Qt 显然不依赖于 RTTI)。qt_metacall 通过索引调用内部信号或槽。在我讨论 moc 生成的代码之前,这里是 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;
};

QMetaObject 中重要的部分是内部的“d”结构体。这个结构体的第一个成员是 QMetaObject 类的指针。这个成员指向父 Qt 对象元数据类。一个像我们这样的类可以继承自不止一个类,但它只能有一个 QObject(或从中派生的)基类,这就是超类。此外,moc 依赖于这样一个事实:在 QObject 派生类的声明中,第一个继承的类是 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 }
};

但是,如果 ConvDialogQDialog 之前继承 Ui::ConvDialog,则 moc 会生成

const QMetaObject ConvDialog::staticMetaObject = {
    { &Ui::ConvDialog::staticMetaObject, qt_meta_stringdata_ConvDialog,
      qt_meta_data_ConvDialog, 0 }
};

这是错误的,因为 Ui::ConvDialog 不是 QObject 的派生类,因此没有 staticMetaObject 成员。这样做会导致编译错误。

d”结构体的第二个成员是一个 char 数组,其中包含类的文字元数据。第三个成员是一个无符号整数数组。这个数组是一个表,其中包含所有元数据偏移量、标志等。因此,如果我们想枚举一个类的槽和信号,我们就必须遍历这个表,获取正确的偏移量,以便从 stringdata 数组中获取方法名。“d”结构体的第四个成员是一个以 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”方法又由 propertyCountpropertyOffsetindexOfProperty 调用。

这是 moc 为我们的 Counter 类生成的代码。

/****************************************************************************
** 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 <QObject>."
#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<void*>(const_cast< Counter*>(this));
    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<void*>(reinterpret_cast<const void>(&_t1)) };
    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;
}

moc 生成的另一个有趣的方 法是 valueChanged,它表示发出“valueChanged”信号的代码。此代码调用 QMetaObjectActivate 方法,该方法只是此 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, &currentSender);
            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, 
                                &currentSender, 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, 
                            &currentSender, 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 的连接,并通过其 ID 调用槽。

需要讨论的最后一部分是动态调用。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<char, 512> sig;
    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 提供的所有元数据。为了做到这一点,让我们重新考虑元数据表。

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
};

正如我们所注意到的,该表不仅告诉我们方法的数量,还告诉我们方法的偏移量(10)。这是该结构的 C++ 声明头文件。

struct QMetaObjectPrivate
{
    int revision;
    int className;
    int classInfoCount, classInfoData;
    int methodCount, methodData;
    int propertyCount, propertyData;
    int enumeratorCount, enumeratorData;
};

此结构包含在“qmetaobject.cpp”中;就像其他信息一样,我们需要解析元数据表。在考虑属性和枚举之前,让我们先考虑方法。我们已经有了方法的数量和偏移量,现在需要分析的是方法本身的数据。这些数据细分为五个整数,根据 moc 的说法,它们代表:签名、参数、类型、标签、标志。“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.”(标签是 moc 识别的特殊宏,可以为方法添加额外信息。目前,moc 不支持任何特殊标签。)最后一个字段“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"
};

同样,我们有属性和枚举的数量以及它们的偏移量。让我们先从属性开始。属性的数据由三个整数组成:名称、类型、标志。“name”字段当然是指名称。“type”字段是指属性的类型,在本例中是:“Priority”。最后,“flags”字段包含标志,这是列表。

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”是项的实际值。

为了从二进制文件中检索元数据信息,我为 IDA 编写了一个脚本。我用 Python 编写了脚本,这要归功于 IDAPython 插件。使用 Python 的原因有很多:用 IDC 脚本编写相同的代码将花费太多精力,而且 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”(位于“QtGui4”模块中)上对其进行了测试。结果如下。

-------------------------------------------------
--- Qt MetaData Displayer by Daniel Pistelli
--- metadata of the class: QWidget

--- Signals:

0 - protected: void customContextMenuRequested(QPoint pos)

--- Slots:

1 - public:    void setEnabled(bool)
2 - public:    void setDisabled(bool)
3 - public:    void setWindowModified(bool)
4 - public:    void setWindowTitle(QString)
5 - public:    void setStyleSheet(QString styleSheet)
6 - public:    void setFocus()
7 - public:    void update()
8 - public:    void repaint()
9 - public:    void setVisible(bool visible)
10 - public:    void setHidden(bool hidden)
11 - public:    void show()
12 - public:    void hide()
13 - public:    void setShown(bool shown)
14 - public:    void showMinimized()
15 - public:    void showMaximized()
16 - public:    void showFullScreen()
17 - public:    void showNormal()
18 - public:    bool close()
19 - public:    void raise()
20 - public:    void lower()
21 - protected: void updateMicroFocus()
22 - private:   void _q_showIfNotHidden()

--- Other Methods:


--- Properties:

0 - bool modal
    flags: Readable | Designable | Scriptable | Stored | ResolveEditable
1 - Qt::WindowModality windowModality
    flags: Readable | Writable | EnumOrFlag | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
2 - bool enabled
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
3 - QRect geometry
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
4 - QRect frameGeometry
    flags: Readable | Designable | Scriptable | Stored | ResolveEditable
5 - QRect normalGeometry
    flags: Readable | Designable | Scriptable | Stored | ResolveEditable
6 - int x
    flags: Readable | Designable | Scriptable | Stored | ResolveEditable
7 - int y
    flags: Readable | Designable | Scriptable | Stored | ResolveEditable
8 - QPoint pos
    flags: Readable | Writable | Scriptable | ResolveEditable
9 - QSize frameSize
    flags: Readable | Designable | Scriptable | Stored | ResolveEditable
10 - QSize size
    flags: Readable | Writable | Scriptable | ResolveEditable
11 - int width
    flags: Readable | Designable | Scriptable | Stored | ResolveEditable
12 - int height
    flags: Readable | Designable | Scriptable | Stored | ResolveEditable
13 - QRect rect
    flags: Readable | Designable | Scriptable | Stored | ResolveEditable
14 - QRect childrenRect
    flags: Readable | Designable | Scriptable | Stored | ResolveEditable
15 - QRegion childrenRegion
    flags: Readable | Designable | Scriptable | Stored | ResolveEditable
16 - QSizePolicy sizePolicy
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
17 - QSize minimumSize
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
18 - QSize maximumSize
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
19 - int minimumWidth
    flags: Readable | Writable | StdCppSet | Scriptable | ResolveEditable
20 - int minimumHeight
    flags: Readable | Writable | StdCppSet | Scriptable | ResolveEditable
21 - int maximumWidth
    flags: Readable | Writable | StdCppSet | Scriptable | ResolveEditable
22 - int maximumHeight
    flags: Readable | Writable | StdCppSet | Scriptable | ResolveEditable
23 - QSize sizeIncrement
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
24 - QSize baseSize
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
25 - QPalette palette
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
26 - QFont font
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
27 - QCursor cursor
    flags: Readable | Writable | Resettable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
28 - bool mouseTracking
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
29 - bool isActiveWindow
    flags: Readable | Designable | Scriptable | Stored | ResolveEditable
30 - Qt::FocusPolicy focusPolicy
    flags: Readable | Writable | EnumOrFlag | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
31 - bool focus
    flags: Readable | Designable | Scriptable | Stored | ResolveEditable
32 - Qt::ContextMenuPolicy contextMenuPolicy
    flags: Readable | Writable | EnumOrFlag | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
33 - bool updatesEnabled
    flags: Readable | Writable | StdCppSet | Scriptable | Stored | ResolveEditable
34 - bool visible
    flags: Readable | Writable | StdCppSet | Scriptable | Stored | ResolveEditable
35 - bool minimized
    flags: Readable | Designable | Scriptable | Stored | ResolveEditable
36 - bool maximized
    flags: Readable | Designable | Scriptable | Stored | ResolveEditable
37 - bool fullScreen
    flags: Readable | Designable | Scriptable | Stored | ResolveEditable
38 - QSize sizeHint
    flags: Readable | Designable | Scriptable | Stored | ResolveEditable
39 - QSize minimumSizeHint
    flags: Readable | Designable | Scriptable | Stored | ResolveEditable
40 - bool acceptDrops
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
41 - QString windowTitle
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
42 - QIcon windowIcon
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
43 - QString windowIconText
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
44 - double windowOpacity
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
45 - bool windowModified
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
46 - QString toolTip
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
47 - QString statusTip
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
48 - QString whatsThis
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
49 - QString accessibleName
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
50 - QString accessibleDescription
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
51 - Qt::LayoutDirection layoutDirection
    flags: Readable | Writable | Resettable | EnumOrFlag | StdCppSet | Designable | 
           Scriptable | Stored | ResolveEditable
52 - bool autoFillBackground
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
53 - QString styleSheet
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
54 - QLocale locale
    flags: Readable | Writable | Resettable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable
55 - QString windowFilePath
    flags: Readable | Writable | StdCppSet | Designable | Scriptable | 
           Stored | ResolveEditable

--- Enums:

-------------------------------------------------

与“QWidget”源代码相比,输出是 100% 正确的。

现在,我将向您展示如何查找类的元数据。让我们考虑“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

上面的汇编的最后一条指令设置了 vtable 指针。“Q_OBJECT”类的 vtable 如下所示。

.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”的地址从 vtable 获取。在分析此方法代码之前,有必要考虑这个枚举。

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 调用。switch 由最后的指令表示。让我们跟随“_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 块解析器,并将其与 Qt MetaData Parser 脚本结合使用,以便自动解析所有方法。但是,switch 块是高度依赖于平台和编译器的;因此,该脚本需要经常修改。此外,小程序有时会内联到“qt_metacall”方法中,在编写此类脚本时应考虑这一点。

结论

这篇文章我只花了一天时间,写得很愉快。虽然这是一篇简单的文章,但有些人可能会觉得它有用,因为在我看来,Qt 框架将被越来越多的软件开发人员使用。

© . All rights reserved.