Commit a6153cfe authored by Bruce Dawson's avatar Bruce Dawson Committed by Commit Bot

Add Windows heap trimming tool

This is an experimental tool which will inject a thread into a Chrome
process (tested on the browser and GPU process) and run code to call
HeapSetInformation with HEAP_OPTIMIZE_RESOURCES_CURRENT_VERSION. This
tells Windows to trim unnecessary memory from the heaps in that process.

This tool uses sketchy techniques such as copying memory from one
executable to another (only works if the code is relocatable and has no
external references), VirtualAllocEx, and CreateRemoteThread. This is
not for production use.

To build run build.bat
To run either run it manually (passing the PIDs of the processes of
interest) or run trim_loop.bat (passing the PIDs of the processes of
interest) to run it every five minutes in a loop.

While it sometimes frees non-trivial amounts of memory the savings are
not persistent.

Bug: 1050059, 982452
Change-Id: I646c515e59598c2c3003500419c154bacd34aeda
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2139127
Commit-Queue: Bruce Dawson <brucedawson@chromium.org>
Reviewed-by: default avatarDavid Bienvenu <davidbienvenu@google.com>
Reviewed-by: default avatarJesse McKenna <jessemckenna@google.com>
Reviewed-by: default avatarJames Forshaw <forshaw@chromium.org>
Cr-Commit-Position: refs/heads/master@{#757644}
parent fe31d7c4
.vs/
trim_heap.obj
trim_heap.exe
brucedawson@chromium.org
davidbienvenu@chromium.org
jessemckenna@chromium.org
@setlocal
where cl
if errorlevel 1 goto no_cl
pushd %~dp0
cl /nologo /Zi /GS- /EHsc trim_heap.cc /link /DEBUG /OPT:REF /OPT:ICF
popd
exit /b
:no_cl
@echo Run "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Auxiliary\Build\vcvarsall.bat" amd64
@echo or equivalent to get the 64-bit compiler tools in the path.
exit /b
// Copyright 2020 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.
// This is an experimental tool which will inject a thread into a Chrome
// process (tested on the browser process) and run code to call
// HeapSetInformation with HEAP_OPTIMIZE_RESOURCES_CURRENT_VERSION. This
// tells Windows to trim unnecessary memory from the heaps in that process.
//
// This tool uses sketchy techniques such as copying memory from one
// executable to another (only works if the code is relocatable and has no
// external references), VirtualAllocEx, and CreateRemoteThread. This is not
// for production use.
//
// The bitness of this tool (32/64) must match that of the target process.
// This tool has only been tested on 64-bit processes. This tool only works
// when compiled with optimizations.
//
// Some error handling and resource cleanup is omitted in order to keep things
// simple.
#include <Windows.h>
// Psapi.h must come after Windows.h.
#include <Psapi.h>
#include <inttypes.h>
#include <stdio.h>
#include <vector>
#ifdef _DEBUG
#error This code only works in optimized (release) builds.
// Non-optimized code may include references to global variables. The
// "#pragma clang optimize on/off" directives do not work, by design, in debug
// builds. They can only lower the optimization level, not raise it.
#endif
#define ADDRESS_COOKIE reinterpret_cast<void*>(0x123456789ABCDEF0)
// Function suitable for copying into another process and invoking with
// CreateRemoteThread. The function address is a placeholder.
DWORD WINAPI ShrinkHeapThread(LPVOID) {
auto pHeapSetInformation =
reinterpret_cast<decltype(&::HeapSetInformation)>(ADDRESS_COOKIE);
HEAP_OPTIMIZE_RESOURCES_INFORMATION info = {
HEAP_OPTIMIZE_RESOURCES_CURRENT_VERSION, 0x0};
pHeapSetInformation(nullptr, HeapOptimizeResources, &info, sizeof(info));
return 0;
}
int main(int argc, char* argv[]) {
const bool verbose = false;
// Verify that we have the correct signature for ShrinkHeapThread.
static_assert(
std::is_same<decltype(ShrinkHeapThread)*, PTHREAD_START_ROUTINE>::value,
"Callback function is wrong type.");
// Copy the thread function's memory to a vector.
std::vector<unsigned char> raw_bytes;
auto* src = reinterpret_cast<uint8_t*>(&ShrinkHeapThread);
// Assume that the only 0xc3 byte we will encounter will be the ret
// instruction.
uint8_t ret = 0xc3;
while (*src != ret) {
raw_bytes.push_back(*src++);
}
raw_bytes.push_back(ret);
if (src[1] != 0xcc) {
printf("Didn't find int 3 after ret. Exiting.\n");
return 1;
}
// This can trigger if incremental linking is enabled since then the function
// pointer will be to a JMP stub.
if (raw_bytes.size() > 1000) {
printf("Code size is suspiciously large - %zu bytes. Exiting.\n",
raw_bytes.size());
return 1;
}
// Update the function pointer address in the copy to match the current
// address of HeapSetInformation. This assumes that the address will be the
// same in all processes, which should be the case.
for (auto* scan = &raw_bytes[0]; /**/; ++scan) {
auto** scan_64 = reinterpret_cast<void**>(scan);
if (*scan_64 == ADDRESS_COOKIE) {
auto* pHeapSetInformation = reinterpret_cast<void*>(GetProcAddress(
GetModuleHandleA("kernel32.dll"), "HeapSetInformation"));
*scan_64 = pHeapSetInformation;
if (verbose)
printf("Found and updated HeapSetInformation.\n");
break;
}
}
if (argc < 2) {
printf("Usage: %s PID.\n", argv[0]);
printf(
"Injects code into the target process to call HeapSetInformation with "
"HEAP_OPTIMIZE_RESOURCES_CURRENT_VERSION.\n");
printf(
"May need to be run from an administrator command prompt for some "
"processes.\n");
return 1;
}
// Get the PIDs from the command line.
for (int i = 1; i < argc; ++i) {
int PID;
if (sscanf(argv[i], "%d", &PID) != 1) {
printf("Error getting PID.\n");
return 1;
}
// Open the process. We'll leak the handle afterwards, but that's okay
// because this is a short-lived tool.
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ |
PROCESS_VM_WRITE | PROCESS_VM_OPERATION |
PROCESS_CREATE_THREAD,
false, PID);
if (!hProcess) {
printf("Error from OpenProcess is %lx.\n", GetLastError());
return 1;
}
#ifdef _M_X64
BOOL wow_64_process = FALSE;
if (!IsWow64Process(hProcess, &wow_64_process) || wow_64_process) {
printf("Specified process is 32-bit. Code injection will not work.\n");
return 1;
}
#else
// Update this with remote-process bitness tests if x86 works.
#error This code is only tested on x64 and may cause failures on x86.
#endif
PROCESS_MEMORY_COUNTERS_EX memory_before = {sizeof(memory_before)};
GetProcessMemoryInfo(
hProcess, reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&memory_before),
sizeof(memory_before));
// Allocate memory in the other process.
void* p = VirtualAllocEx(hProcess, nullptr, raw_bytes.size(),
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (verbose)
printf("Writing %zd bytes to process %d at address 0x%p.\n",
raw_bytes.size(), PID, p);
// Write to the remotely allocated memory.
SIZE_T bytes_written = 0;
if (!WriteProcessMemory(hProcess, p, &raw_bytes[0], raw_bytes.size(),
&bytes_written)) {
printf("Error is %lx.\n", GetLastError());
return 1;
}
if (verbose)
printf("Wrote %zd bytes.\n", bytes_written);
HANDLE hRemoteThread = CreateRemoteThread(
hProcess, nullptr, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(p),
nullptr, 0, nullptr);
if (!hRemoteThread) {
printf("Failed to inject thread in process %d. Error code is %lx.\n", PID,
GetLastError());
return 1;
}
if (verbose)
printf("Successfully injected thread into process %d.\n", PID);
WaitForSingleObject(hRemoteThread, INFINITE);
// Clean up the allocated memory after the thread exits.
VirtualFreeEx(hProcess, p, 0, MEM_RELEASE);
PROCESS_MEMORY_COUNTERS_EX memory_after = {sizeof(memory_after)};
GetProcessMemoryInfo(
hProcess, reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&memory_after),
sizeof(memory_after));
double MiB = 1024.0 * 1024.0;
printf(
" Commit for process %6d went from %8.3f MiB to %8.3f MiB (%7.3f MiB "
"savings).\n",
PID, memory_before.PrivateUsage / MiB, memory_after.PrivateUsage / MiB,
(memory_before.PrivateUsage - memory_after.PrivateUsage) / MiB);
}
return 0;
}
call build
@echo off
:top
echo Trimming heaps at %time%
trim_heap %*
sleep 300
goto top
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment