0CCh Blog

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的解析:

<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了。

<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的子对象个数和类型也能一目了然。

windbg使用qt4.natvis

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

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简直是一件折磨的事情。

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为例:

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,让我们看看加载后的效果:

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

Dump QT objects

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

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了,我们需要做的就是递归遍历对象节点:

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对象时,我们只需要将对象的子节点列表传入函数即可:

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

输出的数据如下:

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,获取其函数地址并且直接调用它。

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, "?dumpObjectInfo@QObject@@QAEXXZ");
*(void **)&myInstallMsgHandler = GetProcAddress(h, "?qInstallMsgHandler@@YAP6AXW4QtMsgType@@PBD@ZP6AX01@Z@Z");
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寄存器并且调用函数指针即可。

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

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只是抛砖引玉。

windbg监听QT事件

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

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

QCoreApplication::sendEvent
QCoreApplication::sendSpontaneousEvent
QCoreApplication::postEvent

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

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中执行后,运行程序就能看到事件一个一个的打印出来了,而且打印出来的并不是冷冰冰的事件数字,而是数字所代表的含义:

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)

一个时间同步脚本

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

# -*- 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()

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的形式。

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

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

在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。

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

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

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

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

// 没有问题
for (...) {
CComPtr<IShellFolder> pDesktop;
SHGetDesktopFolder(&pDesktop);
}

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

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

T** operator&() throw()
{
ATLASSERT(p==NULL);
return &p;
}

所以我们需要手动调用Release

CComPtr<IShellFolder> pDesktop;
for (...) {
SHGetDesktopFolder(&pDesktop);
......
pDesktop.Release();
}

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

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

Interface** operator&() throw()
{
_Release();
m_pInterface = NULL;
return &m_pInterface;
}

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

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

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

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

代码如下:

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

0cchext插件更新 1.0.19.1

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

autocmd更新:

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

[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命令:

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

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相关结构的命令,不需要参数。

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++ 异常名

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: .?AVexception@std@@

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