Understanding C# PE Files and Metadata Formats
When C# source code compiles, it does not produce native machine code directly; instead, it generates a managed Portable Executable (PE) file containing Common Intermediate Language (CIL) instructions and a comprehensive metadata structure. This article explains the internal composition of managed PE files, details the underlying CLI metadata tables and heap systems, and demonstrates how the Common Language Runtime (CLR) uses these components to execute code securely and efficiently.
What Is a Managed Portable Executable (PE) File?
A Portable Executable (PE) file is the standard binary format used by
Windows operating systems for executable files (.exe) and
dynamic-link libraries (.dll). While native applications
compile directly to platform-specific machine code, C# projects target
the Common Language Infrastructure (CLI).
A compiled C# binary retains standard Windows PE structures to ensure backward compatibility with the operating system loader, but it embeds an execution payload tailored for the CLR. The standard structure includes:
- DOS Header and Stub: A legacy 16-bit header enabling the file to display a compatibility message if executed in MS-DOS.
- PE Header: Contains basic execution requirements, such as CPU architecture targets (x86, x64, AnyCPU), subsystem specifications, and timestamps.
- Section Headers: Pointers to the binary's actual
contents, typically mapped into memory sections such as
.text(read-only code and data) and.rsrc(resources like application icons). - CLR Header (
IMAGE_COR20_HEADER): Located inside the.textsection, this unique header marks the binary as managed code. It holds runtime version requirements, flags (such as strong-naming or IL-only execution), the entry-point token, and the Relative Virtual Address (RVA) of the CLI metadata.
When launched, the native operating system loader reads the entry
point specified in the PE header, which typically points to a stub
function (_CorExeMain or _CorDllMain) that
boots the CLR runtime to manage further execution.
The Role of Metadata in .NET Binaries
In traditional native compilation (like C++), type definitions, parameter types, and function names are stripped out during the build process, leaving raw memory offsets. In contrast, C# binaries are fully self-describing through metadata.
Metadata eliminates the need for separate C/C++ header files
(.h) or interface definition languages (IDL). It provides
the CLR with full information about:
- Assembly Manifest: Information regarding the assembly identity (name, version, culture, public key token) and its external dependencies.
- Type Declarations: Names, base types, visibility flags, and implemented interfaces for all classes, structs, enums, and delegates.
- Member Definitions: Method signatures, fields, properties, events, and their associated data types.
- Attributes: Custom metadata applied to code
constructs (e.g.,
[Serializable],[Obsolete]).
Internal Architecture of .NET Metadata
The metadata within a C# PE binary is organized into a compact, relational-style database composed of two primary structures: heaps and tables.
1. The Metadata Heaps
Raw variable-length data is stored in specialized heaps to optimize space and minimize redundancy:
#Strings: Stores null-terminated UTF-8 strings representing identifiers such as type names, method names, and namespace definitions.#US(User Strings): Stores hardcoded string literals defined in the source code as UTF-16 arrays.#Blob: Contains raw binary data, including method signatures, default parameter values, and custom attribute payload bytes.#GUID: Stores 128-bit Globally Unique Identifiers used for COM interoperability and assembly identification.
2. The Metadata Tables
(Streams #~ or #-)
Metadata tables hold structural records. Instead of embedding names directly, rows in these tables store numeric indexes (tokens) pointing to other tables or into the heaps. Key tables include:
TypeDef: Defines every class, interface, and struct within the binary, referencing its base type, flags, and lists of methods and fields.TypeRef: References types defined in external assemblies.MethodDef: Defines methods, linking to parameter lists, signatures in the#Blobheap, and the memory offset (RVA) of the actual CIL instructions.MemberRef: Identifies external methods or fields invoked by the local code.AssemblyandAssemblyRef: Contain identity and cryptographic data for the local assembly and external dependencies, respectively.
Execution and Metadata Integration
During runtime, the Just-In-Time (JIT) compiler utilizes this structural arrangement to convert CIL into machine code:
- Token Resolution: The CIL code uses 4-byte metadata
tokens (e.g.,
0x06000001for aMethodDef) to represent operations rather than direct memory addresses. - Type Safety and Verification: The CLR inspects the metadata tables to verify that argument counts, parameter types, and memory allocations are legal before compilation.
- JIT Compilation: The runtime resolves the token via
the
MethodDeftable to locate the raw CIL bytes, compiles those instructions for the host CPU, and replaces the call address with the native instruction pointer.
This tightly coupled system of PE containers, intermediate instructions, and relational metadata enables core .NET features such as reflection, garbage collection tracking, dynamic type loading, and cross-language interoperability.