vs使用qt4.natvis

上一篇blog讲到了windbg使用qt4.natvis解析qt数据结构的方法,那么接下来就该轮到VS了。在VS中使用natvis会更加简单,只需要将qt4.nativs拷贝到%VSINSTALLDIR%\Common7\Packages\Debugger\Visualizers即可。当然,这篇blog不可能只写这么点东西。qt4.natvis很好,它解析了很多QT的数据结构,比如QMap,QPoint,QVector,QMatrix等等,不过有一点很遗憾,它没有解析QObject。因为解析QObject可以获取对象的父子节点,这样很容易了解对象的树形结构。于是我这里对qt4.natvis进行了一点修改,添加了对QObject的解析:

1
2
3
4
5
6
<Type Name="QObject">
<DisplayString>{{{*(char **)(*(char **)(*(char **)this - 4) + 12) + 12,sb}}}</DisplayString>
<Expand>
<ExpandedItem>d_ptr.d->children</ExpandedItem>
</Expand>
</Type>

解释一下这段xml,<Type Name="QObject">"是指定匹配的对象类型名,这里当然就是QObject了。

1
<DisplayString>{{{*(char **)(*(char **)(*(char **)this - 4) + 12) + 12,sb}}}</DisplayString>

用于展示当前QObject的实际类型,也就是QObject的派生类名。这里采用的方法是获取虚表上的RTTICompleteObjectLocator对象指针,然后在获取TypeDescriptor。具体是怎么获取的那将是一篇长篇大论,这里就不展开了。最后<ExpandedItem>d_ptr.d->children</ExpandedItem>则是将其子对象展示出来,这样就能一目了然的指定其子对象的真实类型了。

20190801183332

从图中可以看到,除了str、str_list和str_map可以直接的看到数据之外,QMsgTest、QToolBar的子对象个数和类型也能一目了然。

Tips

windbg使用qt4.natvis

用windbg调试QT程序或者分析QT程序的dump是一件痛苦的事情,因为windbg缺少对QT基础数据的展示能力,比如:

1
2
3
4
5
6
7
8
int main(int argc, char *argv[])
{
QString str = "hello world";
QList<QString> str_list;
str_list.append(str);
QMap<int, QString> str_map;
str_map[11] = str;
}

这份代码使用windbg查看str,str_list或者str_map简直是一件折磨的事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0:000> dv
argc = 0n1
argv = 0x02cc5e00
str = class QString
str_list = class QList<QString>
str_map = class QMap<int,QString>
a = class QApplication
w = class QMsgTest
0:000> dx str
str [Type: QString]
[+0x000] d : 0x2cc5d80 [Type: QString::Data *]
0:000> dx str_list
str_list [Type: QList<QString>]
[+0x000] p [Type: QListData]
[+0x000] d : 0x2cc7258 [Type: QListData::Data *]
0:000> dx str_map
str_map [Type: QMap<int,QString>]
[+0x000] d : 0x2cc72b8 [Type: QMapData *]
[+0x000] e : 0x2cc72b8 [Type: QMapData::Node *]

可以看到,windbg只会告诉你类型,根本不会给你展示数据本身,如果要查看数据还得自己来算,以最复杂的QMap为例:

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
0:000> ?? str_map.d->forward[0]
struct QMapData * 0x031470f8
+0x000 backward : 0x03147068 QMapData
+0x004 forward : [12] 0x03147068 QMapData
+0x034 ref : QBasicAtomicInt
+0x038 topLevel : 0n-17891602
+0x03c size : 0n-17891602
+0x040 randomBits : 0xfeeefeee
+0x044 insertInOrder : 0y0
+0x044 sharable : 0y1
+0x044 strictAlignment : 0y1
+0x044 reserved : 0y11111110111011101111111011101 (0x1fdddfdd)

0:000> ?? sizeof(@!"qtmsgtest!QMapPayloadNode<int,QString>")-sizeof(qtmsgtest!QMapData::Node*)
unsigned int 8

0:000> dt qtmsgtest!QMapNode<int,QString> (0x31470f8-8)
+0x000 key : 0n11
+0x004 value : QString
+0x008 backward : 0x03147068 QMapData::Node
+0x00c forward : [1] 0x03147068 QMapData::Node

0:000> ?? ((qtmsgtest!QString*)0x31470f4)->d
struct QString::Data * 0x03145b30
+0x000 ref : QBasicAtomicInt
+0x004 alloc : 0n11
+0x008 size : 0n11
+0x00c data : 0x03145b42 -> 0x68
+0x010 clean : 0y0
+0x010 simpletext : 0y0
+0x010 righttoleft : 0y0
+0x010 asciiCache : 0y0
+0x010 capacity : 0y0
+0x010 reserved : 0y11001101110 (0x66e)
+0x012 array : [1] 0x68

0:000> du @@C++(((qtmsgtest!QString*)0x31470f4)->d->data)
03145b42 "hello world"

你们看,为了获取key=11、value=”hello world”需要这么折腾一通,如果数据多了那不得抓狂。

不过幸运了是windbg现在支持natvis了,简单的说就是通过natvis里的配置自动解析和符号匹配的数据结构。接下来要做的就是找一个好用的qt的natvis了。我这里找了一个qt配合vs2012里的qt4.natvis,让我们看看加载后的效果:

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
0:000> .nvload qt4.natvis
Successfully loaded visualizers in "C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\Visualizers\qt4.natvis"
0:000> dv
argc = 0n1
argv = 0x02cc5e00
str = hello world
str_list = { size = 1 }
str_map = { size = 1 }
a = class QApplication
w = class QMsgTest
0:000> dx str
str : hello world [Type: QString]
[<Raw View>] [Type: QString]
[size] : 11 [Type: int]
[referenced] : 3 [Type: long]
[0] : 0x68 [Type: unsigned short]
[1] : 0x65 [Type: unsigned short]
[2] : 0x6c [Type: unsigned short]
[3] : 0x6c [Type: unsigned short]
[4] : 0x6f [Type: unsigned short]
[5] : 0x20 [Type: unsigned short]
[6] : 0x77 [Type: unsigned short]
[7] : 0x6f [Type: unsigned short]
[8] : 0x72 [Type: unsigned short]
[9] : 0x6c [Type: unsigned short]
[10] : 0x64 [Type: unsigned short]
0:000> dx str_list
str_list : { size = 1 } [Type: QList<QString>]
[<Raw View>] [Type: QList<QString>]
[referenced] : 1 [Type: long]
[0x0] : hello world [Type: QString]
0:000> dx str_map
str_map : { size = 1 } [Type: QMap<int,QString>]
[<Raw View>] [Type: QMap<int,QString>]
[referenced] : 1 [Type: long]
[0x0] : 0x2cc7340 : (11, hello world) [Type: QMapNode<int,QString> *]

看到了么,无论是str,str_list还是str_map都直接打印出了内部的数据,无需大费周章的折腾,这简直是windbg爱好者调试qt程序的神器。

最后附上qt4.natvis的地址:https://gist.github.com/gregseth/9bcd0112f8492fa7bfe7

Tips

Dump QT objects

在QT中,QObject有两个函数dumpObjectInfo()dumpObjectTree()分别用于dump相关对象树形结构和相关的连接信息。不过这两个函数有个共同的问题,只能在debug模式下使用。因为在Release模式下,这两个函数不做任何事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void dumpRecursive(int level, QObject *object)
{
#if defined(QT_DEBUG)
...
#else
Q_UNUSED(level)
Q_UNUSED(object)
#endif
}

void QObject::dumpObjectTree()
{
dumpRecursive(0, this);
}

void QObject::dumpObjectInfo()
{
#if defined(QT_DEBUG)
...
#endif
}

为了能在Release模式下使用这两个函数,其中一个办法是删除#if defined(QT_DEBUG)宏,然后重新编译qtcore4.dll。不过重新编译QT需要一些准备工作,而且还需要较长的一段编译时间,所以我果断放弃了这种方法。

第二种想到的方法是自己实现dumpObjectInfo()dumpObjectTree()这两个函数。实际上,实现dumpObjectTree()并不是一件难事,qDebug()本身就能dump QObject了,我们需要做的就是递归遍历对象节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void dumpObjects(const QObjectList &objs, int nIndent = 0)
{
QString strIndent;
for (int i = 0; i < nIndent; i++) {
strIndent.append(" ");
}
Q_FOREACH(const QObject *obj, objs)
{
qDebug() << strIndent.toAscii().data() << obj;
if (!obj->children().isEmpty()) {
dumpObjects(obj->children(), nIndent + 1);
}
}
}

在想要遍历QT对象时,我们只需要将对象的子节点列表传入函数即可:

1
2
QMsgTest w;
dumpObjects(w.children());

输出的数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
QMainWindowLayout(0x36ba208, name = "_layout") 
QRubberBand(0x36ba778, name = "qt_rubberband")
QMenuBar(0x36bab78, name = "menuBar")
QToolButton(0x36bae88, name = "qt_menubar_ext_button")
QToolBar(0x36bb820, name = "mainToolBar")
QToolBarLayout(0x36bbb50)
QToolBarExtension(0x36bbc80, name = "qt_toolbar_ext_button")
QAction(0x36bc5a0)
QPropertyAnimation(0x36bcf90)
QPropertyAnimation(0x35098f8)
QWidget(0x36bcac0, name = "centralWidget")
QPropertyAnimation(0x36bbed8)
QPropertyAnimation(0x3509cf0)
QStatusBar(0x36bcd70, name = "statusBar")
QSizeGrip(0x36bbac0)
QHBoxLayout(0x36b3ad0)
QVBoxLayout(0x36bd5d8)
QHBoxLayout(0x36bd818)

怎么样,是不是很容易实现。不过接下来就要泼一盆冷水了,因为dumpObjectInfo()函数就没有那么容易实现了。主要原因是dumpObjectInfo()函数中有大量的依赖关系,如果单纯的扣代码过来牵扯会非常广,所以这个方法似乎也进行不下去了。

最后,我想到了第三种方法,在Release模式下加载Debug版本的qtcored4.dll,获取其函数地址并且直接调用它。

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
PVOID dumpFunc = NULL;
void* (*myInstallMsgHandler)(void* h) = NULL

void myMsgHandler(QtMsgType t, const char* str)
{
OutputDebugStringW(QString("%1\n").arg(str).utf16());
}

void Init()
{
HMODULE h = LoadLibraryW(L"qtcored4.dll");
dumpFunc = GetProcAddress(h, "[email protected]@@QAEXXZ");
*(void **)&myInstallMsgHandler = GetProcAddress(h, "[email protected]@[email protected]@[email protected]@[email protected]");
myInstallMsgHandler(myMsgHandler);
}

void dumpObjInfo(void *obj)
{
__asm {
mov ecx, obj
call dumpFunc;
}
}

int main(int argc, char *argv[])
{
Init();

QApplication a(argc, argv);
QMsgTest w;
w.show();
dumpObjInfo(&w);
return a.exec();
}

解释一下这段代码,首先Init函数是用来加载qtcored4.dll以及初始化相关的函数,这里GetProcAddress获取的函数分别是qInstallMsgHandlerdumpObjectInfo,之所以代码中的函数名这么复杂是因为C++使用的Decorated Name规则导致的,可以用一些PE工具查看导出函数来获取这个名字。另外我们看到,出了获取dumpObjectInfo函数外,还获取了qInstallMsgHandler函数。因为我们需要使用这个函数注册输出调试信息的函数,在代码中是myMsgHandler。最后,为了方便的调用dumpObjectInfo的函数指针,我采用了内嵌汇编的方式。因为dumpObjectInfo是成员函数,所以肯定是thiscall,于是将obj赋予ecx寄存器并且调用函数指针即可。

编译运行可以看到输出结果:

1
2
3
4
5
6
7
8
9
10
11
BJECT QMsgTest::QMsgTestClass
SIGNALS OUT
signal: destroyed(QObject*)
signal: destroyed()
signal: customContextMenuRequested(QPoint)
signal: iconSizeChanged(QSize)
--> QToolBar::mainToolBar _q_updateIconSize(QSize)
signal: toolButtonStyleChanged(Qt::ToolButtonStyle)
--> QToolBar::mainToolBar _q_updateToolButtonStyle(Qt::ToolButtonStyle)
SIGNALS IN
<None>

当然了,内嵌汇编不是一个好的代码风格,这里只是为了快速验证方案的可行性。更符合C++语法习惯的方式应该是声明一个成员函数指针,然后用GetProcAddress获取对应的函数地址对其赋值,最后调用成员函数指针。具体怎么实现仁者见仁智者见智,我这篇blog只是抛砖引玉。

Tips

windbg监听QT事件

Windows平台下做过界面开发的程序员肯定知道窗口界面是由消息驱动的。为了能方便的调试窗口消息循环,Windows提供了spy++这样的工具帮助我们监控消息。但是Windows的消息在QT的程序上可能只有部分有用。所以我们还需要一个监控QT窗口事件的工具,不过可惜的是我并没有找到这样现成的工具。于是我用Windbg的断点命令写了一个。

不过写断点命令之前必须先搞清楚在QT中事件都是怎么发出来的。经过调试确认了几个函数,分别是:

1
2
3
QCoreApplication::sendEvent
QCoreApplication::sendSpontaneousEvent
QCoreApplication::postEvent

接下来我们可以对它们下断点了。当然,这里不是直接了当下断点那么的简单。我们需要在断点中插入命令,让断点命中后打印我们想要的内容,然后继续运行。我这里的写法是:

1
2
3
4
bm /( qtcored4!QCoreApplication::postEvent ".printf \"P  \";?? @@C++(static_cast<QtGuid4!QEvent::Type>(event->t));g"
bc 2
bm /( qtcored4!QCoreApplication::sendEvent ".printf \"S \";?? @@C++(static_cast<QtGuid4!QEvent::Type>(event->t));g"
bm /( qtcored4!QCoreApplication::sendSpontaneousEvent ".printf \"SS \";?? @@C++(static_cast<QtGuid4!QEvent::Type>(event->t));g"

在windbg中执行后,运行程序就能看到事件一个一个的打印出来了,而且打印出来的并不是冷冰冰的事件数字,而是数字所代表的含义:

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
SS QEvent::Type WindowDeactivate (0n25)
P QEvent::Type UpdateRequest (0n77)
S QEvent::Type WindowDeactivate (0n25)
S QEvent::Type WindowDeactivate (0n25)
SS QEvent::Type ActivationChange (0n99)
SS QEvent::Type ApplicationDeactivate (0n122)
S QEvent::Type UpdateRequest (0n77)
SS QEvent::Type Paint (0n12)
SS QEvent::Type Paint (0n12)
SS QEvent::Type Paint (0n12)
SS QEvent::Type Paint (0n12)
SS QEvent::Type Paint (0n12)
SS QEvent::Type Paint (0n12)
SS QEvent::Type Paint (0n12)
SS QEvent::Type ApplicationActivate (0n121)
SS QEvent::Type WindowActivate (0n24)
P QEvent::Type UpdateRequest (0n77)
S QEvent::Type WindowActivate (0n24)
S QEvent::Type WindowActivate (0n24)
S QEvent::Type WindowActivate (0n24)
SS QEvent::Type ActivationChange (0n99)
S QEvent::Type UpdateRequest (0n77)
SS QEvent::Type Paint (0n12)
SS QEvent::Type Paint (0n12)
SS QEvent::Type Paint (0n12)
SS QEvent::Type Paint (0n12)
SS QEvent::Type Paint (0n12)
SS QEvent::Type Paint (0n12)
SS QEvent::Type Paint (0n12)
SS QEvent::Type Paint (0n12)
SS QEvent::Type Paint (0n12)
SS QEvent::Type Paint (0n12)
SS QEvent::Type NonClientAreaMouseMove (0n173)
S QEvent::Type Enter (0n10)
S QEvent::Type Enter (0n10)
SS QEvent::Type MouseMove (0n5)
S QEvent::Type Leave (0n11)
S QEvent::Type Enter (0n10)
SS QEvent::Type MouseMove (0n5)
SS QEvent::Type MouseMove (0n5)
SS QEvent::Type MouseMove (0n5)
SS QEvent::Type MouseMove (0n5)
SS QEvent::Type MouseMove (0n5)
SS QEvent::Type MouseMove (0n5)
S QEvent::Type Leave (0n11)
S QEvent::Type Leave (0n11)
SS QEvent::Type WindowDeactivate (0n25)
P QEvent::Type UpdateRequest (0n77)
S QEvent::Type WindowDeactivate (0n25)
S QEvent::Type WindowDeactivate (0n25)
S QEvent::Type WindowDeactivate (0n25)
SS QEvent::Type ActivationChange (0n99)
SS QEvent::Type ApplicationDeactivate (0n122)
S QEvent::Type UpdateRequest (0n77)

Tips

一个时间同步脚本

家里有台电脑不知道为啥一直同步不了网络时间,每隔一段时间就慢个几分钟,于是在网上找了点资料和代码拼凑了一个用于Windows时间同步的python脚本。

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
61
62
63
64
65
# -*- coding: utf-8 -*-
"""
@author: 0cch
"""

from socket import *
import struct
import sys
import time
import datetime
from ctypes import Structure, windll, pointer
from ctypes.wintypes import WORD

class SYSTEMTIME(Structure):
_fields_ = [
( 'wYear', WORD ),
( 'wMonth', WORD ),
( 'wDayOfWeek', WORD ),
( 'wDay', WORD ),
( 'wHour', WORD ),
( 'wMinute', WORD ),
( 'wSecond', WORD ),
( 'wMilliseconds', WORD ),
]
SetLocalTime = windll.kernel32.SetLocalTime

NTP_SERVER = "pool.ntp.org"
TIME1970 = 2208988800

clientSocket = socket(AF_INET, SOCK_DGRAM)
clientSocket.settimeout(10)

portNumber = 123
def sntp_client():
data = '\x1b' + 47 * '\0'
clientSocket.sendto( data.encode('utf-8'), ( NTP_SERVER, portNumber ))
data, address = clientSocket.recvfrom(1024)
if data:
print ('Response received from:', address)
t_time = struct.unpack( '!12I', data )[10]
t_time -= TIME1970

dt = datetime.datetime.fromtimestamp( t_time )

dt_tuple = dt.timetuple()
st = SYSTEMTIME()
st.wYear = dt_tuple.tm_year
st.wMonth = dt_tuple.tm_mon
st.wDayOfWeek = ( dt_tuple.tm_wday + 1 ) % 7
st.wDay = dt_tuple.tm_mday
st.wHour = dt_tuple.tm_hour
st.wMinute = dt_tuple.tm_min
st.wSecond = dt_tuple.tm_sec
st.wMilliseconds = 0

ret = SetLocalTime( pointer( st ) )
if ret == 0:
print( 'Setting failed. Try as administrator.' )
else:
print( 'Successfully.' )

clientSocket.close()

if __name__ == '__main__':
sntp_client()

Tips

C++11新增预定义的宏

C++11中新增了4个预定义的宏,他们分别为__STDC____STDC_HOSTED____STDC_VERSION____STDC_ISO_10646__

  1. __STDC__用于指示编译器是否支持ISO标准C语言,如果支持ISO标准C语言则_STDC__定义为1,否为定义为0。这个宏在不同的编译器上可能有不同的定义,甚至有未定义的情况。例如在GCC上,编译并输出该宏的值为1,而在Visual Studio C++上,默认情况下该宏处于未定义状态。

  2. __STDC_HOSTED__用于指示宿主环境是否具有标准C库的完整功能,如果具有标准C库的完整功能则__STDC_HOSTED__定义为1,否为定义为0。

  3. __STDC_VERSION__用于定义C标准的版本号,但是标准文档中并没有明确规定其实现,所以在很多编译器中这个宏处于未定义状态。

  4. __STDC_ISO_10646__用于指示wchar_t是否使用Unicode,如果使用Unicode那么wchar_t展开为yyyymmL的形式。

编译运行下面这段代码用于检测这些宏的定义状态:

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
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
cout << "__cplusplus is " << __cplusplus << endl;
cout << "__DATE__ is " << __DATE__ << endl;
cout << "__FILE__ is " << __FILE__ << endl;
cout << "__LINE__ is " << __LINE__ << endl;
#ifdef __STDC_HOSTED__
cout << "__STDC_HOSTED__ is " << __STDC_HOSTED__ << endl;
#else
cout << "__STDC_HOSTED__ is not defined" << endl;
#endif
cout << "__TIME__ is " << __TIME__ << endl;
#ifdef __STDC__
cout << "__STDC__ is " << __STDC__ << endl;
#else
cout << "__STDC__ is not defined" << endl;
#endif
#ifdef __STDC_MB_MIGHT_NEQ_WC__
cout << "__STDC_MB_MIGHT_NEQ_WC__ is " << __STDC_MB_MIGHT_NEQ_WC__ << endl;
#else
cout << "__STDC_MB_MIGHT_NEQ_WC__ is not defined" << endl;
#endif
#ifdef __STDC_VERSION__
cout << "__STDC_VERSION__ is " << __STDC_VERSION__ << endl;
#else
cout << "__STDC_VERSION__ is not defined" << endl;
#endif
#ifdef __STDC_ISO_10646__
cout << "__STDC_ISO_10646__ is " << __STDC_ISO_10646__ << endl;
#else
cout << "__STDC_ISO_10646__ is not defined" << endl;
#endif
#ifdef __STDCPP_DEFAULT_NEW_ALIGNMENT__
cout << "__STDCPP_DEFAULT_NEW_ALIGNMENT__ is " << __STDCPP_DEFAULT_NEW_ALIGNMENT__ << endl;
#else
cout << "__STDCPP_DEFAULT_NEW_ALIGNMENT__ is not defined" << endl;
#endif
#ifdef __STDCPP_STRICT_POINTER_SAFETY__
cout << "__STDCPP_STRICT_POINTER_SAFETY__ is " << __STDCPP_STRICT_POINTER_SAFETY__ << endl;
#else
cout << "__STDCPP_STRICT_POINTER_SAFETY__ is not defined" << endl;
#endif
#ifdef __STDCPP_THREADS__
cout << "__STDCPP_THREADS__ is " << __STDCPP_THREADS__ << endl;
#else
cout << "__STDCPP_THREADS__ is not defined" << endl;
#endif
}

Tips

在Windows中编译GCC

最近心血来潮想体验下C++2a的标准,但是mingw中的GCC最新版本是8.2。于是乎就产生了编译thunk下最新代码的想法。

要在Windows上编译GCC没有在linux上方便,但是也是可以完成的。首先我们需要一个mingw的环境。自带mingw环境的软件有很多,这里我比较推荐MSYS2,因为这个环境更新的比较快。下载安装好了以后,运行MSYS2,会弹出类似linux终端窗口。这里我们首先下载需要的开发编译环境:

pacman -S –needed mingw-w64-i686-toolchain mingw-w64-x86_64-toolchain

下载安装好了开发环境后,可以下载GCC源代码:

mkdir gcc-latest
cd gcc-latest
git clone git://gcc.gnu.org/git/gcc.git

源代码比较大(2个多G),下载需要一点耐心。
源代码下载完成后,创建编译目录:

mkdir build

在开始编译之前,有一个坑需要注意下,我们需要将usr/bin/下的makeinfo改名。不知道为什么,最新的makeinfo对gcc的texi文件不兼容会导致编译失败。这个操作之后就可以开始配置了。

../gcc/configure –enable-languages=c,c++ –disable-multilib –disable-bootstrap –disable-werror –disable-nls –prefix=/mingw64/gcc-latest –build=x86_64-w64-mingw32 –host=x86_64-w64-mingw32 –target=x86_64-w64-mingw32 –with-native-system-header-dir=/mingw64/x86_64-w64-mingw32/include –with-arch=x86-64 MAKEINFO=missing
export LIBRARY_PATH=/mingw64/x86_64-w64-mingw32/lib

配置的选项比较多,我们可以参考GCC文档进行参照。这里的配置是我尝试后感觉必须加入的,否则编译的时候会出相应的问题。可以说是编译GCC血泪史了。
再然后就可以开始编译了:

make -j 16

使用多线程编译,编译速度会快不少。编译好了以后就可以安装了,注意安装的时候可能会遇到失败,需要注释build/gcc/makefile中,对应s-tm-texi的相关代码。

make install

最后检查gcc版本号:

gcc -v

注意目前最新的版本号为9.0.1。

Tips

在循环中使用CComPtr需要特别注意

CComPtr是ATL里提供给我们管理COM接口的智能指针类,使用它能够让我们无需关心接口的引用释放。例如:

1
2
CComPtr<IShellFolder> pDesktop;
SHGetDesktopFolder(&pDesktop);

但是这个类在循环中使用的时候要特别注意一下,例如:

1
2
3
4
5
6
7
8
9
10
11
// 没有问题
for (...) {
CComPtr<IShellFolder> pDesktop;
SHGetDesktopFolder(&pDesktop);
}

// 有问题,接口没有调用Release,内存泄露
CComPtr<IShellFolder> pDesktop;
for (...) {
SHGetDesktopFolder(&pDesktop);
}

其根本原因是,CComPtr的&操作符重载的时候没有做释放操作,只有Debug版本的assert来提醒程序员这样使用的问题。

1
2
3
4
5
T** operator&() throw()
{
ATLASSERT(p==NULL);
return &p;
}

所以我们需要手动调用Release

1
2
3
4
5
6
CComPtr<IShellFolder> pDesktop;
for (...) {
SHGetDesktopFolder(&pDesktop);
......
pDesktop.Release();
}

注意,这里是调用CComPtr的Release成员函数,而不是其保护的接口对象的Release函数。

另外一个解决方案就是使用<comdef.h>里的_com_ptr_t,这个类对于上述情况做了更加合理的处理。

1
2
3
4
5
6
Interface** operator&() throw()
{
_Release();
m_pInterface = NULL;
return &m_pInterface;
}

可以看到这个类里,重载&操作符的函数会先调用Release,然后再取地址避免了内存泄漏的问题。

Tips

给程序内部菜单增加指定的explorer菜单

为了将explorer的右键菜单项的某个菜单增加到我们程序内部的菜单,我们需要做以下几件事情:

  1. 获得指定文件的IShellFolder
  2. 获得指定文件的IContextMenu
  3. 创建菜单A,并且把IContextMenu的内容填充到菜单A
  4. 查询菜单A,找到我们想要的菜单项
  5. 取出我们想要的菜单项的内容,填充到我们想要真正弹出的菜单B
  6. 弹出菜单B
  7. 用IContextMenu响应用户对菜单的选择

代码如下:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
CComPtr<IShellFolder> GetParentFolder(LPCWSTR szFolder)
{
CComPtr<IShellFolder> pDesktop;
SHGetDesktopFolder(&pDesktop);
if (NULL == pDesktop)
{
return NULL;
}

ULONG pchEaten = 0;
LPITEMIDLIST pidl = NULL;
DWORD dwAttributes = 0;
HRESULT hr = pDesktop->ParseDisplayName(NULL, NULL, (LPTSTR)szFolder, NULL, &pidl, NULL);
if (S_OK != hr)
{
return NULL;
}

CComPtr<IShellFolder> pParentFolder = NULL;
hr = pDesktop->BindToObject(pidl, NULL, IID_IShellFolder, (void**)&pParentFolder);
if (S_OK != hr)
{
CoTaskMemFree(pidl);
return NULL;
}

CoTaskMemFree(pidl);

return pParentFolder;
}

std::wstring GetDirectory(LPCWSTR szFile)
{
WCHAR szDrive[_MAX_DRIVE];
WCHAR szDir[_MAX_DIR];
WCHAR szFName[_MAX_FNAME];
WCHAR szExt[_MAX_EXT];
_wsplitpath_s(szFile, szDrive, szDir, szFName, szExt);

std::wstring strResult = szDrive;
strResult += szDir;
return strResult;
}

std::wstring GetFileNameWithExt(LPCWSTR szFile)
{
WCHAR szDrive[_MAX_DRIVE];
WCHAR szDir[_MAX_DIR];
WCHAR szFName[_MAX_FNAME];
WCHAR szExt[_MAX_EXT];
_wsplitpath_s(szFile, szDrive, szDir, szFName, szExt);

std::wstring strResult = szFName;
strResult += szExt;
return strResult;
}
void qtmenutest::ShowContextMenu(const QPoint &pos)
{
m_pContextMenu.Release();
QMenu contextMenu(tr("Context menu"), this);

std::wstring strFilePath = L"d:\\1.jpg";
CComPtr<IShellFolder> pParentFolder = GetParentFolder(GetDirectory(strFilePath.c_str()).c_str());
if (NULL == pParentFolder)
{
return;
}

std::wstring strFile = GetFileNameWithExt(strFilePath.c_str());
ULONG pchEaten = 0;
LPITEMIDLIST pidl = NULL;
DWORD dwAttributes = 0;
HRESULT hr = pParentFolder->ParseDisplayName(WId(), NULL, (LPWSTR)strFile.c_str(), &pchEaten, &pidl, &dwAttributes);
if (S_OK != hr)
{
return;
}


UINT refReversed = 0;
hr = pParentFolder->GetUIObjectOf(WId(), 1, (LPCITEMIDLIST *)&pidl, IID_IContextMenu, &refReversed, (void **)&m_pContextMenu);
if (S_OK != hr)
{
CoTaskMemFree(pidl);
return;
}

HMENU hMenu = CreatePopupMenu();
if (hMenu == NULL) {
CoTaskMemFree(pidl);
return;
}

m_pContextMenu->QueryContextMenu(hMenu, 0, 100, 200, CMF_EXPLORE | CMF_NORMAL);
int nMenuCount = GetMenuItemCount(hMenu);
HMENU hSubMenu = NULL;
WCHAR szMenuText[MAX_PATH];
for (int i = 0; i < nMenuCount; i++) {
GetMenuStringW(hMenu, i, szMenuText, MAX_PATH, MF_BYPOSITION);
if (wcsstr(szMenuText, L"some_ui_text")) {
hSubMenu = GetSubMenu(hMenu, i);
break;
}
}

QMenu *subMenu = contextMenu.addMenu(QString::fromWCharArray(szMenuText));
int nSubMenuCount = GetMenuItemCount(hSubMenu);
for (int i = 0; i < nSubMenuCount; i++) {
GetMenuStringW(hSubMenu, i, szMenuText, MAX_PATH, MF_BYPOSITION);
int nCmdId = GetMenuItemID(hSubMenu, i);
QAction *curAct = subMenu->addAction(QString::fromWCharArray(szMenuText));
if (curAct) {
curAct->setData(nCmdId);
}
}

QAction *selAct = contextMenu.exec(mapToGlobal(pos));
if (selAct) {
int nSelId = selAct->data().toInt();
if (nSelId) {
CMINVOKECOMMANDINFO info = {0};
info.cbSize = sizeof(CMINVOKECOMMANDINFOEX);
info.lpVerb = MAKEINTRESOURCEA(nSelId - 100);
m_pContextMenu->InvokeCommand(&info);
}
}

DestroyMenu(hMenu);
CoTaskMemFree(pidl);
return;
}

Tips

0cchext插件更新 1.0.19.1

最近把windbg插件0cchext升级到1.0.19.1,完善了autocmd命令,并且增加了accessmask,oledata和cppexcr命令。

autocmd更新:

现在支持全局自动运行命令,区分应用层调试和内核调试,并且区分了普通调试和DUMP分析,配置文件依然是插件同目录下的autocmd.ini。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[all]
? 88 * 66

[kernel]
!process 0 0 explorer.exe

[kernel dump]
!analyze -v

[notepad.exe]
.sympath+ c:\notepad_pdb
~*k

[calc.exe]
.sympath+ c:\calc_pdb
~*k

[calc.exe dump]
.excr

在[all]区间的命令,会在所有情况下执行;[kernel]区间的命令会在内核调试的情况下执行;[kernel dump]区间的命令会在内核调试dump的情况下执行;[app.exe]区间是在调试某exe的时候执行;最后[app.exe dump]命令会在调试指定exe的dump的时候执行。

accessmask命令:

这个命令很简单,就是查询权限标志的,例如

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
0:000> !accessmask process 0x1fffff
Access mask: 0x1fffff

Generic rights:
STANDARD_RIGHTS_READ (0x20000)
STANDARD_RIGHTS_WRITE (0x20000)
STANDARD_RIGHTS_EXECUTE (0x20000)
STANDARD_RIGHTS_REQUIRED (0xf0000)
STANDARD_RIGHTS_ALL (0x1f0000)
READ_CONTROL (0x20000)
DELETE (0x10000)
SYNCHRONIZE (0x100000)
WRITE_DAC (0x40000)
WRITE_OWNER (0x80000)

Specific rights:
PROCESS_QUERY_LIMITED_INFORMATION (0x1000)
PROCESS_SUSPEND_RESUME (0x800)
PROCESS_QUERY_INFORMATION (0x400)
PROCESS_SET_INFORMATION (0x200)
PROCESS_SET_QUOTA (0x100)
PROCESS_CREATE_PROCESS (0x80)
PROCESS_DUP_HANDLE (0x40)
PROCESS_VM_WRITE (0x20)
PROCESS_VM_READ (0x10)
PROCESS_VM_OPERATION (0x8)
PROCESS_CREATE_THREAD (0x2)
PROCESS_TERMINATE (0x1)
PROCESS_ALL_ACCESS (0x1fffff)

其中第一个参数是对象类型,比如process,thread,file;第二个参数则是具体要查询的值。

oledata命令:

这个命令是方便我们当前线程查询com和ole相关结构的命令,不需要参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0:000> !oledata
dt combase!tagSOleTlsData 0x0000019370ad0360
dx (combase!tagSOleTlsData *)0x0000019370ad0360
0:000> dt combase!tagSOleTlsData 0x0000019370ad0360
+0x000 pvThreadBase : (null)
+0x008 pSmAllocator : (null)
+0x010 dwApartmentID : 0x1e3d4
+0x014 dwFlags : 0x81
+0x018 TlsMapIndex : 0n0
+0x020 ppTlsSlot : 0x00000018`66fc9758 -> 0x00000193`70ad0360 Void
+0x028 cComInits : 3
+0x02c cOleInits : 0
+0x030 cCalls : 0
...

另外可以看调试COM的一个tip来简单了解以下这个命令的用途

cppexcrname命令:

这个命令用于查询C++ 异常名

1
2
3
4
5
6
7
8
9
10
0:000> .exr -1
ExceptionAddress: 74e61812 (KERNELBASE!RaiseException+0x00000062)
ExceptionCode: e06d7363 (C++ EH exception)
ExceptionFlags: 00000001
NumberParameters: 3
Parameter[0]: 19930520
Parameter[1]: 006ff46c
Parameter[2]: 00372294
0:000> !cppexcrname
Exception name: [email protected]@@

这个的详情可以参考C++异常的参数分析(0xE06D7363)

Tips