Skip to content

Commit 22411ff

Browse files
Add Mach-O relocation reading
Relocations were bytes a section pointed at that nothing decoded, so an object file could be read but not understood. Adds decoding of both forms, together with the type enumerations for the three architectures this supports. The two forms share eight bytes and pack their fields into them in different orders, told apart by the top bit of the first word. They are unpacked by hand rather than through a struct, since C bitfields have no equivalent here and relying on a layout the compiler chose would be fragile. Encoding is implemented alongside and tested to round-trip, because a field read from the wrong bit still reads back consistently on its own and would otherwise look correct. - src/LibObjectFile/MachO/MachORelocation.cs: decoding and encoding of both forms - src/LibObjectFile/MachO/MachORelocationType.cs: the per-architecture type numbering, which restarts for each - src/LibObjectFile/MachO/MachOFile.Relocations.cs: reads the entries a section points at
1 parent 4f75800 commit 22411ff

5 files changed

Lines changed: 336 additions & 0 deletions

File tree

‎src/LibObjectFile.Tests/MachO/MachOSimpleTests.cs‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,57 @@ public void ReadsTheSymbolTable()
149149
Assert.AreEqual("_printf", symbols[(int)indirect[(int)stubs.Reserved1]].Name);
150150
}
151151

152+
/// <summary>
153+
/// Values cross-checked against llvm-objdump. The two forms of entry pack their fields into
154+
/// the same eight bytes in different orders, so encoding is checked to round-trip as well:
155+
/// a field unpacked from the wrong bit still reads back consistently on its own.
156+
/// </summary>
157+
[TestMethod]
158+
public void ReadsRelocations()
159+
{
160+
var file = LoadMachO("helloworld_x86_64.o");
161+
var text = file.Segments.SelectMany(s => s.Sections).Single(s => s.Name == "__text");
162+
var symbols = file.ReadSymbolTable();
163+
164+
var relocations = file.ReadRelocations(text);
165+
Assert.AreEqual(4, relocations.Count);
166+
167+
var branch = relocations[0];
168+
Assert.AreEqual(0x36, branch.Address);
169+
Assert.AreEqual((byte)MachOX86_64RelocationType.Branch, branch.RawType);
170+
Assert.IsTrue(branch.IsPcRelative);
171+
Assert.IsTrue(branch.IsExternal);
172+
Assert.AreEqual(4, branch.LengthInBytes);
173+
Assert.IsFalse(branch.IsScattered);
174+
Assert.AreEqual("_printf", symbols[(int)branch.SymbolOrSectionNumber].Name);
175+
176+
// A local entry numbers a section instead of a symbol.
177+
var signed = relocations[1];
178+
Assert.AreEqual((byte)MachOX86_64RelocationType.Signed, signed.RawType);
179+
Assert.IsFalse(signed.IsExternal);
180+
Assert.AreEqual(3u, signed.SymbolOrSectionNumber);
181+
182+
Assert.AreEqual("_helloworld_data", symbols[(int)relocations[3].SymbolOrSectionNumber].Name);
183+
184+
foreach (var relocation in relocations)
185+
{
186+
var (word0, word1) = relocation.Encode();
187+
var again = MachORelocation.Decode(word0, word1);
188+
Assert.AreEqual(relocation.ToString(), again.ToString());
189+
Assert.AreEqual(relocation.SymbolOrSectionNumber, again.SymbolOrSectionNumber);
190+
}
191+
192+
// The scattered form has no external flag and carries a target address instead.
193+
var scattered = MachORelocation.Decode(MachORelocation.ScatteredMask | (2u << 28) | (1u << 24) | 0x123, 0x4567);
194+
Assert.IsTrue(scattered.IsScattered);
195+
Assert.AreEqual(0x123, scattered.Address);
196+
Assert.AreEqual(1, scattered.RawType);
197+
Assert.AreEqual(4, scattered.LengthInBytes);
198+
Assert.AreEqual(0x4567, scattered.Value);
199+
Assert.IsFalse(scattered.IsExternal);
200+
Assert.AreEqual((MachORelocation.ScatteredMask | (2u << 28) | (1u << 24) | 0x123, 0x4567u), scattered.Encode());
201+
}
202+
152203
[TestMethod]
153204
public void RejectsANonMachOStream()
154205
{
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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.Runtime.InteropServices;
6+
7+
namespace LibObjectFile.MachO.Internal;
8+
9+
#pragma warning disable CS0649
10+
11+
/// <summary>
12+
/// A relocation entry (<c>relocation_info</c> or <c>scattered_relocation_info</c>).
13+
/// </summary>
14+
/// <remarks>
15+
/// The two forms share a size and are told apart by the top bit of the first word. C bitfields
16+
/// have no equivalent here, so both words are kept whole and unpacked by hand, which also avoids
17+
/// depending on how a compiler happens to lay the fields out.
18+
/// </remarks>
19+
[StructLayout(LayoutKind.Sequential, Pack = 4)]
20+
internal struct RawRelocationInfo
21+
{
22+
public uint Word0;
23+
public uint Word1;
24+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
8+
namespace LibObjectFile.MachO;
9+
10+
partial class MachOFile
11+
{
12+
/// <summary>
13+
/// Reads the relocations of a section.
14+
/// </summary>
15+
/// <param name="section">The section whose relocations to read.</param>
16+
/// <returns>The relocations, or an empty list if the section has none.</returns>
17+
/// <remarks>
18+
/// Only an object file normally carries these. A linked image resolves them at link time and
19+
/// keeps just what the loader still needs, as dyld info opcodes or chained fixups.
20+
/// <para>
21+
/// A pair of entries can describe one fixup between them, with the second carrying the other
22+
/// half. They are returned in file order so a caller can pair them up.
23+
/// </para>
24+
/// </remarks>
25+
/// <exception cref="ArgumentNullException"><paramref name="section"/> is null.</exception>
26+
/// <exception cref="ObjectFileException">The relocations lie outside the content of this image.</exception>
27+
public IReadOnlyList<MachORelocation> ReadRelocations(MachOSection section)
28+
{
29+
ArgumentNullException.ThrowIfNull(section);
30+
31+
if (section.NumberOfRelocations == 0) return [];
32+
33+
var bytes = ReadFileBytes(section.RelocationOffset, section.NumberOfRelocations * 8, $"relocations of {section.SegmentName},{section.Name}");
34+
var relocations = new MachORelocation[section.NumberOfRelocations];
35+
for (var i = 0; i < relocations.Length; i++)
36+
{
37+
relocations[i] = MachORelocation.Decode(
38+
BitConverter.ToUInt32(bytes, i * 8),
39+
BitConverter.ToUInt32(bytes, i * 8 + 4));
40+
}
41+
42+
return relocations;
43+
}
44+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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+
7+
namespace LibObjectFile.MachO;
8+
9+
/// <summary>
10+
/// A relocation entry, telling the linker how to fix up one place in a section.
11+
/// </summary>
12+
/// <remarks>
13+
/// Two forms share the same eight bytes. The ordinary form names a symbol or a section by number.
14+
/// The scattered form carries the target address outright, for when the fixed-up value does not
15+
/// land inside the thing it refers to and a section number would be ambiguous; it has no room
16+
/// left for an external flag, so it is always local.
17+
/// </remarks>
18+
public sealed class MachORelocation
19+
{
20+
/// <summary>Bit of the first word marking an entry as scattered (<c>R_SCATTERED</c>).</summary>
21+
public const uint ScatteredMask = 0x80000000;
22+
23+
/// <summary>
24+
/// Gets or sets the offset of the fixed-up field from the start of its section.
25+
/// </summary>
26+
public int Address { get; set; }
27+
28+
/// <summary>
29+
/// Gets or sets the symbol index when <see cref="IsExternal"/> is true, and the one-based
30+
/// section number otherwise. Always zero for a scattered entry.
31+
/// </summary>
32+
public uint SymbolOrSectionNumber { get; set; }
33+
34+
/// <summary>
35+
/// Gets or sets the address of the target, for a scattered entry only.
36+
/// </summary>
37+
public int Value { get; set; }
38+
39+
/// <summary>
40+
/// Gets or sets whether the fixed-up field holds a displacement from the program counter
41+
/// rather than an address.
42+
/// </summary>
43+
public bool IsPcRelative { get; set; }
44+
45+
/// <summary>
46+
/// Gets or sets the width of the fixed-up field as a power of two, so 2 means four bytes.
47+
/// </summary>
48+
public byte LengthLog2 { get; set; }
49+
50+
/// <summary>
51+
/// Gets or sets whether <see cref="SymbolOrSectionNumber"/> is a symbol index. Never true
52+
/// for a scattered entry, which has no room for the flag.
53+
/// </summary>
54+
public bool IsExternal { get; set; }
55+
56+
/// <summary>
57+
/// Gets or sets whether this entry carries its target address rather than a symbol or
58+
/// section number.
59+
/// </summary>
60+
public bool IsScattered { get; set; }
61+
62+
/// <summary>
63+
/// Gets or sets the type, whose meaning depends on the image's architecture. Interpret it as
64+
/// <see cref="MachOX86_64RelocationType"/>, <see cref="MachOArm64RelocationType"/> or
65+
/// <see cref="MachOGenericRelocationType"/> accordingly.
66+
/// </summary>
67+
public byte RawType { get; set; }
68+
69+
/// <summary>
70+
/// Gets the width of the fixed-up field in bytes.
71+
/// </summary>
72+
public int LengthInBytes => 1 << LengthLog2;
73+
74+
/// <summary>
75+
/// Decodes an entry from the two words it occupies.
76+
/// </summary>
77+
/// <param name="word0">The first word, which is the address unless the top bit marks it scattered.</param>
78+
/// <param name="word1">The second word.</param>
79+
/// <returns>The decoded entry.</returns>
80+
public static MachORelocation Decode(uint word0, uint word1)
81+
{
82+
if ((word0 & ScatteredMask) != 0)
83+
{
84+
return new MachORelocation
85+
{
86+
IsScattered = true,
87+
Address = (int)(word0 & 0x00ffffff),
88+
RawType = (byte)((word0 >> 24) & 0xf),
89+
LengthLog2 = (byte)((word0 >> 28) & 0x3),
90+
IsPcRelative = ((word0 >> 30) & 1) != 0,
91+
Value = (int)word1,
92+
};
93+
}
94+
95+
return new MachORelocation
96+
{
97+
Address = (int)word0,
98+
SymbolOrSectionNumber = word1 & 0x00ffffff,
99+
IsPcRelative = ((word1 >> 24) & 1) != 0,
100+
LengthLog2 = (byte)((word1 >> 25) & 0x3),
101+
IsExternal = ((word1 >> 27) & 1) != 0,
102+
RawType = (byte)((word1 >> 28) & 0xf),
103+
};
104+
}
105+
106+
/// <summary>
107+
/// Encodes this entry back into the two words it occupies.
108+
/// </summary>
109+
/// <returns>The first and second words.</returns>
110+
public (uint Word0, uint Word1) Encode()
111+
{
112+
if (IsScattered)
113+
{
114+
var packed = ScatteredMask
115+
| ((uint)Address & 0x00ffffff)
116+
| ((uint)(RawType & 0xf) << 24)
117+
| ((uint)(LengthLog2 & 0x3) << 28)
118+
| (IsPcRelative ? 1u << 30 : 0u);
119+
return (packed, (uint)Value);
120+
}
121+
122+
var info = (SymbolOrSectionNumber & 0x00ffffff)
123+
| (IsPcRelative ? 1u << 24 : 0u)
124+
| ((uint)(LengthLog2 & 0x3) << 25)
125+
| (IsExternal ? 1u << 27 : 0u)
126+
| ((uint)(RawType & 0xf) << 28);
127+
return ((uint)Address, info);
128+
}
129+
130+
/// <inheritdoc />
131+
public override string ToString()
132+
=> $"{nameof(MachORelocation)} {{ Address = 0x{Address:X}, Type = {RawType}, {LengthInBytes} bytes{(IsPcRelative ? ", pcrel" : string.Empty)}{(IsExternal ? ", external" : string.Empty)}{(IsScattered ? ", scattered" : string.Empty)} }}";
133+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+
namespace LibObjectFile.MachO;
6+
7+
/// <summary>
8+
/// Relocation types for 32-bit Intel and the other architectures using the generic set.
9+
/// </summary>
10+
/// <remarks>
11+
/// The numbering restarts for every architecture, so a value only means something once the
12+
/// image's <see cref="MachOCpuType"/> is known.
13+
/// </remarks>
14+
public enum MachOGenericRelocationType : byte
15+
{
16+
/// <summary>The relocated field takes the symbol's address (<c>GENERIC_RELOC_VANILLA</c>).</summary>
17+
Vanilla = 0,
18+
/// <summary>The second half of a pair, following the entry it belongs to (<c>GENERIC_RELOC_PAIR</c>).</summary>
19+
Pair = 1,
20+
/// <summary>The difference between two section-relative addresses (<c>GENERIC_RELOC_SECTDIFF</c>).</summary>
21+
SectionDifference = 2,
22+
/// <summary>A prebound lazy pointer (<c>GENERIC_RELOC_PB_LA_PTR</c>).</summary>
23+
PreboundLazyPointer = 3,
24+
/// <summary>A section difference where the subtrahend is local (<c>GENERIC_RELOC_LOCAL_SECTDIFF</c>).</summary>
25+
LocalSectionDifference = 4,
26+
/// <summary>A thread local variable reference (<c>GENERIC_RELOC_TLV</c>).</summary>
27+
ThreadLocalVariable = 5,
28+
}
29+
30+
/// <summary>
31+
/// Relocation types for 64-bit Intel.
32+
/// </summary>
33+
public enum MachOX86_64RelocationType : byte
34+
{
35+
/// <summary>An absolute address (<c>X86_64_RELOC_UNSIGNED</c>).</summary>
36+
Unsigned = 0,
37+
/// <summary>A signed 32-bit displacement (<c>X86_64_RELOC_SIGNED</c>).</summary>
38+
Signed = 1,
39+
/// <summary>The target of a call or jump (<c>X86_64_RELOC_BRANCH</c>).</summary>
40+
Branch = 2,
41+
/// <summary>A load through the global offset table (<c>X86_64_RELOC_GOT_LOAD</c>).</summary>
42+
GotLoad = 3,
43+
/// <summary>Any other global offset table reference (<c>X86_64_RELOC_GOT</c>).</summary>
44+
Got = 4,
45+
/// <summary>The subtrahend of a difference, paired with the entry after it (<c>X86_64_RELOC_SUBTRACTOR</c>).</summary>
46+
Subtractor = 5,
47+
/// <summary>A signed displacement with an implicit addend of one (<c>X86_64_RELOC_SIGNED_1</c>).</summary>
48+
Signed1 = 6,
49+
/// <summary>A signed displacement with an implicit addend of two (<c>X86_64_RELOC_SIGNED_2</c>).</summary>
50+
Signed2 = 7,
51+
/// <summary>A signed displacement with an implicit addend of four (<c>X86_64_RELOC_SIGNED_4</c>).</summary>
52+
Signed4 = 8,
53+
/// <summary>A thread local variable reference (<c>X86_64_RELOC_TLV</c>).</summary>
54+
ThreadLocalVariable = 9,
55+
}
56+
57+
/// <summary>
58+
/// Relocation types for 64-bit ARM.
59+
/// </summary>
60+
public enum MachOArm64RelocationType : byte
61+
{
62+
/// <summary>An absolute address (<c>ARM64_RELOC_UNSIGNED</c>).</summary>
63+
Unsigned = 0,
64+
/// <summary>The subtrahend of a difference, paired with the entry after it (<c>ARM64_RELOC_SUBTRACTOR</c>).</summary>
65+
Subtractor = 1,
66+
/// <summary>The target of a 26-bit branch (<c>ARM64_RELOC_BRANCH26</c>).</summary>
67+
Branch26 = 2,
68+
/// <summary>The page of a target, for <c>adrp</c> (<c>ARM64_RELOC_PAGE21</c>).</summary>
69+
Page21 = 3,
70+
/// <summary>The offset within a page (<c>ARM64_RELOC_PAGEOFF12</c>).</summary>
71+
PageOffset12 = 4,
72+
/// <summary>The page of a global offset table entry (<c>ARM64_RELOC_GOT_LOAD_PAGE21</c>).</summary>
73+
GotLoadPage21 = 5,
74+
/// <summary>The offset of a global offset table entry within its page (<c>ARM64_RELOC_GOT_LOAD_PAGEOFF12</c>).</summary>
75+
GotLoadPageOffset12 = 6,
76+
/// <summary>A pointer to a global offset table entry (<c>ARM64_RELOC_POINTER_TO_GOT</c>).</summary>
77+
PointerToGot = 7,
78+
/// <summary>The page of a thread local variable pointer (<c>ARM64_RELOC_TLVP_LOAD_PAGE21</c>).</summary>
79+
ThreadLocalPage21 = 8,
80+
/// <summary>The offset of a thread local variable pointer within its page (<c>ARM64_RELOC_TLVP_LOAD_PAGEOFF12</c>).</summary>
81+
ThreadLocalPageOffset12 = 9,
82+
/// <summary>An addend applied to the entry after it (<c>ARM64_RELOC_ADDEND</c>).</summary>
83+
Addend = 10,
84+
}

0 commit comments

Comments
 (0)