Last active
March 18, 2026 22:11
-
-
Save tomtzook/40f069b9bfbe8ca5b8237365fa406ca5 to your computer and use it in GitHub Desktop.
Print Call Stack from PE info of running program
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <iostream> | |
| #include <cstdint> | |
| #include <vector> | |
| #include <fstream> | |
| #include <exception> | |
| #include "winnt.h" | |
| struct image_info { | |
| const void* base; | |
| const IMAGE_NT_HEADERS64* header; | |
| }; | |
| struct frame { | |
| uint64_t rip; | |
| uint64_t rbp; | |
| }; | |
| static const IMAGE_NT_HEADERS64* find_nt_header(const void* base) { | |
| const auto dos_header = static_cast<const IMAGE_DOS_HEADER*>(base); | |
| if (dos_header->e_magic != IMAGE_DOS_SIGNATURE) { | |
| return nullptr; | |
| } | |
| const auto nt_header = reinterpret_cast<const IMAGE_NT_HEADERS64*>(static_cast<const uint8_t*>(base) + dos_header->e_lfanew); | |
| if (nt_header->Signature != IMAGE_NT_SIGNATURE) { | |
| return nullptr; | |
| } | |
| if (nt_header->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) { | |
| return nullptr; | |
| } | |
| return nt_header; | |
| } | |
| static const IMAGE_DATA_DIRECTORY* find_data_directory(const image_info& image, const int type) { | |
| if (type >= image.header->OptionalHeader.NumberOfRvaAndSizes) { | |
| return nullptr; | |
| } | |
| const auto& data_directory = image.header->OptionalHeader.DataDirectory[type]; | |
| if (data_directory.VirtualAddress == 0) { | |
| return nullptr; | |
| } | |
| return &data_directory; | |
| } | |
| template<typename T> | |
| const T* rva_to_pointer(const image_info& image, const uint32_t rva) { | |
| // when pe is loaded into memory, the rva is an offset from image base | |
| return reinterpret_cast<const T*>(static_cast<const uint8_t*>(image.base) + rva); | |
| } | |
| const IMAGE_RUNTIME_FUNCTION_ENTRY* find_function_entry(const image_info& image, const uint32_t offset) { | |
| const auto data_directory = find_data_directory(image, IMAGE_DIRECTORY_ENTRY_EXCEPTION); | |
| if (data_directory == nullptr) { | |
| return nullptr; | |
| } | |
| const auto exception_table = rva_to_pointer<IMAGE_RUNTIME_FUNCTION_ENTRY>(image, data_directory->VirtualAddress); | |
| if (exception_table == nullptr) { | |
| return nullptr; | |
| } | |
| for (const auto* function_entry = exception_table; function_entry->BeginAddress; function_entry++) { | |
| if (offset >= function_entry->BeginAddress && offset < function_entry->EndAddress) { | |
| return function_entry; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| static ssize_t find_setfp_code_index(const UNWIND_INFO* unwind_info) { | |
| const auto* codes_start = reinterpret_cast<const UNWIND_CODE*>(reinterpret_cast<const uint8_t*>(unwind_info) + sizeof(UNWIND_INFO)); | |
| for (int i = 0; i < unwind_info->CountOfCodes; i++) { | |
| switch (const auto code = codes_start[i]; code.UnwindOp) { | |
| case UWOP_SET_FPREG: | |
| return i; | |
| case UWOP_ALLOC_LARGE: | |
| if (code.OpInfo == 0) { | |
| i += 1; | |
| } else { | |
| i += 2; | |
| } | |
| break; | |
| case UWOP_ALLOC_SMALL: | |
| case UWOP_PUSH_NONVOL: | |
| case UWOP_SAVE_NONVOL: | |
| case UWOP_SAVE_NONVOL_FAR: | |
| case UWOP_SAVE_XMM128: | |
| case UWOP_SAVE_XMM128_FAR: | |
| case UWOP_EPILOG: | |
| case UWOP_SPARE_CODE: | |
| case UWOP_PUSH_MACHFRAME: | |
| default: | |
| break; | |
| } | |
| } | |
| return -1; | |
| } | |
| static size_t calc_prolog_rsp_offset(const UNWIND_INFO* unwind_info) { | |
| const auto start_idx = find_setfp_code_index(unwind_info) + 1; | |
| size_t offset = 0; | |
| const auto* codes_start = reinterpret_cast<const UNWIND_CODE*>(reinterpret_cast<const uint8_t*>(unwind_info) + sizeof(UNWIND_INFO)); | |
| for (auto i = start_idx; i < unwind_info->CountOfCodes; i++) { | |
| const auto code = codes_start[i]; | |
| switch (code.UnwindOp) {; | |
| case UWOP_ALLOC_LARGE: | |
| offset += code.OpInfo == 0 ? codes_start[i + 1].FrameOffset * 8 : *reinterpret_cast<const unsigned int*>(&codes_start[i + 1]); | |
| i += code.OpInfo == 0 ? 1 : 2; | |
| break; | |
| case UWOP_ALLOC_SMALL: | |
| offset += (code.OpInfo * 8) + 8; | |
| break; | |
| case UWOP_PUSH_NONVOL: | |
| case UWOP_SAVE_NONVOL: | |
| case UWOP_SAVE_NONVOL_FAR: | |
| offset += 8; | |
| break; | |
| case UWOP_SAVE_XMM128: | |
| case UWOP_SAVE_XMM128_FAR: | |
| // todo: handle | |
| break; | |
| case UWOP_EPILOG: | |
| case UWOP_SPARE_CODE: | |
| case UWOP_PUSH_MACHFRAME: | |
| // todo: handle | |
| break; | |
| case UWOP_SET_FPREG: | |
| default: | |
| break; | |
| } | |
| } | |
| return offset; | |
| } | |
| static frame unwind_frame_next(const image_info& image, const uint64_t current_rip, const uint64_t current_rbp) { | |
| const auto offset_into_function = current_rip - reinterpret_cast<uint64_t>(image.base); | |
| const auto function_entry = find_function_entry(image, offset_into_function); | |
| if (function_entry == nullptr) { | |
| throw std::runtime_error("unable to find function entry"); | |
| } | |
| const auto unwind_info = rva_to_pointer<UNWIND_INFO>(image, function_entry->DUMMYUNIONNAME.UnwindInfoAddress); | |
| if (unwind_info == nullptr) { | |
| throw std::runtime_error("unwable to find unwind info"); | |
| } | |
| // we only work for frame pointer being stored in rbp | |
| if (unwind_info->FrameRegister != 5) { | |
| throw std::runtime_error("only supporting unwind with fp being rbp"); | |
| } | |
| const auto fp = current_rbp; | |
| const auto offset = unwind_info->FrameOffset * 16; | |
| const auto prolog_offset = calc_prolog_rsp_offset(unwind_info); | |
| const auto previous_rbp = *reinterpret_cast<uint64_t*>(fp + prolog_offset - offset - 8); | |
| const auto return_address = *reinterpret_cast<uint64_t*>(fp + prolog_offset - offset); | |
| return {return_address, previous_rbp}; | |
| } | |
| static void print_call_stack(const image_info& image, uint64_t current_rip, uint64_t current_rbp) { | |
| do { | |
| const auto frame = unwind_frame_next(image, current_rip, current_rbp); | |
| // technically we should stop unwinding when exiting our pe or when reaching the top of the stack. | |
| // we can unwind other PEs if we find their headers. | |
| if (!frame.rip || !frame.rbp) { | |
| break; | |
| } | |
| current_rip = frame.rip; | |
| current_rbp = frame.rbp; | |
| printf("Frame: rip=0x%lx, rbp=0x%lx\n", current_rip, current_rbp); | |
| } while (true); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #pragma once | |
| typedef unsigned short WORD; | |
| typedef unsigned int DWORD; | |
| typedef unsigned long long ULONGLONG; | |
| typedef unsigned char BYTE; | |
| typedef unsigned char UBYTE; | |
| #define IMAGE_DOS_SIGNATURE 0x5A4D // MZ | |
| #define IMAGE_NT_SIGNATURE 0x00004550 // PE00 | |
| #define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b | |
| #define IMAGE_SIZEOF_SHORT_NAME 8 | |
| #define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 | |
| #define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory | |
| #define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory | |
| #define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory | |
| #define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory | |
| #define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory | |
| #define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table | |
| #define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory | |
| // IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage) | |
| #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data | |
| #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP | |
| #define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory | |
| #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory | |
| #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers | |
| #define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table | |
| #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors | |
| #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor | |
| #define UWOP_PUSH_NONVOL 0 | |
| #define UWOP_ALLOC_LARGE 1 | |
| #define UWOP_ALLOC_SMALL 2 | |
| #define UWOP_SET_FPREG 3 | |
| #define UWOP_SAVE_NONVOL 4 | |
| #define UWOP_SAVE_NONVOL_FAR 5 | |
| #if 0 // These are deprecated / not for x64 | |
| #define UWOP_SAVE_XMM 6 | |
| #define UWOP_SAVE_XMM_FAR 7 | |
| #else | |
| #define UWOP_EPILOG 6 | |
| #define UWOP_SPARE_CODE 7 | |
| #endif | |
| #define UWOP_SAVE_XMM128 8 | |
| #define UWOP_SAVE_XMM128_FAR 9 | |
| #define UWOP_PUSH_MACHFRAME 10 | |
| #pragma pack(push, 1) | |
| typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header | |
| WORD e_magic; // Magic number | |
| WORD e_cblp; // Bytes on last page of file | |
| WORD e_cp; // Pages in file | |
| WORD e_crlc; // Relocations | |
| WORD e_cparhdr; // Size of header in paragraphs | |
| WORD e_minalloc; // Minimum extra paragraphs needed | |
| WORD e_maxalloc; // Maximum extra paragraphs needed | |
| WORD e_ss; // Initial (relative) SS value | |
| WORD e_sp; // Initial SP value | |
| WORD e_csum; // Checksum | |
| WORD e_ip; // Initial IP value | |
| WORD e_cs; // Initial (relative) CS value | |
| WORD e_lfarlc; // File address of relocation table | |
| WORD e_ovno; // Overlay number | |
| WORD e_res[4]; // Reserved words | |
| WORD e_oemid; // OEM identifier (for e_oeminfo) | |
| WORD e_oeminfo; // OEM information; e_oemid specific | |
| WORD e_res2[10]; // Reserved words | |
| DWORD e_lfanew; // File address of new exe header | |
| } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER; | |
| typedef struct _IMAGE_DATA_DIRECTORY { | |
| DWORD VirtualAddress; | |
| DWORD Size; | |
| } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; | |
| typedef struct _IMAGE_FILE_HEADER { | |
| WORD Machine; | |
| WORD NumberOfSections; | |
| DWORD TimeDateStamp; | |
| DWORD PointerToSymbolTable; | |
| DWORD NumberOfSymbols; | |
| WORD SizeOfOptionalHeader; | |
| WORD Characteristics; | |
| } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER; | |
| typedef struct _IMAGE_OPTIONAL_HEADER64 { | |
| WORD Magic; | |
| BYTE MajorLinkerVersion; | |
| BYTE MinorLinkerVersion; | |
| DWORD SizeOfCode; | |
| DWORD SizeOfInitializedData; | |
| DWORD SizeOfUninitializedData; | |
| DWORD AddressOfEntryPoint; | |
| DWORD BaseOfCode; | |
| ULONGLONG ImageBase; | |
| DWORD SectionAlignment; | |
| DWORD FileAlignment; | |
| WORD MajorOperatingSystemVersion; | |
| WORD MinorOperatingSystemVersion; | |
| WORD MajorImageVersion; | |
| WORD MinorImageVersion; | |
| WORD MajorSubsystemVersion; | |
| WORD MinorSubsystemVersion; | |
| DWORD Win32VersionValue; | |
| DWORD SizeOfImage; | |
| DWORD SizeOfHeaders; | |
| DWORD CheckSum; | |
| WORD Subsystem; | |
| WORD DllCharacteristics; | |
| ULONGLONG SizeOfStackReserve; | |
| ULONGLONG SizeOfStackCommit; | |
| ULONGLONG SizeOfHeapReserve; | |
| ULONGLONG SizeOfHeapCommit; | |
| DWORD LoaderFlags; | |
| DWORD NumberOfRvaAndSizes; | |
| IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; | |
| } IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64; | |
| typedef struct _IMAGE_NT_HEADERS64 { | |
| DWORD Signature; | |
| IMAGE_FILE_HEADER FileHeader; | |
| IMAGE_OPTIONAL_HEADER64 OptionalHeader; | |
| } IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64; | |
| typedef struct _IMAGE_SECTION_HEADER { | |
| BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; | |
| union { | |
| DWORD PhysicalAddress; | |
| DWORD VirtualSize; | |
| } Misc; | |
| DWORD VirtualAddress; | |
| DWORD SizeOfRawData; | |
| DWORD PointerToRawData; | |
| DWORD PointerToRelocations; | |
| DWORD PointerToLinenumbers; | |
| WORD NumberOfRelocations; | |
| WORD NumberOfLinenumbers; | |
| DWORD Characteristics; | |
| } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; | |
| typedef struct _IMAGE_RUNTIME_FUNCTION_ENTRY { | |
| DWORD BeginAddress; | |
| DWORD EndAddress; | |
| union { | |
| DWORD UnwindInfoAddress; | |
| DWORD UnwindData; | |
| } DUMMYUNIONNAME; | |
| } IMAGE_RUNTIME_FUNCTION_ENTRY, *PIMAGE_RUNTIME_FUNCTION_ENTRY; | |
| typedef union _UNWIND_CODE { | |
| struct { | |
| UBYTE CodeOffset; | |
| UBYTE UnwindOp:4; | |
| UBYTE OpInfo:4; | |
| }; | |
| WORD FrameOffset; | |
| } UNWIND_CODE, *PUNWIND_CODE; | |
| typedef struct _UNWIND_INFO { | |
| UBYTE Version : 3; | |
| UBYTE Flags : 5; | |
| UBYTE SizeOfProlog; | |
| UBYTE CountOfCodes; | |
| UBYTE FrameRegister : 4; | |
| UBYTE FrameOffset : 4; | |
| UNWIND_CODE UnwindCode[1]; // Variable size array of unwind codes | |
| // OPTIONAL: union and ExceptionData follow here, depending on Flags | |
| } UNWIND_INFO, *PUNWIND_INFO; | |
| #pragma pack(pop) |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This snippet was extracted from one of my projects, so it was simplified. It won't work as is probably, but it illustrates the process. Of course, one needs the initial rbp and rip to start the unwind from. The code also makes some assumptions and does not work for every situation.
There are other ways to do this, this specifically implements the process using the exception function table in PE.