Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,10 @@ public void Add(ReadyToRunSectionType id, DependencyNodeCore<NodeFactory> node,

public override bool StaticDependenciesAreComputed => true;

public override ObjectNodeSection GetSection(NodeFactory factory)
{
if (factory.Target.IsWindows)
return ObjectNodeSection.ReadOnlyDataSection;
else
return ObjectNodeSection.DataSection;
}
// For R2R, we can put the header in the read-only section on non-Windows as well. Since we emit a PE image
// and do our own mapping, we don't need it to be writeable for the OS loader to handle absolute pointer relocs.
// Our R2R PE images group read-only data into the .text section, so this doesn't result in more work to map.
public override ObjectNodeSection GetSection(NodeFactory factory) => ObjectNodeSection.ReadOnlyDataSection;

public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
{
Expand Down
187 changes: 107 additions & 80 deletions src/coreclr/tools/aot/ILCompiler.ReadyToRun/ObjectWriter/R2RPEBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ namespace ILCompiler.PEWriter
/// metadata and IL and adding new code and data representing the R2R JITted code and
/// additional runtime structures (R2R header and tables).
/// </summary>
public class R2RPEBuilder : PEBuilder
public sealed class R2RPEBuilder : PEBuilder
{
/// <summary>
/// Number of low-order RVA bits that must match file position on Linux.
Expand Down Expand Up @@ -73,7 +73,7 @@ public SectionRVADelta(int startRVA, int endRVA, int deltaRVA)
/// Name of the initialized data section.
/// </summary>
public const string SDataSectionName = ".sdata";

/// <summary>
/// Name of the relocation section.
/// </summary>
Expand All @@ -94,11 +94,6 @@ public SectionRVADelta(int startRVA, int endRVA, int deltaRVA)
/// </summary>
private TargetDetails _target;

/// <summary>
/// Complete list of sections to emit into the output R2R executable.
/// </summary>
private ImmutableArray<Section> _sections;

/// <summary>
/// Callback to retrieve the runtime function table which needs setting to the
/// ExceptionTable PE directory entry.
Expand All @@ -112,28 +107,48 @@ public SectionRVADelta(int startRVA, int endRVA, int deltaRVA)
/// </summary>
private List<SectionRVADelta> _sectionRvaDeltas;

/// <summary>
/// Logical section start RVAs. When emitting R2R PE executables for Linux, we must
/// align RVA's so that their 'RVABitsToMatchFilePos' lowest-order bits match the
/// file position (otherwise memory mapping of the file fails and CoreCLR silently
/// switches over to runtime JIT). PEBuilder doesn't support this today so that we
/// must store the RVA's and post-process the produced PE by patching the section
/// headers in the PE header.
/// </summary>
private int[] _sectionRVAs;
private class SerializedSectionData
{
/// <summary>
/// Name of the section
/// </summary>
public string Name;

/// <summary>
/// Pointers to the location of the raw data. Needed to allow phyical file alignment
/// beyond 4KB. PEBuilder doesn't support this today so that we
/// must store the RVA's and post-process the produced PE by patching the section
/// headers in the PE header.
/// </summary>
private int[] _sectionPointerToRawData;
/// <summary>
/// Logical section start RVAs. When emitting R2R PE executables for Linux, we must
/// align RVA's so that their 'RVABitsToMatchFilePos' lowest-order bits match the
/// file position (otherwise memory mapping of the file fails and CoreCLR silently
/// switches over to runtime JIT). PEBuilder doesn't support this today so that we
/// must store the RVA's and post-process the produced PE by patching the section
/// headers in the PE header.
/// </summary>
public int RVA;

/// <summary>
/// Pointers to the location of the raw data. Needed to allow phyical file alignment
/// beyond 4KB. PEBuilder doesn't support this today so that we
/// must store the RVA's and post-process the produced PE by patching the section
/// headers in the PE header.
/// </summary>
public int PointerToRawData;

/// <summary>
/// Maximum of virtual and physical size for each section.
/// </summary>
public int RawSize;

/// <summary>
/// Whether or not the section has been serialized - if the RVA, pointer to raw data,
/// and size have been set.
/// </summary>
public bool IsSerialized;
}

/// <summary>
/// Maximum of virtual and physical size for each section.
/// List of possible sections to emit into the output R2R executable in the order in which
/// they are expected to be serialized. Data (aside from name) is set during serialization.
/// </summary>
private int[] _sectionRawSizes;
private readonly SerializedSectionData[] _sectionData;

/// <summary>
/// R2R PE section builder &amp; relocator.
Expand Down Expand Up @@ -206,18 +221,13 @@ public R2RPEBuilder(
PEHeaderConstants.SectionAlignment);
}

ImmutableArray<Section>.Builder sectionListBuilder = ImmutableArray.CreateBuilder<Section>();
List<SerializedSectionData> sectionData = new List<SerializedSectionData>();
foreach (SectionInfo sectionInfo in _sectionBuilder.GetSections())
{
ILCompiler.PEWriter.Section builderSection = _sectionBuilder.FindSection(sectionInfo.SectionName);
Debug.Assert(builderSection != null);
sectionListBuilder.Add(new Section(builderSection.Name, builderSection.Characteristics));
sectionData.Add(new SerializedSectionData() { Name = sectionInfo.SectionName });
}

_sections = sectionListBuilder.ToImmutableArray();
_sectionRVAs = new int[_sections.Length];
_sectionPointerToRawData = new int[_sections.Length];
_sectionRawSizes = new int[_sections.Length];
_sectionData = sectionData.ToArray();
}

public void SetCorHeader(ISymbolNode symbol, int headerSize)
Expand Down Expand Up @@ -400,13 +410,17 @@ private void UpdateSectionRVAs(Stream outputStream)
16 * sizeof(long); // directory entries

int sectionHeaderOffset = DosHeaderSize + PESignatureSize + COFFHeaderSize + peHeaderSize;
int sectionCount = _sectionRVAs.Length;
int sectionCount = _sectionData.Length;
for (int sectionIndex = 0; sectionIndex < sectionCount; sectionIndex++)
{
SerializedSectionData section = _sectionData[sectionIndex];
if (!section.IsSerialized)
continue;

if (_customPESectionAlignment != 0)
{
// When _customPESectionAlignment is set, the physical and virtual sizes are the same
byte[] sizeBytes = BitConverter.GetBytes(_sectionRawSizes[sectionIndex]);
byte[] sizeBytes = BitConverter.GetBytes(section.RawSize);
Debug.Assert(sizeBytes.Length == sizeof(int));

// Update VirtualSize
Expand All @@ -424,23 +438,33 @@ private void UpdateSectionRVAs(Stream outputStream)
// Update RVAs
{
outputStream.Seek(sectionHeaderOffset + SectionHeaderSize * sectionIndex + SectionHeaderRVAOffset, SeekOrigin.Begin);
byte[] rvaBytes = BitConverter.GetBytes(_sectionRVAs[sectionIndex]);
byte[] rvaBytes = BitConverter.GetBytes(section.RVA);
Debug.Assert(rvaBytes.Length == sizeof(int));
outputStream.Write(rvaBytes, 0, rvaBytes.Length);
}

// Update pointer to raw data
{
outputStream.Seek(sectionHeaderOffset + SectionHeaderSize * sectionIndex + SectionHeaderPointerToRawDataOffset, SeekOrigin.Begin);
byte[] rawDataBytesBytes = BitConverter.GetBytes(_sectionPointerToRawData[sectionIndex]);
byte[] rawDataBytesBytes = BitConverter.GetBytes(section.PointerToRawData);
Debug.Assert(rawDataBytesBytes.Length == sizeof(int));
outputStream.Write(rawDataBytesBytes, 0, rawDataBytesBytes.Length);
}
}

// Patch SizeOfImage to point past the end of the last section
SerializedSectionData lastSection = null;
for (int i = sectionCount - 1; i >= 0; i--)
{
if (_sectionData[i].IsSerialized)
{
lastSection = _sectionData[i];
break;
}
}
Debug.Assert(lastSection != null);
outputStream.Seek(DosHeaderSize + PESignatureSize + COFFHeaderSize + OffsetOfSizeOfImage, SeekOrigin.Begin);
int sizeOfImage = AlignmentHelper.AlignUp(_sectionRVAs[sectionCount - 1] + _sectionRawSizes[sectionCount - 1], Header.SectionAlignment);
int sizeOfImage = AlignmentHelper.AlignUp(lastSection.RVA + lastSection.RawSize, Header.SectionAlignment);
byte[] sizeOfImageBytes = BitConverter.GetBytes(sizeOfImage);
Debug.Assert(sizeOfImageBytes.Length == sizeof(int));
outputStream.Write(sizeOfImageBytes, 0, sizeOfImageBytes.Length);
Expand Down Expand Up @@ -557,14 +581,21 @@ private int RelocateRVA(int rva)
/// </summary>
protected override ImmutableArray<Section> CreateSections()
{
return _sections;
ImmutableArray<Section>.Builder sectionListBuilder = ImmutableArray.CreateBuilder<Section>();
foreach (SectionInfo sectionInfo in _sectionBuilder.GetSections())
{
// Only include sections that have content.
if (!_sectionBuilder.HasContent(sectionInfo.SectionName))
continue;

sectionListBuilder.Add(new Section(sectionInfo.SectionName, sectionInfo.Characteristics));
}

return sectionListBuilder.ToImmutable();
}

/// <summary>
/// Output the section with a given name. For sections existent in the source MSIL PE file
/// (.text, optionally .rsrc and .reloc), we first copy the content of the input MSIL PE file
/// and then call the section serialization callback to emit the extra content after the input
/// section content.
/// Output the section with a given name.
/// </summary>
/// <param name="name">Section name</param>
/// <param name="location">RVA and file location where the section will be put</param>
Expand All @@ -574,18 +605,33 @@ protected override BlobBuilder SerializeSection(string name, SectionLocation loc
BlobBuilder sectionDataBuilder = null;
int sectionStartRva = location.RelativeVirtualAddress;

int outputSectionIndex = _sections.Length - 1;
while (outputSectionIndex >= 0 && _sections[outputSectionIndex].Name != name)
int outputSectionIndex = _sectionData.Length - 1;
while (outputSectionIndex >= 0 && _sectionData[outputSectionIndex].Name != name)
{
outputSectionIndex--;
}

if (outputSectionIndex < 0)
throw new ArgumentException($"Unknown section name: '{name}'", nameof(name));

Debug.Assert(_sectionBuilder.HasContent(name));
SerializedSectionData outputSection = _sectionData[outputSectionIndex];
SerializedSectionData previousSection = null;
for (int i = outputSectionIndex - 1; i >= 0; i--)
{
if (_sectionData[i].IsSerialized)
{
previousSection = _sectionData[i];
break;
}
}

int injectedPadding = 0;
if (_customPESectionAlignment != 0)
{
if (outputSectionIndex > 0)
if (previousSection is not null)
{
sectionStartRva = Math.Max(sectionStartRva, _sectionRVAs[outputSectionIndex - 1] + _sectionRawSizes[outputSectionIndex - 1]);
sectionStartRva = Math.Max(sectionStartRva, previousSection.RVA + previousSection.RawSize);
}

int newSectionStartRva = AlignmentHelper.AlignUp(sectionStartRva, _customPESectionAlignment);
Expand All @@ -603,13 +649,13 @@ protected override BlobBuilder SerializeSection(string name, SectionLocation loc
if (!_target.IsWindows)
{
const int RVAAlign = 1 << RVABitsToMatchFilePos;
if (outputSectionIndex > 0)
if (previousSection is not null)
{
sectionStartRva = Math.Max(sectionStartRva, _sectionRVAs[outputSectionIndex - 1] + _sectionRawSizes[outputSectionIndex - 1]);
sectionStartRva = Math.Max(sectionStartRva, previousSection.RVA + previousSection.RawSize);

// when assembly is stored in a singlefile bundle, an additional skew is introduced
// as the streams inside the bundle are not necessarily page aligned as we do not
// know the actual page size on the target system.
// as the streams inside the bundle are not necessarily page aligned as we do not
// know the actual page size on the target system.
// We may need one page gap of unused VA space before the next section starts.
// We will assume the page size is <= RVAAlign
sectionStartRva += RVAAlign;
Expand All @@ -622,36 +668,19 @@ protected override BlobBuilder SerializeSection(string name, SectionLocation loc
location = new SectionLocation(sectionStartRva, location.PointerToRawData);
}

if (outputSectionIndex >= 0)
{
_sectionRVAs[outputSectionIndex] = sectionStartRva;
_sectionPointerToRawData[outputSectionIndex] = location.PointerToRawData;
}
outputSection.RVA = sectionStartRva;
outputSection.PointerToRawData = location.PointerToRawData;

BlobBuilder extraData = _sectionBuilder.SerializeSection(name, location);
if (extraData != null)
{
if (sectionDataBuilder == null)
{
// See above - there's a bug due to which LinkSuffix to an empty BlobBuilder screws up the blob content.
sectionDataBuilder = extraData;
}
else
{
sectionDataBuilder.LinkSuffix(extraData);
}
}

// Make sure the section has at least 1 byte, otherwise the PE emitter goes mad,
// messes up the section map and corrups the output executable.
Debug.Assert(extraData != null);

Copilot AI May 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change replaces a runtime fallback that ensured a non-empty BlobBuilder with a Debug.Assert, which only triggers in debug builds. In a release build, if _sectionBuilder.SerializeSection unexpectedly returns null or an empty blob, this could lead to invalid output; consider explicitly handling or guarding against null/empty extraData in production.

Suggested change
Debug.Assert(extraData != null);
Debug.Assert(extraData != null);
if (extraData == null || extraData.Count == 0)
{
throw new InvalidOperationException($"SerializeSection returned a null or empty BlobBuilder for section '{name}'.");
}

Copilot uses AI. Check for mistakes.
if (sectionDataBuilder == null)
{
sectionDataBuilder = new BlobBuilder();
// See above - there's a bug due to which LinkSuffix to an empty BlobBuilder screws up the blob content.
sectionDataBuilder = extraData;
}

if (sectionDataBuilder.Count == 0)
else
{
sectionDataBuilder.WriteByte(0);
sectionDataBuilder.LinkSuffix(extraData);
}

int sectionRawSize = sectionDataBuilder.Count - injectedPadding;
Expand All @@ -664,15 +693,13 @@ protected override BlobBuilder SerializeSection(string name, SectionLocation loc
sectionRawSize = count;
}

if (outputSectionIndex >= 0)
{
_sectionRawSizes[outputSectionIndex] = sectionRawSize;
}
outputSection.RawSize = sectionRawSize;
outputSection.IsSerialized = true;

return sectionDataBuilder;
}
}

/// <summary>
/// Simple helper for filling in PE header information.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -944,5 +944,22 @@ public void RelocateOutputFile(
// Flush remaining PE file blocks after the last relocation
relocationHelper.CopyRestOfFile();
}

internal bool HasContent(string sectionName)
{
if (sectionName == R2RPEBuilder.ExportDataSectionName)
return _exportSymbols.Count > 0 && _dllNameForExportDirectoryTable != null;

if (sectionName == R2RPEBuilder.RelocSectionName)
{
return _sections.Any(
s => s.PlacedObjectDataToRelocate.Any(
d => d.Relocs.Any(
r => Relocation.GetFileRelocationType(r.RelocType) != RelocType.IMAGE_REL_BASED_ABSOLUTE)));
}

Section section = FindSection(sectionName);
return section != null && section.Content.Count > 0;
}
}
}
4 changes: 3 additions & 1 deletion src/coreclr/vm/peimagelayout.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ PEImageLayout* PEImageLayout::LoadConverted(PEImage* pOwner, bool disableMapping
_ASSERTE(!pOwner->IsFile() || !pFlat->HasReadyToRunHeader() || disableMapping);
#endif

if ((pFlat->HasReadyToRunHeader() && AllowR2RForImage(pOwner))
// If the image is R2R with native code (that is, not a component assembly of composite R2R) or has writeable sections,
// we need to actually load/map it into virtual addresses
if ((pFlat->HasReadyToRunHeader() && !pFlat->IsComponentAssembly() && AllowR2RForImage(pOwner))
|| pFlat->HasWriteableSections())
{
return new ConvertedImageLayout(pFlat, disableMapping);
Expand Down
Loading