File: VTableFixupSupport.cs
Web Access
Project: src\src\runtime\src\tools\ilasm\src\ILAssembler\ILAssembler.csproj (ILAssembler)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Immutable;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;

namespace ILAssembler;

/// <summary>
/// Support for VTable fixups and native exports in IL assembly.
/// </summary>
/// <remarks>
/// VTable fixups allow managed methods to be exported as unmanaged entry points
/// that can be called from native code. This is used for:
/// - DllExport functionality (exporting managed methods from a DLL)
/// - COM interop with custom vtables
/// - Reverse P/Invoke scenarios
///
/// The implementation requires:
/// 1. VTableFixups directory in the CLR header - points to an array of VTableFixup entries
/// 2. Each VTableFixup entry contains: RVA to slot data, slot count, and flags
/// 3. The slot data contains method tokens that the runtime patches with method addresses
/// 4. For exports, jump stubs in the text section that indirect through the vtable slots
/// 5. PE Export directory pointing to the jump stubs
/// </remarks>
internal static class VTableFixupSupport
{
    // COR_VTABLE_* flags from corhdr.h
    public const ushort COR_VTABLE_32BIT = 0x01;
    public const ushort COR_VTABLE_64BIT = 0x02;
    public const ushort COR_VTABLE_FROM_UNMANAGED = 0x04;
    public const ushort COR_VTABLE_FROM_UNMANAGED_RETAIN_APPDOMAIN = 0x08;
    public const ushort COR_VTABLE_CALL_MOST_DERIVED = 0x10;

    /// <summary>
    /// Represents a VTable fixup entry parsed from .vtfixup directive.
    /// </summary>
    /// <param name="SlotCount">Number of slots in this VTable.</param>
    /// <param name="Flags">COR_VTABLE_* flags.</param>
    /// <param name="DataLabel">Label name in the data section where method tokens are stored.</param>
    public readonly record struct VTableFixupEntry(int SlotCount, ushort Flags, string DataLabel);

    /// <summary>
    /// Represents a method export from .export directive.
    /// </summary>
    /// <param name="Ordinal">Export ordinal number.</param>
    /// <param name="Name">Export name (method name or alias).</param>
    /// <param name="MethodToken">Method definition token.</param>
    /// <param name="VTableEntryIndex">1-based index of the VTable fixup entry.</param>
    /// <param name="VTableSlotIndex">1-based slot index within the VTable entry.</param>
    public readonly record struct MethodExport(int Ordinal, string Name, int MethodToken, int VTableEntryIndex, int VTableSlotIndex);

    /// <summary>
    /// Checks if any VTable fixups or exports are defined.
    /// </summary>
    public static bool HasVTableFixupsOrExports(
        ImmutableArray<VTableFixupEntry> vtableFixups,
        ImmutableArray<MethodExport> exports)
    {
        return !vtableFixups.IsDefaultOrEmpty || !exports.IsDefaultOrEmpty;
    }

    /// <summary>
    /// Validates that exports have corresponding vtable entries.
    /// </summary>
    public static void ValidateExports(
        ImmutableArray<VTableFixupEntry> vtableFixups,
        ImmutableArray<MethodExport> exports,
        Action<string> reportError)
    {
        if (exports.IsDefaultOrEmpty)
            return;

        foreach (var export in exports)
        {
            if (export.VTableEntryIndex <= 0 || export.VTableEntryIndex > vtableFixups.Length)
            {
                reportError($"Export '{export.Name}' references invalid VTable entry index {export.VTableEntryIndex}");
                continue;
            }

            var vtfEntry = vtableFixups[export.VTableEntryIndex - 1];
            if (export.VTableSlotIndex <= 0 || export.VTableSlotIndex > vtfEntry.SlotCount)
            {
                reportError($"Export '{export.Name}' references invalid VTable slot {export.VTableSlotIndex} (entry has {vtfEntry.SlotCount} slots)");
            }
        }
    }

    /// <summary>
    /// Calculates the size of the VTableFixups directory data.
    /// </summary>
    /// <remarks>
    /// The VTableFixups directory is an array of IMAGE_COR_VTABLEFIXUP structures:
    /// struct IMAGE_COR_VTABLEFIXUP {
    ///     DWORD RVA;      // RVA of the vtable slot data
    ///     WORD  Count;    // Number of entries
    ///     WORD  Type;     // COR_VTABLE_* flags
    /// };
    /// Total: 8 bytes per entry
    /// </remarks>
    public static int CalculateVTableFixupsDirectorySize(ImmutableArray<VTableFixupEntry> vtableFixups)
    {
        if (vtableFixups.IsDefaultOrEmpty)
            return 0;

        return vtableFixups.Length * 8; // 8 bytes per entry
    }

    /// <summary>
    /// Calculates the size of the vtable slot data (method tokens).
    /// </summary>
    public static int CalculateVTableSlotDataSize(ImmutableArray<VTableFixupEntry> vtableFixups)
    {
        if (vtableFixups.IsDefaultOrEmpty)
            return 0;

        int totalSize = 0;
        foreach (var entry in vtableFixups)
        {
            int slotSize = (entry.Flags & COR_VTABLE_64BIT) != 0 ? 8 : 4;
            totalSize += entry.SlotCount * slotSize;
        }

        return totalSize;
    }

    /// <summary>
    /// Gets the size of an export stub for the given machine type.
    /// </summary>
    public static int GetExportStubSize(Machine machine)
    {
        return machine switch
        {
            Machine.Amd64 => 12,  // mov rax, [addr]; jmp rax
            Machine.I386 => 6,    // jmp [addr]
            Machine.Arm => 8,     // ldr pc, [pc, #0]; addr
            Machine.Arm64 => 12,  // adrp x16, addr; ldr x16, [x16]; br x16
            _ => 0
        };
    }

    /// <summary>
    /// Writes the VTableFixups directory entries to a blob.
    /// </summary>
    /// <param name="builder">The blob builder to write to.</param>
    /// <param name="vtableFixups">The vtable fixup entries.</param>
    /// <param name="slotDataRvas">RVAs of the slot data for each entry.</param>
    public static void WriteVTableFixupsDirectory(
        BlobBuilder builder,
        ImmutableArray<VTableFixupEntry> vtableFixups,
        ReadOnlySpan<int> slotDataRvas)
    {
        if (vtableFixups.IsDefaultOrEmpty)
            return;

        for (int i = 0; i < vtableFixups.Length; i++)
        {
            var entry = vtableFixups[i];
            builder.WriteInt32(slotDataRvas[i]);        // RVA
            builder.WriteUInt16((ushort)entry.SlotCount); // Count
            builder.WriteUInt16(entry.Flags);             // Type
        }
    }

    /// <summary>
    /// Writes the vtable slot data (method tokens) to a blob.
    /// </summary>
    /// <param name="builder">The blob builder to write to.</param>
    /// <param name="vtableFixups">The vtable fixup entries.</param>
    /// <param name="getMethodToken">Function to get method token for a given vtable entry and slot.</param>
    public static void WriteVTableSlotData(
        BlobBuilder builder,
        ImmutableArray<VTableFixupEntry> vtableFixups,
        Func<int, int, int> getMethodToken)
    {
        if (vtableFixups.IsDefaultOrEmpty)
            return;

        for (int entryIndex = 0; entryIndex < vtableFixups.Length; entryIndex++)
        {
            var entry = vtableFixups[entryIndex];
            bool is64Bit = (entry.Flags & COR_VTABLE_64BIT) != 0;

            for (int slotIndex = 0; slotIndex < entry.SlotCount; slotIndex++)
            {
                int token = getMethodToken(entryIndex + 1, slotIndex + 1);
                if (is64Bit)
                {
                    builder.WriteInt64(token);
                }
                else
                {
                    builder.WriteInt32(token);
                }
            }
        }
    }

    /// <summary>
    /// Writes an export stub for AMD64.
    /// </summary>
    public static void WriteExportStubAmd64(BlobBuilder builder, long vtableSlotAddress)
    {
        // mov rax, [vtableSlotAddress]
        builder.WriteByte(0x48); // REX.W
        builder.WriteByte(0xA1); // mov rax, moffs64
        builder.WriteInt64(vtableSlotAddress);
        // jmp rax
        builder.WriteByte(0xFF);
        builder.WriteByte(0xE0);
    }

    /// <summary>
    /// Writes an export stub for x86.
    /// </summary>
    public static void WriteExportStubX86(BlobBuilder builder, int vtableSlotAddress)
    {
        // jmp [vtableSlotAddress]
        builder.WriteByte(0xFF);
        builder.WriteByte(0x25);
        builder.WriteInt32(vtableSlotAddress);
    }

    /// <summary>
    /// Writes an export stub for ARM (Thumb-2).
    /// </summary>
    public static void WriteExportStubArm(BlobBuilder builder, int vtableSlotAddress)
    {
        // ldr pc, [pc, #0]
        builder.WriteUInt16(0xF8DF);
        builder.WriteUInt16(0xF000);
        // address
        builder.WriteInt32(vtableSlotAddress);
    }
}