|
|
用户名:天蝎星座 笔名:Lionel 地区: 中国-上海 行业:其他 |
| 日 | 一 | 二 | 三 | 四 | 五 | 六 |
本blog记录了我情感的各个片断,写的都是些婆婆妈妈的伤心事,反映了我感性的一面。如果你更喜欢讨论技术,请访问EROSiON的THE OTHER SiDE,那是我理性的一面。“情感”与“理性”二字则刻画了一个活生生的人。
读书计划三
(作者置顶)
通过代码实现EXE文件图标的替换
最近,好多人问我如何通过写个小程序,动态替换可执行文件的图标。这个问题看起来虽小,但却涉及到很多问题。网上也只能找到一些零零散散的资料,却没有详细的指导性文档。所以我决定把这个问题写下来,以方便大家查阅。
EXE文件图标的替换有很多方法,例如用一个EXE文件的图标替换另外一个EXE文件的图标;用一个ICO文件内的图标替换EXE文件的图标。这两种情况替换的方法不太相同,下面会详细讨论。
EXE文件图标的替换更一般的情形,是PE(Portable Executable)文件图标的替换。只不过Windows操作系统只会显示EXE文件的图标罢了。但DLL、OCX等PE文件也都可以包含图标资源。下面我们从ICO文件格式说起,一步步讲解替换EXE文件图标的方法和原理。
对于一个扩展名是.ico的文件,大部分人会认为一个ICO文件里面只能包含一个图标。但事实上,一个ICO文件里面可以包含很多图标。而且,目前大部分ICO文件里面都包含有不同尺寸、不同色深的好几个图标。我们以MSN安装包里的msnmsn.ico为例,这个图标文件就包含了9个不同尺寸、不同色深的图标,如图所示:

图表 1 msnms.ico
这样做的目的,是为了保证不同的操作系统、不同的桌面色深,图标显示均可达到最佳效果。操作系统会选择并显示一个最合适的图标。Windows XP支持32位色的图标,Windows 2000最多只支持256色的图标。所以,如果我们开发的软件若要同时支持Windows XP和2000,那么为了达到视觉上的最佳效果,每一个ICO文件应至少包含两个图标,一个是32位色的,一个是256色的。
ICO文件头部结构定义如下:
typedef struct
{
WORD idReserved; // Reserved (must be 0)
WORD idType; // Resource Type (1 for icons)
WORD idCount; // How many images?
ICONDIRENTRY idEntries[1]; // An entry for each image (idCount of 'em)
} ICONDIR, *LPICONDIR;
idCount表示该ICO文件包含图标的数量,所以理论上,一个ICO文件最多可以包含65535个图标。接下来,是该文件所包含的每一个图标的描述。
typedef struct
{
BYTE bWidth; // Width, in pixels, of the image
BYTE bHeight; // Height, in pixels, of the image
BYTE bColorCount; // Number of colors in image (0 if >=8bpp)
BYTE bReserved; // Reserved ( must be 0)
WORD wPlanes; // Color Planes
WORD wBitCount; // Bits per pixel
DWORD dwBytesInRes; // How many bytes in this resource?
DWORD dwImageOffset; // Where in the file is this image?
} ICONDIRENTRY, *LPICONDIRENTRY;
ICONDIRENTRY中记录了每一个图标的尺寸、色深、图标资源占用的字节数。dwImageOffset是一个文件偏移地址,指向图标资源数据起始位置。至于每一个图标资源内部的具体格式,与本文关系不大,这里就不再详细介绍了。
二、PE文件中的图标保存格式
PE文件中的图标保存格式与.ico文件中图标的保存格式略有不同。PE文件中,把ICONDIR和图标资源作为两种资源类型分别保存,前者是RT_GROUP_ICON类型,后者是RT_ICON类型。为了与.ico文件中图标的保存格式做以区分,我们把PE文件中的图标保存格式重新定义如下:
// #pragmas are used here to insure that the structure's
// packing in memory matches the packing of the EXE or DLL.
#pragma pack( push )
#pragma pack( 2 )
typedef struct
{
WORD idReserved; // Reserved (must be 0)
WORD idType; // Resource type (1 for icons)
WORD idCount; // How many images?
GRPICONDIRENTRY idEntries[1]; // The entries for each image
} GRPICONDIR, *LPGRPICONDIR;
typedef struct
{
BYTE bWidth; // Width, in pixels, of the image
BYTE bHeight; // Height, in pixels, of the image
BYTE bColorCount; // Number of colors in image (0 if >=8bpp)
BYTE bReserved; // Reserved
WORD wPlanes; // Color Planes
WORD wBitCount; // Bits per pixel
DWORD dwBytesInRes; // how many bytes in this resource?
WORD nID; // the ID
} GRPICONDIRENTRY, *LPGRPICONDIRENTRY;
#pragma pack( pop )
这里有一个区别,就是在.ico文件中,ICONDIRENTRY结构最后一个成员dwImageOffset表示的是图标资源文件偏移地址。而PE文件中,GRPICONDIRENTRY结构最后一个成员nID表示的是图标的索引ID。
三、Windows API
Windows操作系统为我们提供了几个API函数,用来更新PE文件中资源的函数有:BeginUpdateResource, UpdateResource, EndUpdateResource。用来枚举PE文件中资源的函数有:EnumResourceTypes,EnumResourceNames,EnumResourceLanguages。具体的使用方法可以参见MSDN。
下面我们通过具体的例子,来验证上面的方案是否可行。
四、用一个EXE中的图标替换另外一个EXE文件的图标
在这个例子中,我们用Windows XP自带的记事本的图标替换计算器的图标。

图表 2 记事本图标

图表 3 计算器图标
下面代码演示了如何替换32x32 32bits的图标:
HMODULE hModule = ::LoadLibrary("notepad.exe");
HRSRC hResInfo = ::FindResource(hModule, MAKEINTRESOURCE(8), RT_ICON);
HGLOBAL hGlobal = ::LoadResource(hModule, hResInfo);
DWORD dwSize = ::SizeofResource(hModule, hResInfo);
void* pData = ::LockResource(hGlobal);
HANDLE hUpdate = ::BeginUpdateResource("calc.exe", FALSE);
VERIFY(::UpdateResource(hUpdate, RT_ICON, MAKEINTRESOURCE(7),
MAKELANGID(LANG_ENGLISH, SUBLANG_DEFAULT),
pData, dwSize));
VERIFY(::EndUpdateResource(hUpdate, FALSE));
VERIFY(::FreeLibrary(hModule));
大家肯定有个疑问,上面代码中MAKEINTRESOURCE(8)和MAKEINTRESOURCE(7)是怎么来的呢?其实索引8和7分别是notepad.exe和calc.exe中,32x32 32bits图标的索引。我们可以通过加载RT_GROUP_ICON资源,然后遍历GRPICONDIRENTRY中每一个图标的大小、色深,找到这个图标的索引。为了简便,这里直接写死的索引号,省略了这一动态查找的过程。
还有一个疑问应该就是MAKELANGID(LANG_ENGLISH, SUBLANG_DEFAULT)了。PE文件中,每一个资源都至少对应一种语言。因为我的操作系统是英文的,所以记事本和计算器中的图标资源语言也是英文的。对于简体中文Windows XP操作系统所自带的记事本和计算器,这个值应该是MAKELANGID(LANG_CHINESE, SUBLANG_SYS_DEFAULT)。
那么我们怎么才能知道一个PE文件中,图标资源的语言是什么呢?我们可以通过资源枚举API,枚举所有图标、语言。可以参考上面提到过的那几个API函数,并查阅MSDN获取这些函数的帮助文档。
我们用记事本32x32 32bits图标替换计算器同样尺寸、色深的图标后,效果如下,在Titles显示方式下,图标大小是48x48的,图标没有被改变:

图表 4 48x48图标
在Icons显示方式下,图标大小是32x32的,图标被我们改变了:

图表 5 32x32图标
五、用一个ICO文件中的图标替换另外一个EXE文件的图标
用ICO文件中的图标替换EXE文件图标稍微有点麻烦,我们必须借助数据结构ICONDIR和ICONDIRENTRY来完成。我们使用msnms.ico中的32x32 32bits图标替换计算器中同样大小色深的图标:
DWORD dwSize = sizeof(ICONDIRENTRY);
HANDLE hFile = ::CreateFile("msnms.ico", GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
::SetFilePointer(hFile, sizeof(ICONDIR) + dwSize * 6, NULL, FILE_BEGIN);
DWORD dwRead = 0;
ICONDIRENTRY Entry;
VERIFY(::ReadFile(hFile, &Entry, dwSize, &dwRead, NULL));
::SetFilePointer(hFile, Entry.dwImageOffset, NULL, FILE_BEGIN);
void* pData = new char[Entry.dwImageOffset];
VERIFY(::ReadFile(hFile, pData, Entry.dwBytesInRes, &dwRead, NULL));
HANDLE hUpdate = ::BeginUpdateResource("calc.exe", FALSE);
VERIFY(::UpdateResource(hUpdate, RT_ICON, MAKEINTRESOURCE(7),
MAKELANGID(LANG_ENGLISH, SUBLANG_DEFAULT),
pData, Entry.dwBytesInRes));
VERIFY(::EndUpdateResource(hUpdate, FALSE));
delete[] pData;
pData = NULL;
VERIFY(::CloseHandle(hFile));
上面代码中,sizeof(ICONDIR) + dwSize * 6的意思是定位到第8个图标结构体ICONDIRENTRY的位置,这个图标是32x32 32bits的。我们可以通过遍历每一个ICONDIRENTRY来判断,到底哪个图标是这个尺寸的。这里我们为了简便,把这部分代码省略了。
定位到第8个图标结构体ICONDIRENTRY的位置后,Entry.dwImageOffset的值就是第8个图标资源的文件偏移地址,Entry.dwBytesInRes的值是第8个图标图标资源的大小。然后我们将文件指针定位到Entry.dwImageOffset,并读取Entry.dwBytesInRes大小的数据到指针pData指向的内存当中。
最后,是替换文件图标资源的代码,这部分代码跟上一个例子是相同的。
Advanced Listening - Dealing with Stress
下面是DynEd高级听力中遇到的生词,记录下来,以便回顾:)
Part 1
1.scarlet fever 猩红热
2.malaria 疟疾
3.diabetes 糖尿病
4.alzheimer 老年痴呆
5.tuberculosis 肺结核
6.mammal 哺乳动物
7.hygiene 卫生学
8.turn of the century
9.bring across
喜多屋国际海鲜料理(正大店)自助攻略
部门集体腐败,选择了正大广场的喜多屋海鲜自助。因为晚上和周末价位比较高,200左右,所以选择了工作日的中午,120多块钱每人,比较划算。
喜多屋还算是比较大的,海鲜品种也比较齐全。每张桌子上有3个夹子,架子上有号码。吃比较昂贵的产品,比如霸王蟹、东星斑、鱼翅、鳗鱼等,需要把夹子给服务员,做好后服务员会帮忙送过来。所以,想每样海鲜都尝一遍,时间上恐怕来不及。
霸王蟹,我第一次吃,个头的确比普通的螃蟹大很多啊,非常大。但是味道一般,感觉蟹肉纤维比较粗,还不如普通螃蟹好吃了。但是来喜多屋,霸王蟹还是必吃的,外面恐怕买不到。而且霸王蟹看起来非常酷啊。

东星斑,喜多屋里最大的鱼了,味道也是一般般吧。需要夹子排队,一份量还是挺多的。
我还是比较喜欢吃烤鳗鱼,只要把夹子给服务员看一下,他就会把已经烤好的鳗鱼切几块给你。所以,最好是在吃需要夹子的东西前先去多弄点烤鳗鱼。别的都是菜做好了才会把夹子还给我们,防止我们不停的吃一样东西。不过大部分东西都不需要夹子的。
鱼翅,貌似我长这么大还是第一次吃鱼翅啊!可是分量非常少,一个夹子可以换一小碗。里面有点米粒,有点鱼翅。味道还是挺不错的,大家都说这个是酱油泡饭。
三文鱼,这个也是比较抢手的,在生鱼片那里有。基本上也是一上来就被强光了,别的生鱼片吃的人不多,而且上面没有标明是什么鱼。
鱼籽,个头挺大的,跟小时候吃的鱼肝油差不多,但是最好就着黄瓜一起吃,光吃鱼籽很腥的,味道不太好。
其它的还有好多鱼,海参也是非常便宜的那种,吃这些东西基本上吃不回来。饮料有葡萄汁、柳橙汁、西瓜汁、黄瓜汁、冰咖啡,还有两种沙冰。葡萄汁、柳橙汁、沙冰还比较好喝。西瓜、黄瓜不太好喝。
排队吃哈根达斯的人还是挺多的,而且刚开始的时候是服务员帮忙,一个人两个球。可是吃到后来,服务员说累的不行了,没力气挖冰淇淋了,所以就让我们自己挖。我们自己每人挖了不少,我一个人就挖了7个球,哈哈。所以刚开始最好别排队吃哈根达斯,留到后来服务员没劲了,我们再去排队自己挖比较好。
另外还有小点心和水果,我基本没吃,光吃海鲜了。吃完后同事评价基本上觉得是,去喜多屋尝尝鲜还不错,特别好吃的东西不多。但里面的确有好多外面吃不到的东西,算是小开眼界吧。
浅谈MFC内存泄露检测及内存越界访问保护机制
本文所有代码均在VC2008下编译、调试。如果您使用的编译器不同,结果可能会有差别,但本文讲述的原理对于大部分编译器应该是相似的。对于本文的标题,实在不知道用什么表示更恰当,因为本文不仅淡了内存泄露检测机制,也谈到了指针越界的检测机制。到底应该说是MFC的机制,还是C++的机制?Anyway,相信你看了一定会有所收获。并欢迎常来本博客http://lionel.bokee.com留言讨论。
在我们开发MFC应用程序的时候,不知大家是否注意到Debug版本输出窗口经常会有下面这样的信息:
Detected memory leaks!
Dumping objects ->
c:\my.data\my.codes\memleak\memleak\memleak.cpp(34) : {126} normal block at 0x00A321A0, 4 bytes long.
Data: < > 01 00 00 00
Object dump complete.
编译器是怎么知道我们写的代码有内存泄露并能精确到文件、行号的呢?事实上也并不是所有情况都能精确到文件、行号,看看下面这种情况:
Detected memory leaks!
Dumping objects ->
First-chance exception at 0x75c739e5 (kernel32.dll) in MemLeak.exe:
0xC0000005: Access violation reading location 0x711af9f4.
#File Error#(62) : {137} normal block at 0x00A721A0, 4 bytes long.
Data: < > CD CD CD CD
Object dump complete.
虽然检测出了内存泄露,但我们只能知道内存地址、行号,文件名是#File Error#,而且还伴随着内存非法访问的异常。这个异常看似是MFC在检测内存泄露的时候产生的。
下面我们从C++内存分配与回收的两个操作符new, delete一步步分析C++内存管理以及MFC内存泄露检测机制。所有这些都是针对Debug版本的,最后我们再看看Release版本的情况。
一、内存分配操作符new
新建一个MFC应用程序,无论是Win32 Console Application + MFC Support,还是MFC Application或者是MFC DLL。编译器为我们生成的代码最前面,在#include下面都会有下面这三行代码:
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
这三句话的意思是,如果是Debug版本,那么将new操作符定义为DEBUG_NEW。在afx.h中有对DEBUG_NEW的定义:
// Memory tracking allocation
void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine);
#define DEBUG_NEW new(THIS_FILE, __LINE__)
看来MFC是重新定义了一个new操作符,并把文件名、行号调试信息传给了new。下面是这个new操作符调用的其它函数。可见是按照MFC -> C++ -> C -> Win32 API的流程分配的内存:
DEBUG_NEW
-> void* __cdecl operator new(size_t nSize, LPCSTR lpszFileName, int nLine) afxmem.cpp
-> void* __cdecl operator new(size_t nSize, int nType, LPCSTR lpszFileName, int nLine) afxmem.cpp
-> extern "C" _CRTIMP void* __cdecl _malloc_dbg(…) dbgheap.c
-> extern "C" void* __cdecl _nh_malloc_dbg(…) dbgheap.c
-> extern "C" static void * __cdecl _nh_malloc_dbg_impl(…) dbgheap.c
-> extern "C" static void * __cdecl _heap_alloc_dbg_impl(…) dbgheap.c
-> __forceinline void * __cdecl _heap_alloc (size_t size) malloc.c
-> LPVOID WINAPI HeapAlloc(…); winbase.h
二、内存回收操作符delete
MFC并没有重新定义delete操作符,因为所有调试信息已经传给了new操作符。delete操作符只要依然按照MFC -> C++ -> C -> Win32 API的流程将之前分配的内存释放掉就可以了:
operator delete
-> class CCRTAllocator::static void Free(void* p) throw() atlalloc.h
-> extern "C" _CRTIMP void __cdecl _free_dbg(void * pUserData, int nBlockUse) dbgheap.c
-> extern "C" void __cdecl _free_dbg_nolock(void * pUserData, int nBlockUse) dbgheap.c
-> void __cdecl _free_base (void * pBlock) free.c
-> BOOL WINAPI HeapFree(…); winbase.h
三、C++内存链
内存链是MFC检测内存泄露的基础,当我们每new一块内存,_heap_alloc_dbg_impl就会把这块内存加入内存链,当我们delete一块内存,_free_dbg_nolock就会把这块内存从内存链中删除。VC的实现是使用了一个双向链表。每一个节点的结构定义如下:
typedef struct _CrtMemBlockHeader
{
struct _CrtMemBlockHeader * pBlockHeaderNext; // 下一个节点指针
struct _CrtMemBlockHeader * pBlockHeaderPrev; // 前一个节点指针
char * szFileName; // 调用new的文件名
int nLine; // 调用new的行号
size_t nDataSize; // 调用new分配内存大小
int nBlockUse; // 本块内存使用目的
long lRequest; // 请求编号
unsigned char gap[nNoMansLandSize]; // 内存前面的空白
/* followed by:
* unsigned char data[nDataSize]; // 真正的内存
* unsigned char anotherGap[nNoMansLandSize]; // 内存后面的空白
*/
} _CrtMemBlockHeader;
结构体中有几个成员可能需要解释一下。nBlockUse表示本块内存的用途,一般取值为_NORMAL_BLOCK。lRequest表示请求内存的编号,初始值为1,每请求一次,该值加1。我们在输出窗口看到的normal block就表示nBlockUse=_NORMAL_BLOCK, {137} 就是lRequest的值。data是真正返回给我们的指针,编译器在data前后用gap, anotherGap将数据保护起来并赋予特殊的值,以检测我们对指针操作是否越界。这些空白区域内存大小为#define nNoMansLandSize 4。data同样被赋予特殊的值,特殊值总共有四种:
static unsigned char _bNoMansLandFill = 0xFD; /* fill no-man's land with this */
static unsigned char _bAlignLandFill = 0xED; /* fill no-man's land for aligned routines */
static unsigned char _bDeadLandFill = 0xDD; /* fill free objects with this */
static unsigned char _bCleanLandFill = 0xCD; /* fill new objects with this */
比如说我们new了一个int对象,int* p = new int;那么上面这个结构体内容如下:
+------------------------------------------------------------------------------+
| pBlockHeaderNext | …… | gap: FDFDFDFD | p: CDCDCDCD | anotherGap: FDFDFDFD |
+------------------------------------------------------------------------------+
比如说我们内存访问越界了:*(p+1) = 0,那么在delete这个指针的时候,_free_dbg_nolock会对gap, anotherGap的值进行检查,发现不等于_bNoMansLandFill,就报错。如果我们写*(p+1) = 0xFDFDFDFD,那么就把编译器骗了,编译器认为内存访问并没有越界。当我们delete一块内存的时候,这块内存会被用_bDeadLandFill填充。如果我们new了多个对象,那么这些对象就链接再了一起,例如:
int* pB = new int;
int* pA = new int;
内存布局如下:
+--------------------------------------------------------------------------+
| +--------------------------+ +--------------------------+ |
+-> | pHead = pBlockHeaderNext | -----------> | pBlockHeaderNext = NULL | |
|--------------------------| |--------------------------| |
| pBlockHeaderPrev = NULL | | pBlockHeaderPrev ->-|-+
|--------------------------| |--------------------------|
| ...... | | ...... |
|--------------------------| |--------------------------|
|gap: FDFDFDFD | |gap: FDFDFDFD &bsp; |
|--------------------------| |--------------------------|
|pA: CDCDCDCD | |pB: CDCDCDCD |
|--------------------------| |--------------------------|
|anotherGap: FDFDFDFD | |anotherGap: FDFDFDFD |
+--------------------------+ +--------------------------+
知道了内存块的布局,我们甚至可以通过一个指针,打印出当前new过的所有对象内存地址及大小。为了验证上述内容的正确性,我们不妨写一个简单的验证程序:
int* pB = new int(2);
int* pA = new int(1);
cout << "*pA = " << *pA << ", *pB = " << *pB << endl; // *pA = 1, *pB = 2
*((int*)(*(pA - 8)) + 8) = 1;
*((int*)(*(pB - 7)) + 8) = 2;
cout << "*pA = " << *pA << ", *pB = " << *pB << endl; // *pA = 2, *pB = 1
delete pA;
delete pB;
四、内存泄露检测机制
MFC正是因为有了内存链,才可以检测出哪些内存还没有被释放。在程序退出的时候,dbgheap.c中的extern "C" _CRTIMP int __cdecl _CrtDumpMemoryLeaks(void)函数会被调用,然后遍历当前的内存链,看看还有哪些内存没有被释放,然后打印出内存泄露的信息。原理很简单,这里不再赘述。那么为什么有的情况下我们无法通过输出的信息定位到具体泄露的文件呢?为什么有的时候会显示#File Error#?
看看上面提到的结构体中文件名的保存char * szFileName,仅仅保存了一个指向文件名的指针而已。这个文件名是作为一个字符串,保存在.exe或.dll的.rdata中的。如果在.exe文件退出的时候,我们显式加载的.dll文件已经被我们卸载了,并且在该.dll文件内存存在内存泄露的话,虽然_CrtDumpMemoryLeaks会尝试读取并显示文件名,但szFileName指针指向的内存空间已经是无效的了。_CrtDumpMemoryLeaks在读取文件名之前会先调用API函数IsBadReadPtr判断该指针是否有效。如果已经无效则显示#File Error#。本文最开始所提到的异常,正是由IsBadReadPtr导致的。
五、Release版本
对于Release版本,就没有上面提到的内存链了。对于new和delete的调用将会被直接转到malloc.c和free.c。
因为没有内存链,没有多余的保护数据填充,没有内存越界检测机制,所以有些时候Debug版本会崩溃,但是Release版本却没有。这并不代表代码没有问题,而是内存非法访问更难发现了,当Release版本崩溃的时候,问题也更难定位了。
上述内存泄露检测、内存越界访问检测的原理很简单,但并不能查出所有内存非法访问。所以永远不要乱用指针,然后把所有对指针的判断都用try{}catch{}规避。因为并不是所有指针非法访问都能catch到,即使catch到了,内存也可能已经被写坏了。