Refactor main module resolution & HMODULE handle behavior

This commit is contained in:
Luke Street 2025-09-29 13:50:27 -06:00
parent 4a2ba45620
commit c17953b318
13 changed files with 355 additions and 224 deletions

View File

@ -7,7 +7,7 @@
- Sample fixtures for exercising the loader live in `test/`; keep new repros small and self-contained.
## Build, Test, and Development Commands
- `cmake -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON` configures a 32-bit toolchain; ensure multilib packages are present.
- `cmake -B build -GNinja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON` configures a 32-bit toolchain; ensure multilib packages are present.
- `cmake --build build --target wibo` compiles the shim; switch to `-DCMAKE_BUILD_TYPE=Release` for optimised binaries.
- `./build/wibo /path/to/program.exe` runs a Windows binary. Use `WIBO_DEBUG=1` (or `--debug`/`-D`) for verbose logging. Use `--chdir`/`-C` to set the working directory.
- `cmake -B build -DBUILD_TESTING=ON` + `ctest --test-dir build --output-on-failure` runs the self-checking WinAPI fixtures (requires `i686-w64-mingw32-gcc` and `i686-w64-mingw32-windres`).

View File

@ -7,11 +7,11 @@ Don't run this on any untrusted executables, I implore you. (Or probably just do
## Building
```sh
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build build --target wibo
```
`cmake -B build -DCMAKE_BUILD_TYPE=Release` to produce an optimized binary instead.
Set `-DCMAKE_BUILD_TYPE=Release` to produce an optimized binary instead.
## Running

View File

@ -70,6 +70,7 @@ typedef unsigned char BYTE;
#define ERROR_RESOURCE_NAME_NOT_FOUND 1814
#define ERROR_RESOURCE_LANG_NOT_FOUND 1815
#define ERROR_MOD_NOT_FOUND 126
#define ERROR_PROC_NOT_FOUND 127
#define ERROR_NEGATIVE_SEEK 131
#define ERROR_BAD_EXE_FORMAT 193
#define ERROR_ALREADY_EXISTS 183
@ -102,6 +103,7 @@ namespace wibo {
extern uint32_t lastError;
extern char **argv;
extern int argc;
extern std::filesystem::path guestExecutablePath;
extern std::string executableName;
extern std::string commandLine;
extern std::vector<uint16_t> commandLineW;
@ -125,16 +127,16 @@ namespace wibo {
void setDllDirectoryOverride(const std::filesystem::path &path);
void clearDllDirectoryOverride();
std::optional<std::filesystem::path> dllDirectoryOverride();
HMODULE findLoadedModule(const char *name);
ModuleInfo *findLoadedModule(const char *name);
void registerOnExitTable(void *table);
void addOnExitFunction(void *table, void (*func)());
void executeOnExitTable(void *table);
void runPendingOnExit(ModuleInfo &info);
HMODULE loadModule(const char *name);
void freeModule(HMODULE module);
void *resolveFuncByName(HMODULE module, const char *funcName);
void *resolveFuncByOrdinal(HMODULE module, uint16_t ordinal);
ModuleInfo *loadModule(const char *name);
void freeModule(ModuleInfo *info);
void *resolveFuncByName(ModuleInfo *info, const char *funcName);
void *resolveFuncByOrdinal(ModuleInfo *info, uint16_t ordinal);
void *resolveMissingImportByName(const char *dllName, const char *funcName);
void *resolveMissingImportByOrdinal(const char *dllName, uint16_t ordinal);
@ -172,51 +174,63 @@ namespace wibo {
};
struct Executable {
Executable();
Executable() = default;
~Executable();
bool loadPE(FILE *file, bool exec);
bool resolveImports();
void *imageBuffer;
size_t imageSize;
void *entryPoint;
void *rsrcBase;
uint32_t rsrcSize;
uintptr_t preferredImageBase;
intptr_t relocationDelta;
uint32_t exportDirectoryRVA;
uint32_t exportDirectorySize;
uint32_t relocationDirectoryRVA;
uint32_t relocationDirectorySize;
void *imageBase = nullptr;
size_t imageSize = 0;
void *entryPoint = nullptr;
void *rsrcBase = nullptr;
uint32_t rsrcSize = 0;
uintptr_t preferredImageBase = 0;
intptr_t relocationDelta = 0;
uint32_t exportDirectoryRVA = 0;
uint32_t exportDirectorySize = 0;
uint32_t relocationDirectoryRVA = 0;
uint32_t relocationDirectorySize = 0;
uint32_t importDirectoryRVA = 0;
uint32_t importDirectorySize = 0;
uint32_t delayImportDirectoryRVA = 0;
uint32_t delayImportDirectorySize = 0;
bool execMapped = false;
bool importsResolved = false;
bool importsResolving = false;
bool findResource(const ResourceIdentifier &type,
const ResourceIdentifier &name,
std::optional<uint16_t> language,
ResourceLocation &out) const;
bool findResource(const ResourceIdentifier &type, const ResourceIdentifier &name,
std::optional<uint16_t> language, ResourceLocation &out) const;
template <typename T>
T *fromRVA(uint32_t rva) const {
return (T *) (rva + (uint8_t *) imageBuffer);
T *fromRVA(uintptr_t rva) const {
return (T *) (rva + (uint8_t *) imageBase);
}
template <typename T>
T *fromRVA(T *rva) const {
return fromRVA<T>((uint32_t) rva);
return fromRVA<T>((uintptr_t) rva);
}
};
extern ModuleInfo *mainModule;
struct ModuleInfo {
// Windows-style handle to the module. For the main module, this is the image base.
// For other modules, this is a pointer to the ModuleInfo structure.
HMODULE handle;
// Original name used to load the module
std::string originalName;
// Normalized module name
std::string normalizedName;
// Full path to the loaded module
std::filesystem::path resolvedPath;
// Pointer to the built-in module, nullptr if loaded from file
const wibo::Module *module = nullptr;
// Loaded PE executable
std::unique_ptr<wibo::Executable> executable;
void *entryPoint = nullptr;
void *imageBase = nullptr;
size_t imageSize = 0;
// Reference count, or UINT_MAX for built-in modules
unsigned int refCount = 0;
bool dataFile = false;
bool processAttachCalled = false;
bool processAttachSucceeded = false;
bool dontResolveReferences = false;
uint32_t exportOrdinalBase = 0;
std::vector<void *> exportsByOrdinal;
std::unordered_map<std::string, uint16_t> exportNameToOrdinal;
@ -224,14 +238,16 @@ namespace wibo {
std::vector<void *> onExitFunctions;
};
extern Executable *mainModule;
ModuleInfo *registerProcessModule(std::unique_ptr<Executable> executable, std::filesystem::path resolvedPath,
std::string originalName);
Executable *executableFromModule(HMODULE module);
/**
* HMODULE will be `nullptr` or `mainModule->imageBuffer` if it's the main module,
* HMODULE will be `nullptr` or `mainModule->imageBase` if it's the main module,
* otherwise it will be a pointer to a `wibo::ModuleInfo`.
*/
inline bool isMainModule(HMODULE hModule) {
return hModule == nullptr || hModule == mainModule->imageBuffer;
return hModule == nullptr || hModule == reinterpret_cast<HMODULE>(mainModule) ||
(mainModule && mainModule->executable && hModule == mainModule->executable->imageBase);
}
} // namespace wibo

View File

@ -522,7 +522,7 @@ namespace advapi32 {
return FALSE;
}
auto *stats = reinterpret_cast<TokenStatisticsData *>(TokenInformation);
memset(stats, 0, required);
*stats = {};
stats->tokenType = 1; // TokenPrimary
stats->impersonationLevel = 0; // SecurityAnonymous
stats->tokenId.LowPart = 1;

View File

@ -122,6 +122,8 @@ int WIN_ENTRY strcmp(const char *lhs, const char *rhs) { return ::strcmp(lhs, rh
int WIN_ENTRY strncmp(const char *lhs, const char *rhs, size_t count) { return ::strncmp(lhs, rhs, count); }
char *WIN_ENTRY strcpy(char *dest, const char *src) { return ::strcpy(dest, src); }
void *WIN_ENTRY malloc(size_t size) { return ::malloc(size); }
void *WIN_ENTRY calloc(size_t count, size_t size) { return ::calloc(count, size); }
@ -262,6 +264,8 @@ static void *resolveByName(const char *name) {
return (void *)crt::strcmp;
if (strcmp(name, "strncmp") == 0)
return (void *)crt::strncmp;
if (strcmp(name, "strcpy") == 0)
return (void *)crt::strcpy;
if (strcmp(name, "malloc") == 0)
return (void *)crt::malloc;
if (strcmp(name, "calloc") == 0)

View File

@ -2685,17 +2685,13 @@ namespace kernel32 {
HMODULE WIN_FUNC GetModuleHandleA(LPCSTR lpModuleName) {
DEBUG_LOG("GetModuleHandleA(%s)\n", lpModuleName);
if (!lpModuleName) {
// If lpModuleName is NULL, GetModuleHandle returns a handle to the file
// used to create the calling process (.exe file).
// This handle needs to equal the actual image buffer, from which data can be read.
return wibo::mainModule->imageBuffer;
const auto* module = wibo::findLoadedModule(lpModuleName);
if (!module) {
wibo::lastError = ERROR_MOD_NOT_FOUND;
return nullptr;
}
HMODULE module = wibo::findLoadedModule(lpModuleName);
wibo::lastError = module ? ERROR_SUCCESS : ERROR_MOD_NOT_FOUND;
return module;
wibo::lastError = ERROR_SUCCESS;
return module->handle;
}
HMODULE WIN_FUNC GetModuleHandleW(LPCWSTR lpModuleName) {
@ -2715,22 +2711,16 @@ namespace kernel32 {
return 0;
}
auto *info = wibo::moduleInfoFromHandle(hModule);
if (!info) {
wibo::lastError = ERROR_INVALID_PARAMETER;
return 0;
}
std::string path;
if (wibo::isMainModule(hModule)) {
const auto exePath = files::pathFromWindows(wibo::argv[0]);
const auto absPath = std::filesystem::absolute(exePath);
path = files::pathToWindows(absPath);
if (!info->resolvedPath.empty()) {
path = files::pathToWindows(info->resolvedPath);
} else {
auto *info = wibo::moduleInfoFromHandle(hModule);
if (!info) {
wibo::lastError = ERROR_INVALID_PARAMETER;
return 0;
}
if (!info->resolvedPath.empty()) {
path = files::pathToWindows(info->resolvedPath);
} else {
path = info->originalName;
}
path = info->originalName;
}
const size_t len = path.size();
if (nSize == 0) {
@ -2759,22 +2749,16 @@ namespace kernel32 {
return 0;
}
auto *info = wibo::moduleInfoFromHandle(hModule);
if (!info) {
wibo::lastError = ERROR_INVALID_PARAMETER;
return 0;
}
std::string path;
if (wibo::isMainModule(hModule)) {
const auto exePath = files::pathFromWindows(wibo::argv[0]);
const auto absPath = std::filesystem::absolute(exePath);
path = files::pathToWindows(absPath);
if (!info->resolvedPath.empty()) {
path = files::pathToWindows(info->resolvedPath);
} else {
auto *info = wibo::moduleInfoFromHandle(hModule);
if (!info) {
wibo::lastError = ERROR_INVALID_PARAMETER;
return 0;
}
if (!info->resolvedPath.empty()) {
path = files::pathToWindows(info->resolvedPath);
} else {
path = info->originalName;
}
path = info->originalName;
}
if (nSize == 0) {
wibo::lastError = ERROR_INSUFFICIENT_BUFFER;
@ -2802,18 +2786,9 @@ namespace kernel32 {
return copyLen;
}
static wibo::Executable *module_executable_for_resource(void *hModule) {
if (!hModule) {
hModule = GetModuleHandleA(nullptr);
}
return wibo::executableFromModule((HMODULE) hModule);
}
static void *find_resource_internal(void *hModule,
const wibo::ResourceIdentifier &type,
const wibo::ResourceIdentifier &name,
std::optional<uint16_t> language) {
auto *exe = module_executable_for_resource(hModule);
static void *findResourceInternal(HMODULE hModule, const wibo::ResourceIdentifier &type,
const wibo::ResourceIdentifier &name, std::optional<uint16_t> language) {
auto *exe = wibo::executableFromModule(hModule);
if (!exe) {
wibo::lastError = ERROR_RESOURCE_DATA_NOT_FOUND;
return nullptr;
@ -2825,41 +2800,41 @@ namespace kernel32 {
return const_cast<void *>(loc.dataEntry);
}
void *WIN_FUNC FindResourceA(void *hModule, const char *lpName, const char *lpType) {
void *WIN_FUNC FindResourceA(HMODULE hModule, const char *lpName, const char *lpType) {
DEBUG_LOG("FindResourceA %p %p %p\n", hModule, lpName, lpType);
auto type = wibo::resourceIdentifierFromAnsi(lpType);
auto name = wibo::resourceIdentifierFromAnsi(lpName);
return find_resource_internal(hModule, type, name, std::nullopt);
return findResourceInternal(hModule, type, name, std::nullopt);
}
void *WIN_FUNC FindResourceExA(void *hModule, const char *lpType, const char *lpName, uint16_t wLanguage) {
void *WIN_FUNC FindResourceExA(HMODULE hModule, const char *lpType, const char *lpName, uint16_t wLanguage) {
DEBUG_LOG("FindResourceExA %p %p %p %u\n", hModule, lpName, lpType, wLanguage);
auto type = wibo::resourceIdentifierFromAnsi(lpType);
auto name = wibo::resourceIdentifierFromAnsi(lpName);
return find_resource_internal(hModule, type, name, wLanguage);
return findResourceInternal(hModule, type, name, wLanguage);
}
void *WIN_FUNC FindResourceW(void *hModule, const uint16_t *lpName, const uint16_t *lpType) {
void *WIN_FUNC FindResourceW(HMODULE hModule, const uint16_t *lpName, const uint16_t *lpType) {
DEBUG_LOG("FindResourceW %p\n", hModule);
auto type = wibo::resourceIdentifierFromWide(lpType);
auto name = wibo::resourceIdentifierFromWide(lpName);
return find_resource_internal(hModule, type, name, std::nullopt);
return findResourceInternal(hModule, type, name, std::nullopt);
}
void *WIN_FUNC FindResourceExW(void *hModule, const uint16_t *lpType, const uint16_t *lpName, uint16_t wLanguage) {
void *WIN_FUNC FindResourceExW(HMODULE hModule, const uint16_t *lpType, const uint16_t *lpName, uint16_t wLanguage) {
DEBUG_LOG("FindResourceExW %p %u\n", hModule, wLanguage);
auto type = wibo::resourceIdentifierFromWide(lpType);
auto name = wibo::resourceIdentifierFromWide(lpName);
return find_resource_internal(hModule, type, name, wLanguage);
return findResourceInternal(hModule, type, name, wLanguage);
}
void* WIN_FUNC LoadResource(void* hModule, void* res) {
void *WIN_FUNC LoadResource(HMODULE hModule, void *res) {
DEBUG_LOG("LoadResource %p %p\n", hModule, res);
if (!res) {
wibo::lastError = ERROR_RESOURCE_DATA_NOT_FOUND;
return nullptr;
}
auto *exe = module_executable_for_resource(hModule);
auto *exe = wibo::executableFromModule(hModule);
if (!exe || !exe->rsrcBase) {
wibo::lastError = ERROR_RESOURCE_DATA_NOT_FOUND;
return nullptr;
@ -2901,13 +2876,13 @@ namespace kernel32 {
return res;
}
unsigned int WIN_FUNC SizeofResource(void* hModule, void* res) {
unsigned int WIN_FUNC SizeofResource(HMODULE hModule, void* res) {
DEBUG_LOG("SizeofResource %p %p\n", hModule, res);
if (!res) {
wibo::lastError = ERROR_RESOURCE_DATA_NOT_FOUND;
return 0;
}
auto *exe = module_executable_for_resource(hModule);
auto *exe = wibo::executableFromModule(hModule);
if (!exe || !exe->rsrcBase) {
wibo::lastError = ERROR_RESOURCE_DATA_NOT_FOUND;
return 0;
@ -2922,7 +2897,13 @@ namespace kernel32 {
HMODULE WIN_FUNC LoadLibraryA(LPCSTR lpLibFileName) {
DEBUG_LOG("LoadLibraryA(%s)\n", lpLibFileName);
return wibo::loadModule(lpLibFileName);
const auto *info = wibo::loadModule(lpLibFileName);
if (!info) {
// loadModule already sets lastError
return nullptr;
}
wibo::lastError = ERROR_SUCCESS;
return info->handle;
}
HMODULE WIN_FUNC LoadLibraryW(LPCWSTR lpLibFileName) {
@ -2943,7 +2924,12 @@ namespace kernel32 {
BOOL WIN_FUNC FreeLibrary(HMODULE hLibModule) {
DEBUG_LOG("FreeLibrary(%p)\n", hLibModule);
wibo::freeModule(hLibModule);
auto *info = wibo::moduleInfoFromHandle(hLibModule);
if (!info) {
wibo::lastError = ERROR_INVALID_HANDLE;
return FALSE;
}
wibo::freeModule(info);
return TRUE;
}
@ -3664,15 +3650,26 @@ namespace kernel32 {
FARPROC WIN_FUNC GetProcAddress(HMODULE hModule, LPCSTR lpProcName) {
FARPROC result;
const auto info = wibo::moduleInfoFromHandle(hModule);
if (!info) {
DEBUG_LOG("GetProcAddress: invalid module handle %p\n", hModule);
wibo::lastError = ERROR_INVALID_HANDLE;
return nullptr;
}
const auto proc = reinterpret_cast<uintptr_t>(lpProcName);
if (proc & ~0xFFFF) {
DEBUG_LOG("GetProcAddress(%p, %s) ", hModule, lpProcName);
result = wibo::resolveFuncByName(hModule, lpProcName);
result = wibo::resolveFuncByName(info, lpProcName);
} else {
DEBUG_LOG("GetProcAddress(%p, %u) ", hModule, proc);
result = wibo::resolveFuncByOrdinal(hModule, static_cast<uint16_t>(proc));
result = wibo::resolveFuncByOrdinal(info, static_cast<uint16_t>(proc));
}
DEBUG_LOG("-> %p\n", result);
if (!result) {
wibo::lastError = ERROR_PROC_NOT_FOUND;
} else {
wibo::lastError = ERROR_SUCCESS;
}
return result;
}

View File

@ -169,8 +169,8 @@ namespace msvcrt {
std::string converted = files::hostPathListToWindows(value);
std::string result = converted.empty() ? value : converted;
std::string exeDir;
if (wibo::argv && wibo::argv[0]) {
std::filesystem::path exePath = std::filesystem::absolute(std::filesystem::path(wibo::argv[0])).parent_path();
if (!wibo::guestExecutablePath.empty()) {
auto exePath = wibo::guestExecutablePath.parent_path();
if (!exePath.empty()) {
exeDir = files::pathToWindows(exePath);
}

View File

@ -17,7 +17,7 @@ BOOL WIN_FUNC EnumProcessModules(HANDLE hProcess, HMODULE *lphModule, DWORD cb,
return FALSE;
}
HMODULE currentModule = wibo::mainModule ? reinterpret_cast<HMODULE>(wibo::mainModule->imageBuffer) : nullptr;
HMODULE currentModule = wibo::mainModule ? wibo::mainModule->handle : nullptr;
DWORD required = currentModule ? sizeof(HMODULE) : 0;
if (lpcbNeeded) {
*lpcbNeeded = required;

View File

@ -52,12 +52,12 @@ struct PE32Header {
uint32_t loaderFlags;
uint32_t numberOfRvaAndSizes;
PEImageDataDirectory exportTable;
PEImageDataDirectory importTable; // *
PEImageDataDirectory importTable; // *
PEImageDataDirectory resourceTable; // *
PEImageDataDirectory exceptionTable;
PEImageDataDirectory certificateTable;
PEImageDataDirectory baseRelocationTable; // *
PEImageDataDirectory debug; // *
PEImageDataDirectory debug; // *
PEImageDataDirectory architecture;
PEImageDataDirectory globalPtr;
PEImageDataDirectory tlsTable;
@ -122,24 +122,10 @@ uint32_t read32(FILE *file) {
return v;
}
wibo::Executable::Executable() {
imageBuffer = nullptr;
imageSize = 0;
entryPoint = nullptr;
rsrcBase = 0;
rsrcSize = 0;
preferredImageBase = 0;
relocationDelta = 0;
exportDirectoryRVA = 0;
exportDirectorySize = 0;
relocationDirectoryRVA = 0;
relocationDirectorySize = 0;
}
wibo::Executable::~Executable() {
if (imageBuffer) {
munmap(imageBuffer, imageSize);
if (imageBase) {
munmap(imageBase, imageSize);
imageBase = nullptr;
}
}
@ -167,7 +153,7 @@ bool wibo::Executable::loadPE(FILE *file, bool exec) {
PE32Header header32;
memset(&header32, 0, sizeof header32);
fread(&header32, std::min(sizeof(header32), (size_t) header.sizeOfOptionalHeader), 1, file);
fread(&header32, std::min(sizeof(header32), (size_t)header.sizeOfOptionalHeader), 1, file);
if (header32.magic != 0x10B)
return false;
@ -181,6 +167,13 @@ bool wibo::Executable::loadPE(FILE *file, bool exec) {
exportDirectorySize = header32.exportTable.size;
relocationDirectoryRVA = header32.baseRelocationTable.virtualAddress;
relocationDirectorySize = header32.baseRelocationTable.size;
importDirectoryRVA = header32.importTable.virtualAddress;
importDirectorySize = header32.importTable.size;
delayImportDirectoryRVA = header32.delayImportDescriptor.virtualAddress;
delayImportDirectorySize = header32.delayImportDescriptor.size;
execMapped = exec;
importsResolved = false;
importsResolving = false;
// Build buffer
imageSize = header32.sizeOfImage;
@ -188,17 +181,17 @@ bool wibo::Executable::loadPE(FILE *file, bool exec) {
if (exec)
prot |= PROT_EXEC;
void *preferredBase = (void *)(uintptr_t)header32.imageBase;
imageBuffer = mmap(preferredBase, header32.sizeOfImage, prot, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (imageBuffer == MAP_FAILED) {
imageBuffer = mmap(nullptr, header32.sizeOfImage, prot, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
imageBase = mmap(preferredBase, header32.sizeOfImage, prot, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (imageBase == MAP_FAILED) {
imageBase = mmap(nullptr, header32.sizeOfImage, prot, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
}
if (imageBuffer == MAP_FAILED) {
if (imageBase == MAP_FAILED) {
perror("Image mapping failed!");
imageBuffer = nullptr;
imageBase = nullptr;
return false;
}
relocationDelta = (intptr_t)((uintptr_t)imageBuffer - (uintptr_t)header32.imageBase);
memset(imageBuffer, 0, header32.sizeOfImage);
relocationDelta = (intptr_t)((uintptr_t)imageBase - (uintptr_t)header32.imageBase);
memset(imageBase, 0, header32.sizeOfImage);
// Read the sections
fseek(file, offsetToPE + sizeof header + header.sizeOfOptionalHeader, SEEK_SET);
@ -210,9 +203,10 @@ bool wibo::Executable::loadPE(FILE *file, bool exec) {
char name[9];
memcpy(name, section.name, 8);
name[8] = 0;
DEBUG_LOG("Section %d: name=%s addr=%x size=%x (raw=%x) ptr=%x\n", i, name, section.virtualAddress, section.virtualSize, section.sizeOfRawData, section.pointerToRawData);
DEBUG_LOG("Section %d: name=%s addr=%x size=%x (raw=%x) ptr=%x\n", i, name, section.virtualAddress,
section.virtualSize, section.sizeOfRawData, section.pointerToRawData);
void *sectionBase = (void *) ((uintptr_t) imageBuffer + section.virtualAddress);
void *sectionBase = (void *)((uintptr_t)imageBase + section.virtualAddress);
if (section.pointerToRawData > 0 && section.sizeOfRawData > 0) {
// Grab this data
long savePos = ftell(file);
@ -230,8 +224,8 @@ bool wibo::Executable::loadPE(FILE *file, bool exec) {
if (exec && relocationDelta != 0) {
if (relocationDirectoryRVA == 0 || relocationDirectorySize == 0) {
DEBUG_LOG("Relocation required but no relocation directory present\n");
munmap(imageBuffer, imageSize);
imageBuffer = nullptr;
munmap(imageBase, imageSize);
imageBase = nullptr;
return false;
}
@ -239,7 +233,8 @@ bool wibo::Executable::loadPE(FILE *file, bool exec) {
uint8_t *relocEnd = relocCursor + relocationDirectorySize;
while (relocCursor < relocEnd) {
auto *block = reinterpret_cast<PEBaseRelocationBlock *>(relocCursor);
if (block->sizeOfBlock < sizeof(PEBaseRelocationBlock) || block->sizeOfBlock > static_cast<uint32_t>(relocEnd - relocCursor)) {
if (block->sizeOfBlock < sizeof(PEBaseRelocationBlock) ||
block->sizeOfBlock > static_cast<uint32_t>(relocEnd - relocCursor)) {
break;
}
if (block->sizeOfBlock == sizeof(PEBaseRelocationBlock)) {
@ -253,7 +248,7 @@ bool wibo::Executable::loadPE(FILE *file, bool exec) {
uint16_t offset = entry & 0x0FFF;
if (type == IMAGE_REL_BASED_ABSOLUTE)
continue;
uintptr_t target = reinterpret_cast<uintptr_t>(imageBuffer) + block->virtualAddress + offset;
uintptr_t target = reinterpret_cast<uintptr_t>(imageBase) + block->virtualAddress + offset;
switch (type) {
case IMAGE_REL_BASED_HIGHLOW: {
auto *addr = reinterpret_cast<uint32_t *>(target);
@ -269,13 +264,34 @@ bool wibo::Executable::loadPE(FILE *file, bool exec) {
}
}
if (!exec) {
// No need to resolve imports
entryPoint = header32.addressOfEntryPoint ? fromRVA<void>(header32.addressOfEntryPoint) : nullptr;
return true;
}
bool wibo::Executable::resolveImports() {
if (importsResolved || !execMapped) {
importsResolved = true;
importsResolving = false;
return true;
}
if (importsResolving) {
return true;
}
importsResolving = true;
if (!importDirectoryRVA) {
importsResolved = true;
importsResolving = false;
return true;
}
// Handle imports
PEImportDirectoryEntry *dir = fromRVA<PEImportDirectoryEntry>(header32.importTable.virtualAddress);
PEImportDirectoryEntry *dir = fromRVA<PEImportDirectoryEntry>(importDirectoryRVA);
if (!dir) {
importsResolved = true;
importsResolving = false;
return true;
}
while (dir->name) {
char *dllName = fromRVA(dir->name);
@ -283,15 +299,15 @@ bool wibo::Executable::loadPE(FILE *file, bool exec) {
uint32_t *lookupTable = fromRVA(dir->importLookupTable);
uint32_t *addressTable = fromRVA(dir->importAddressTable);
HMODULE module = loadModule(dllName);
ModuleInfo *module = loadModule(dllName);
while (*lookupTable) {
uint32_t lookup = *lookupTable;
if (lookup & 0x80000000) {
// Import by ordinal
uint16_t ordinal = lookup & 0xFFFF;
DEBUG_LOG(" Ordinal: %d\n", ordinal);
void *func = module ? resolveFuncByOrdinal(module, ordinal)
: resolveMissingImportByOrdinal(dllName, ordinal);
void *func =
module ? resolveFuncByOrdinal(module, ordinal) : resolveMissingImportByOrdinal(dllName, ordinal);
DEBUG_LOG(" -> %p\n", func);
*addressTable = reinterpret_cast<uintptr_t>(func);
} else {
@ -299,7 +315,7 @@ bool wibo::Executable::loadPE(FILE *file, bool exec) {
PEHintNameTableEntry *hintName = fromRVA<PEHintNameTableEntry>(lookup);
DEBUG_LOG(" Name: %s (IAT=%p)\n", hintName->name, addressTable);
void *func = module ? resolveFuncByName(module, hintName->name)
: resolveMissingImportByName(dllName, hintName->name);
: resolveMissingImportByName(dllName, hintName->name);
DEBUG_LOG(" -> %p\n", func);
*addressTable = reinterpret_cast<uintptr_t>(func);
}
@ -309,28 +325,29 @@ bool wibo::Executable::loadPE(FILE *file, bool exec) {
++dir;
}
if (header32.delayImportDescriptor.virtualAddress) {
DEBUG_LOG("Processing delay import table at RVA %x\n", header32.delayImportDescriptor.virtualAddress);
PEDelayImportDescriptor *delay = fromRVA<PEDelayImportDescriptor>(header32.delayImportDescriptor.virtualAddress);
while (delay->name) {
// TODO: actual delay loading from __delayLoadHelper2
if (delayImportDirectoryRVA) {
DEBUG_LOG("Processing delay import table at RVA %x\n", delayImportDirectoryRVA);
PEDelayImportDescriptor *delay = fromRVA<PEDelayImportDescriptor>(delayImportDirectoryRVA);
while (delay && delay->name) {
char *dllName = fromRVA<char>(delay->name);
DEBUG_LOG("Delay DLL Name: %s\n", dllName);
uint32_t *lookupTable = fromRVA<uint32_t>(delay->importNameTable);
uint32_t *addressTable = fromRVA<uint32_t>(delay->importAddressTable);
HMODULE module = loadModule(dllName);
ModuleInfo *module = loadModule(dllName);
while (*lookupTable) {
uint32_t lookup = *lookupTable;
if (lookup & 0x80000000) {
uint16_t ordinal = lookup & 0xFFFF;
DEBUG_LOG(" Ordinal: %d (IAT=%p)\n", ordinal, addressTable);
DEBUG_LOG(" Ordinal: %d (IAT=%p)\n", ordinal, addressTable);
void *func = module ? resolveFuncByOrdinal(module, ordinal)
: resolveMissingImportByOrdinal(dllName, ordinal);
: resolveMissingImportByOrdinal(dllName, ordinal);
*addressTable = reinterpret_cast<uintptr_t>(func);
} else {
PEHintNameTableEntry *hintName = fromRVA<PEHintNameTableEntry>(lookup);
DEBUG_LOG(" Name: %s\n", hintName->name);
void *func = module ? resolveFuncByName(module, hintName->name)
: resolveMissingImportByName(dllName, hintName->name);
: resolveMissingImportByName(dllName, hintName->name);
*addressTable = reinterpret_cast<uintptr_t>(func);
}
++lookupTable;
@ -346,7 +363,7 @@ bool wibo::Executable::loadPE(FILE *file, bool exec) {
}
}
entryPoint = header32.addressOfEntryPoint ? fromRVA<void>(header32.addressOfEntryPoint) : nullptr;
importsResolved = true;
importsResolving = false;
return true;
}

View File

@ -1,26 +1,28 @@
#include "common.h"
#include "files.h"
#include "processes.h"
#include "strutil.h"
#include <asm/ldt.h>
#include <charconv>
#include <cstdarg>
#include <cstring>
#include <fcntl.h>
#include <filesystem>
#include <memory>
#include <stdarg.h>
#include <system_error>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <system_error>
#include <unistd.h>
#include <vector>
uint32_t wibo::lastError = 0;
char** wibo::argv;
char **wibo::argv;
int wibo::argc;
std::filesystem::path wibo::guestExecutablePath;
std::string wibo::executableName;
std::string wibo::commandLine;
std::vector<uint16_t> wibo::commandLineW;
wibo::Executable *wibo::mainModule = 0;
wibo::ModuleInfo *wibo::mainModule = nullptr;
bool wibo::debugEnabled = false;
unsigned int wibo::debugIndent = 0;
uint16_t wibo::tibSelector = 0;
@ -109,7 +111,7 @@ static void printHelp(const char *argv0) {
* @param buffer The buffer to read into.
* @return The number of bytes read.
*/
static size_t readMaps(char* buffer) {
static size_t readMaps(char *buffer) {
int fd = open("/proc/self/maps", O_RDONLY);
if (fd == -1) {
perror("Failed to open /proc/self/maps");
@ -187,7 +189,8 @@ static void blockUpper2GB() {
holdingMapStart = std::max(holdingMapStart, FILL_MEMORY_ABOVE);
// DEBUG_LOG("Mapping %08x-%08x\n", holdingMapStart, holdingMapEnd);
void* holdingMap = mmap((void*) holdingMapStart, holdingMapEnd - holdingMapStart, PROT_READ | PROT_WRITE, MAP_ANONYMOUS|MAP_FIXED|MAP_PRIVATE, -1, 0);
void *holdingMap = mmap((void *)holdingMapStart, holdingMapEnd - holdingMapStart, PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_FIXED | MAP_PRIVATE, -1, 0);
if (holdingMap == MAP_FAILED) {
perror("Failed to create holding map");
@ -288,13 +291,13 @@ int main(int argc, char **argv) {
// Create TIB
memset(&tib, 0, sizeof(tib));
tib.tib = &tib;
tib.peb = (PEB*)calloc(sizeof(PEB), 1);
tib.peb->ProcessParameters = (RTL_USER_PROCESS_PARAMETERS*)calloc(sizeof(RTL_USER_PROCESS_PARAMETERS), 1);
tib.peb = (PEB *)calloc(sizeof(PEB), 1);
tib.peb->ProcessParameters = (RTL_USER_PROCESS_PARAMETERS *)calloc(sizeof(RTL_USER_PROCESS_PARAMETERS), 1);
struct user_desc tibDesc;
memset(&tibDesc, 0, sizeof tibDesc);
tibDesc.entry_number = 0;
tibDesc.base_addr = (unsigned int) &tib;
tibDesc.base_addr = (unsigned int)&tib;
tibDesc.limit = 0x1000;
tibDesc.seg_32bit = 1;
tibDesc.contents = 0; // hopefully this is ok
@ -312,12 +315,25 @@ int main(int argc, char **argv) {
char **guestArgv = argv + programIndex;
int guestArgc = argc - programIndex;
const char *pePath = guestArgv[0];
if (!pePath || pePath[0] == '\0') {
fprintf(stderr, "No guest binary specified\n");
return 1;
}
std::string originalName = pePath;
std::filesystem::path resolvedGuestPath = processes::resolveExecutable(originalName, true).value_or({});
if (resolvedGuestPath.empty()) {
fprintf(stderr, "Failed to resolve path to guest binary %s\n", originalName.c_str());
return 1;
}
// Build a command line
std::string cmdLine;
for (int i = 0; i < guestArgc; ++i) {
std::string arg;
if (i == 0) {
arg = files::pathToWindows(std::filesystem::absolute(guestArgv[0]));
arg = files::pathToWindows(resolvedGuestPath);
} else {
cmdLine += ' ';
arg = guestArgv[i];
@ -326,7 +342,7 @@ int main(int argc, char **argv) {
if (needQuotes)
cmdLine += '"';
int backslashes = 0;
for (const char *p = arg.c_str(); ; p++) {
for (const char *p = arg.c_str();; p++) {
char c = *p;
if (c == '\\') {
backslashes++;
@ -356,32 +372,50 @@ int main(int argc, char **argv) {
wibo::commandLineW = stringToWideString(wibo::commandLine.c_str());
DEBUG_LOG("Command line: %s\n", wibo::commandLine.c_str());
wibo::guestExecutablePath = resolvedGuestPath;
wibo::executableName = executablePath;
wibo::argv = guestArgv;
wibo::argc = guestArgc;
wibo::initializeModuleRegistry();
wibo::Executable exec;
wibo::mainModule = &exec;
char* pe_path = guestArgv[0];
FILE *f = fopen(pe_path, "rb");
FILE *f = fopen(resolvedGuestPath.c_str(), "rb");
if (!f) {
std::string mesg = std::string("Failed to open file ") + pe_path;
std::string mesg = std::string("Failed to open file ") + resolvedGuestPath.string();
perror(mesg.c_str());
return 1;
}
exec.loadPE(f, true);
auto executable = std::make_unique<wibo::Executable>();
if (!executable->loadPE(f, true)) {
fclose(f);
fprintf(stderr, "Failed to load PE image %s\n", resolvedGuestPath.c_str());
return 1;
}
fclose(f);
const auto entryPoint = executable->entryPoint;
if (!entryPoint) {
fprintf(stderr, "Executable %s has no entry point\n", resolvedGuestPath.c_str());
return 1;
}
wibo::mainModule =
wibo::registerProcessModule(std::move(executable), std::move(resolvedGuestPath), std::move(originalName));
if (!wibo::mainModule || !wibo::mainModule->executable) {
fprintf(stderr, "Failed to register process module\n");
return 1;
}
DEBUG_LOG("Registered main module %s at %p\n", wibo::mainModule->normalizedName.c_str(),
wibo::mainModule->executable->imageBase);
if (!wibo::mainModule->executable->resolveImports()) {
fprintf(stderr, "Failed to resolve imports for main module\n");
return 1;
}
// Invoke the damn thing
asm(
"movw %0, %%fs; call *%1"
:
: "r"(wibo::tibSelector), "r"(exec.entryPoint)
);
asm("movw %0, %%fs; call *%1" : : "r"(wibo::tibSelector), "r"(entryPoint));
DEBUG_LOG("We came back\n");
wibo::shutdownModuleRegistry();

View File

@ -4,7 +4,6 @@
#include <algorithm>
#include <array>
#include <cerrno>
#include <climits>
#include <cstdio>
#include <cstdlib>
@ -253,10 +252,10 @@ std::vector<std::filesystem::path> collectSearchDirectories(ModuleRegistry &reg,
}
};
if (wibo::argv && wibo::argc > 0 && wibo::argv[0]) {
std::filesystem::path mainBinary = std::filesystem::absolute(wibo::argv[0]);
if (mainBinary.has_parent_path()) {
addDirectory(mainBinary.parent_path());
if (!wibo::guestExecutablePath.empty()) {
auto parent = wibo::guestExecutablePath.parent_path();
if (!parent.empty()) {
addDirectory(parent);
}
}
@ -369,6 +368,7 @@ void registerBuiltinModule(ModuleRegistry &reg, const wibo::Module *module) {
return;
}
ModulePtr entry = std::make_unique<wibo::ModuleInfo>();
entry->handle = entry.get();
entry->module = module;
entry->refCount = UINT_MAX;
entry->originalName = module->names[0] ? module->names[0] : "";
@ -406,24 +406,24 @@ void registerBuiltinModule(ModuleRegistry &reg, const wibo::Module *module) {
}
void callDllMain(wibo::ModuleInfo &info, DWORD reason) {
if (!info.entryPoint || info.module) {
if (!info.executable) {
return;
}
using DllMainFunc = BOOL(WIN_FUNC *)(HMODULE, DWORD, LPVOID);
auto dllMain = reinterpret_cast<DllMainFunc>(info.entryPoint);
auto dllMain = reinterpret_cast<DllMainFunc>(info.executable->entryPoint);
if (!dllMain) {
return;
}
auto invokeWithGuestTIB = [&](DWORD callReason) -> BOOL {
if (!wibo::tibSelector) {
return dllMain(reinterpret_cast<HMODULE>(info.imageBase), callReason, nullptr);
return dllMain(reinterpret_cast<HMODULE>(info.executable->imageBase), callReason, nullptr);
}
uint16_t previousSegment = 0;
asm volatile("mov %%fs, %0" : "=r"(previousSegment));
asm volatile("movw %0, %%fs" : : "r"(wibo::tibSelector) : "memory");
BOOL result = dllMain(reinterpret_cast<HMODULE>(info.imageBase), callReason, nullptr);
BOOL result = dllMain(reinterpret_cast<HMODULE>(info.executable->imageBase), callReason, nullptr);
asm volatile("movw %0, %%fs" : : "r"(previousSegment) : "memory");
return result;
};
@ -455,20 +455,13 @@ wibo::ModuleInfo *moduleFromAddress(ModuleRegistry &reg, void *addr) {
return nullptr;
for (auto &pair : reg.modulesByKey) {
wibo::ModuleInfo *info = pair.second.get();
if (!info)
if (!info || !info->executable)
continue;
uint8_t *base = nullptr;
size_t size = 0;
if (info->imageBase && info->imageSize) {
base = static_cast<uint8_t *>(info->imageBase);
size = info->imageSize;
} else if (info->executable) {
base = static_cast<uint8_t *>(info->executable->imageBuffer);
size = info->executable->imageSize;
}
const auto *base = static_cast<const uint8_t *>(info->executable->imageBase);
size_t size = info->executable->imageSize;
if (!base || size == 0)
continue;
auto *ptr = static_cast<uint8_t *>(addr);
const auto *ptr = static_cast<const uint8_t *>(addr);
if (ptr >= base && ptr < base + size) {
return info;
}
@ -530,6 +523,68 @@ namespace wibo {
void initializeModuleRegistry() { registry(); }
ModuleInfo *registerProcessModule(std::unique_ptr<Executable> executable, std::filesystem::path resolvedPath,
std::string originalName) {
if (!executable) {
return nullptr;
}
if (originalName.empty() && !resolvedPath.empty()) {
originalName = resolvedPath.filename().string();
}
ParsedModuleName parsed = parseModuleName(originalName);
std::string normalizedName = normalizedBaseKey(parsed);
ModulePtr info = std::make_unique<ModuleInfo>();
info->handle = executable->imageBase; // Use image base as handle for main module
info->module = nullptr;
info->originalName = std::move(originalName);
info->normalizedName = std::move(normalizedName);
info->resolvedPath = std::move(resolvedPath);
info->executable = std::move(executable);
info->refCount = UINT_MAX;
ModuleInfo *raw = info.get();
std::string storageKey;
if (!raw->resolvedPath.empty()) {
storageKey = storageKeyForPath(raw->resolvedPath);
} else if (!raw->normalizedName.empty()) {
storageKey = storageKeyForBuiltin(raw->normalizedName);
}
if (storageKey.empty()) {
storageKey = normalizeAlias(raw->originalName);
}
auto reg = registry();
reg->modulesByKey[storageKey] = std::move(info);
if (!raw->resolvedPath.empty()) {
registerExternalModuleAliases(*reg, raw->originalName, raw->resolvedPath, raw);
} else {
registerAlias(*reg, normalizeAlias(raw->originalName), raw);
std::string baseAlias = normalizedBaseKey(parsed);
if (baseAlias != raw->originalName) {
registerAlias(*reg, baseAlias, raw);
}
}
ensureExportsInitialized(*raw);
auto pinAlias = [&](const std::string &alias) {
if (!alias.empty()) {
reg->pinnedAliases.insert(alias);
}
};
reg->pinnedModules.insert(raw);
pinAlias(storageKey);
pinAlias(normalizeAlias(raw->originalName));
pinAlias(normalizedName);
return raw;
}
void shutdownModuleRegistry() {
auto reg = registry();
for (auto &pair : reg->modulesByKey) {
@ -549,7 +604,12 @@ void shutdownModuleRegistry() {
reg->onExitTables.clear();
}
ModuleInfo *moduleInfoFromHandle(HMODULE module) { return static_cast<ModuleInfo *>(module); }
ModuleInfo *moduleInfoFromHandle(HMODULE module) {
if (isMainModule(module)) {
return wibo::mainModule;
}
return static_cast<ModuleInfo *>(module);
}
void setDllDirectoryOverride(const std::filesystem::path &path) {
auto canonical = files::canonicalPath(path);
@ -598,7 +658,7 @@ void addOnExitFunction(void *table, void (*func)()) {
void runPendingOnExit(ModuleInfo &info) {
for (auto it = info.onExitFunctions.rbegin(); it != info.onExitFunctions.rend(); ++it) {
auto fn = reinterpret_cast<void (*)(void)>(*it);
auto fn = reinterpret_cast<void (*)()>(*it);
if (fn) {
fn();
}
@ -623,9 +683,9 @@ void executeOnExitTable(void *table) {
}
}
HMODULE findLoadedModule(const char *name) {
if (!name) {
return nullptr;
ModuleInfo *findLoadedModule(const char *name) {
if (!name || *name == '\0') {
return wibo::mainModule;
}
auto reg = registry();
ParsedModuleName parsed = parseModuleName(name);
@ -637,8 +697,8 @@ HMODULE findLoadedModule(const char *name) {
return info;
}
HMODULE loadModule(const char *dllName) {
if (!dllName) {
ModuleInfo *loadModule(const char *dllName) {
if (!dllName || *dllName == '\0') {
lastError = ERROR_INVALID_PARAMETER;
return nullptr;
}
@ -680,22 +740,19 @@ HMODULE loadModule(const char *dllName) {
fclose(file);
ModulePtr info = std::make_unique<ModuleInfo>();
info->handle = info.get();
info->module = nullptr;
info->originalName = requested;
info->normalizedName = normalizedBaseKey(parsed);
info->resolvedPath = files::canonicalPath(path);
info->executable = std::move(executable);
info->entryPoint = info->executable->entryPoint;
info->imageBase = info->executable->imageBuffer;
info->imageSize = info->executable->imageSize;
info->refCount = 1;
info->dataFile = false;
info->dontResolveReferences = false;
ModuleInfo *raw = info.get();
reg->modulesByKey[key] = std::move(info);
registerExternalModuleAliases(*reg, requested, raw->resolvedPath, raw);
ensureExportsInitialized(*raw);
raw->executable->resolveImports();
callDllMain(*raw, DLL_PROCESS_ATTACH);
return raw;
};
@ -765,12 +822,8 @@ HMODULE loadModule(const char *dllName) {
return nullptr;
}
void freeModule(HMODULE module) {
if (!module) {
return;
}
void freeModule(ModuleInfo *info) {
auto reg = registry();
ModuleInfo *info = moduleInfoFromHandle(module);
if (!info || info->refCount == UINT_MAX) {
return;
}
@ -801,8 +854,7 @@ void freeModule(HMODULE module) {
}
}
void *resolveFuncByName(HMODULE module, const char *funcName) {
ModuleInfo *info = moduleInfoFromHandle(module);
void *resolveFuncByName(ModuleInfo *info, const char *funcName) {
if (!info) {
return nullptr;
}
@ -816,14 +868,13 @@ void *resolveFuncByName(HMODULE module, const char *funcName) {
if (!info->module) {
auto it = info->exportNameToOrdinal.find(funcName);
if (it != info->exportNameToOrdinal.end()) {
return resolveFuncByOrdinal(module, it->second);
return resolveFuncByOrdinal(info, it->second);
}
}
return reinterpret_cast<void *>(resolveMissingFuncName(info->originalName.c_str(), funcName));
}
void *resolveFuncByOrdinal(HMODULE module, uint16_t ordinal) {
ModuleInfo *info = moduleInfoFromHandle(module);
void *resolveFuncByOrdinal(ModuleInfo *info, uint16_t ordinal) {
if (!info) {
return nullptr;
}
@ -862,9 +913,6 @@ void *resolveMissingImportByOrdinal(const char *dllName, uint16_t ordinal) {
}
Executable *executableFromModule(HMODULE module) {
if (isMainModule(module)) {
return mainModule;
}
ModuleInfo *info = moduleInfoFromHandle(module);
if (!info) {
return nullptr;

View File

@ -1,12 +1,14 @@
#include <windows.h>
static int attached = 0;
static HMODULE observedMainModule = NULL;
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {
(void) hinstDLL;
(void) lpReserved;
if (fdwReason == DLL_PROCESS_ATTACH) {
attached = 1;
observedMainModule = GetModuleHandleW(NULL);
} else if (fdwReason == DLL_PROCESS_DETACH) {
attached = 2;
}
@ -20,3 +22,7 @@ __declspec(dllexport) int __stdcall add_numbers(int a, int b) {
__declspec(dllexport) int __stdcall was_attached(void) {
return attached;
}
__declspec(dllexport) HMODULE __stdcall observed_main_module(void) {
return observedMainModule;
}

View File

@ -7,23 +7,32 @@
int main(void) {
typedef int(__stdcall *add_numbers_fn)(int, int);
typedef int(__stdcall *was_attached_fn)(void);
typedef HMODULE(__stdcall *observed_main_module_fn)(void);
HMODULE initial_main = GetModuleHandleW(NULL);
TEST_CHECK(initial_main != NULL);
HMODULE mod = LoadLibraryA("external_exports.dll");
TEST_CHECK_MSG(mod != NULL, "LoadLibraryA failed: %lu", (unsigned long)GetLastError());
FARPROC raw_add_numbers = GetProcAddress(mod, "add_numbers@8");
FARPROC raw_was_attached = GetProcAddress(mod, "was_attached@0");
FARPROC raw_observed_main = GetProcAddress(mod, "observed_main_module@0");
TEST_CHECK_MSG(raw_add_numbers != NULL, "GetProcAddress(add_numbers@8) failed: %lu", (unsigned long)GetLastError());
TEST_CHECK_MSG(raw_was_attached != NULL, "GetProcAddress(was_attached@0) failed: %lu", (unsigned long)GetLastError());
TEST_CHECK_MSG(raw_observed_main != NULL, "GetProcAddress(observed_main_module@0) failed: %lu", (unsigned long)GetLastError());
add_numbers_fn add_numbers = (add_numbers_fn)(uintptr_t)raw_add_numbers;
was_attached_fn was_attached = (was_attached_fn)(uintptr_t)raw_was_attached;
observed_main_module_fn observed_main = (observed_main_module_fn)(uintptr_t)raw_observed_main;
int sum = add_numbers(2, 40);
int attached = was_attached();
HMODULE observed_main_module = observed_main();
TEST_CHECK_EQ(42, sum);
TEST_CHECK_EQ(1, attached);
TEST_CHECK_EQ(initial_main, observed_main_module);
TEST_CHECK_MSG(FreeLibrary(mod) != 0, "FreeLibrary failed: %lu", (unsigned long)GetLastError());