|
| 1 | +// Copyright (c) Alexandre Mutel. All rights reserved. |
| 2 | +// This file is licensed under the BSD-Clause 2 license. |
| 3 | +// See the license.txt file in the project root for more information. |
| 4 | + |
| 5 | +using System; |
| 6 | +using System.Collections.Generic; |
| 7 | +using System.IO; |
| 8 | +using System.Linq; |
| 9 | +using LibObjectFile.Diagnostics; |
| 10 | + |
| 11 | +namespace LibObjectFile.Elf; |
| 12 | + |
| 13 | +/// <summary> |
| 14 | +/// Address-preserving edits of a dynamically-linked <see cref="ElfFile"/>. |
| 15 | +/// </summary> |
| 16 | +/// <remarks> |
| 17 | +/// These operations never change the byte offset of an existing section, so existing symbols, relocations and |
| 18 | +/// code stay valid. The sections that must grow (<c>.dynamic</c> and <c>.dynstr</c>) are relocated to the end of |
| 19 | +/// the file and mapped by a fresh <c>PT_LOAD</c> built from a spare <c>PT_NOTE</c> program header (so the |
| 20 | +/// program-header table does not grow). This is the same technique used by <c>patchelf --add-needed</c>. |
| 21 | +/// </remarks> |
| 22 | +public static class ElfDynamicEditing |
| 23 | +{ |
| 24 | + private const ulong PageSize = 0x1000; |
| 25 | + |
| 26 | + /// <summary> |
| 27 | + /// Injects a <c>DT_NEEDED</c> dependency on <paramref name="libraryName"/> into an existing dynamically-linked |
| 28 | + /// image, so the dynamic loader loads that library at startup. The <c>.dynamic</c> and <c>.dynstr</c> sections |
| 29 | + /// are relocated (as sections, kept consistent with <c>PT_DYNAMIC</c> and <c>DT_STRTAB</c>), while every other |
| 30 | + /// section keeps its byte offset. The vacated space is backfilled so no existing reference shifts. |
| 31 | + /// </summary> |
| 32 | + /// <exception cref="InvalidOperationException"> |
| 33 | + /// The file is not dynamically linked, or has no <c>PT_NOTE</c> segment to repurpose as the new <c>PT_LOAD</c>. |
| 34 | + /// </exception> |
| 35 | + public static void AddNeededLibrary(this ElfFile elf, string libraryName) |
| 36 | + { |
| 37 | + ArgumentNullException.ThrowIfNull(elf); |
| 38 | + ArgumentException.ThrowIfNullOrEmpty(libraryName); |
| 39 | + |
| 40 | + var dynamicSection = elf.Sections.OfType<ElfDynamicLinkingTable>().FirstOrDefault() |
| 41 | + ?? throw new InvalidOperationException("The ELF file has no dynamic section (.dynamic); it is not dynamically linked."); |
| 42 | + var stringSection = dynamicSection.StringTable |
| 43 | + ?? throw new InvalidOperationException("The dynamic section is not linked to a string table (.dynstr)."); |
| 44 | + var dynamicSegment = elf.Segments.FirstOrDefault(s => s.Type == ElfSegmentTypeCore.Dynamic) |
| 45 | + ?? throw new InvalidOperationException("The ELF file has no PT_DYNAMIC segment."); |
| 46 | + var noteSegment = elf.Segments.FirstOrDefault(s => s.Type == ElfSegmentTypeCore.Note) |
| 47 | + ?? throw new InvalidOperationException("The ELF file has no PT_NOTE segment to repurpose as a new PT_LOAD."); |
| 48 | + |
| 49 | + // Original layout of the sections we relocate, captured before any mutation. |
| 50 | + ulong dynamicOldVaddr = dynamicSection.VirtualAddress; |
| 51 | + ulong stringOldVaddr = stringSection.VirtualAddress; |
| 52 | + ulong dynamicOldSize = dynamicSection.Size; |
| 53 | + ulong stringOldSize = stringSection.Size; |
| 54 | + uint dynamicOldAlign = dynamicSection.FileAlignment; |
| 55 | + uint stringOldAlign = stringSection.FileAlignment; |
| 56 | + |
| 57 | + // Grow .dynstr with the library name and add the DT_NEEDED entry to .dynamic. |
| 58 | + dynamicSection.AddNeededLibrary(libraryName); |
| 59 | + |
| 60 | + // Relocate .dynamic and .dynstr to end-of-file, backfilling their old positions with equal-size padding |
| 61 | + // so every other section keeps its byte offset. .dynamic goes first so PT_DYNAMIC covers the start of the |
| 62 | + // new region; page-align it so the new PT_LOAD is vaddr/offset congruent. |
| 63 | + RelocateToEnd(elf, dynamicSection, dynamicOldSize, dynamicOldAlign, (uint)PageSize); |
| 64 | + RelocateToEnd(elf, stringSection, stringOldSize, stringOldAlign, 1); |
| 65 | + |
| 66 | + var diagnostics = new DiagnosticBag(); |
| 67 | + elf.UpdateLayout(diagnostics); |
| 68 | + if (diagnostics.HasErrors) |
| 69 | + { |
| 70 | + throw new ObjectFileException("Failed to update the ELF layout while injecting a dependency.", diagnostics); |
| 71 | + } |
| 72 | + |
| 73 | + ulong baseVaddr = AlignUp(MaxLoadedVirtualAddressEnd(elf), PageSize); |
| 74 | + ulong firstPosition = dynamicSection.Position; |
| 75 | + dynamicSection.VirtualAddress = baseVaddr + (dynamicSection.Position - firstPosition); |
| 76 | + stringSection.VirtualAddress = baseVaddr + (stringSection.Position - firstPosition); |
| 77 | + |
| 78 | + // Point DT_STRTAB/DT_STRSZ at the relocated .dynstr. These are value-only updates (no entry count change), |
| 79 | + // so the layout computed above stays valid. |
| 80 | + UpdateTagValue(dynamicSection.Entries, ElfDynamicTag.StrTab, stringSection.VirtualAddress); |
| 81 | + UpdateTagValue(dynamicSection.Entries, ElfDynamicTag.StrSz, stringSection.Size); |
| 82 | + |
| 83 | + // The _DYNAMIC symbol (and any other symbol inside the moved sections) must follow the new address. |
| 84 | + RelocateSymbols(elf, dynamicSection, dynamicOldVaddr); |
| 85 | + RelocateSymbols(elf, stringSection, stringOldVaddr); |
| 86 | + |
| 87 | + // Repurpose the PT_NOTE program header into a writable PT_LOAD mapping both relocated sections. It must be |
| 88 | + // writable because the loader writes DT_DEBUG back into .dynamic. |
| 89 | + noteSegment.Type = ElfSegmentTypeCore.Load; |
| 90 | + noteSegment.Flags = ElfSegmentFlagsCore.Readable | ElfSegmentFlagsCore.Writable; |
| 91 | + noteSegment.VirtualAddress = baseVaddr; |
| 92 | + noteSegment.PhysicalAddress = baseVaddr; |
| 93 | + noteSegment.VirtualAddressAlignment = PageSize; |
| 94 | + noteSegment.Range = new ElfContentRange(dynamicSection, 0, stringSection, 0); |
| 95 | + noteSegment.SizeInMemory = stringSection.Position + stringSection.Size - dynamicSection.Position; |
| 96 | + |
| 97 | + // Point PT_DYNAMIC at the relocated .dynamic. |
| 98 | + dynamicSegment.VirtualAddress = dynamicSection.VirtualAddress; |
| 99 | + dynamicSegment.PhysicalAddress = dynamicSection.VirtualAddress; |
| 100 | + dynamicSegment.SizeInMemory = dynamicSection.Size; |
| 101 | + dynamicSegment.Range = new ElfContentRange(dynamicSection); |
| 102 | + } |
| 103 | + |
| 104 | + /// <summary> |
| 105 | + /// Moves <paramref name="section"/> to the end of the content list and backfills the space it vacated with a |
| 106 | + /// zero-filled shadow content of the same size and alignment, so every following section keeps its byte offset. |
| 107 | + /// The section header keeps its index (driven by <see cref="ElfSection.OrderInSectionHeaderTable"/>), only its |
| 108 | + /// file offset moves. |
| 109 | + /// </summary> |
| 110 | + private static void RelocateToEnd(ElfFile elf, ElfSection section, ulong originalSize, uint originalAlignment, uint appendAlignment) |
| 111 | + { |
| 112 | + // RemoveAt (not Remove) is required: only RemoveAt fires the content hooks that keep the section list |
| 113 | + // in sync, otherwise the section would be registered twice and produce a duplicate section header. |
| 114 | + int index = elf.Content.IndexOf(section); |
| 115 | + elf.Content.RemoveAt(index); |
| 116 | + var padding = new ElfStreamContentData(new MemoryStream(new byte[originalSize])) |
| 117 | + { |
| 118 | + FileAlignment = originalAlignment, |
| 119 | + }; |
| 120 | + elf.Content.Insert(index, padding); |
| 121 | + section.FileAlignment = appendAlignment; |
| 122 | + elf.Content.Add(section); |
| 123 | + } |
| 124 | + |
| 125 | + private static void RelocateSymbols(ElfFile elf, ElfSection section, ulong oldVirtualAddress) |
| 126 | + { |
| 127 | + ulong newVirtualAddress = section.VirtualAddress; |
| 128 | + if (newVirtualAddress == oldVirtualAddress) return; |
| 129 | + |
| 130 | + foreach (var symbolTable in elf.Sections.OfType<ElfSymbolTable>()) |
| 131 | + { |
| 132 | + var entries = symbolTable.Entries; |
| 133 | + for (int i = 0; i < entries.Count; i++) |
| 134 | + { |
| 135 | + var entry = entries[i]; |
| 136 | + if (ReferenceEquals(entry.SectionLink.Section, section)) |
| 137 | + { |
| 138 | + entry.Value = newVirtualAddress + (entry.Value - oldVirtualAddress); |
| 139 | + entries[i] = entry; |
| 140 | + } |
| 141 | + } |
| 142 | + } |
| 143 | + } |
| 144 | + |
| 145 | + private static void UpdateTagValue(List<ElfDynamic> entries, ElfDynamicTag tag, ulong value) |
| 146 | + { |
| 147 | + int index = entries.FindIndex(e => e.TagType == tag); |
| 148 | + if (index < 0) |
| 149 | + { |
| 150 | + throw new InvalidOperationException($"The dynamic section is missing the required {tag} entry."); |
| 151 | + } |
| 152 | + |
| 153 | + var entry = entries[index]; |
| 154 | + entry.Value = value; |
| 155 | + entries[index] = entry; |
| 156 | + } |
| 157 | + |
| 158 | + private static ulong MaxLoadedVirtualAddressEnd(ElfFile elf) |
| 159 | + { |
| 160 | + ulong max = 0; |
| 161 | + foreach (var segment in elf.Segments) |
| 162 | + { |
| 163 | + if (segment.SizeInMemory == 0) continue; |
| 164 | + max = Math.Max(max, segment.VirtualAddress + segment.SizeInMemory); |
| 165 | + } |
| 166 | + foreach (var section in elf.Sections) |
| 167 | + { |
| 168 | + if ((section.Flags & ElfSectionFlags.Alloc) == 0) continue; |
| 169 | + max = Math.Max(max, section.VirtualAddress + section.Size); |
| 170 | + } |
| 171 | + return max; |
| 172 | + } |
| 173 | + |
| 174 | + private static ulong AlignUp(ulong value, ulong align) => (value + align - 1) & ~(align - 1); |
| 175 | +} |
0 commit comments