// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Static class for hooking Win32 API routines. // Some notes about how to hook Memory Allocation Routines in Windows. // // For our purposes we do not hook the libc routines. There are two // reasons for this. First, the libc routines all go through HeapAlloc // anyway. So, it's redundant to log both HeapAlloc and malloc. // Second, it can be tricky to hook in both static and dynamic linkages // of libc. #include #include "memory_hook.h" #include "memory_watcher.h" #include "preamble_patcher.h" // Calls GetProcAddress, but casts to the correct type. #define GET_PROC_ADDRESS(hmodule, name) \ ( (Type_##name)(::GetProcAddress(hmodule, #name)) ) // Macro to declare Patch functions. #define DECLARE_PATCH(name) Patch patch_##name // Macro to install Patch functions. #define INSTALL_PATCH(name) do { \ patch_##name.set_original(GET_PROC_ADDRESS(hkernel32, ##name)); \ patch_##name.Install(&Perftools_##name); \ } while (0) // Macro to install Patch functions. #define INSTALL_NTDLLPATCH(name) do { \ patch_##name.set_original(GET_PROC_ADDRESS(hntdll, ##name)); \ patch_##name.Install(&Perftools_##name); \ } while (0) // Macro to uninstall Patch functions. #define UNINSTALL_PATCH(name) patch_##name.Uninstall(); // Windows APIs to be hooked // HeapAlloc routines typedef HANDLE (WINAPI *Type_HeapCreate)(DWORD flOptions, SIZE_T dwInitialSize, SIZE_T dwMaximumSize); typedef BOOL (WINAPI *Type_HeapDestroy)(HANDLE hHeap); typedef LPVOID (WINAPI *Type_HeapAlloc)(HANDLE hHeap, DWORD dwFlags, DWORD_PTR dwBytes); typedef LPVOID (WINAPI *Type_HeapReAlloc)(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem, SIZE_T dwBytes); typedef BOOL (WINAPI *Type_HeapFree)(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem); // GlobalAlloc routines typedef HGLOBAL (WINAPI *Type_GlobalAlloc)(UINT uFlags, SIZE_T dwBytes); typedef HGLOBAL (WINAPI *Type_GlobalReAlloc)(HGLOBAL hMem, SIZE_T dwBytes, UINT uFlags); typedef HGLOBAL (WINAPI *Type_GlobalFree)(HGLOBAL hMem); // LocalAlloc routines typedef HLOCAL (WINAPI *Type_LocalAlloc)(UINT uFlags, SIZE_T uBytes); typedef HLOCAL (WINAPI *Type_LocalReAlloc)(HLOCAL hMem, SIZE_T uBytes, UINT uFlags); typedef HLOCAL (WINAPI *Type_LocalFree)(HLOCAL hMem); // A Windows-API equivalent of mmap and munmap, for "anonymous regions" typedef LPVOID (WINAPI *Type_VirtualAllocEx)(HANDLE process, LPVOID address, SIZE_T size, DWORD type, DWORD protect); typedef BOOL (WINAPI *Type_VirtualFreeEx)(HANDLE process, LPVOID address, SIZE_T size, DWORD type); // A Windows-API equivalent of mmap and munmap, for actual files typedef LPVOID (WINAPI *Type_MapViewOfFile)(HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap); typedef LPVOID (WINAPI *Type_MapViewOfFileEx)(HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap, LPVOID lpBaseAddress); typedef BOOL (WINAPI *Type_UnmapViewOfFile)(LPVOID lpBaseAddress); typedef DWORD (WINAPI *Type_NtUnmapViewOfSection)(HANDLE process, LPVOID lpBaseAddress); // Patch is a template for keeping the pointer to the original // hooked routine, the function to call when hooked, and the // stub routine which is patched. template class Patch { public: // Constructor. Does not hook the function yet. Patch() : original_function_(NULL), patch_function_(NULL), stub_function_(NULL) { } // Destructor. Unhooks the function if it has been hooked. ~Patch() { Uninstall(); } // Patches original function with func. // Must have called set_original to set the original function. void Install(T func) { patch_function_ = func; CHECK(patch_function_ != NULL); CHECK(original_function_ != NULL); CHECK(stub_function_ == NULL); CHECK(sidestep::SIDESTEP_SUCCESS == sidestep::PreamblePatcher::Patch(original_function_, patch_function_, &stub_function_)); } // Un-patches the function. void Uninstall() { if (stub_function_) sidestep::PreamblePatcher::Unpatch(original_function_, patch_function_, stub_function_); stub_function_ = NULL; } // Set the function to be patched. void set_original(T original) { original_function_ = original; } // Get the original function being patched. T original() { return original_function_; } // Get the patched function. (e.g. the replacement function) T patched() { return patch_function_; } // Access to the stub for calling the original function // while it is patched. T operator()() { DCHECK(stub_function_); return stub_function_; } private: // The function that we plan to patch. T original_function_; // The function to replace the original with. T patch_function_; // To unpatch, we also need to keep around a "stub" that points to the // pre-patched Windows function. T stub_function_; }; // All Windows memory-allocation routines call through to one of these. DECLARE_PATCH(HeapCreate); DECLARE_PATCH(HeapDestroy); DECLARE_PATCH(HeapAlloc); DECLARE_PATCH(HeapReAlloc); DECLARE_PATCH(HeapFree); DECLARE_PATCH(VirtualAllocEx); DECLARE_PATCH(VirtualFreeEx); DECLARE_PATCH(MapViewOfFile); DECLARE_PATCH(MapViewOfFileEx); DECLARE_PATCH(UnmapViewOfFile); DECLARE_PATCH(GlobalAlloc); DECLARE_PATCH(GlobalReAlloc); DECLARE_PATCH(GlobalFree); DECLARE_PATCH(LocalAlloc); DECLARE_PATCH(LocalReAlloc); DECLARE_PATCH(LocalFree); DECLARE_PATCH(NtUnmapViewOfSection); // Our replacement functions. static HANDLE WINAPI Perftools_HeapCreate(DWORD flOptions, SIZE_T dwInitialSize, SIZE_T dwMaximumSize) { if (dwInitialSize > 4096) dwInitialSize = 4096; return patch_HeapCreate()(flOptions, dwInitialSize, dwMaximumSize); } static BOOL WINAPI Perftools_HeapDestroy(HANDLE hHeap) { return patch_HeapDestroy()(hHeap); } static LPVOID WINAPI Perftools_HeapAlloc(HANDLE hHeap, DWORD dwFlags, DWORD_PTR dwBytes) { LPVOID rv = patch_HeapAlloc()(hHeap, dwFlags, dwBytes); MemoryHook::hook()->OnTrack(hHeap, reinterpret_cast(rv), dwBytes); return rv; } static BOOL WINAPI Perftools_HeapFree(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem) { size_t size = 0; if (lpMem != 0) { size = HeapSize(hHeap, 0, lpMem); // Will crash if lpMem is 0. // Note: size could be 0; HeapAlloc does allocate 0 length buffers. } MemoryHook::hook()->OnUntrack(hHeap, reinterpret_cast(lpMem), size); return patch_HeapFree()(hHeap, dwFlags, lpMem); } static LPVOID WINAPI Perftools_HeapReAlloc(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem, SIZE_T dwBytes) { // Don't call realloc, but instead do a free/malloc. The problem is that // the builtin realloc may either expand a buffer, or it may simply // just call free/malloc. If so, we will already have tracked the new // block via Perftools_HeapAlloc. LPVOID rv = Perftools_HeapAlloc(hHeap, dwFlags, dwBytes); DCHECK_EQ((HEAP_REALLOC_IN_PLACE_ONLY & dwFlags), 0); // If there was an old buffer, now copy the data to the new buffer. if (lpMem != 0) { size_t size = HeapSize(hHeap, 0, lpMem); if (size > dwBytes) size = dwBytes; // Note: size could be 0; HeapAlloc does allocate 0 length buffers. memcpy(rv, lpMem, size); Perftools_HeapFree(hHeap, dwFlags, lpMem); } return rv; } static LPVOID WINAPI Perftools_VirtualAllocEx(HANDLE process, LPVOID address, SIZE_T size, DWORD type, DWORD protect) { bool already_committed = false; if (address != NULL) { MEMORY_BASIC_INFORMATION info; CHECK(VirtualQuery(address, &info, sizeof(info))); if (info.State & MEM_COMMIT) { already_committed = true; CHECK(size >= info.RegionSize); } } bool reserving = (address == NULL) || (type & MEM_RESERVE); bool committing = !already_committed && (type & MEM_COMMIT); LPVOID result = patch_VirtualAllocEx()(process, address, size, type, protect); MEMORY_BASIC_INFORMATION info; CHECK(VirtualQuery(result, &info, sizeof(info))); size = info.RegionSize; if (committing) MemoryHook::hook()->OnTrack(0, reinterpret_cast(result), size); return result; } static BOOL WINAPI Perftools_VirtualFreeEx(HANDLE process, LPVOID address, SIZE_T size, DWORD type) { int chunk_size = size; MEMORY_BASIC_INFORMATION info; CHECK(VirtualQuery(address, &info, sizeof(info))); if (chunk_size == 0) chunk_size = info.RegionSize; bool decommit = (info.State & MEM_COMMIT); if (decommit) MemoryHook::hook()->OnUntrack(0, reinterpret_cast(address), chunk_size); return patch_VirtualFreeEx()(process, address, size, type); } static base::Lock known_maps_lock; static std::map known_maps; static LPVOID WINAPI Perftools_MapViewOfFileEx(HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap, LPVOID lpBaseAddress) { // For this function pair, you always deallocate the full block of // data that you allocate, so NewHook/DeleteHook is the right API. LPVOID result = patch_MapViewOfFileEx()(hFileMappingObject, dwDesiredAccess, dwFileOffsetHigh, dwFileOffsetLow, dwNumberOfBytesToMap, lpBaseAddress); { base::AutoLock lock(known_maps_lock); MEMORY_BASIC_INFORMATION info; if (known_maps.find(result) == known_maps.end()) { CHECK(VirtualQuery(result, &info, sizeof(info))); // TODO(mbelshe): THIS map uses the standard heap!!!! known_maps[result] = 1; MemoryHook::hook()->OnTrack(0, reinterpret_cast(result), info.RegionSize); } else { known_maps[result] = known_maps[result] + 1; } } return result; } static LPVOID WINAPI Perftools_MapViewOfFile(HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap) { return Perftools_MapViewOfFileEx(hFileMappingObject, dwDesiredAccess, dwFileOffsetHigh, dwFileOffsetLow, dwNumberOfBytesToMap, 0); } static BOOL WINAPI Perftools_UnmapViewOfFile(LPVOID lpBaseAddress) { // This will call into NtUnmapViewOfSection(). return patch_UnmapViewOfFile()(lpBaseAddress); } static DWORD WINAPI Perftools_NtUnmapViewOfSection(HANDLE process, LPVOID lpBaseAddress) { // Some windows APIs call directly into this routine rather // than calling UnmapViewOfFile. If we didn't trap this function, // then we appear to have bogus leaks. { base::AutoLock lock(known_maps_lock); MEMORY_BASIC_INFORMATION info; CHECK(VirtualQuery(lpBaseAddress, &info, sizeof(info))); if (known_maps.find(lpBaseAddress) != known_maps.end()) { if (known_maps[lpBaseAddress] == 1) { MemoryHook::hook()->OnUntrack(0, reinterpret_cast(lpBaseAddress), info.RegionSize); known_maps.erase(lpBaseAddress); } else { known_maps[lpBaseAddress] = known_maps[lpBaseAddress] - 1; } } } return patch_NtUnmapViewOfSection()(process, lpBaseAddress); } static HGLOBAL WINAPI Perftools_GlobalAlloc(UINT uFlags, SIZE_T dwBytes) { // GlobalAlloc is built atop HeapAlloc anyway. So we don't track these. // GlobalAlloc will internally call into HeapAlloc and we track there. // Force all memory to be fixed. uFlags &= ~GMEM_MOVEABLE; HGLOBAL rv = patch_GlobalAlloc()(uFlags, dwBytes); return rv; } static HGLOBAL WINAPI Perftools_GlobalFree(HGLOBAL hMem) { return patch_GlobalFree()(hMem); } static HGLOBAL WINAPI Perftools_GlobalReAlloc(HGLOBAL hMem, SIZE_T dwBytes, UINT uFlags) { // TODO(jar): [The following looks like a copy/paste typo from LocalRealloc.] // GlobalDiscard is a macro which calls LocalReAlloc with size 0. if (dwBytes == 0) { return patch_GlobalReAlloc()(hMem, dwBytes, uFlags); } HGLOBAL rv = Perftools_GlobalAlloc(uFlags, dwBytes); if (hMem != 0) { size_t size = GlobalSize(hMem); if (size > dwBytes) size = dwBytes; // Note: size could be 0; HeapAlloc does allocate 0 length buffers. memcpy(rv, hMem, size); Perftools_GlobalFree(hMem); } return rv; } static HLOCAL WINAPI Perftools_LocalAlloc(UINT uFlags, SIZE_T dwBytes) { // LocalAlloc is built atop HeapAlloc anyway. So we don't track these. // LocalAlloc will internally call into HeapAlloc and we track there. // Force all memory to be fixed. uFlags &= ~LMEM_MOVEABLE; HLOCAL rv = patch_LocalAlloc()(uFlags, dwBytes); return rv; } static HLOCAL WINAPI Perftools_LocalFree(HLOCAL hMem) { return patch_LocalFree()(hMem); } static HLOCAL WINAPI Perftools_LocalReAlloc(HLOCAL hMem, SIZE_T dwBytes, UINT uFlags) { // LocalDiscard is a macro which calls LocalReAlloc with size 0. if (dwBytes == 0) { return patch_LocalReAlloc()(hMem, dwBytes, uFlags); } HGLOBAL rv = Perftools_LocalAlloc(uFlags, dwBytes); if (hMem != 0) { size_t size = LocalSize(hMem); if (size > dwBytes) size = dwBytes; // Note: size could be 0; HeapAlloc does allocate 0 length buffers. memcpy(rv, hMem, size); Perftools_LocalFree(hMem); } return rv; } bool MemoryHook::hooked_ = false; MemoryHook* MemoryHook::global_hook_ = NULL; MemoryHook::MemoryHook() : watcher_(NULL), heap_(NULL) { CreateHeap(); } MemoryHook::~MemoryHook() { // It's a bit dangerous to ever close this heap; MemoryWatchers may have // used this heap for their tracking data. Closing the heap while any // MemoryWatchers still exist is pretty dangerous. CloseHeap(); } bool MemoryHook::Initialize() { if (global_hook_ == NULL) global_hook_ = new MemoryHook(); return true; } bool MemoryHook::Hook() { DCHECK(!hooked_); if (!hooked_) { DCHECK(global_hook_); // Luckily, Patch() doesn't call malloc or windows alloc routines // itself -- though it does call new (we can use PatchWithStub to // get around that, and will need to if we need to patch new). HMODULE hkernel32 = ::GetModuleHandle(L"kernel32"); CHECK(hkernel32 != NULL); HMODULE hntdll = ::GetModuleHandle(L"ntdll"); CHECK(hntdll != NULL); // Now that we've found all the functions, patch them INSTALL_PATCH(HeapCreate); INSTALL_PATCH(HeapDestroy); INSTALL_PATCH(HeapAlloc); INSTALL_PATCH(HeapReAlloc); INSTALL_PATCH(HeapFree); INSTALL_PATCH(VirtualAllocEx); INSTALL_PATCH(VirtualFreeEx); INSTALL_PATCH(MapViewOfFileEx); INSTALL_PATCH(MapViewOfFile); INSTALL_PATCH(UnmapViewOfFile); INSTALL_NTDLLPATCH(NtUnmapViewOfSection); INSTALL_PATCH(GlobalAlloc); INSTALL_PATCH(GlobalReAlloc); INSTALL_PATCH(GlobalFree); INSTALL_PATCH(LocalAlloc); INSTALL_PATCH(LocalReAlloc); INSTALL_PATCH(LocalFree); // We are finally completely hooked. hooked_ = true; } return true; } bool MemoryHook::Unhook() { if (hooked_) { // We need to go back to the system malloc/etc at global destruct time, // so objects that were constructed before tcmalloc, using the system // malloc, can destroy themselves using the system free. This depends // on DLLs unloading in the reverse order in which they load! // // We also go back to the default HeapAlloc/etc, just for consistency. // Who knows, it may help avoid weird bugs in some situations. UNINSTALL_PATCH(HeapCreate); UNINSTALL_PATCH(HeapDestroy); UNINSTALL_PATCH(HeapAlloc); UNINSTALL_PATCH(HeapReAlloc); UNINSTALL_PATCH(HeapFree); UNINSTALL_PATCH(VirtualAllocEx); UNINSTALL_PATCH(VirtualFreeEx); UNINSTALL_PATCH(MapViewOfFile); UNINSTALL_PATCH(MapViewOfFileEx); UNINSTALL_PATCH(UnmapViewOfFile); UNINSTALL_PATCH(NtUnmapViewOfSection); UNINSTALL_PATCH(GlobalAlloc); UNINSTALL_PATCH(GlobalReAlloc); UNINSTALL_PATCH(GlobalFree); UNINSTALL_PATCH(LocalAlloc); UNINSTALL_PATCH(LocalReAlloc); UNINSTALL_PATCH(LocalFree); hooked_ = false; } return true; } bool MemoryHook::RegisterWatcher(MemoryObserver* watcher) { DCHECK(global_hook_->watcher_ == NULL); if (!hooked_) Hook(); DCHECK(global_hook_); global_hook_->watcher_ = watcher; return true; } bool MemoryHook::UnregisterWatcher(MemoryObserver* watcher) { DCHECK(hooked_); DCHECK(global_hook_->watcher_ == watcher); // TODO(jar): changing watcher_ here is very racy. Other threads may (without // a lock) testing, and then calling through this value. We probably can't // remove this until we are single threaded. global_hook_->watcher_ = NULL; // For now, since there are no more watchers, unhook memory. return Unhook(); } bool MemoryHook::CreateHeap() { // Create a heap for our own memory. DCHECK(heap_ == NULL); heap_ = HeapCreate(0, 0, 0); DCHECK(heap_ != NULL); return heap_ != NULL; } bool MemoryHook::CloseHeap() { DCHECK(heap_ != NULL); HeapDestroy(heap_); heap_ = NULL; return true; } void MemoryHook::OnTrack(HANDLE heap, int32 id, int32 size) { // Don't notify about allocations to our internal heap. if (heap == heap_) return; if (watcher_) watcher_->OnTrack(heap, id, size); } void MemoryHook::OnUntrack(HANDLE heap, int32 id, int32 size) { // Don't notify about allocations to our internal heap. if (heap == heap_) return; if (watcher_) watcher_->OnUntrack(heap, id, size); }