authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-08-15 08:59:55+01:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-08-16 10:37:20+01:00
logede0017eb9e72cb5a799ad000ebe45cf326524d6
treeb003b53a823715fab390ab128702f4128fb4c001
parent507f2d0ab154fabe3d81c80e74aa82a59fff837b
signaturelock-open Commit is signed but in an unrecognized format.

Elf2: include the ehdr in the rodata segment

Although to my knowledge this is not strictly required by the format or by any OS, it is highly conventional, and not doing so can definitely break things in practice---including one of the standard library tests! To achieve this, we add all of the segment nodes *before* the ehdr, initializing rodata as a fixed "header" node in the `.elf` node, and then the `.ehdr` node goes within that rodata segment node.

1 files changed, 89 insertions(+), 72 deletions(-)

src/link/Elf2.zig+89-72
......@@ -3645,77 +3645,22 @@ fn initHeaders(
36453645
36463646 const entsize: struct { ph: u32, sh: u32 } = switch (class) {
36473647 .NONE, _ => unreachable,
3648 inline else => |ct_class| entsize: {
3649 const ElfN = ct_class.ElfN();
3650 elf.ni.ehdr = try elf.mf.addLastChildNode(gpa, elf.ni.elf, .{
3651 .size = @sizeOf(ElfN.Ehdr),
3652 .alignment = addr_align,
3653 .fixed = true,
3654 });
3655 elf.nodes.appendAssumeCapacity(.ehdr);
3656
3657 const ehdr: *ElfN.Ehdr = @ptrCast(@alignCast(elf.ni.ehdr.slice(&elf.mf)));
3658 ehdr.ident = .{
3659 .class = class,
3660 .data = data,
3661 .version = 1,
3662 .osabi = osabi,
3663 .abiversion = 0,
3664 };
3665 ehdr.type = @"type".toElf();
3666 ehdr.machine = machine.toElf();
3667 ehdr.version = 1;
3668 ehdr.entry = 0;
3669 ehdr.phoff = 0;
3670 ehdr.shoff = 0;
3671 ehdr.flags = switch (machine) {
3672 .LOONGARCH => .{ .loongarch = .{
3673 .base_abi_modifier = mod: {
3674 const cpu = comp.getTarget().cpu;
3675 if (cpu.has(.loongarch, .d)) break :mod .d;
3676 if (cpu.has(.loongarch, .f)) break :mod .f;
3677 break :mod .s;
3678 },
3679 .abi_extension = .base,
3680 .abi_version = 1,
3681 } },
3682 .SPARCV9 => .{ .sparc = .{
3683 .mm = .rmo,
3684 .ext = .{
3685 .@"32plus" = false,
3686 .sun_us1 = false,
3687 .hal_r1 = false,
3688 .sun_us3 = false,
3689 .le_data = false,
3690 },
3691 } },
3692 .X86_64 => .{ .int = 0 },
3693 .AARCH64, .PPC64, .RISCV => @panic(@tagName(machine)),
3694 };
3695 ehdr.ehsize = @sizeOf(ElfN.Ehdr);
3696 ehdr.phentsize = @sizeOf(ElfN.Phdr);
3697 ehdr.phnum = @min(phnum, std.elf.PN_XNUM);
3698 ehdr.shentsize = @sizeOf(ElfN.Shdr);
3699 ehdr.shnum = 1; // Only the null shdr initially---will be incremented by `addSection`
3700 ehdr.shstrndx = std.elf.SHN_UNDEF;
3701 if (elf.targetEndian() != native_endian) std.mem.byteSwapAllFields(ElfN.Ehdr, ehdr);
3702
3703 break :entsize .{ .ph = @sizeOf(ElfN.Phdr), .sh = @sizeOf(ElfN.Shdr) };
3648 inline else => |ct_class| .{
3649 .ph = @sizeOf(ct_class.ElfN().Phdr),
3650 .sh = @sizeOf(ct_class.ElfN().Shdr),
37043651 },
37053652 };
37063653
3707 elf.ni.shdr = try elf.mf.addLastChildNode(gpa, elf.ni.elf, .{
3708 .size = 1 * entsize.sh, // as above, only the null shdr initially
3709 .alignment = addr_align.max(node_block_align),
3710 .moved = true,
3711 .resized = true,
3712 });
3713 elf.nodes.appendAssumeCapacity(.shdr);
3714
3654 // We want to create the segment nodes *before* the ehdr, because the ehdr should go inside of
3655 // the rodata segment. Although to my knowledge neither ELF nor any ELF-based OS strictly
3656 // requires this, it is highly conventional and therefore sometimes relied upon.
37153657 if (@"type" != .REL) {
3716 elf.ni.rodata = try elf.mf.addLastChildNode(gpa, elf.ni.elf, .{
3658 elf.ni.rodata = try elf.mf.addOnlyChildNode(gpa, elf.ni.elf, .{
37173659 // Must be at least `addr_align` for `elf.ni.phdr` to be placed inside this node
37183660 .alignment = node_block_align.max(addr_align),
3661 // This node will contain the ehdr, which must be at the start of the ELF file, so this
3662 // node must itself be fixed.
3663 .fixed = true,
37193664 .moved = true,
37203665 .bubbles_moved = false,
37213666 });
......@@ -3781,6 +3726,79 @@ fn initHeaders(
37813726
37823727 elf.phdrs.items[phndx.gnu_stack] = .none;
37833728 }
3729
3730 switch (class) {
3731 .NONE, _ => unreachable,
3732 inline else => |ct_class| {
3733 const ElfN = ct_class.ElfN();
3734 // In loadable modules, the ehdr goes in the rodata segment, as described above.
3735 const parent_ni = switch (@"type") {
3736 .REL => elf.ni.elf,
3737 .DYN, .EXEC => elf.ni.rodata,
3738 };
3739 elf.ni.ehdr = try elf.mf.addFirstChildNode(gpa, parent_ni, .{
3740 .size = @sizeOf(ElfN.Ehdr),
3741 .alignment = addr_align,
3742 .fixed = true,
3743 });
3744 elf.nodes.appendAssumeCapacity(.ehdr);
3745
3746 const ehdr: *ElfN.Ehdr = @ptrCast(@alignCast(elf.ni.ehdr.slice(&elf.mf)));
3747 ehdr.ident = .{
3748 .class = class,
3749 .data = data,
3750 .version = 1,
3751 .osabi = osabi,
3752 .abiversion = 0,
3753 };
3754 ehdr.type = @"type".toElf();
3755 ehdr.machine = machine.toElf();
3756 ehdr.version = 1;
3757 ehdr.entry = 0;
3758 ehdr.phoff = 0;
3759 ehdr.shoff = 0;
3760 ehdr.flags = switch (machine) {
3761 .LOONGARCH => .{ .loongarch = .{
3762 .base_abi_modifier = mod: {
3763 const cpu = comp.getTarget().cpu;
3764 if (cpu.has(.loongarch, .d)) break :mod .d;
3765 if (cpu.has(.loongarch, .f)) break :mod .f;
3766 break :mod .s;
3767 },
3768 .abi_extension = .base,
3769 .abi_version = 1,
3770 } },
3771 .SPARCV9 => .{ .sparc = .{
3772 .mm = .rmo,
3773 .ext = .{
3774 .@"32plus" = false,
3775 .sun_us1 = false,
3776 .hal_r1 = false,
3777 .sun_us3 = false,
3778 .le_data = false,
3779 },
3780 } },
3781 .X86_64 => .{ .int = 0 },
3782 .AARCH64, .PPC64, .RISCV => @panic(@tagName(machine)),
3783 };
3784 ehdr.ehsize = @sizeOf(ElfN.Ehdr);
3785 ehdr.phentsize = @sizeOf(ElfN.Phdr);
3786 ehdr.phnum = @min(phnum, std.elf.PN_XNUM);
3787 ehdr.shentsize = @sizeOf(ElfN.Shdr);
3788 ehdr.shnum = 1; // Only the null shdr initially---will be incremented by `addSection`
3789 ehdr.shstrndx = std.elf.SHN_UNDEF;
3790 if (elf.targetEndian() != native_endian) std.mem.byteSwapAllFields(ElfN.Ehdr, ehdr);
3791 },
3792 }
3793
3794 elf.ni.shdr = try elf.mf.addLastChildNode(gpa, elf.ni.elf, .{
3795 .size = 1 * entsize.sh, // as above, only the null shdr initially
3796 .alignment = addr_align.max(node_block_align),
3797 .moved = true,
3798 .resized = true,
3799 });
3800 elf.nodes.appendAssumeCapacity(.shdr);
3801
37843802 switch (class) {
37853803 .NONE, _ => unreachable,
37863804 inline else => |ct_class| {
......@@ -3818,7 +3836,9 @@ fn initHeaders(
38183836 // actually `PT_NULL` for now, because we initialize `filesz` and `memsz` to zero.
38193837 // Any which end up non-empty will have their size populated (and their type set to
38203838 // `PT_LOAD`) by the segment virtual address space allocation logic.
3821 const phdr: []ElfN.Phdr = @ptrCast(@alignCast(elf.ni.phdr.slice(&elf.mf)));
3839 const phdr: []ElfN.Phdr = @ptrCast(@alignCast(
3840 elf.ni.phdr.slice(&elf.mf)[0 .. phnum * @sizeOf(ElfN.Phdr)],
3841 ));
38223842
38233843 const ph_phdr = &phdr[phndx.phdr];
38243844 ph_phdr.* = .{
......@@ -4900,14 +4920,11 @@ const PhdrSlice = union(std.elf.CLASS) {
49004920};
49014921fn phdrSlice(elf: *Elf) PhdrSlice {
49024922 assert(elf.ehdrType() != .REL);
4903 const slice = elf.ni.phdr.slice(&elf.mf);
49044923 return switch (elf.identClass()) {
49054924 .NONE, _ => unreachable,
4906 inline else => |class| @unionInit(
4907 PhdrSlice,
4908 @tagName(class),
4909 @ptrCast(@alignCast(slice)),
4910 ),
4925 inline else => |class| @unionInit(PhdrSlice, @tagName(class), @ptrCast(@alignCast(
4926 elf.ni.phdr.slice(&elf.mf)[0 .. elf.phdrs.items.len * @sizeOf(class.ElfN().Phdr)],
4927 ))),
49114928 };
49124929}
49134930