DEV Community

Stefor07
Stefor07

Posted on • Edited on

How to Embed Binary Data into a Win32 Executable File in 4 Methods

This guide presents 4 methods to embed arbitrary binary files (images, data, etc.) inside a Win32 executable. From standard approaches like byte arrays to low-level techniques such as binary appending (copy /b), each method includes ready-to-use examples in Visual C++ and highlights their specific advantages and disadvantages. It is aimed at C/C++ developers who want to incorporate binary data (e.g., images, configuration files, resources) within their executables.

Use cases:

  • Single-file deployment (no external resources).
  • Hiding data inside .exe/.elf (no encryption).
  • Quick prototyping or educational purposes.

⚠️ Note:

  • Methods 1 and 3 are clean but platform-specific (Windows only).
  • Method 2 (embedding as a C array) is both clean and portable across platforms.
  • Method 4 (copy /b) is a hack and may cause issues with checksums or antivirus.
  • All examples assume an x86_64 context (they can be adapted for other architectures).

Method 1: Win32 Resource Files (.rc)

Best for: Native Windows executables where you want integrated resource handling


Step 1: Add resource files in Visual Studio

  1. Right-click project → Add → Resource...
  2. Choose "Import..." and select your binary file (e.g., data.bin)
  3. Set resource type as RCDATA and assign an ID (e.g., IDR_MY_DATA)

Visual Studio will automatically:

  • Generate/modify resource.h with your ID
  • Create/update the .rc file with:
  IDR_MY_DATA RCDATA "file.bin"
Enter fullscreen mode Exit fullscreen mode

Step 2: Access the resource from code

Here’s an example demonstrating how to load and access the embedded data in memory:

main.cpp

#include <windows.h>
#include "resource.h"  // Auto-generated by Visual Studio

void LoadEmbeddedData()
{
    // Find the resource handle
    HRSRC hRes = FindResource(nullptr, MAKEINTRESOURCE(IDR_MY_DATA), RT_RCDATA);
    if (!hRes)
    {
        // Handle error: resource not found
        return;
    }

    // Get the size of the resource
    DWORD hResSize = SizeofResource(nullptr, hRes);
    if (hResSize == 0)
    {
        // Handle error: resource size invalid
        return;
    }

    // Load the resource into memory
    HGLOBAL hData = LoadResource(nullptr, hRes);
    if (!hData)
    {
        // Handle error: failed to load resource
        return;
    }

    // Lock the resource to get a pointer to the data
    const BYTE* pData = static_cast(LockResource(hData));
    if (!pData)
    {
        // Handle error: failed to lock resource
        return;
    }

    // At this point, pData points to the embedded binary data
    // You can process the data here, e.g., write to disk, parse, etc.
}
Enter fullscreen mode Exit fullscreen mode

Key Notes:

  • 🔧 No manual compilation – VS handles .rc.res conversion during build
  • 🔒 Data is read-only in memory (safe from modification)
  • 📦 Supports any binary format (use RT_RCDATA or custom names for raw data)
  • Possible external changes - Resources can be replaced by external tools
  • Avoid too large files - Avoid using too large resources with LoadResource and LockResource, because Windows exposes the entire contents in memory as a single block. This can cause high RAM consumption for very large resources.

Method 2: Manual Hex Offset on Source Code

Best for: Precise binary embedding without compiler dependencies

Required Tools

HxD Editor - A utility to export the binary file array offset as a C/C++ source file


Step 1: Export binary to C++ offset array

  1. Open your binary file (.bin, .dat, etc.) in HxD Editor
  2. Go to File → Export → C and choose a path to save the source .cpp file
  3. Move the source .cpp file into your project folder and include it

The source file should look like:

/* [FILENAME] ([DATE])
   StartOffset(h): 00000000, EndOffset(h): 000003FF, Length(h): 00000400 */

unsigned char rawData[1024] = 
{
    0x7F, 0x45, 0x4C, 0x46, 0x02, 0x01, 0x01, 0x00, // ELF magic bytes
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x03, 0x00, 0x3E, 0x00, 0x01, 0x00, 0x00, 0x00,
    // ... (truncated for brevity) ...
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
Enter fullscreen mode Exit fullscreen mode

Step 2: Accessing the offset from the code

Now create a new H header file from Visual Studio and insert the following declarations:

embedded_data.h

#pragma once

extern const size_t embedded_data_size;
extern unsigned char embedded_data[];
Enter fullscreen mode Exit fullscreen mode

After remember to move the size of the bytes array into a const variable and assign it to the size of the array, do not forget to include the header with the declarations

embedded_data.cpp

#pragma once
#include "embedded_data.h" // include the header

const size_t embedded_data_size = 1024; // this is the value of the array size offset below

unsigned char embedded_data[embedded_data_size] = 
{
    0x7F, 0x45, 0x4C, 0x46, 0x02, 0x01, 0x01, 0x00, // ELF magic bytes
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x03, 0x00, 0x3E, 0x00, 0x01, 0x00, 0x00, 0x00,
    // ... (truncated for brevity) ...
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
Enter fullscreen mode Exit fullscreen mode

At this point from any part of our application code we include the header that declares our offset and declare a pointer of the binary data in the code to be able to use it in memory

main.cpp

#include "windows.h"
#include "embedded_data.h"

void LoadEmbeddedData()
{
  const BYTE* embedded_data_ptr = embedded_data;
  const size_t embedded_data_ptr_size = embedded_data_size;

  if (embedded_data_ptr)
  {
     // At this point, embedded_data_ptr pointer points the embedded data
     // You can do anything with the buffer and size
     // Example: processing or writing to disk
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Notes:

  • Manual binary precision – It allows direct embedding of binary data into source code, eliminating dependencies on automated tools.
  • 🔒 Data is read-only in memory (safe from modification)
  • 📦 Immutable data Embedded data is read-only, ensuring safety and integrity during execution.
  • Manual updates required - To change or update embedded data, you need to regenerate the offset and recompile the source code.
  • Avoid too large files - Embedding large files can increase the size of the source code, reduce readability, and cause compilation slowdowns or even errors.

Method 3: COFF Object Binary Linking

Best for: Windows applications (MSVC or MinGW) that need tightly integrated, read-only binary data.

Required Tools

  • bin2obj - Includes a preconfigured portable MinGW (ucrt64) setup with objcopy and a small utility GUI tool to simplify conversion.

Step 1: Convert Binary to COFF Object File

Option A: Manual objcopy Command

To manually convert a binary file to a COFF object, open a terminal (CMD or Git Bash) inside the ucrt64 folder and run one of the following commands depending on your target architecture:

For 32-bit COFF:

objcopy -I binary -O pe-i386 -B i386 binary.bin binary.obj 
Enter fullscreen mode Exit fullscreen mode

For 64-bit COFF:

objcopy -I binary -O pe-x86-64 -B i386:x86-64 binary.bin binary.obj 
Enter fullscreen mode Exit fullscreen mode
  • binary.bin is your input file (raw binary).
  • binary.obj is the output object file that you'll link into your project.

Option B: Use the bin2obj Utility (Recommended)

The GUI tool allows for quick conversion:

1. Select your binary file.
2. Choose a destination .obj file.
3. Click "Convert to COFF".

Be sure to leave the "Export C++ header file" checkbox enabled to automatically generate a header file with symbol declarations.


Generated Header Example

The tool will generate a binary_data.h like the following:

For 32-bit:

#pragma once
#include <cstdint>

// include this header in your application and link the COFF as additional dependencies

extern "C"
{
   extern uint8_t binary_filename_start[];

   extern uint8_t binary_filename_end[];

   const size_t binary_filename_size = binary_filename_end - binary_filename_start;
}
Enter fullscreen mode Exit fullscreen mode

For 64-bit:

#pragma once
#include <cstdint>

// include this header in your application and link the COFF as additional dependencies

extern "C"
{
   extern uint8_t _binary_filename_start[];

   extern uint8_t _binary_filename_end[];

   const size_t binary_filename_size = _binary_filename_end - _binary_filename_start;
}
Enter fullscreen mode Exit fullscreen mode

🔁 You can reuse the same header on every build if the binary file name doesn’t change.


Step 2 (Optional): Manually Create Header Using dumpbin

If you didn’t use the utility and created the .obj manually, you’ll need to retrieve symbol names:

1. Open Developer Command Prompt for Visual Studio
2. Navigate to the folder with the .obj file
3. Run:

dumpbin /symbols binary.obj
Enter fullscreen mode Exit fullscreen mode

You’ll see output like:

000 00000000 SECT1  notype       External     | _binary_filename_start
001 000069DD SECT1  notype       External     | _binary_filename_end
Enter fullscreen mode Exit fullscreen mode

Header Template:

For 32-bit (remove underscores):

#pragma once
#include <cstdint>

extern "C"
{
   extern uint8_t binary_filename_start[];
   extern uint8_t binary_filename_end[];

   const size_t binary_filename_size = binary_filename_end - binary_filename_start;
}
Enter fullscreen mode Exit fullscreen mode

For 64-bit (keep underscores):

#pragma once
#include <cstdint>

extern "C"
{
   extern uint8_t _binary_filename_start[];
   extern uint8_t _binary_filename_end[];

   const size_t binary_filename_size = _binary_filename_end - _binary_filename_start;
}
Enter fullscreen mode Exit fullscreen mode

ℹ️ Tip: Symbol names always reflect the original file name, so using consistent filenames helps with version control and updates.

ℹ️ Tip: You can rename the .obj symbols always with the GUI utility bin2obj without having too much difficulty with the command lines


Step 3: Linking and Accessing Embedded Data

Now that we have all the necessary COFF symbols declared we can declare from anywhere in our application code the pointer that points to the embedded data.

  • Move the .obj file into your project.
  • Add it to your linker input (Additional Dependencies in Visual Studio).
  • Include the generated .h header in your source code.

Usage Example:

main.cpp

#include "windows.h"
#include "binary_data.h"

void LoadEmbeddedData()
{
    const BYTE* buffer = binary_filename_start;
    const size_t bufferSize = binary_filename_size;

    if (buffer)
    {
        // the buffer pointer points the embedded binary data
        // You can process it in memory, decompress, write to disk, etc.
    }
}
Enter fullscreen mode Exit fullscreen mode

For 64-bit:

const BYTE* binary_filename_ptr = _binary_filename_start;
Enter fullscreen mode Exit fullscreen mode

⚠️Remember to compile your application in 32 bit / 64 bit depending on your COFF architecture

Key Notes:

  • Efficient binary embedding – Converts binary data into a COFF object, allowing direct linking in Windows executables (ideal for MSVC/MinGW).
  • 🔒 Data is read-only in memory (safe from modification)
  • 📦 Immutable data Embedded data is read-only, ensuring safety and integrity during execution.
  • Manual updates required - To change or update embedded data, you need to regenerate the COFF and recompile the source code.
  • Symbol consistency - If the binary file has the same name, the generated symbols (e.g. binary_filename_start) will be identical, allowing easy updates without changes to the source code.
  • ⚙️ Custom section alignment possible - You can specify additional options with objcopy to control the alignment of the section, which is useful for certain data types or alignment-sensitive binary structures.

Method 4: Binary concatenation of files into executable (copy /b)

Best for: Perfect for portable or self-extracting programs: no need to attach external files

This is a "dirty but effective" method that allows you to append binary files (ZIP, DLLs, images, etc.) to the end of a Windows executable using the copy /b command. The resulting executable remains fully functional and can load and use the appended data at runtime.

No external tools are required — everything is done via the Windows command line and API.


Step 1: Declaring helper functions

Add the following helper functions to your C++ application to:

1. Determine the real size of the executable (excluding any appended binary data).
2. Load the appended binary data into memory.

main.cpp

#include <windows.h>

// Returns the size of the actual executable without appended data
DWORD GetRealExeSize(const TCHAR* exePath)
{
    HANDLE hFile = CreateFile(exePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) return 0;

    HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
    if (!hMapping) { CloseHandle(hFile); return 0; }

    LPBYTE pBase = (LPBYTE)MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
    if (!pBase) { CloseHandle(hMapping); CloseHandle(hFile); return 0; }

    IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)pBase;
    if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
    {
        UnmapViewOfFile(pBase);
        CloseHandle(hMapping);
        CloseHandle(hFile);
        return 0;
    }

    IMAGE_NT_HEADERS* pNtHeaders = (IMAGE_NT_HEADERS*)(pBase + pDosHeader->e_lfanew);
    if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE)
    {
        UnmapViewOfFile(pBase);
        CloseHandle(hMapping);
        CloseHandle(hFile);
        return 0;
    };

    DWORD maxEnd = 0;
    IMAGE_SECTION_HEADER* pSection = IMAGE_FIRST_SECTION(pNtHeaders);
    for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; ++i)
    {
        DWORD sectionEnd = pSection[i].PointerToRawData + pSection[i].SizeOfRawData;
        if (sectionEnd > maxEnd) maxEnd = sectionEnd;
    }


    UnmapViewOfFile(pBase);
    CloseHandle(hMapping);
    CloseHandle(hFile);
    return maxEnd;
}
Enter fullscreen mode Exit fullscreen mode

Version 1: Heap allocation (for small/medium data)

BYTE* LoadAppendedData(DWORD& outSize)
{
    TCHAR exePath[MAX_PATH] = { 0 };
    GetModuleFileName(NULL, exePath, MAX_PATH);

    DWORD exeSize = GetRealExeSize(exePath);
    HANDLE hFile = CreateFile(exePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) return nullptr;

    LARGE_INTEGER fileSize;
    if (!GetFileSizeEx(hFile, &fileSize)) { CloseHandle(hFile); return nullptr; }

    if (fileSize.QuadPart <= exeSize) { CloseHandle(hFile); return nullptr; }

    DWORD appendedSize = (DWORD)(fileSize.QuadPart - exeSize);
    BYTE* data = new BYTE[appendedSize];

    SetFilePointer(hFile, exeSize, NULL, FILE_BEGIN);
    DWORD bytesRead = 0;
    ReadFile(hFile, data, appendedSize, &bytesRead, NULL);
    CloseHandle(hFile);

    if (bytesRead != appendedSize) { delete[] data; return nullptr; }

    outSize = appendedSize;
    return data;
}
Enter fullscreen mode Exit fullscreen mode

Version 2: Memory-mapped (recommended for large files)

BYTE* LoadAppendedData(DWORD& outSize)
{
    TCHAR exePath[MAX_PATH] = { 0 };
    GetModuleFileName(NULL, exePath, MAX_PATH);

    DWORD exeSize = GetRealExeSize(exePath);
    HANDLE hFile = CreateFile(exePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) return nullptr;

    HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
    if (!hMapping) { CloseHandle(hFile); return nullptr; }

    LPBYTE pBase = (LPBYTE)MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
    if (!pBase) { CloseHandle(hMapping); CloseHandle(hFile); return nullptr; }

    LARGE_INTEGER fileSize;
    if (!GetFileSizeEx(hFile, &fileSize)) {
        UnmapViewOfFile(pBase);
        CloseHandle(hMapping);
        CloseHandle(hFile);
        return nullptr;
    }

    DWORD appendedSize = (DWORD)(fileSize.QuadPart - exeSize);
    if (appendedSize == 0) {
        UnmapViewOfFile(pBase);
        CloseHandle(hMapping);
        CloseHandle(hFile);
        return nullptr;
    }

    outSize = appendedSize;
    return pBase + exeSize; // Direct pointer to the mapped binary data
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Accessing appended data from code

Now you can declare a pointer to the appended file from anywhere in your code and use it in memory

main.cpp

#include "windows.h"

void LoadEmbeddedData()
{
    DWORD dataSize = 0;
    BYTE* data = LoadAppendedData(dataSize);

    if (data)
    {
        // Use the embedded binary data here
        // e.g., save to disk, decompress, process, etc.

        // Only use delete[] if allocated on the heap (Version 1)
        // delete[] data;
    }
}
Enter fullscreen mode Exit fullscreen mode

After completing the application compilation, open the command prompt in the build folder (Debug/Release).


Step 3: Appending binary data to executable

Use this command to append any binary file to the application in a command prompt (inside the build output folder):

copy /b app.exe + payload.bin final_app.exe
Enter fullscreen mode Exit fullscreen mode

⚠️ Important: It is recommended that you do not overwrite the original .exe. Always create a copy (final_app.exe) with the added data.

Key Notes

  • Easy loading of concatenated data – It uses the Windows API to read data added to an executable, allowing you to manipulate combined files without additional dependencies like MFC.
  • No dependencies — all code uses pure WinAPI.
  • 📦 Works best with a single binary file.
  • Manual updates may be required – Every time you change the binary data you will need to re-run the copy /b command after each compilation.
  • Does not support multiple files unless structured properly:
    • Use a container format (e.g., ZIP) to store multiple files.
    • Alternatively, insert unique markers (magic bytes) between files to identify boundaries.
  • 🔁 Appended data must be reattached after each rebuild using copy /b.
  • ⚠️ Appended data is not part of the PE format — It is added after the last section of the executable and ignored by the Windows loader. This is the only method where embedded data is not stored inside the PE structure.

If you're interested, feel free to check out my other articles where I explore several enhancements and advanced techniques based on this method:

🔐 Advanced: Protect the embedded data with digital signature

You can enhance this method by validating the appended data through the executable’s digital signature. This ensures integrity and authenticity of both the executable and the binary payload.👉Read: How to Protect Binary Data Appended into an Executable and Validate it with a Digital Signature

📦 Concatenate and manage multiple appended files in Win32 executable

I have discovered a fairly simple method to distinguish the various files concatenated together and access them separately. 👉Read: How to Easily Distinguish Multiple Binary Data Files Concatenated to a Win32 Executable


Methods Summary

Both methods have their pros and cons, below is the summary table for everything

Method Best For Pros Cons Example Use Cases
Method 1: Win32 Resource Files (.rc) Native Windows executables where integrated resource handling is preferred No manual compilation – VS handles .rc.res conversion
Data is read-only (safe from modification)
Supports any binary format
Possible external changes – Resources can be replaced by external tools ❌ Avoid too large files - Avoid using too large resources with LoadResource and LockResource, because Windows exposes the entire contents in memory as a single block. This can cause high RAM consumption for very large resources. Embedding configuration files, certificates, or game assets; Storing default data for single-file apps
Method 2: Manual Hex Offset on Source Code Precise binary embedding without compiler dependencies Manual binary precision – Directly embeds binary data into source code
Data is read-only
Immutable data
No additional dependencies
Custom section alignment possible
Manual updates required – Must regenerate offsets and recompile
Avoid too large files – Large files reduce readability and cause compilation slowdowns
Embedding small binary data (e.g., logos, icons, or small assets) for small projects
Method 3: COFF Object Binary Linking Windows executables (MSVC/MinGW) requiring direct binary embedding Efficient binary embedding – Converts binary into a COFF object for direct linking
Symbol consistency – Same file name results in identical symbols
Custom section alignment possible
Manual updates required – Regenerate the COFF and recompile for updates
External tool dependency – Requires bin2obj utility or manual symbol creation
Embedding large binary files in executables, such as libraries or other large assets
Method 4: Binary Concatenation with copy /b Self-extracting programs, or when no external files are allowed Easy loading of concatenated data – No dependencies like MFC
Portable – Doesn’t need additional external libraries
Fast to implement – Simple concatenation command
Manual updates may be required – Every time you change the binary data you will need to re-run the copy /b command every time after compilation
"Dirty" method – Can cause issues with checksums or antivirus detection
Embedding data like ZIP files, DLLs, or other large data into executables for self-extraction

Performance Comparison

Here’s a comprehensive performance and usability comparison of the four binary embedding methods. This will help you determine the most suitable approach based on your project’s specific needs (e.g. platform, file size, performance, deployment constraints):

Method Binary Size Impact Load Time Memory Usage Compilation Time Best Use Case Notes
Resource (.rc) +100% Fast High (entire file in memory) Medium Native Windows applications with small to medium assets Integrated into .exe as a resource; requires Windows-specific tools
Hex Array +200–300% Fastest High (static allocation in .data) Slow (especially for large arrays) Very small files where portability is key Increases binary size significantly; manually embedded as C array
COFF Linking +100% Fast Medium (lazy loading possible) Fast Professional development with large assets Leverages external object files; clean separation of data/code
Binary Concatenation +100% Medium Low (supports streaming) Instant (post-build) Large files, self-extracting apps, installers App reads its own binary; doesn't require linking

🔍 In-Depth: Memory Usage of the Four Methods

Memory usage varies significantly between these four methods, especially in terms of when and how the binary data is loaded into memory.

The Resource (.rc) and Hex Array methods are generally the least memory-efficient. With .rc files, binary resources are embedded into the .rsrc section of the executable. Although the OS technically loads resources on demand, the entire resource is usually mapped into memory when accessed. This becomes problematic with medium or large files, as the whole content must be held in RAM to be usable.

The Hex Array method is even more memory-hungry. Here, the binary data is hardcoded into the source as a static byte array, which is then stored in the .data section of the executable. This data is always loaded into memory when the program starts — even if it’s never used at runtime. This leads to unnecessary memory consumption and should be avoided unless the binary file is very small (e.g. under 50–100KB).

In contrast, COFF Linking is significantly more efficient. Binary data is linked into the executable as an object file (.obj or .o), and, depending on how it's accessed, can support lazy loading. This means that only the portions of the data you explicitly access are mapped into memory. It's a solid option for applications that deal with large or multiple binary assets but don’t need them all loaded at once.

Finally, Binary Concatenation is by far the most memory-efficient method. In this approach, the binary data is appended directly to the end of the executable file. The program can read this data at runtime using standard file I/O (e.g. via argv[0] or offset reads). Nothing is loaded into memory by default — it’s entirely under your control. This makes it ideal for working with large files or streamed content, especially in scenarios where memory constraints are critical.

Summary: If minimizing memory usage is a top priority, Binary Concatenation offers the best control and lowest footprint, followed by COFF Linking. The Hex Array and .rc approaches should be reserved for small binary payloads or cases where convenience outweighs efficiency.

🧠 Detailed Performance Analysis

Each embedding method comes with trade-offs in terms of performance, memory, and developer experience.

Resource (.rc) offers excellent integration with the Windows resource system, making it a natural choice for native Windows applications. In some cases, embedded resources benefit from automatic compression by the OS, reducing storage impact. However, at runtime, the entire resource is typically loaded into memory at once. This design works well for small assets like icons or dialogs, but it can cause excessive memory usage or startup delays when dealing with larger binary files (e.g. fonts, images, or audio).

Hex Array is the fastest at runtime because the binary data is already part of the process image — statically embedded into the .data segment of the executable. Access is instantaneous and requires no file I/O. However, this convenience comes at a heavy cost: the source code size balloons due to the conversion of binary data into hexadecimal (text-based) representation. This can dramatically increase compilation time — especially when embedding files over 1MB — and also leads to large executable sizes, often 2–3× the original binary. It's best used for very small and frequently accessed assets, where portability and startup speed are more important than scalability.

COFF Linking is widely considered the most professional approach, especially in C/C++ projects. Binary data is compiled into object files, which are then linked into the application like any other static symbol. This method results in a much smaller size overhead compared to Hex Arrays and allows for clean, symbol-based access to the data. Importantly, it also enables techniques like lazy loading and memory mapping, which make it well-suited for large files or multi-resource applications. It does require extra build configuration (e.g. using ld, objcopy, or llvm-objcopy), but the runtime performance and memory flexibility are excellent.

Binary Concatenation, the most flexible and lightweight method, deserves special attention. Unlike the other approaches, it has zero impact on compilation time because the binary file is not embedded during build — it's appended to the executable as a post-build step (e.g. via a script or cat/copy). At runtime, the program can open its own executable file and read the binary data using standard file I/O, allowing full control over how much is loaded and when. This enables streaming, chunked reading, or even memory-mapped access with minimal RAM usage. It’s especially useful for bundling large assets (e.g. game data, language packs, media files) without bloating memory or recompiling on every change. The trade-off is that it adds complexity to deployment, since you need to handle file offsets and maintain a separate post-build integration step. Still, for large-scale or memory-sensitive applications, Binary Concatenation offers the best balance between flexibility, performance, and minimal resource usage.

✅ Final Thoughts

Choosing the right embedding method depends on your priorities: ease of integration, memory efficiency, build complexity, or runtime performance. For small, native Windows applications, .rc resources are quick and familiar. For cross-platform use with tiny binaries, Hex Arrays offer simplicity and speed. If you're embedding large assets and care about memory usage and maintainability, COFF Linking is the most professional choice. But if you want complete control with minimal memory overhead — and can manage a simple post-build step — Binary Concatenation is the most flexible and scalable solution. There's no one-size-fits-all answer, but understanding these trade-offs will help you pick the method that best fits your specific use case.

Conclusion

In this article, we've covered four distinct methods for embedding binary data into a Win32 executable, each offering its own strengths and trade-offs:

  • Method 1: Win32 Resource Files (.rc): Ideal for straightforward integration with Windows resource handling. Best for applications where the embedded data does not change often and is part of the resource management system.
  • Method 2: Manual Hex Offset on Source Code: Provides precise control over the embedded data but requires manual management of offsets. Best suited for small to medium binary data embedded directly within the source code.
  • Method 3: COFF Object Binary Linking: A more sophisticated method for embedding large binary objects, ideal for developers using MSVC or MinGW who need to efficiently manage binary assets.
  • Method 4: Binary Concatenation with copy /b: A "dirty" but quick solution for appending data directly to an executable. This method is great for self-extracting applications but requires careful handling of offsets and file sizes.

Best Practices & Recommendations

  • Keep Your Executable Small: While embedding binary data can make your application self-contained, avoid adding too much data to prevent bloating the executable and affecting performance.
  • Use Proper Memory Management: Always ensure to free any allocated memory once you're done using the embedded data to prevent memory leaks.
  • Consider Security: If you're planning to hide sensitive data (e.g., encryption keys), be mindful that these methods are not inherently secure and can be easily extracted by someone with access to the executable. You may want to consider additional security measures, like encryption.
  • Avoid loading very large files entirely into memory: For large binaries (e.g. ZIP archives, videos, DLLs), consider block processing (streaming) or memory mapping (CreateFileMapping + MapViewOfFile) instead of allocating a huge buffer, this can reduce RAM usage and make everything more performant

Final Thoughts

Each of these methods is suitable for different scenarios, and the best choice depends on your specific needs: portability, ease of use, and how frequently the embedded data needs to be updated. For quick prototyping or simple use cases, copy /b might be the easiest choice, but for more robust applications, using Win32 resource files or manual hex offsets could be more effective.

Feel free to experiment with these methods and choose the one that fits best with your project goals!

Contributions & Questions

If you have any suggestions, improvements, or questions, feel free to contribute or reach out! I'm open to discussing different techniques and ways to enhance the methods presented here.

Happy coding and stay creative! 🎉

Top comments (0)