| assembler's profileUnknownPhotosBlogLists | Help |
|
May 28 From Compiler to Loader - II继续看看内存管理在Visual C++中是如何实现的. mainCRTStartup的头几步工作是初始化一些预定义的全局变量, _osplatform, _winmajor, _winminor, _osver, _winver等与Windows Version 有关的. 然后很重要的一步就是初始化 Heap. 大多的内存都是 Heap 上分配的, 如 new, malloc. 所以必须要先初始化才能干别的. 这个函数是_heap_init. 以前的Compiler 有些用自己的 Heap 管理, 但是现在我手上的 Visual Studio .NET 2003, 完全是用的 Windows 系统提供的 Heap management. 所以 _heap_init 很简单: int __cdecl _heap_init (int mtflag) { // Initialize the "big-block" heap first. if ( (_crtheap = HeapCreate( mtflag ? 0 : HEAP_NO_SERIALIZE, BYTES_PER_PAGE, 0 )) == NULL ) return 0; ... } 系统默认的 new, malloc 等等的分配都是在这个_crtheap 上进行的. 试试写个简单的程序: int main() { int* p = new int; return 0; } 在int* p = new int; 这一行设个断点, 调试进去. 可以看见new 是这样的: void * operator new( size_t cb ) { void *res = _nh_malloc( cb, 1 ); ... return res; } 下面以Debug version为例, 因为Debug version比较有意思. _nh_malloc 只是简单调用_nh_malloc_dbg, 而malloc 也是调用_nh_malloc_dbg来完成内存分配. _nh_malloc_dbg最终调用_heap_alloc_dbg, 在这里进行真正的分配工作. Debug Version中, 实际分配的是这样一个结构: typedef struct _CrtMemBlockHeader { struct _CrtMemBlockHeader * pBlockHeaderNext; struct _CrtMemBlockHeader * pBlockHeaderPrev; char * szFileName; int nLine; size_t nDataSize; int nBlockUse; long lRequest; unsigned char gap[nNoMansLandSize]; /* followed by: * unsigned char data[nDataSize]; * unsigned char anotherGap[nNoMansLandSize]; */ } _CrtMemBlockHeader; 假如你要分配一个大小为100的块, 则实际分配的块结构如下: _CrtMemBlockHeader + <You Data> (100 bytes) + gap[nNoMansLandSize] _CrtMemBlockHeader最后有个gap[nNoMansLandSize], 这个nNoMansLandSize目前的值是 4, 所以在你的数据前后各有4个字节的 gap. _heap_alloc_dbg会把 <Your Data> 的所有字节置为 0xCD, 前后的gap置成 0xFD. 如果你在自己的Data里写, 不小心越了界(前面或者后面), 系统在delete的时候通过检查 gap 的数据是否已被破坏,就知道你有没有越界. 当然了, 如果你恰好写的都是0xFD, 那就没法知道了. 试试如下程序: int* p = new int; p[1] = 0; delete p; 在Debug 下运行时,delete 时系统会报错: DAMAGE: after normal block (#59) at 0x00375C80. _heap_alloc_dbg调用 Windows System Call HeapAlloc 完成分配. HeapAlloc返回的指针, 要先初始化 _CrtMemBlockHeader. 这个 Header 中有前后两个指针, 事实上所有的内存块连接在一起形成一个双向链表. 在 delete 或者 free 的时候, 链表指针需要调整. 如果没有内存泄漏, 程序结束的时候链表应该为空. 否则说明有内存泄漏. 如下面的程序: #include <stdio.h> #include <stdlib.h> #include <crtdbg.h> int main() { int* p = new int; //delete p; void *px = malloc(100); free(px); _CrtDumpMemoryLeaks(); return 0; } _CrtDumpMemoryLeaks 通过检查分配链表, 来查找是否有泄漏. 在 Debug 下编译并且在 VC 中跟踪运行, 最后在 VC 的 Output 中会有如下输出: Detected memory leaks! Dumping objects -> {58} normal block at 0x00375FC0, 4 bytes long. Data: < > CD CD CD CD Object dump complete. 这里只有分配的序号, 还不能知道到底是哪一行程序产生的泄漏. 但是注意看_CrtMemBlockHeader, 事实上它还能记录源程序文件名和行号. 在 MFC 里就利用了这个技术. 在 afx.h 里, 有如下声明: // Memory tracking allocation void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine); #define DEBUG_NEW new(THIS_FILE, __LINE__) 同时,使用 MFC 时产生的 cpp 文件开始都有如下定义: #ifdef _DEBUG #define new DEBUG_NEW 这个 afx 的 new operator 把 new 时发生的源文件和行号传给 _malloc_dbg .这样在 Dump memory leak 的时候就可以同时知道泄漏的数据最初是在什么地方分配的. delete 和 free 最终都是用的 _free_dbg. _free_dbg 首先检查前后的 gap 有没有被破坏, 然后把该块从链表中去掉, 最后把数据块全部置成 0xDD.这样如果你不小心使用了已经被删除的数据时,通常数据已经被破坏而出错. 以上说的都是 Debug Version. 如果是 Release version, 内存分配更简单, 没有任何 overhead, 系统直接调用 HeapAlloc 分配所需的内存块. 同时分配的内存块也不会被初始化为 0xCD. 基本上VC Runtime Library 的内存分配管理是直接交给了 Windows System, 除了 Debug Version 用了一些技术来检测越界, 泄漏等. 如果要了解 Windows System 的 Heap management , 当然还是得看Microsoft Windows Internals 这本书了. From Compiler to Loader - IBLOG上保存一下,有时间把它写全. 要知道一个程序怎么从编译到执行,首先得了解一下 PE 文件格式,也就是我们常用的 Windows Executable 的格式。(事实上这种格式并不只限于 Windows Exectuable)。关于这个 topic 最好的文章是 Matt Pietrek写的An In-Depth Look into the Win32 Portable Executable File Format,网上 google 一下即得。简单来讲,PE格式的文件 Header里包含下面的结构: IMAGE_DOS_HEADER IMAGE_NT_HEADERS 如果你用Hex editor打开一个executable文件,最前面是IMAGE_DOS_HEADER,这个一般不太重要.紧跟其后的就是IMAGE_NT_HEADERS. 这些结构定义在 <winnt.h>. IMAGE_NT_HEADERS定义如下 (以32bit为例): typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; 其中,IMAGE_OPTIONAL_HEADER32定义如下: typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; //Entry Point DWORD BaseOfCode; DWORD BaseOfData; DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; ... DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; ... IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32; IMAGE_OPTIONAL_HEADER32 中, AddressOfEntryPoint定义了程序的入口点, 即当Windows系统完成Loading之后, 从哪个Virtual Address开始把控制转给executable本身. 不过这个入口函数既不是Windows启动一个Process时开始执行的第一个函数, 也不是C++程序里面的main函数. 关于Windows Kernel 如何完成Process loading的, 在Mark E. Russinovich, David A. Solomon的Microsoft Windows Internals, Fourth Edition: Microsoft Windows Server(TM) 2003, Windows XP, and Windows 2000 里面讲得比较清楚. 我们就考虑系统把控制转到EntryPoint之后吧. Visual C++的编译器的executable的入口函数是mainCRTStartup, 或wmainCRTStartup if compiled as UNICODE, 以上是编译Console Executable.如果是Windows程序, 则是 (w)WinMainCRTStartup. 函数定义在 crt0.c 里可以找到. 通常在main函数里设个断点, 停下来后在 Callstack里就可以点到mainCRTStartup里去看看. 你也可以定义自己的入口函数, 然后用Linker选项: /ENTRYR指定自己的入口函数. 但是因为入口函数干了很多初始化 C++ Library之类的工作, 所以如果你自己的入口函数没有做类似的初始化的话, 多半程序没法运行. Linker 编译的时候, 确定了入口函数的地址, 就把这个地址写入PE Header中. May 27 CreateRemoteThread NotesIt's easy to forget. So write it down: 1. Disable Project setting: Linker->General->Enable Incremental Linking. Otherwise the function pointer will only point to a small stub and not the real function body. 2. In Project setting->Code Generation->Basic Runtime Checks, set it to default. Otherwise it will generate stack/cookie checking code before and after function ends, calling functions that don't exist in the remote process. Assembly code is like this: { 00419FB0 push ebp 00419FB1 mov ebp,esp 00419FB3 push ecx 00419FB4 push esi 00419FB5 mov dword ptr [ebp-4],0CCCCCCCCh return 0; } 00419FBC xor eax,eax 00419FBE pop esi 00419FBF add esp,4 00419FC2 cmp ebp,esp 00419FC4 call _RTC_CheckEsp (4B49B0h) 00419FC9 mov esp,ebp 00419FCB pop ebp 00419FCC ret 4 May 17 体验真熊上个星期三开始,坏消息不断.各方面来看,经济在放缓,Inflation在增加.于是市场不断狂泻.可惜我没有及时止损,以为自己手里主要拿的是油股就没事.但真熊来了谁也挡不住,眼睁睁看着每天的LOSS逐渐增大.今天终于忍痛割了.这样今年本来所有帐号总共 20%的gain 只剩下不到 5%.残酷. 教训:cut loss一定要坚决.如果我在个股掉下 2-3%的时候 cut,完全可以守住战略成果.最后基本上都损失了 10-20%左右才cut,极不明智. 准备持币观望一阵,至少不要买什么股票了.等到下星期局势明朗再说. |
|
|