-
-
[原创]Qt5--相对qt4的改动以及通过信号查找槽函数的方法
-
2021-3-7 00:26 4711
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | 关于Qt的槽函数与信号的组织逻辑可以参考站内文章“QT内部机制及逆向”和https: / / blog. 51cto .com / 9291927 / 2070398 。珠玉在前,我做的只是锦上添花 QT中最重要的结构莫过于QMetaObject和QObjectPrivate。前者保存了一个QObject的所有函数名,参数名,参数类型以及属性。后者是查找槽函数的重要途径。 一个简单的类作为测试 class MainWindow : public QObject { Q_OBJECT private: int value; public: explicit MainWindow(); ~MainWindow(); signals: int Value(); public slots: void ChangeValue( int value){ this - >value = value; } }; Qt官方的源码中有一个重要的函数metaObject为我们提供了查找QMetaObject的途径。 virtual const QMetaObject * metaObject() const; metaObject是一个虚函数,存在于QObject虚函数表的第一项,通过他,就可以查找到QMetaObject的地址。 QMetaObject在内存中的结构为 struct { / / private data const QMetaObject * superdata; const char * stringdata; const uint * data; const QMetaObject * * extradata; } ; const QMetaObject MainWindow::staticMetaObject = { { &QObject::staticMetaObject, qt_meta_stringdata_MainWindow.data, qt_meta_data_MainWindow, qt_static_metacall, nullptr, nullptr} }; superdata是指向父类的QMetaObject的指针,stringdata为字面元数据,data保存着关于函数及参数个数,属性等信息。extradata被填充为qt_static_metacall,qt的所有信号通过activate路由到这里,并找到对应的槽函数执行。 qt5相对qt4有一点点小改动具体在QMetaObject的QMetaObject.stringdata中。由原来static const char qt_meta_stringdata_MainWindow [] = { "MainWindow\0SetValue\0\ChangeValue" }; 变为了 struct qt_meta_stringdata_MainWindow_t { QByteArrayData data[ 4 ]; char stringdata0[ 33 ]; }; static const qt_meta_stringdata_MainWindow_t static const qt_meta_stringdata_MainWindow_t qt_meta_stringdata_MainWindow = { { QT_MOC_LITERAL( 0 , 0 , 10 ), / / "MainWindow" QT_MOC_LITERAL( 1 , 11 , 5 ), / / "Value" QT_MOC_LITERAL( 2 , 17 , 0 ), / / "" QT_MOC_LITERAL( 3 , 18 , 11 ), / / "ChangeValue" QT_MOC_LITERAL( 4 , 30 , 5 ) / / "value" }, "MainWindow\0Value\0\0ChangeValue\0value" }; 不讨论源码,直接看在内存中的存储方式 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | 接下来发挥我们出色的数学能力, 0x404100 + 0x50 = 0x404150 ,刚好是QMetaObject.stringdata.stringdata0第一项的起始地址。那么QMetaObject.stringdata.data结构就可以表示为。 struct QMetaStringDataContent { dword sep; / / 分隔符 0xFFFFFFF dword len ; / / 字符串的长度 dword fill; / / 填充为 0 dword offset; / / 这里的偏移是相对当前项起始地址的偏移 }; 有了索引函数名的方法,那么如何分清楚槽和信号呢?大家有没有注意到QMetaObject.stringdata中有一个填充为 0 的项。 qt_meta_stringdata_MainWindow = { { QT_MOC_LITERAL( 0 , 0 , 10 ), / / "MainWindow" QT_MOC_LITERAL( 1 , 11 , 5 ), / / "Value" QT_MOC_LITERAL( 2 , 17 , 0 ), / / "" QT_MOC_LITERAL( 3 , 18 , 11 ), / / "ChangeValue" QT_MOC_LITERAL( 4 , 30 , 5 ) / / "value" }, 这就是分割信号和槽的关键。分割项的上面为信号,下面为槽,不过也不绝对,更可靠的方法是根据data结构中的signalCount来计算。然而在QMetaObject.stringdata中还保存着函数参数名干扰视线。那么如何分清楚参数名,接下来看data结构。data分为两个部分第一部分为目录也就是QMetaObjectPrivate。 static const uint qt_meta_data_MainWindow[] = { / / content: 7 , / / revision 0 , / / classname 0 , 0 , / / classinfo 2 , 14 , / / 函数个数,函数偏移 0 , 0 , / / properties 0 , 0 , / / enums / sets 0 , 0 , / / constructors 0 , / / flags 1 , / / signalCount / / signals: name, argc, parameters, tag, flags 1 , 0 , 24 , 2 , 0x06 / * Public * / , / / slots: name, argc, parameters, tag, flags 3 , 1 , 25 , 2 , 0x0a / * Public * / , / / signals: parameters QMetaType:: Int , / / slots: parameters QMetaType::Void, QMetaType:: Int , 4 , 0 / / eod }; 中间有两个被我标注的数据,其中函数个数为 2 ,函数偏移为 14 大家可以数一数,从第一项到signalCount正好 14 项,下面被注释为signal和slot的项,为函数签名,其中包含两个重要信息,name(函数名的索引),argc参数大小. 而name为QMetaObject.stringdata.data的索引,可以看到name并不是连续的中间正好可以放一个argc的大小。并且与QMetaObject.stringdata.data中的存储方式正好相同。 那么可以通过name索引QMetaObject.stringdata.data来分别查找函数名,通过signalCount来界定信号和槽,同过QMetaObject.stringdata.data的第一项是否为 0xFFFFFFFF 来界定QMetaObject.stringdata.data大小,而QMetaObject.stringdata.stringdata0的第一项为类名称。通过在是否存在QMetaObject.data的函数签名,判断函数名和参数名. 到此我们已经查找到了函数名以及参数,接下来就是索引槽函数了。 qt因为松耦合的关系导致非常难以找到槽函数。qt通过QObjectPrivate中的ConnectionList来保存对应的槽函数,可以有多个槽函数,这些槽函数通过链表存储,通过信号索引ConnectionList。如此这个链表就形成了一张极其庞大的网,横向是信号,纵向是连接的槽. |
1 | 那么先要找到QObjectPrivate.在x32dbg查找函数的时候我意外发现了一个导出函数。 |
1 | 这是一个静态函数,他被导出真的是意外之喜。通过这个函数我们可以查找到QObjectPrivate也就可以找到connectList。QObjectPrivate是QObject的私有成员,也就是说先得找到QObject。结合类在内存中的布局方式我们可以断定,我们可以直接把MainWindow传给QobjectPrivate::get.因为有些程序 x32dbg并没有解码QobjectPrivate::get的签名,所以我决定自己索引QobjectPrivate,不过QobjectPrivate的索引方式极其简单。 |
QObjectPrivate找到了接下来是ConnectionList。通过逆向addConnectionList函数,可以发现ConnectionList在QObjectPrivat+0x24处
1 | 那么接下来就是找到信号的索引计算方法了,qt中索引分为相对索引和本地索引,两个相加才能的出正确的索引。d = Qmetaobject |
int QMetaObjectPrivate::indexOfSignalRelative(const QMetaObject *baseObject,
const QByteArray &name, int argc,
const QArgumentType types)
{
int i = indexOfMethodRelative<MethodSignal>(baseObject, name, argc, types);
#ifndef QT_NO_DEBUG
const QMetaObject m = baseObject;
if (i >= 0 && m && m->d.superdata) {
int conflict = indexOfMethod(m->d.superdata, name, argc, types);
if (conflict >= 0) {
QMetaMethod conflictMethod = m->d.superdata->method(conflict);
qWarning("QMetaObject::indexOfSignal: signal %s from %s redefined in %s",
conflictMethod.methodSignature().constData(),
objectClassName(m->d.superdata), objectClassName(m));
}
}
#endif
return i;
}
template<int MethodType>
static inline int indexOfMethodRelative(const QMetaObject *baseObject,
const QByteArray &name, int argc,
const QArgumentType types)
{
for (const QMetaObject m = baseObject; m; m = m->d.superdata) {
Q_ASSERT(priv(m->d.data)->revision >= 7);
int i = (MethodType == MethodSignal)
? (priv(m->d.data)->signalCount - 1) : (priv(m->d.data)->methodCount - 1);
const int end = (MethodType == MethodSlot)
? (priv(m->d.data)->signalCount) : 0;
1 2 3 4 5 6 7 8 9 | for (; i > = end; - - i) { int handle = priv(m - >d.data) - >methodData + 5 * i; if (methodMatch(m, handle, name, argc, types)) { * baseObject = m; return i; } } } return - 1 ; |
}
static bool methodMatch(const QMetaObject m, int handle,
const QByteArray &name, int argc,
const QArgumentType types)
{
Q_ASSERT(priv(m->d.data)->revision >= 7);
if (int(m->d.data[handle + 1]) != argc)
return false;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | if (stringData(m, m - >d.data[handle]) ! = name) return false; int paramsIndex = m - >d.data[handle + 2 ] + 1 ; for ( int i = 0 ; i < argc; + + i) { uint typeInfo = m - >d.data[paramsIndex + i]; if (types[i]. type ()) { if (types[i]. type () ! = typeFromTypeInfo(m, typeInfo)) return false; } else { if (types[i].name() ! = typeNameFromTypeInfo(m, typeInfo)) return false; } } return true; |
}
1 | 上面为本地索引的计算,首先在本类中查找,查找不到就到父类中查找。本地索引为相对当前类的函数距离。 |
int QMetaObjectPrivate::signalOffset(const QMetaObject *m)
{
Q_ASSERT(m != 0);
int offset = 0;
1 2 3 | for (m = m - >d.superdata; m; m = m - >d.superdata) offset + = priv(m - >d.data) - >signalCount; return offset; |
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 这里是相对索引的计算,到此就计算出了信号索引。 接下来又是一个难题,如何通过信号索引connectionlist。 QObjectConnectionListVector * connectionLists; class QObjectConnectionListVector : public QVector<QObjectPrivate::ConnectionList>; QObjectPrivate::ConnectionList &operator[]( int at) { if (at < 0 ) return allsignals; return QVector<QObjectPrivate::ConnectionList>::operator[](at); } QObjectConnectionListVector仅仅只是具象化了QVector,而QVector只有一个成员QTypedArrayData,并且没有虚函数 class QObjectConnectionListVector : public QVector<QObjectPrivate::ConnectionList>; template< class T> QVector{ typedef QTypedArrayData<T> Data; Data * d; } QTypedArrayData继承自QArrayData QArrayData有四个重要的成员: QtPrivate::RefCount ref; / / 引用计数 int size; / / 大小 uint alloc : 31 ; / / 预分配的大小,与std::string的分配逻辑相同 uint capacityReserved : 1 ; / / 分配模式 qptrdiff offset; / / 相对数据起始地址偏移 |
enum AllocationOption {
CapacityReserved = 0x1,
#if !defined(QT_NO_UNSHARABLE_CONTAINERS)
Unsharable = 0x2,
#endif
RawData = 0x4,
Grow = 0x8,
Default = 0};
QArrayData *QArrayData::allocate(size_t objectSize, size_t alignment,
size_t capacity, AllocationOptions options) Q_DECL_NOTHROW
{
// Alignment is a power of two
Q_ASSERT(alignment >= Q_ALIGNOF(QArrayData)
&& !(alignment & (alignment - 1)));
1 2 | / / Don't allocate empty headers if (!(options & RawData) && !capacity) { |
#if !defined(QT_NO_UNSHARABLE_CONTAINERS)
if (options & Unsharable)
return const_cast<QArrayData *>(&qt_array_unsharable_empty);
#endif
return const_cast<QArrayData *>(&qt_array_empty);
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | size_t headerSize = sizeof(QArrayData); / / Allocate extra (alignment - Q_ALIGNOF(QArrayData)) padding bytes so we / / can properly align the data array. This assumes malloc is able to / / provide appropriate alignment for the header - - as it should! / / Padding is skipped when allocating a header for RawData. if (!(options & RawData)) headerSize + = (alignment - Q_ALIGNOF(QArrayData)); if (headerSize > size_t(MaxAllocSize)) return 0 ; size_t allocSize = calculateBlockSize(capacity, objectSize, headerSize, options); QArrayData * header = static_cast<QArrayData * >(::malloc(allocSize)); if (header) { quintptr data = (quintptr(header) + sizeof(QArrayData) + alignment - 1 ) & ~(alignment - 1 ); |
#if !defined(QT_NO_UNSHARABLE_CONTAINERS)
header->ref.atomic.store(bool(!(options & Unsharable)));
#else
header->ref.atomic.store(1);
#endif
header->size = 0;
header->alloc = capacity;
header->capacityReserved = bool(options & CapacityReserved);
header->offset = data - quintptr(header);
}
1 | return header; |
}
查看 QArrayData QArrayData::allocate的分配逻辑可以发现
QArrayData分为两个部分头和数据,数据连续分配,类似于windows的堆结构。
查看QTypedArrayData 的分配逻辑:
Q_REQUIRED_RESULT static QTypedArrayData allocate(size_t capacity,
AllocationOptions options = Default)
{
Q_STATIC_ASSERT(sizeof(QTypedArrayData) == sizeof(QArrayData));
return static_cast<QTypedArrayData *>(QArrayData::allocate(sizeof(T),
Q_ALIGNOF(AlignmentDummy), capacity, options));
}
1 2 | 他只是重用了QArrayData的分配逻辑。而QVector并没有改变任何分配逻辑,只是将任务交给QTypedArrayData ; 接下来查看QArrayData的索引逻辑,但是QArrayData并没有索引函数,只有一个data函数来索引第一个数据结构 |
void data()
{
Q_ASSERT(size == 0
|| offset < 0 || size_t(offset) >= sizeof(QArrayData));
return reinterpret_cast<char >(this) + offset;
}
const void data() const
{
Q_ASSERT(size == 0
|| offset < 0 || size_t(offset) >= sizeof(QArrayData));
return reinterpret_cast<const char >(this) + offset;
}
1 | 可以看到只是单纯的加上offset便得到了第一个数据的地址,而QTypedArrayData只是添加了迭代器,并没有重写QArrayData::data。 |
iterator begin(iterator = iterator()) { return data(); }
QVector更是只重用了QTypedArrayData的迭代器来制作operator[]。
1 2 | 通过源码观察结合类的布局可以推测出QVector查找数据的方法。 Qvector - >QTypedArrayData = QArrayData便可以找到数据存放地址,接下来验证这个猜测。 |
struct D{
DWORD A;
DWORD B;
};
int main(int argc, char *argv[])
{ D l;
l.A=0x123;
l.B=0x456;
QVector<D> d;
d.push_back(l);
d.push_back(l);
stringstream ss;
ss<<hex<<&d;
MessageBoxA(nullptr,ss.str().c_str(),"",MB_OK);
QApplication a(argc, argv);
MainWindow w;
w.Print();
return a.exec();
}
1 2 3 4 5 6 | 证明了我们的猜想是正确。下面是ConnectionList的结构和Connection结构的源码。 struct ConnectionList { ConnectionList() : first( 0 ), last( 0 ) {} Connection * first; Connection * last; }; |
struct Connection
{
QObject sender;
QObject receiver;
union {
StaticMetaCallFunction callFunction;
QtPrivate::QSlotObjectBase slotObj;
};
// The next pointer for the singly-linked ConnectionList
Connection nextConnectionList;
//senders linked list
Connection next;
Connection *prev;
QAtomicPointer<const int> argumentTypes;
QAtomicInt ref_;
ushort method_offset;
ushort method_relative;
uint signalindex : 27; // In signal range (see QObjectPrivate::signalIndex())
ushort connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
ushort isSlotObject : 1;
ushort ownArgumentTypes : 1;
Connection() : nextConnectionList(0), ref(2), ownArgumentTypes(true) {
//ref_ is 2 for the use in the internal lists, and for the use in QMetaObject::Connection
}
~Connection();
int method() const { Q_ASSERT(!isSlotObject); return method_offset + methodrelative; }
void ref() { ref.ref(); }
void deref() {
if (!ref_.deref()) {
Q_ASSERT(!receiver);
delete this;
}
}
};
关于Qt5的脚本暂时没有写好,我也没有想到一天就找到了索引槽函数的方法,接下来有机会会补上x32dbg qt5_resolver的脚本源码。