VS2022 Link – Prevent custom section from being removed?

Summary

This postmortem analyzes a subtle Visual Studio 2022 linker behavior where a custom section declared with #pragma section and __declspec(allocate) is silently merged or discarded, even when the symbol is explicitly forced into the link via /INCLUDE. The engineer expected a .sbat section to appear in the final binary, but the linker optimized it away or folded it into another section.

Root Cause

The root cause is that MSVC’s linker aggressively merges or removes custom sections unless they meet specific criteria. In particular:

  • Custom sections must be marked with proper characteristics (e.g., read, write, shared, discardable, etc.).
  • The linker may merge sections with identical flags, even if the section name is unique.
  • The /INCLUDE directive forces the symbol to exist, not the section, so the variable survives but the section does not.
  • MSVC requires /SECTION:.sbat,R or similar flags to preserve a custom section as a standalone region.

Why This Happens in Real Systems

Real-world linkers optimize aggressively:

  • They coalesce sections with matching attributes to reduce fragmentation.
  • They discard sections that appear unused or redundant.
  • They reassign symbols to default sections when custom sections lack explicit linker-level definitions.
  • They assume that custom sections are rare, so they require explicit instructions to preserve them.

Real-World Impact

This behavior can cause:

  • Loss of metadata required by bootloaders, firmware, or security frameworks.
  • Unexpected binary layout changes, breaking tools that parse or validate sections.
  • Incompatibility with specifications (e.g., SBAT metadata for UEFI).
  • Difficult debugging, because the symbol exists but the section does not.

Example or Code (if necessary and relevant)

Below is a minimal example showing the correct way to force MSVC to keep a custom section:

#pragma section(".sbat", read)
__declspec(allocate(".sbat"))
extern "C" const char sbat[] = "sbat,1,SBAT Version,...";

#pragma comment(linker, "/INCLUDE:sbat")
#pragma comment(linker, "/SECTION:.sbat,R")

How Senior Engineers Fix It

Experienced engineers know that MSVC requires both declaration-time and link-time section attributes. They typically:

  • Add /SECTION:.sbat,R to explicitly define the section’s characteristics.
  • Ensure the section is not marked discardable.
  • Use const to avoid the compiler placing it in .rdata.
  • Verify the final binary using tools like:
    • dumpbin /headers
    • llvm-readobj
    • objdump -h

They also understand that forcing a symbol does not force a section, so they configure both.

Why Juniors Miss It

Less experienced engineers often overlook:

  • The difference between compiler-level section placement and linker-level section retention.
  • That /INCLUDE only preserves the symbol, not the section.
  • That MSVC merges sections based on flags, not names.
  • That custom sections require explicit /SECTION: directives to survive optimization.

They assume that declaring a section in C/C++ is enough, not realizing the linker has the final say.

Leave a Comment