Implement a workaround for ld-prime hitting an assert on large addends#124721
Implement a workaround for ld-prime hitting an assert on large addends#124721filipnavara wants to merge 9 commits intodotnet:mainfrom
Conversation
We translate the IMAGE_REL_BASED_RELPTR32 relocation into ARM64_RELOC_SUBTRACTOR and ARM64_RELOC_UNSIGNED pair on ARM64. To emulate the behavior of PC relative relocation we bake the section-relative PC offset into the addend with negative sign. The ARM64_RELOC_SUBTRACTOR relocation then subtract the base address of the section and finally the ARM64_RELOC_UNSIGNED relocation adds the target symbol address. This works fine for addends that fit into signed 24-bit integer, but ld-prime hits an assert when the addend is larger. The workaround is to generate a temporary label at the offset of the relocated address and adjust the addend to be relative to that label. We reuse the last temporary label for following relocations in the same section until we hit another overflow. This way we minimize the number of temporary labels we need to generate while still satisfying the ld-prime requirement. Same logic applies to X86_64_RELOC_SUBTRACTOR + X86_64_RELOC_UNSIGNED pair for x64 targets.
|
Needs more work. I'll investigate the unit test failures first. |
|
I don't get the System.Linq.Expressions test failure on Xcode 26.2 anymore. According the log the compiler/linker version on CI is |
|
Going back to the drawing board now. |
|
@akoeplinger Is there some easy way to switch some CI lane to use newer Xcode? They should be available in the runner images, just not as default. |
|
I assume adding xcode-select to the path mentioned in https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md#xcode somewhere early in the build should suffice? |
Thanks, seems to pass the smoke test with Xcode 26.2. I'll probably open a separate PR to check what is the behavior just with the Xcode bump alone. |
|
Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it. |
There was a problem hiding this comment.
Pull request overview
This PR adds a workaround in the Mach-O object writer to avoid Apple ld-prime assertions when emitting IMAGE_REL_BASED_RELPTR32 as *_RELOC_SUBTRACTOR + *_RELOC_UNSIGNED with large addends, by introducing reusable per-section temporary labels to keep addends within the linker’s expected signed 20-bit range.
Changes:
- Track and emit per-section temporary labels to bound relocation addends for
IMAGE_REL_BASED_RELPTR32on ARM64 and x64.eh_frame. - Adjust Mach relocation emission to optionally reference a temporary label symbol (instead of the section base symbol) for SUBTRACTOR relocs.
- Modify
build.shto attempt switching Xcode versions on macOS.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/coreclr/tools/Common/Compiler/ObjectWriter/MachObjectWriter.cs | Adds temporary-label generation/reuse and uses label symbol indices to avoid ld-prime large-addend assertions. |
| build.sh | Adds macOS-only logic to switch Xcode via sudo xcode-select. |
| var temporaryLabelOffset = _sections[sectionIndex].TemporaryLabelOffset; | ||
| var temporaryLabelIndex = _sections[sectionIndex].TemporaryLabelIndex; | ||
| addend -= offset - temporaryLabelOffset; | ||
| if ((int)addend != (((int)addend << 12) >> 12)) | ||
| { | ||
| addend += offset - temporaryLabelOffset; | ||
| _temporaryLabels.Add(new SymbolDefinition(sectionIndex, offset, 0, false)); | ||
| _sections[sectionIndex].TemporaryLabelOffset = temporaryLabelOffset = offset; | ||
| _sections[sectionIndex].TemporaryLabelIndex = temporaryLabelIndex = (uint)_temporaryLabels.Count; | ||
| } |
There was a problem hiding this comment.
The signed-20-bit overflow check is done after casting addend to int. Since addend is a long and is adjusted by offset (also long), this cast can truncate for large section offsets/addends and make the overflow detection unreliable (potentially skipping label generation when it’s actually needed). Consider performing the range check using long arithmetic (e.g., compare against the signed 20-bit bounds or use a sign-extension check on long) before any narrowing conversions.
| // Abuse the addend to store the temporary label index. | ||
| addend = temporaryLabelIndex; |
There was a problem hiding this comment.
SymbolicRelocation.Addend is being repurposed to carry a temporary-label index (“Abuse the addend to store the temporary label index”). This makes the meaning of Addend context-dependent and couples EmitRelocation tightly to the later Mach relocation emission logic, which is easy to break during future refactors. Consider storing this index in a dedicated field/structure (or a side table keyed by relocation) so Addend consistently remains an addend.
| # switch to different Xcode version | ||
| if [[ "$OSTYPE" == "darwin"* ]]; then | ||
| if [[ -d "/Applications/Xcode_26.2.app" ]]; then | ||
| echo "Switching to Xcode 26.2 for build" | ||
| sudo xcode-select -s "/Applications/Xcode_26.2.app" |
There was a problem hiding this comment.
This change makes the repo-level build.sh attempt to switch the active Xcode via sudo xcode-select whenever a specific path exists. This has major operational and security implications: it mutates global machine state, requires elevated privileges/interactive sudo in many environments, and can break CI or local builds unexpectedly. Repo build scripts should not implicitly run privileged commands; prefer documenting the required Xcode version or allowing an opt-in environment variable/argument without sudo.
| # switch to different Xcode version | |
| if [[ "$OSTYPE" == "darwin"* ]]; then | |
| if [[ -d "/Applications/Xcode_26.2.app" ]]; then | |
| echo "Switching to Xcode 26.2 for build" | |
| sudo xcode-select -s "/Applications/Xcode_26.2.app" | |
| # do not implicitly change the active Xcode for the machine | |
| if [[ "$OSTYPE" == "darwin"* ]]; then | |
| if [[ -d "/Applications/Xcode_26.2.app" ]]; then | |
| echo "Xcode 26.2 is installed at /Applications/Xcode_26.2.app." | |
| echo "This build does not automatically switch the active Xcode." | |
| echo "If needed, select it explicitly before building:" | |
| echo " sudo xcode-select -s \"/Applications/Xcode_26.2.app/Contents/Developer\"" |
We translate the IMAGE_REL_BASED_RELPTR32 relocation into ARM64_RELOC_SUBTRACTOR and ARM64_RELOC_UNSIGNED pair on ARM64. To emulate the behavior of PC relative relocation we bake the section-relative PC offset into the addend with negative sign. The ARM64_RELOC_SUBTRACTOR relocation then subtract the base address of the section and finally the ARM64_RELOC_UNSIGNED relocation adds the target symbol address.
This works fine for addends that fit into signed 20-bit integer, but ld-prime hits an assert when the addend is larger. The workaround is to generate a temporary label at the offset of the relocated address and adjust the addend to be relative to that label. We reuse the last temporary label for following relocations in the same section until we hit another overflow. This way we minimize the number of temporary labels we need to generate while still satisfying the ld-prime requirement.
Same logic applies to X86_64_RELOC_SUBTRACTOR + X86_64_RELOC_UNSIGNED pair for x64 targets.
Fixes #119380