File: Compiler\ObjectWriter\MachObjectWriter.Aot.cs
Web Access
Project: src\src\runtime\src\coreclr\tools\aot\ILCompiler.Compiler\ILCompiler.Compiler.csproj (ILCompiler.Compiler)
// 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.Buffers.Binary;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text;
using ILCompiler.DependencyAnalysis;
using ILCompiler.DependencyAnalysisFramework;
using Internal.Text;
using Internal.TypeSystem;
using static ILCompiler.DependencyAnalysis.RelocType;
using static ILCompiler.ObjectWriter.MachNative;

namespace ILCompiler.ObjectWriter
{
    /// <summary>
    /// Mach-O object file format writer for Apple macOS and iOS-like targets.
    /// </summary>
    /// <remarks>
    /// Old version of the Mach-O file format specification is mirrored at
    /// https://github.com/aidansteele/osx-abi-macho-file-format-reference.
    ///
    /// There are some notable differences when compared to ELF or COFF:
    /// - The maximum number of sections in object file is limited to 255.
    /// - Sections are subdivided by their symbols and treated by the
    ///   linker as subsections (often referred to as atoms by the linker).
    ///
    /// The consequences of these design decisions is the COMDAT sections are
    /// modeled in entirely different way. Dead code elimination works on the
    /// atom level, so relative relocations within the same section have to be
    /// preserved.
    ///
    /// Debug information uses the standard DWARF format. It is, however, not
    /// linked into the intermediate executable files. Instead the linker creates
    /// a map between the final executable and the object files. Debuggers like
    /// lldb then use this map to read the debug information from the object
    /// file directly. As a consequence the DWARF information is not generated
    /// with relocations for the DWARF sections themselves since it's never
    /// needed.
    ///
    /// While Mach-O uses the DWARF exception handling information for unwind
    /// tables it also supports a compact representation for common prolog types.
    /// Unofficial reference of the format can be found at
    /// https://faultlore.com/blah/compact-unwinding/. It's necessary to emit
    /// at least the stub entries pointing to the DWARF information but due
    /// to limits in the linked file format it's advisable to use the compact
    /// encoding whenever possible.
    ///
    /// The Apple linker is extremely picky in which relocation types are allowed
    /// inside the DWARF sections, both for debugging and exception handling.
    /// </remarks>
    internal sealed partial class MachObjectWriter : UnixObjectWriter
    {
        private sealed record CompactUnwindCode(Utf8String PcStartSymbolName, uint PcLength, uint Code, Utf8String LsdaSymbolName, Utf8String PersonalitySymbolName);

        // Exception handling sections
        private MachSection _compactUnwindSection;
        private MemoryStream _compactUnwindStream;
        private readonly List<CompactUnwindCode> _compactUnwindCodes = new();
        private readonly uint _compactUnwindDwarfCode;

        private bool IsEhFrameSection(int sectionIndex) => sectionIndex == EhFrameSectionIndex;

        partial void EmitCompactUnwindTable(IDictionary<Utf8String, SymbolDefinition> definedSymbols)
        {
            _compactUnwindStream = new MemoryStream(32 * _compactUnwindCodes.Count);
            // Preset the size of the compact unwind section which is not generated yet
            _compactUnwindStream.SetLength(32 * _compactUnwindCodes.Count);

            _compactUnwindSection = new MachSection("__LD", "__compact_unwind", _compactUnwindStream)
            {
                Log2Alignment = 3,
                Flags = S_REGULAR | S_ATTR_DEBUG,
            };

            IList<MachSymbol> symbols = _symbolTable;
            Span<byte> tempBuffer = stackalloc byte[8];
            foreach (var cu in _compactUnwindCodes)
            {
                EmitCompactUnwindSymbol(cu.PcStartSymbolName);
                BinaryPrimitives.WriteUInt32LittleEndian(tempBuffer, cu.PcLength);
                BinaryPrimitives.WriteUInt32LittleEndian(tempBuffer.Slice(4), cu.Code);
                _compactUnwindStream.Write(tempBuffer);
                EmitCompactUnwindSymbol(cu.PersonalitySymbolName);
                EmitCompactUnwindSymbol(cu.LsdaSymbolName);
            }

            void EmitCompactUnwindSymbol(Utf8String symbolName)
            {
                Span<byte> tempBuffer = stackalloc byte[8];
                if (!symbolName.IsNull)
                {
                    SymbolDefinition symbol = definedSymbols[symbolName];
                    MachSection section = _sections[symbol.SectionIndex];
                    BinaryPrimitives.WriteUInt64LittleEndian(tempBuffer, section.VirtualAddress + (ulong)symbol.Value);
                    _compactUnwindSection.Relocations.Add(
                        new MachRelocation
                        {
                            Address = (int)_compactUnwindStream.Position,
                            SymbolOrSectionIndex = (byte)(1 + symbol.SectionIndex), // 1-based
                            Length = 8,
                            RelocationType = ARM64_RELOC_UNSIGNED,
                            IsExternal = false,
                            IsPCRelative = false,
                        }
                    );
                }
                _compactUnwindStream.Write(tempBuffer);
            }
        }

        private static uint GetArm64CompactUnwindCode(byte[] blobData)
        {
            if (blobData == null || blobData.Length == 0)
            {
                return UNWIND_ARM64_MODE_FRAMELESS;
            }

            Debug.Assert(blobData.Length % 8 == 0);

            short spReg = -1;

            int codeOffset = 0;
            short cfaRegister = spReg;
            int cfaOffset = 0;
            int spOffset = 0;

            const int REG_DWARF_X19 = 19;
            const int REG_DWARF_X30 = 30;
            const int REG_DWARF_FP = 29;
            const int REG_DWARF_D8 = 72;
            const int REG_DWARF_D15 = 79;
            const int REG_IDX_X19 = 0;
            const int REG_IDX_X28 = 9;
            const int REG_IDX_FP = 10;
            const int REG_IDX_LR = 11;
            const int REG_IDX_D8 = 12;
            const int REG_IDX_D15 = 19;
            Span<int> registerOffset = stackalloc int[20];

            registerOffset.Fill(int.MinValue);

            // First process all the CFI codes to figure out the layout of X19-X28, FP, LR, and
            // D8-D15 on the stack.
            int offset = 0;
            while (offset < blobData.Length)
            {
                codeOffset = Math.Max(codeOffset, blobData[offset++]);
                CFI_OPCODE opcode = (CFI_OPCODE)blobData[offset++];
                short dwarfReg = BinaryPrimitives.ReadInt16LittleEndian(blobData.AsSpan(offset));
                offset += sizeof(short);
                int cfiOffset = BinaryPrimitives.ReadInt32LittleEndian(blobData.AsSpan(offset));
                offset += sizeof(int);

                switch (opcode)
                {
                    case CFI_OPCODE.CFI_DEF_CFA_REGISTER:
                        cfaRegister = dwarfReg;

                        if (spOffset != 0)
                        {
                            for (int i = 0; i < registerOffset.Length; i++)
                                if (registerOffset[i] != int.MinValue)
                                    registerOffset[i] -= spOffset;

                            cfaOffset += spOffset;
                            spOffset = 0;
                        }

                        break;

                    case CFI_OPCODE.CFI_REL_OFFSET:
                        Debug.Assert(cfaRegister == spReg);
                        if (dwarfReg >= REG_DWARF_X19 && dwarfReg <= REG_DWARF_X30) // X19 - X28, FP, LR
                        {
                            registerOffset[dwarfReg - REG_DWARF_X19 + REG_IDX_X19] = cfiOffset;
                        }
                        else if (dwarfReg >= REG_DWARF_D8 && dwarfReg <= REG_DWARF_D15) // D8 - D15
                        {
                            registerOffset[dwarfReg - REG_DWARF_D8 + REG_IDX_D8] = cfiOffset;
                        }
                        else
                        {
                            // We cannot represent this register in the compact unwinding format,
                            // fallback to DWARF immediately.
                            return UNWIND_ARM64_MODE_DWARF;
                        }
                        break;

                    case CFI_OPCODE.CFI_ADJUST_CFA_OFFSET:
                        if (cfaRegister != spReg)
                        {
                            cfaOffset += cfiOffset;
                        }
                        else
                        {
                            spOffset += cfiOffset;

                            for (int i = 0; i < registerOffset.Length; i++)
                                if (registerOffset[i] != int.MinValue)
                                    registerOffset[i] += cfiOffset;
                        }
                        break;
                }
            }

            uint unwindCode;
            int nextOffset;

            if (cfaRegister == REG_DWARF_FP &&
                cfaOffset == 16 &&
                registerOffset[REG_IDX_FP] == -16 &&
                registerOffset[REG_IDX_LR] == -8)
            {
                // Frame format - FP/LR are saved on the top. SP is restored to FP+16
                unwindCode = UNWIND_ARM64_MODE_FRAME;
                nextOffset = -24;
            }
            else if (cfaRegister == -1 && spOffset <= 65520 &&
                     registerOffset[REG_IDX_FP] == int.MinValue && registerOffset[REG_IDX_LR] == int.MinValue)
            {
                // Frameless format - FP/LR are not saved, SP must fit within the representable range
                uint encodedSpOffset = (uint)(spOffset / 16) << 12;
                unwindCode = UNWIND_ARM64_MODE_FRAMELESS | encodedSpOffset;
                nextOffset = spOffset - 8;
            }
            else
            {
                return UNWIND_ARM64_MODE_DWARF;
            }

            // Check that the integer register pairs are in the right order and mark
            // a flag for each successive pair that is present.
            for (int i = REG_IDX_X19; i < REG_IDX_X28; i += 2)
            {
                if (registerOffset[i] == int.MinValue)
                {
                    if (registerOffset[i + 1] != int.MinValue)
                        return UNWIND_ARM64_MODE_DWARF;
                }
                else if (registerOffset[i] == nextOffset)
                {
                    if (registerOffset[i + 1] != nextOffset - 8)
                        return UNWIND_ARM64_MODE_DWARF;
                    nextOffset -= 16;
                    unwindCode |= UNWIND_ARM64_FRAME_X19_X20_PAIR << (i >> 1);
                }
            }

            // Check that the floating point register pairs are in the right order and mark
            // a flag for each successive pair that is present.
            for (int i = REG_IDX_D8; i < REG_IDX_D15; i += 2)
            {
                if (registerOffset[i] == int.MinValue)
                {
                    if (registerOffset[i + 1] != int.MinValue)
                        return UNWIND_ARM64_MODE_DWARF;
                }
                else if (registerOffset[i] == nextOffset)
                {
                    if (registerOffset[i + 1] != nextOffset - 8)
                        return UNWIND_ARM64_MODE_DWARF;
                    nextOffset -= 16;
                    unwindCode |= UNWIND_ARM64_FRAME_D8_D9_PAIR << (i >> 1);
                }
            }

            return unwindCode;
        }

        private protected override bool EmitCompactUnwinding(Utf8String startSymbolName, ulong length, Utf8String lsdaSymbolName, byte[] blob)
        {
            uint encoding = _compactUnwindDwarfCode;

            if (_cpuType == CPU_TYPE_ARM64)
            {
                encoding = GetArm64CompactUnwindCode(blob);
            }

            _compactUnwindCodes.Add(new CompactUnwindCode(
                PcStartSymbolName: startSymbolName,
                PcLength: (uint)length,
                Code: encoding | (encoding != _compactUnwindDwarfCode && !lsdaSymbolName.IsNull ? 0x40000000u : 0), // UNWIND_HAS_LSDA
                LsdaSymbolName: encoding != _compactUnwindDwarfCode ? lsdaSymbolName : default,
                PersonalitySymbolName: default
            ));

            return encoding != _compactUnwindDwarfCode;
        }

        private protected override bool UseFrameNames => true;
    }
}