//! The main driver of the self-hosted COFF linker. base: link.File, image_base: u64, /// TODO this and minor_subsystem_version should be combined into one property and left as /// default or populated together. They should not be separate fields. major_subsystem_version: u16, minor_subsystem_version: u16, entry: link.File.OpenOptions.Entry, entry_addr: ?u32, module_definition_file: ?[]const u8, repro: bool, ptr_width: PtrWidth, page_size: u32, sections: std.MultiArrayList(Section) = .{}, data_directories: [coff_util.IMAGE_NUMBEROF_DIRECTORY_ENTRIES]coff_util.ImageDataDirectory, text_section_index: ?u16 = null, got_section_index: ?u16 = null, rdata_section_index: ?u16 = null, data_section_index: ?u16 = null, reloc_section_index: ?u16 = null, idata_section_index: ?u16 = null, locals: std.ArrayListUnmanaged(coff_util.Symbol) = .empty, globals: std.ArrayListUnmanaged(SymbolWithLoc) = .empty, resolver: std.StringHashMapUnmanaged(u32) = .empty, unresolved: std.AutoArrayHashMapUnmanaged(u32, bool) = .empty, need_got_table: std.AutoHashMapUnmanaged(u32, void) = .empty, locals_free_list: std.ArrayListUnmanaged(u32) = .empty, globals_free_list: std.ArrayListUnmanaged(u32) = .empty, strtab: StringTable = .{}, strtab_offset: ?u32 = null, temp_strtab: StringTable = .{}, got_table: TableSection(SymbolWithLoc) = .{}, /// A table of ImportTables partitioned by the library name. /// Key is an offset into the interning string table `temp_strtab`. import_tables: std.AutoArrayHashMapUnmanaged(u32, ImportTable) = .empty, got_table_count_dirty: bool = true, got_table_contents_dirty: bool = true, imports_count_dirty: bool = true, /// Table of tracked LazySymbols. lazy_syms: LazySymbolTable = .{}, /// Table of tracked `Nav`s. navs: NavTable = .{}, /// List of atoms that are either synthetic or map directly to the Zig source program. atoms: std.ArrayListUnmanaged(Atom) = .empty, /// Table of atoms indexed by the symbol index. atom_by_index_table: std.AutoHashMapUnmanaged(u32, Atom.Index) = .empty, uavs: UavTable = .{}, /// A table of relocations indexed by the owning them `Atom`. /// Note that once we refactor `Atom`'s lifetime and ownership rules, /// this will be a table indexed by index into the list of Atoms. relocs: RelocTable = .{}, /// A table of base relocations indexed by the owning them `Atom`. /// Note that once we refactor `Atom`'s lifetime and ownership rules, /// this will be a table indexed by index into the list of Atoms. base_relocs: BaseRelocationTable = .{}, /// Hot-code swapping state. hot_state: if (is_hot_update_compatible) HotUpdateState else struct {} = .{}, const is_hot_update_compatible = switch (builtin.target.os.tag) { .windows => true, else => false, }; const HotUpdateState = struct { /// Base address at which the process (image) got loaded. /// We need this info to correctly slide pointers when relocating. loaded_base_address: ?std.os.windows.HMODULE = null, }; const NavTable = std.AutoArrayHashMapUnmanaged(InternPool.Nav.Index, AvMetadata); const UavTable = std.AutoHashMapUnmanaged(InternPool.Index, AvMetadata); const RelocTable = std.AutoArrayHashMapUnmanaged(Atom.Index, std.ArrayListUnmanaged(Relocation)); const BaseRelocationTable = std.AutoArrayHashMapUnmanaged(Atom.Index, std.ArrayListUnmanaged(u32)); const default_file_alignment: u16 = 0x200; const default_size_of_stack_reserve: u32 = 0x1000000; const default_size_of_stack_commit: u32 = 0x1000; const default_size_of_heap_reserve: u32 = 0x100000; const default_size_of_heap_commit: u32 = 0x1000; const Section = struct { header: coff_util.SectionHeader, last_atom_index: ?Atom.Index = null, /// A list of atoms that have surplus capacity. This list can have false /// positives, as functions grow and shrink over time, only sometimes being added /// or removed from the freelist. /// /// An atom has surplus capacity when its overcapacity value is greater than /// padToIdeal(minimum_atom_size). That is, when it has so /// much extra capacity, that we could fit a small new symbol in it, itself with /// ideal_capacity or more. /// /// Ideal capacity is defined by size + (size / ideal_factor). /// /// Overcapacity is measured by actual_capacity - ideal_capacity. Note that /// overcapacity can be negative. A simple way to have negative overcapacity is to /// allocate a fresh atom, which will have ideal capacity, and then grow it /// by 1 byte. It will then have -1 overcapacity. free_list: std.ArrayListUnmanaged(Atom.Index) = .empty, }; const LazySymbolTable = std.AutoArrayHashMapUnmanaged(InternPool.Index, LazySymbolMetadata); const LazySymbolMetadata = struct { const State = enum { unused, pending_flush, flushed }; text_atom: Atom.Index = undefined, rdata_atom: Atom.Index = undefined, text_state: State = .unused, rdata_state: State = .unused, }; const AvMetadata = struct { atom: Atom.Index, section: u16, /// A list of all exports aliases of this Decl. exports: std.ArrayListUnmanaged(u32) = .empty, fn deinit(m: *AvMetadata, allocator: Allocator) void { m.exports.deinit(allocator); } fn getExport(m: AvMetadata, coff: *const Coff, name: []const u8) ?u32 { for (m.exports.items) |exp| { if (mem.eql(u8, name, coff.getSymbolName(.{ .sym_index = exp, .file = null, }))) return exp; } return null; } fn getExportPtr(m: *AvMetadata, coff: *Coff, name: []const u8) ?*u32 { for (m.exports.items) |*exp| { if (mem.eql(u8, name, coff.getSymbolName(.{ .sym_index = exp.*, .file = null, }))) return exp; } return null; } }; pub const PtrWidth = enum { p32, p64, /// Size in bytes. pub fn size(pw: PtrWidth) u4 { return switch (pw) { .p32 => 4, .p64 => 8, }; } }; pub const SymbolWithLoc = struct { // Index into the respective symbol table. sym_index: u32, // null means it's a synthetic global or Zig source. file: ?u32 = null, pub fn eql(this: SymbolWithLoc, other: SymbolWithLoc) bool { if (this.file == null and other.file == null) { return this.sym_index == other.sym_index; } if (this.file != null and other.file != null) { return this.sym_index == other.sym_index and this.file.? == other.file.?; } return false; } }; /// When allocating, the ideal_capacity is calculated by /// actual_capacity + (actual_capacity / ideal_factor) const ideal_factor = 3; /// In order for a slice of bytes to be considered eligible to keep metadata pointing at /// it as a possible place to put new symbols, it must have enough room for this many bytes /// (plus extra for reserved capacity). const minimum_text_block_size = 64; pub const min_text_capacity = padToIdeal(minimum_text_block_size); pub fn createEmpty( arena: Allocator, comp: *Compilation, emit: Path, options: link.File.OpenOptions, ) !*Coff { const target = &comp.root_mod.resolved_target.result; assert(target.ofmt == .coff); const optimize_mode = comp.root_mod.optimize_mode; const output_mode = comp.config.output_mode; const link_mode = comp.config.link_mode; const use_llvm = comp.config.use_llvm; const ptr_width: PtrWidth = switch (target.ptrBitWidth()) { 0...32 => .p32, 33...64 => .p64, else => return error.UnsupportedCOFFArchitecture, }; const page_size: u32 = switch (target.cpu.arch) { else => 0x1000, }; const coff = try arena.create(Coff); coff.* = .{ .base = .{ .tag = .coff, .comp = comp, .emit = emit, .zcu_object_basename = if (use_llvm) try std.fmt.allocPrint(arena, "{s}_zcu.obj", .{fs.path.stem(emit.sub_path)}) else null, .stack_size = options.stack_size orelse 16777216, .gc_sections = options.gc_sections orelse (optimize_mode != .Debug), .print_gc_sections = options.print_gc_sections, .allow_shlib_undefined = options.allow_shlib_undefined orelse false, .file = null, .build_id = options.build_id, }, .ptr_width = ptr_width, .page_size = page_size, .data_directories = [1]coff_util.ImageDataDirectory{.{ .virtual_address = 0, .size = 0, }} ** coff_util.IMAGE_NUMBEROF_DIRECTORY_ENTRIES, .image_base = options.image_base orelse switch (output_mode) { .Exe => switch (target.cpu.arch) { .aarch64, .x86_64 => 0x140000000, .thumb, .x86 => 0x400000, else => unreachable, }, .Lib => switch (target.cpu.arch) { .aarch64, .x86_64 => 0x180000000, .thumb, .x86 => 0x10000000, else => unreachable, }, .Obj => 0, }, .entry = options.entry, .major_subsystem_version = options.major_subsystem_version orelse 6, .minor_subsystem_version = options.minor_subsystem_version orelse 0, .entry_addr = math.cast(u32, options.entry_addr orelse 0) orelse return error.EntryAddressTooBig, .module_definition_file = options.module_definition_file, .repro = options.repro, }; errdefer coff.base.destroy(); coff.base.file = try emit.root_dir.handle.createFile(emit.sub_path, .{ .truncate = true, .read = true, .mode = link.File.determineMode(output_mode, link_mode), }); const gpa = comp.gpa; try coff.strtab.buffer.ensureUnusedCapacity(gpa, @sizeOf(u32)); coff.strtab.buffer.appendNTimesAssumeCapacity(0, @sizeOf(u32)); try coff.temp_strtab.buffer.append(gpa, 0); // Index 0 is always a null symbol. try coff.locals.append(gpa, .{ .name = [_]u8{0} ** 8, .value = 0, .section_number = .UNDEFINED, .type = .{ .base_type = .NULL, .complex_type = .NULL }, .storage_class = .NULL, .number_of_aux_symbols = 0, }); if (coff.text_section_index == null) { const file_size: u32 = @intCast(options.program_code_size_hint); coff.text_section_index = try coff.allocateSection(".text", file_size, .{ .CNT_CODE = 1, .MEM_EXECUTE = 1, .MEM_READ = 1, }); } if (coff.got_section_index == null) { const file_size = @as(u32, @intCast(options.symbol_count_hint)) * coff.ptr_width.size(); coff.got_section_index = try coff.allocateSection(".got", file_size, .{ .CNT_INITIALIZED_DATA = 1, .MEM_READ = 1, }); } if (coff.rdata_section_index == null) { const file_size: u32 = coff.page_size; coff.rdata_section_index = try coff.allocateSection(".rdata", file_size, .{ .CNT_INITIALIZED_DATA = 1, .MEM_READ = 1, }); } if (coff.data_section_index == null) { const file_size: u32 = coff.page_size; coff.data_section_index = try coff.allocateSection(".data", file_size, .{ .CNT_INITIALIZED_DATA = 1, .MEM_READ = 1, .MEM_WRITE = 1, }); } if (coff.idata_section_index == null) { const file_size = @as(u32, @intCast(options.symbol_count_hint)) * coff.ptr_width.size(); coff.idata_section_index = try coff.allocateSection(".idata", file_size, .{ .CNT_INITIALIZED_DATA = 1, .MEM_READ = 1, }); } if (coff.reloc_section_index == null) { const file_size = @as(u32, @intCast(options.symbol_count_hint)) * @sizeOf(coff_util.BaseRelocation); coff.reloc_section_index = try coff.allocateSection(".reloc", file_size, .{ .CNT_INITIALIZED_DATA = 1, .MEM_DISCARDABLE = 1, .MEM_READ = 1, }); } if (coff.strtab_offset == null) { const file_size = @as(u32, @intCast(coff.strtab.buffer.items.len)); coff.strtab_offset = coff.findFreeSpace(file_size, @alignOf(u32)); // 4bytes aligned seems like a good idea here log.debug("found strtab free space 0x{x} to 0x{x}", .{ coff.strtab_offset.?, coff.strtab_offset.? + file_size }); } { // We need to find out what the max file offset is according to section headers. // Otherwise, we may end up with an COFF binary with file size not matching the final section's // offset + it's filesize. // TODO I don't like this here one bit var max_file_offset: u64 = 0; for (coff.sections.items(.header)) |header| { if (header.pointer_to_raw_data + header.size_of_raw_data > max_file_offset) { max_file_offset = header.pointer_to_raw_data + header.size_of_raw_data; } } try coff.pwriteAll(&[_]u8{0}, max_file_offset); } return coff; } pub fn open( arena: Allocator, comp: *Compilation, emit: Path, options: link.File.OpenOptions, ) !*Coff { // TODO: restore saved linker state, don't truncate the file, and // participate in incremental compilation. return createEmpty(arena, comp, emit, options); } pub fn deinit(coff: *Coff) void { const gpa = coff.base.comp.gpa; for (coff.sections.items(.free_list)) |*free_list| { free_list.deinit(gpa); } coff.sections.deinit(gpa); coff.atoms.deinit(gpa); coff.locals.deinit(gpa); coff.globals.deinit(gpa); { var it = coff.resolver.keyIterator(); while (it.next()) |key_ptr| { gpa.free(key_ptr.*); } coff.resolver.deinit(gpa); } coff.unresolved.deinit(gpa); coff.need_got_table.deinit(gpa); coff.locals_free_list.deinit(gpa); coff.globals_free_list.deinit(gpa); coff.strtab.deinit(gpa); coff.temp_strtab.deinit(gpa); coff.got_table.deinit(gpa); for (coff.import_tables.values()) |*itab| { itab.deinit(gpa); } coff.import_tables.deinit(gpa); coff.lazy_syms.deinit(gpa); for (coff.navs.values()) |*metadata| { metadata.deinit(gpa); } coff.navs.deinit(gpa); coff.atom_by_index_table.deinit(gpa); { var it = coff.uavs.iterator(); while (it.next()) |entry| { entry.value_ptr.exports.deinit(gpa); } coff.uavs.deinit(gpa); } for (coff.relocs.values()) |*relocs| { relocs.deinit(gpa); } coff.relocs.deinit(gpa); for (coff.base_relocs.values()) |*relocs| { relocs.deinit(gpa); } coff.base_relocs.deinit(gpa); } fn allocateSection(coff: *Coff, name: []const u8, size: u32, flags: coff_util.SectionHeaderFlags) !u16 { const index = @as(u16, @intCast(coff.sections.slice().len)); const off = coff.findFreeSpace(size, default_file_alignment); // Memory is always allocated in sequence // TODO: investigate if we can allocate .text last; this way it would never need to grow in memory! const vaddr = blk: { if (index == 0) break :blk coff.page_size; const prev_header = coff.sections.items(.header)[index - 1]; break :blk mem.alignForward(u32, prev_header.virtual_address + prev_header.virtual_size, coff.page_size); }; // We commit more memory than needed upfront so that we don't have to reallocate too soon. const memsz = mem.alignForward(u32, size, coff.page_size) * 100; log.debug("found {s} free space 0x{x} to 0x{x} (0x{x} - 0x{x})", .{ name, off, off + size, vaddr, vaddr + size, }); var header = coff_util.SectionHeader{ .name = undefined, .virtual_size = memsz, .virtual_address = vaddr, .size_of_raw_data = size, .pointer_to_raw_data = off, .pointer_to_relocations = 0, .pointer_to_linenumbers = 0, .number_of_relocations = 0, .number_of_linenumbers = 0, .flags = flags, }; const gpa = coff.base.comp.gpa; try coff.setSectionName(&header, name); try coff.sections.append(gpa, .{ .header = header }); return index; } fn growSection(coff: *Coff, sect_id: u32, needed_size: u32) !void { const header = &coff.sections.items(.header)[sect_id]; const maybe_last_atom_index = coff.sections.items(.last_atom_index)[sect_id]; const sect_capacity = coff.allocatedSize(header.pointer_to_raw_data); if (needed_size > sect_capacity) { const new_offset = coff.findFreeSpace(needed_size, default_file_alignment); const current_size = if (maybe_last_atom_index) |last_atom_index| blk: { const last_atom = coff.getAtom(last_atom_index); const sym = last_atom.getSymbol(coff); break :blk (sym.value + last_atom.size) - header.virtual_address; } else 0; log.debug("moving {s} from 0x{x} to 0x{x}", .{ coff.getSectionName(header), header.pointer_to_raw_data, new_offset, }); const amt = try coff.base.file.?.copyRangeAll( header.pointer_to_raw_data, coff.base.file.?, new_offset, current_size, ); if (amt != current_size) return error.InputOutput; header.pointer_to_raw_data = new_offset; } const sect_vm_capacity = coff.allocatedVirtualSize(header.virtual_address); if (needed_size > sect_vm_capacity) { coff.markRelocsDirtyByAddress(header.virtual_address + header.virtual_size); try coff.growSectionVirtualMemory(sect_id, needed_size); } header.virtual_size = @max(header.virtual_size, needed_size); header.size_of_raw_data = needed_size; } fn growSectionVirtualMemory(coff: *Coff, sect_id: u32, needed_size: u32) !void { const header = &coff.sections.items(.header)[sect_id]; const increased_size = padToIdeal(needed_size); const old_aligned_end = header.virtual_address + mem.alignForward(u32, header.virtual_size, coff.page_size); const new_aligned_end = header.virtual_address + mem.alignForward(u32, increased_size, coff.page_size); const diff = new_aligned_end - old_aligned_end; log.debug("growing {s} in virtual memory by {x}", .{ coff.getSectionName(header), diff }); // TODO: enforce order by increasing VM addresses in coff.sections container. // This is required by the loader anyhow as far as I can tell. for (coff.sections.items(.header)[sect_id + 1 ..], 0..) |*next_header, next_sect_id| { const maybe_last_atom_index = coff.sections.items(.last_atom_index)[sect_id + 1 + next_sect_id]; next_header.virtual_address += diff; if (maybe_last_atom_index) |last_atom_index| { var atom_index = last_atom_index; while (true) { const atom = coff.getAtom(atom_index); const sym = atom.getSymbolPtr(coff); sym.value += diff; if (atom.prev_index) |prev_index| { atom_index = prev_index; } else break; } } } header.virtual_size = increased_size; } fn allocateAtom(coff: *Coff, atom_index: Atom.Index, new_atom_size: u32, alignment: u32) !u32 { const tracy = trace(@src()); defer tracy.end(); const atom = coff.getAtom(atom_index); const sect_id = @intFromEnum(atom.getSymbol(coff).section_number) - 1; const header = &coff.sections.items(.header)[sect_id]; const free_list = &coff.sections.items(.free_list)[sect_id]; const maybe_last_atom_index = &coff.sections.items(.last_atom_index)[sect_id]; const new_atom_ideal_capacity = if (header.isCode()) padToIdeal(new_atom_size) else new_atom_size; // We use these to indicate our intention to update metadata, placing the new atom, // and possibly removing a free list node. // It would be simpler to do it inside the for loop below, but that would cause a // problem if an error was returned later in the function. So this action // is actually carried out at the end of the function, when errors are no longer possible. var atom_placement: ?Atom.Index = null; var free_list_removal: ?usize = null; // First we look for an appropriately sized free list node. // The list is unordered. We'll just take the first thing that works. const vaddr = blk: { var i: usize = 0; while (i < free_list.items.len) { const big_atom_index = free_list.items[i]; const big_atom = coff.getAtom(big_atom_index); // We now have a pointer to a live atom that has too much capacity. // Is it enough that we could fit this new atom? const sym = big_atom.getSymbol(coff); const capacity = big_atom.capacity(coff); const ideal_capacity = if (header.isCode()) padToIdeal(capacity) else capacity; const ideal_capacity_end_vaddr = math.add(u32, sym.value, ideal_capacity) catch ideal_capacity; const capacity_end_vaddr = sym.value + capacity; const new_start_vaddr_unaligned = capacity_end_vaddr - new_atom_ideal_capacity; const new_start_vaddr = mem.alignBackward(u32, new_start_vaddr_unaligned, alignment); if (new_start_vaddr < ideal_capacity_end_vaddr) { // Additional bookkeeping here to notice if this free list node // should be deleted because the atom that it points to has grown to take up // more of the extra capacity. if (!big_atom.freeListEligible(coff)) { _ = free_list.swapRemove(i); } else { i += 1; } continue; } // At this point we know that we will place the new atom here. But the // remaining question is whether there is still yet enough capacity left // over for there to still be a free list node. const remaining_capacity = new_start_vaddr - ideal_capacity_end_vaddr; const keep_free_list_node = remaining_capacity >= min_text_capacity; // Set up the metadata to be updated, after errors are no longer possible. atom_placement = big_atom_index; if (!keep_free_list_node) { free_list_removal = i; } break :blk new_start_vaddr; } else if (maybe_last_atom_index.*) |last_index| { const last = coff.getAtom(last_index); const last_symbol = last.getSymbol(coff); const ideal_capacity = if (header.isCode()) padToIdeal(last.size) else last.size; const ideal_capacity_end_vaddr = last_symbol.value + ideal_capacity; const new_start_vaddr = mem.alignForward(u32, ideal_capacity_end_vaddr, alignment); atom_placement = last_index; break :blk new_start_vaddr; } else { break :blk mem.alignForward(u32, header.virtual_address, alignment); } }; const expand_section = if (atom_placement) |placement_index| coff.getAtom(placement_index).next_index == null else true; if (expand_section) { const needed_size: u32 = (vaddr + new_atom_size) - header.virtual_address; try coff.growSection(sect_id, needed_size); maybe_last_atom_index.* = atom_index; } coff.getAtomPtr(atom_index).size = new_atom_size; if (atom.prev_index) |prev_index| { const prev = coff.getAtomPtr(prev_index); prev.next_index = atom.next_index; } if (atom.next_index) |next_index| { const next = coff.getAtomPtr(next_index); next.prev_index = atom.prev_index; } if (atom_placement) |big_atom_index| { const big_atom = coff.getAtomPtr(big_atom_index); const atom_ptr = coff.getAtomPtr(atom_index); atom_ptr.prev_index = big_atom_index; atom_ptr.next_index = big_atom.next_index; big_atom.next_index = atom_index; } else { const atom_ptr = coff.getAtomPtr(atom_index); atom_ptr.prev_index = null; atom_ptr.next_index = null; } if (free_list_removal) |i| { _ = free_list.swapRemove(i); } return vaddr; } pub fn allocateSymbol(coff: *Coff) !u32 { const gpa = coff.base.comp.gpa; try coff.locals.ensureUnusedCapacity(gpa, 1); const index = blk: { if (coff.locals_free_list.pop()) |index| { log.debug(" (reusing symbol index {d})", .{index}); break :blk index; } else { log.debug(" (allocating symbol index {d})", .{coff.locals.items.len}); const index = @as(u32, @intCast(coff.locals.items.len)); _ = coff.locals.addOneAssumeCapacity(); break :blk index; } }; coff.locals.items[index] = .{ .name = [_]u8{0} ** 8, .value = 0, .section_number = .UNDEFINED, .type = .{ .base_type = .NULL, .complex_type = .NULL }, .storage_class = .NULL, .number_of_aux_symbols = 0, }; return index; } fn allocateGlobal(coff: *Coff) !u32 { const gpa = coff.base.comp.gpa; try coff.globals.ensureUnusedCapacity(gpa, 1); const index = blk: { if (coff.globals_free_list.pop()) |index| { log.debug(" (reusing global index {d})", .{index}); break :blk index; } else { log.debug(" (allocating global index {d})", .{coff.globals.items.len}); const index = @as(u32, @intCast(coff.globals.items.len)); _ = coff.globals.addOneAssumeCapacity(); break :blk index; } }; coff.globals.items[index] = .{ .sym_index = 0, .file = null, }; return index; } fn addGotEntry(coff: *Coff, target: SymbolWithLoc) !void { const gpa = coff.base.comp.gpa; if (coff.got_table.lookup.contains(target)) return; const got_index = try coff.got_table.allocateEntry(gpa, target); try coff.writeOffsetTableEntry(got_index); coff.got_table_count_dirty = true; coff.markRelocsDirtyByTarget(target); } pub fn createAtom(coff: *Coff) !Atom.Index { const gpa = coff.base.comp.gpa; const atom_index = @as(Atom.Index, @intCast(coff.atoms.items.len)); const atom = try coff.atoms.addOne(gpa); const sym_index = try coff.allocateSymbol(); try coff.atom_by_index_table.putNoClobber(gpa, sym_index, atom_index); atom.* = .{ .sym_index = sym_index, .file = null, .size = 0, .prev_index = null, .next_index = null, }; log.debug("creating ATOM(%{d}) at index {d}", .{ sym_index, atom_index }); return atom_index; } fn growAtom(coff: *Coff, atom_index: Atom.Index, new_atom_size: u32, alignment: u32) !u32 { const atom = coff.getAtom(atom_index); const sym = atom.getSymbol(coff); const align_ok = mem.alignBackward(u32, sym.value, alignment) == sym.value; const need_realloc = !align_ok or new_atom_size > atom.capacity(coff); if (!need_realloc) return sym.value; return coff.allocateAtom(atom_index, new_atom_size, alignment); } fn shrinkAtom(coff: *Coff, atom_index: Atom.Index, new_block_size: u32) void { _ = coff; _ = atom_index; _ = new_block_size; // TODO check the new capacity, and if it crosses the size threshold into a big enough // capacity, insert a free list node for it. } fn writeAtom(coff: *Coff, atom_index: Atom.Index, code: []u8, resolve_relocs: bool) !void { const atom = coff.getAtom(atom_index); const sym = atom.getSymbol(coff); const section = coff.sections.get(@intFromEnum(sym.section_number) - 1); const file_offset = section.header.pointer_to_raw_data + sym.value - section.header.virtual_address; log.debug("writing atom for symbol {s} at file offset 0x{x} to 0x{x}", .{ atom.getName(coff), file_offset, file_offset + code.len, }); const gpa = coff.base.comp.gpa; // Gather relocs which can be resolved. // We need to do this as we will be applying different slide values depending // if we are running in hot-code swapping mode or not. // TODO: how crazy would it be to try and apply the actual image base of the loaded // process for the in-file values rather than the Windows defaults? var relocs = std.ArrayList(*Relocation).init(gpa); defer relocs.deinit(); if (resolve_relocs) { if (coff.relocs.getPtr(atom_index)) |rels| { try relocs.ensureTotalCapacityPrecise(rels.items.len); for (rels.items) |*reloc| { if (reloc.isResolvable(coff) and reloc.dirty) { relocs.appendAssumeCapacity(reloc); } } } } if (is_hot_update_compatible) { if (coff.base.child_pid) |handle| { const slide = @intFromPtr(coff.hot_state.loaded_base_address.?); const mem_code = try gpa.dupe(u8, code); defer gpa.free(mem_code); coff.resolveRelocs(atom_index, relocs.items, mem_code, slide); const vaddr = sym.value + slide; const pvaddr = @as(*anyopaque, @ptrFromInt(vaddr)); log.debug("writing to memory at address {x}", .{vaddr}); if (build_options.enable_logging) { try debugMem(gpa, handle, pvaddr, mem_code); } if (section.header.flags.MEM_WRITE == 0) { writeMemProtected(handle, pvaddr, mem_code) catch |err| { log.warn("writing to protected memory failed with error: {s}", .{@errorName(err)}); }; } else { writeMem(handle, pvaddr, mem_code) catch |err| { log.warn("writing to protected memory failed with error: {s}", .{@errorName(err)}); }; } } } if (resolve_relocs) { coff.resolveRelocs(atom_index, relocs.items, code, coff.image_base); } try coff.pwriteAll(code, file_offset); if (resolve_relocs) { // Now we can mark the relocs as resolved. while (relocs.pop()) |reloc| { reloc.dirty = false; } } } fn debugMem(allocator: Allocator, handle: std.process.Child.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { const buffer = try allocator.alloc(u8, code.len); defer allocator.free(buffer); const memread = try std.os.windows.ReadProcessMemory(handle, pvaddr, buffer); log.debug("to write: {x}", .{code}); log.debug("in memory: {x}", .{memread}); } fn writeMemProtected(handle: std.process.Child.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { const old_prot = try std.os.windows.VirtualProtectEx(handle, pvaddr, code.len, std.os.windows.PAGE_EXECUTE_WRITECOPY); try writeMem(handle, pvaddr, code); // TODO: We can probably just set the pages writeable and leave it at that without having to restore the attributes. // For that though, we want to track which page has already been modified. _ = try std.os.windows.VirtualProtectEx(handle, pvaddr, code.len, old_prot); } fn writeMem(handle: std.process.Child.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { const amt = try std.os.windows.WriteProcessMemory(handle, pvaddr, code); if (amt != code.len) return error.InputOutput; } fn writeOffsetTableEntry(coff: *Coff, index: usize) !void { const sect_id = coff.got_section_index.?; if (coff.got_table_count_dirty) { const needed_size: u32 = @intCast(coff.got_table.entries.items.len * coff.ptr_width.size()); try coff.growSection(sect_id, needed_size); coff.got_table_count_dirty = false; } const header = &coff.sections.items(.header)[sect_id]; const entry = coff.got_table.entries.items[index]; const entry_value = coff.getSymbol(entry).value; const entry_offset = index * coff.ptr_width.size(); const file_offset = header.pointer_to_raw_data + entry_offset; const vmaddr = header.virtual_address + entry_offset; log.debug("writing GOT entry {d}: @{x} => {x}", .{ index, vmaddr, entry_value + coff.image_base }); switch (coff.ptr_width) { .p32 => { var buf: [4]u8 = undefined; mem.writeInt(u32, &buf, @intCast(entry_value + coff.image_base), .little); try coff.base.file.?.pwriteAll(&buf, file_offset); }, .p64 => { var buf: [8]u8 = undefined; mem.writeInt(u64, &buf, entry_value + coff.image_base, .little); try coff.base.file.?.pwriteAll(&buf, file_offset); }, } if (is_hot_update_compatible) { if (coff.base.child_pid) |handle| { const gpa = coff.base.comp.gpa; const slide = @intFromPtr(coff.hot_state.loaded_base_address.?); const actual_vmaddr = vmaddr + slide; const pvaddr = @as(*anyopaque, @ptrFromInt(actual_vmaddr)); log.debug("writing GOT entry to memory at address {x}", .{actual_vmaddr}); if (build_options.enable_logging) { switch (coff.ptr_width) { .p32 => { var buf: [4]u8 = undefined; try debugMem(gpa, handle, pvaddr, &buf); }, .p64 => { var buf: [8]u8 = undefined; try debugMem(gpa, handle, pvaddr, &buf); }, } } switch (coff.ptr_width) { .p32 => { var buf: [4]u8 = undefined; mem.writeInt(u32, &buf, @as(u32, @intCast(entry_value + slide)), .little); writeMem(handle, pvaddr, &buf) catch |err| { log.warn("writing to protected memory failed with error: {s}", .{@errorName(err)}); }; }, .p64 => { var buf: [8]u8 = undefined; mem.writeInt(u64, &buf, entry_value + slide, .little); writeMem(handle, pvaddr, &buf) catch |err| { log.warn("writing to protected memory failed with error: {s}", .{@errorName(err)}); }; }, } } } } fn markRelocsDirtyByTarget(coff: *Coff, target: SymbolWithLoc) void { if (!coff.base.comp.incremental) return; // TODO: reverse-lookup might come in handy here for (coff.relocs.values()) |*relocs| { for (relocs.items) |*reloc| { if (!reloc.target.eql(target)) continue; reloc.dirty = true; } } } fn markRelocsDirtyByAddress(coff: *Coff, addr: u32) void { if (!coff.base.comp.incremental) return; const got_moved = blk: { const sect_id = coff.got_section_index orelse break :blk false; break :blk coff.sections.items(.header)[sect_id].virtual_address >= addr; }; // TODO: dirty relocations targeting import table if that got moved in memory for (coff.relocs.values()) |*relocs| { for (relocs.items) |*reloc| { if (reloc.isGotIndirection()) { reloc.dirty = reloc.dirty or got_moved; } else { const target_vaddr = reloc.getTargetAddress(coff) orelse continue; if (target_vaddr >= addr) reloc.dirty = true; } } } // TODO: dirty only really affected GOT cells for (coff.got_table.entries.items) |entry| { const target_addr = coff.getSymbol(entry).value; if (target_addr >= addr) { coff.got_table_contents_dirty = true; break; } } } fn resolveRelocs(coff: *Coff, atom_index: Atom.Index, relocs: []const *const Relocation, code: []u8, image_base: u64) void { log.debug("relocating '{s}'", .{coff.getAtom(atom_index).getName(coff)}); for (relocs) |reloc| { reloc.resolve(atom_index, code, image_base, coff); } } pub fn ptraceAttach(coff: *Coff, handle: std.process.Child.Id) !void { if (!is_hot_update_compatible) return; log.debug("attaching to process with handle {*}", .{handle}); coff.hot_state.loaded_base_address = std.os.windows.ProcessBaseAddress(handle) catch |err| { log.warn("failed to get base address for the process with error: {s}", .{@errorName(err)}); return; }; } pub fn ptraceDetach(coff: *Coff, handle: std.process.Child.Id) void { if (!is_hot_update_compatible) return; log.debug("detaching from process with handle {*}", .{handle}); coff.hot_state.loaded_base_address = null; } fn freeAtom(coff: *Coff, atom_index: Atom.Index) void { log.debug("freeAtom {d}", .{atom_index}); const gpa = coff.base.comp.gpa; // Remove any relocs and base relocs associated with this Atom coff.freeRelocations(atom_index); const atom = coff.getAtom(atom_index); const sym = atom.getSymbol(coff); const sect_id = @intFromEnum(sym.section_number) - 1; const free_list = &coff.sections.items(.free_list)[sect_id]; var already_have_free_list_node = false; { var i: usize = 0; // TODO turn free_list into a hash map while (i < free_list.items.len) { if (free_list.items[i] == atom_index) { _ = free_list.swapRemove(i); continue; } if (free_list.items[i] == atom.prev_index) { already_have_free_list_node = true; } i += 1; } } const maybe_last_atom_index = &coff.sections.items(.last_atom_index)[sect_id]; if (maybe_last_atom_index.*) |last_atom_index| { if (last_atom_index == atom_index) { if (atom.prev_index) |prev_index| { // TODO shrink the section size here maybe_last_atom_index.* = prev_index; } else { maybe_last_atom_index.* = null; } } } if (atom.prev_index) |prev_index| { const prev = coff.getAtomPtr(prev_index); prev.next_index = atom.next_index; if (!already_have_free_list_node and prev.*.freeListEligible(coff)) { // The free list is heuristics, it doesn't have to be perfect, so we can // ignore the OOM here. free_list.append(gpa, prev_index) catch {}; } } else { coff.getAtomPtr(atom_index).prev_index = null; } if (atom.next_index) |next_index| { coff.getAtomPtr(next_index).prev_index = atom.prev_index; } else { coff.getAtomPtr(atom_index).next_index = null; } // Appending to free lists is allowed to fail because the free lists are heuristics based anyway. const sym_index = atom.getSymbolIndex().?; coff.locals_free_list.append(gpa, sym_index) catch {}; // Try freeing GOT atom if this decl had one coff.got_table.freeEntry(gpa, .{ .sym_index = sym_index }); coff.locals.items[sym_index].section_number = .UNDEFINED; _ = coff.atom_by_index_table.remove(sym_index); log.debug(" adding local symbol index {d} to free list", .{sym_index}); coff.getAtomPtr(atom_index).sym_index = 0; } pub fn updateFunc( coff: *Coff, pt: Zcu.PerThread, func_index: InternPool.Index, mir: *const codegen.AnyMir, ) link.File.UpdateNavError!void { if (build_options.skip_non_native and builtin.object_format != .coff) { @panic("Attempted to compile for object format that was disabled by build configuration"); } const tracy = trace(@src()); defer tracy.end(); const zcu = pt.zcu; const gpa = zcu.gpa; const func = zcu.funcInfo(func_index); const nav_index = func.owner_nav; const atom_index = try coff.getOrCreateAtomForNav(nav_index); coff.freeRelocations(atom_index); coff.navs.getPtr(func.owner_nav).?.section = coff.text_section_index.?; var code_buffer: std.ArrayListUnmanaged(u8) = .empty; defer code_buffer.deinit(gpa); try codegen.emitFunction( &coff.base, pt, zcu.navSrcLoc(nav_index), func_index, mir, &code_buffer, .none, ); try coff.updateNavCode(pt, nav_index, code_buffer.items, .FUNCTION); // Exports will be updated by `Zcu.processExports` after the update. } const LowerConstResult = union(enum) { ok: Atom.Index, fail: *Zcu.ErrorMsg, }; fn lowerConst( coff: *Coff, pt: Zcu.PerThread, name: []const u8, val: Value, required_alignment: InternPool.Alignment, sect_id: u16, src_loc: Zcu.LazySrcLoc, ) !LowerConstResult { const gpa = coff.base.comp.gpa; var code_buffer: std.ArrayListUnmanaged(u8) = .empty; defer code_buffer.deinit(gpa); const atom_index = try coff.createAtom(); const sym = coff.getAtom(atom_index).getSymbolPtr(coff); try coff.setSymbolName(sym, name); sym.section_number = @as(coff_util.SectionNumber, @enumFromInt(sect_id + 1)); try codegen.generateSymbol(&coff.base, pt, src_loc, val, &code_buffer, .{ .atom_index = coff.getAtom(atom_index).getSymbolIndex().?, }); const code = code_buffer.items; const atom = coff.getAtomPtr(atom_index); atom.size = @intCast(code.len); atom.getSymbolPtr(coff).value = try coff.allocateAtom( atom_index, atom.size, @intCast(required_alignment.toByteUnits().?), ); errdefer coff.freeAtom(atom_index); log.debug("allocated atom for {s} at 0x{x}", .{ name, atom.getSymbol(coff).value }); log.debug(" (required alignment 0x{x})", .{required_alignment}); try coff.writeAtom(atom_index, code, coff.base.comp.incremental); return .{ .ok = atom_index }; } pub fn updateNav( coff: *Coff, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index, ) link.File.UpdateNavError!void { if (build_options.skip_non_native and builtin.object_format != .coff) { @panic("Attempted to compile for object format that was disabled by build configuration"); } const tracy = trace(@src()); defer tracy.end(); const zcu = pt.zcu; const gpa = zcu.gpa; const ip = &zcu.intern_pool; const nav = ip.getNav(nav_index); const nav_val = zcu.navValue(nav_index); const nav_init = switch (ip.indexToKey(nav_val.toIntern())) { .func => return, .variable => |variable| Value.fromInterned(variable.init), .@"extern" => |@"extern"| { if (ip.isFunctionType(@"extern".ty)) return; // TODO make this part of getGlobalSymbol const name = nav.name.toSlice(ip); const lib_name = @"extern".lib_name.toSlice(ip); const global_index = try coff.getGlobalSymbol(name, lib_name); try coff.need_got_table.put(gpa, global_index, {}); return; }, else => nav_val, }; if (nav_init.typeOf(zcu).hasRuntimeBits(zcu)) { const atom_index = try coff.getOrCreateAtomForNav(nav_index); coff.freeRelocations(atom_index); const atom = coff.getAtom(atom_index); coff.navs.getPtr(nav_index).?.section = coff.getNavOutputSection(nav_index); var code_buffer: std.ArrayListUnmanaged(u8) = .empty; defer code_buffer.deinit(gpa); try codegen.generateSymbol( &coff.base, pt, zcu.navSrcLoc(nav_index), nav_init, &code_buffer, .{ .atom_index = atom.getSymbolIndex().? }, ); try coff.updateNavCode(pt, nav_index, code_buffer.items, .NULL); } // Exports will be updated by `Zcu.processExports` after the update. } fn updateLazySymbolAtom( coff: *Coff, pt: Zcu.PerThread, sym: link.File.LazySymbol, atom_index: Atom.Index, section_index: u16, ) !void { const zcu = pt.zcu; const comp = coff.base.comp; const gpa = comp.gpa; var required_alignment: InternPool.Alignment = .none; var code_buffer: std.ArrayListUnmanaged(u8) = .empty; defer code_buffer.deinit(gpa); const name = try allocPrint(gpa, "__lazy_{s}_{f}", .{ @tagName(sym.kind), Type.fromInterned(sym.ty).fmt(pt), }); defer gpa.free(name); const local_sym_index = coff.getAtomPtr(atom_index).getSymbolIndex().?; const src = Type.fromInterned(sym.ty).srcLocOrNull(zcu) orelse Zcu.LazySrcLoc.unneeded; try codegen.generateLazySymbol( &coff.base, pt, src, sym, &required_alignment, &code_buffer, .none, .{ .atom_index = local_sym_index }, ); const code = code_buffer.items; const atom = coff.getAtomPtr(atom_index); const symbol = atom.getSymbolPtr(coff); try coff.setSymbolName(symbol, name); symbol.section_number = @enumFromInt(section_index + 1); symbol.type = .{ .complex_type = .NULL, .base_type = .NULL }; const code_len: u32 = @intCast(code.len); const vaddr = try coff.allocateAtom(atom_index, code_len, @intCast(required_alignment.toByteUnits() orelse 0)); errdefer coff.freeAtom(atom_index); log.debug("allocated atom for {s} at 0x{x}", .{ name, vaddr }); log.debug(" (required alignment 0x{x})", .{required_alignment}); atom.size = code_len; symbol.value = vaddr; try coff.addGotEntry(.{ .sym_index = local_sym_index }); try coff.writeAtom(atom_index, code, coff.base.comp.incremental); } pub fn getOrCreateAtomForLazySymbol( coff: *Coff, pt: Zcu.PerThread, lazy_sym: link.File.LazySymbol, ) !Atom.Index { const gop = try coff.lazy_syms.getOrPut(pt.zcu.gpa, lazy_sym.ty); errdefer _ = if (!gop.found_existing) coff.lazy_syms.pop(); if (!gop.found_existing) gop.value_ptr.* = .{}; const atom_ptr, const state_ptr = switch (lazy_sym.kind) { .code => .{ &gop.value_ptr.text_atom, &gop.value_ptr.text_state }, .const_data => .{ &gop.value_ptr.rdata_atom, &gop.value_ptr.rdata_state }, }; switch (state_ptr.*) { .unused => atom_ptr.* = try coff.createAtom(), .pending_flush => return atom_ptr.*, .flushed => {}, } state_ptr.* = .pending_flush; const atom = atom_ptr.*; // anyerror needs to be deferred until flush if (lazy_sym.ty != .anyerror_type) try coff.updateLazySymbolAtom(pt, lazy_sym, atom, switch (lazy_sym.kind) { .code => coff.text_section_index.?, .const_data => coff.rdata_section_index.?, }); return atom; } pub fn getOrCreateAtomForNav(coff: *Coff, nav_index: InternPool.Nav.Index) !Atom.Index { const gpa = coff.base.comp.gpa; const gop = try coff.navs.getOrPut(gpa, nav_index); if (!gop.found_existing) { gop.value_ptr.* = .{ .atom = try coff.createAtom(), // If necessary, this will be modified by `updateNav` or `updateFunc`. .section = coff.rdata_section_index.?, .exports = .{}, }; } return gop.value_ptr.atom; } fn getNavOutputSection(coff: *Coff, nav_index: InternPool.Nav.Index) u16 { const zcu = coff.base.comp.zcu.?; const ip = &zcu.intern_pool; const nav = ip.getNav(nav_index); const ty = Type.fromInterned(nav.typeOf(ip)); const zig_ty = ty.zigTypeTag(zcu); const val = Value.fromInterned(nav.status.fully_resolved.val); const index: u16 = blk: { if (val.isUndefDeep(zcu)) { // TODO in release-fast and release-small, we should put undef in .bss break :blk coff.data_section_index.?; } switch (zig_ty) { // TODO: what if this is a function pointer? .@"fn" => break :blk coff.text_section_index.?, else => { if (val.getVariable(zcu)) |_| { break :blk coff.data_section_index.?; } break :blk coff.rdata_section_index.?; }, } }; return index; } fn updateNavCode( coff: *Coff, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index, code: []u8, complex_type: coff_util.ComplexType, ) link.File.UpdateNavError!void { const zcu = pt.zcu; const ip = &zcu.intern_pool; const nav = ip.getNav(nav_index); log.debug("updateNavCode {f} 0x{x}", .{ nav.fqn.fmt(ip), nav_index }); const target = &zcu.navFileScope(nav_index).mod.?.resolved_target.result; const required_alignment = switch (pt.navAlignment(nav_index)) { .none => target_util.defaultFunctionAlignment(target), else => |a| a.maxStrict(target_util.minFunctionAlignment(target)), }; const nav_metadata = coff.navs.get(nav_index).?; const atom_index = nav_metadata.atom; const atom = coff.getAtom(atom_index); const sym_index = atom.getSymbolIndex().?; const sect_index = nav_metadata.section; const code_len: u32 = @intCast(code.len); if (atom.size != 0) { const sym = atom.getSymbolPtr(coff); try coff.setSymbolName(sym, nav.fqn.toSlice(ip)); sym.section_number = @enumFromInt(sect_index + 1); sym.type = .{ .complex_type = complex_type, .base_type = .NULL }; const capacity = atom.capacity(coff); const need_realloc = code.len > capacity or !required_alignment.check(sym.value); if (need_realloc) { const vaddr = coff.growAtom(atom_index, code_len, @intCast(required_alignment.toByteUnits() orelse 0)) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => |e| return coff.base.cgFail(nav_index, "failed to grow atom: {s}", .{@errorName(e)}), }; log.debug("growing {f} from 0x{x} to 0x{x}", .{ nav.fqn.fmt(ip), sym.value, vaddr }); log.debug(" (required alignment 0x{x}", .{required_alignment}); if (vaddr != sym.value) { sym.value = vaddr; log.debug(" (updating GOT entry)", .{}); const got_entry_index = coff.got_table.lookup.get(.{ .sym_index = sym_index }).?; coff.writeOffsetTableEntry(got_entry_index) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => |e| return coff.base.cgFail(nav_index, "failed to write offset table entry: {s}", .{@errorName(e)}), }; coff.markRelocsDirtyByTarget(.{ .sym_index = sym_index }); } } else if (code_len < atom.size) { coff.shrinkAtom(atom_index, code_len); } coff.getAtomPtr(atom_index).size = code_len; } else { const sym = atom.getSymbolPtr(coff); try coff.setSymbolName(sym, nav.fqn.toSlice(ip)); sym.section_number = @enumFromInt(sect_index + 1); sym.type = .{ .complex_type = complex_type, .base_type = .NULL }; const vaddr = coff.allocateAtom(atom_index, code_len, @intCast(required_alignment.toByteUnits() orelse 0)) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => |e| return coff.base.cgFail(nav_index, "failed to allocate atom: {s}", .{@errorName(e)}), }; errdefer coff.freeAtom(atom_index); log.debug("allocated atom for {f} at 0x{x}", .{ nav.fqn.fmt(ip), vaddr }); coff.getAtomPtr(atom_index).size = code_len; sym.value = vaddr; coff.addGotEntry(.{ .sym_index = sym_index }) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => |e| return coff.base.cgFail(nav_index, "failed to add GOT entry: {s}", .{@errorName(e)}), }; } coff.writeAtom(atom_index, code, coff.base.comp.incremental) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => |e| return coff.base.cgFail(nav_index, "failed to write atom: {s}", .{@errorName(e)}), }; } pub fn freeNav(coff: *Coff, nav_index: InternPool.NavIndex) void { const gpa = coff.base.comp.gpa; if (coff.decls.fetchOrderedRemove(nav_index)) |const_kv| { var kv = const_kv; coff.freeAtom(kv.value.atom); kv.value.exports.deinit(gpa); } } pub fn updateExports( coff: *Coff, pt: Zcu.PerThread, exported: Zcu.Exported, export_indices: []const Zcu.Export.Index, ) link.File.UpdateExportsError!void { if (build_options.skip_non_native and builtin.object_format != .coff) { @panic("Attempted to compile for object format that was disabled by build configuration"); } const zcu = pt.zcu; const gpa = zcu.gpa; const metadata = switch (exported) { .nav => |nav| blk: { _ = try coff.getOrCreateAtomForNav(nav); break :blk coff.navs.getPtr(nav).?; }, .uav => |uav| coff.uavs.getPtr(uav) orelse blk: { const first_exp = export_indices[0].ptr(zcu); const res = try coff.lowerUav(pt, uav, .none, first_exp.src); switch (res) { .sym_index => {}, .fail => |em| { // TODO maybe it's enough to return an error here and let Module.processExportsInner // handle the error? try zcu.failed_exports.ensureUnusedCapacity(zcu.gpa, 1); zcu.failed_exports.putAssumeCapacityNoClobber(export_indices[0], em); return; }, } break :blk coff.uavs.getPtr(uav).?; }, }; const atom_index = metadata.atom; const atom = coff.getAtom(atom_index); for (export_indices) |export_idx| { const exp = export_idx.ptr(zcu); log.debug("adding new export '{f}'", .{exp.opts.name.fmt(&zcu.intern_pool)}); if (exp.opts.section.toSlice(&zcu.intern_pool)) |section_name| { if (!mem.eql(u8, section_name, ".text")) { try zcu.failed_exports.putNoClobber(gpa, export_idx, try Zcu.ErrorMsg.create( gpa, exp.src, "Unimplemented: ExportOptions.section", .{}, )); continue; } } if (exp.opts.linkage == .link_once) { try zcu.failed_exports.putNoClobber(gpa, export_idx, try Zcu.ErrorMsg.create( gpa, exp.src, "Unimplemented: GlobalLinkage.link_once", .{}, )); continue; } const exp_name = exp.opts.name.toSlice(&zcu.intern_pool); const sym_index = metadata.getExport(coff, exp_name) orelse blk: { const sym_index = if (coff.getGlobalIndex(exp_name)) |global_index| ind: { const global = coff.globals.items[global_index]; // TODO this is just plain wrong as it all should happen in a single `resolveSymbols` // pass. This will go away once we abstact away Zig's incremental compilation into // its own module. if (global.file == null and coff.getSymbol(global).section_number == .UNDEFINED) { _ = coff.unresolved.swapRemove(global_index); break :ind global.sym_index; } break :ind try coff.allocateSymbol(); } else try coff.allocateSymbol(); try metadata.exports.append(gpa, sym_index); break :blk sym_index; }; const sym_loc = SymbolWithLoc{ .sym_index = sym_index, .file = null }; const sym = coff.getSymbolPtr(sym_loc); try coff.setSymbolName(sym, exp_name); sym.value = atom.getSymbol(coff).value; sym.section_number = @as(coff_util.SectionNumber, @enumFromInt(metadata.section + 1)); sym.type = atom.getSymbol(coff).type; switch (exp.opts.linkage) { .strong => { sym.storage_class = .EXTERNAL; }, .internal => @panic("TODO Internal"), .weak => @panic("TODO WeakExternal"), else => unreachable, } try coff.resolveGlobalSymbol(sym_loc); } } pub fn deleteExport( coff: *Coff, exported: Zcu.Exported, name: InternPool.NullTerminatedString, ) void { const metadata = switch (exported) { .nav => |nav| coff.navs.getPtr(nav), .uav => |uav| coff.uavs.getPtr(uav), } orelse return; const zcu = coff.base.comp.zcu.?; const name_slice = name.toSlice(&zcu.intern_pool); const sym_index = metadata.getExportPtr(coff, name_slice) orelse return; const gpa = coff.base.comp.gpa; const sym_loc = SymbolWithLoc{ .sym_index = sym_index.*, .file = null }; const sym = coff.getSymbolPtr(sym_loc); log.debug("deleting export '{f}'", .{name.fmt(&zcu.intern_pool)}); assert(sym.storage_class == .EXTERNAL and sym.section_number != .UNDEFINED); sym.* = .{ .name = [_]u8{0} ** 8, .value = 0, .section_number = .UNDEFINED, .type = .{ .base_type = .NULL, .complex_type = .NULL }, .storage_class = .NULL, .number_of_aux_symbols = 0, }; coff.locals_free_list.append(gpa, sym_index.*) catch {}; if (coff.resolver.fetchRemove(name_slice)) |entry| { defer gpa.free(entry.key); coff.globals_free_list.append(gpa, entry.value) catch {}; coff.globals.items[entry.value] = .{ .sym_index = 0, .file = null, }; } sym_index.* = 0; } fn resolveGlobalSymbol(coff: *Coff, current: SymbolWithLoc) !void { const gpa = coff.base.comp.gpa; const sym = coff.getSymbol(current); const sym_name = coff.getSymbolName(current); const gop = try coff.getOrPutGlobalPtr(sym_name); if (!gop.found_existing) { gop.value_ptr.* = current; if (sym.section_number == .UNDEFINED) { try coff.unresolved.putNoClobber(gpa, coff.getGlobalIndex(sym_name).?, false); } return; } log.debug("TODO finish resolveGlobalSymbols implementation", .{}); if (sym.section_number == .UNDEFINED) return; _ = coff.unresolved.swapRemove(coff.getGlobalIndex(sym_name).?); gop.value_ptr.* = current; } pub fn flush( coff: *Coff, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node, ) link.File.FlushError!void { const tracy = trace(@src()); defer tracy.end(); const comp = coff.base.comp; const diags = &comp.link_diags; switch (coff.base.comp.config.output_mode) { .Exe, .Obj => {}, .Lib => return diags.fail("writing lib files not yet implemented for COFF", .{}), } const sub_prog_node = prog_node.start("COFF Flush", 0); defer sub_prog_node.end(); return flushInner(coff, arena, tid) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.LinkFailure => return error.LinkFailure, else => |e| return diags.fail("COFF flush failed: {s}", .{@errorName(e)}), }; } fn flushInner(coff: *Coff, arena: Allocator, tid: Zcu.PerThread.Id) !void { _ = arena; const comp = coff.base.comp; const gpa = comp.gpa; const diags = &comp.link_diags; const pt: Zcu.PerThread = .activate( comp.zcu orelse return diags.fail("linking without zig source is not yet implemented", .{}), tid, ); defer pt.deactivate(); if (coff.lazy_syms.getPtr(.anyerror_type)) |metadata| { // Most lazy symbols can be updated on first use, but // anyerror needs to wait for everything to be flushed. if (metadata.text_state != .unused) try coff.updateLazySymbolAtom( pt, .{ .kind = .code, .ty = .anyerror_type }, metadata.text_atom, coff.text_section_index.?, ); if (metadata.rdata_state != .unused) try coff.updateLazySymbolAtom( pt, .{ .kind = .const_data, .ty = .anyerror_type }, metadata.rdata_atom, coff.rdata_section_index.?, ); } for (coff.lazy_syms.values()) |*metadata| { if (metadata.text_state != .unused) metadata.text_state = .flushed; if (metadata.rdata_state != .unused) metadata.rdata_state = .flushed; } { var it = coff.need_got_table.iterator(); while (it.next()) |entry| { const global = coff.globals.items[entry.key_ptr.*]; try coff.addGotEntry(global); } } while (coff.unresolved.pop()) |entry| { assert(entry.value); const global = coff.globals.items[entry.key]; const sym = coff.getSymbol(global); const res = try coff.import_tables.getOrPut(gpa, sym.value); const itable = res.value_ptr; if (!res.found_existing) { itable.* = .{}; } if (itable.lookup.contains(global)) continue; // TODO: we could technically write the pointer placeholder for to-be-bound import here, // but since this happens in flush, there is currently no point. _ = try itable.addImport(gpa, global); coff.imports_count_dirty = true; } try coff.writeImportTables(); for (coff.relocs.keys(), coff.relocs.values()) |atom_index, relocs| { const needs_update = for (relocs.items) |reloc| { if (reloc.dirty) break true; } else false; if (!needs_update) continue; const atom = coff.getAtom(atom_index); const sym = atom.getSymbol(coff); const section = coff.sections.get(@intFromEnum(sym.section_number) - 1).header; const file_offset = section.pointer_to_raw_data + sym.value - section.virtual_address; var code = std.ArrayList(u8).init(gpa); defer code.deinit(); try code.resize(math.cast(usize, atom.size) orelse return error.Overflow); assert(atom.size > 0); const amt = try coff.base.file.?.preadAll(code.items, file_offset); if (amt != code.items.len) return error.InputOutput; try coff.writeAtom(atom_index, code.items, true); } // Update GOT if it got moved in memory. if (coff.got_table_contents_dirty) { for (coff.got_table.entries.items, 0..) |entry, i| { if (!coff.got_table.lookup.contains(entry)) continue; // TODO: write all in one go rather than incrementally. try coff.writeOffsetTableEntry(i); } coff.got_table_contents_dirty = false; } try coff.writeBaseRelocations(); if (coff.getEntryPoint()) |entry_sym_loc| { coff.entry_addr = coff.getSymbol(entry_sym_loc).value; } if (build_options.enable_logging) { coff.logSymtab(); coff.logImportTables(); } try coff.writeStrtab(); try coff.writeDataDirectoriesHeaders(); try coff.writeSectionHeaders(); if (coff.entry_addr == null and comp.config.output_mode == .Exe) { log.debug("flushing. no_entry_point_found = true\n", .{}); diags.flags.no_entry_point_found = true; } else { log.debug("flushing. no_entry_point_found = false\n", .{}); diags.flags.no_entry_point_found = false; try coff.writeHeader(); } assert(!coff.imports_count_dirty); // hack for stage2_x86_64 + coff if (comp.compiler_rt_dyn_lib) |crt_file| { const compiler_rt_sub_path = try std.fs.path.join(gpa, &.{ std.fs.path.dirname(coff.base.emit.sub_path) orelse "", std.fs.path.basename(crt_file.full_object_path.sub_path), }); defer gpa.free(compiler_rt_sub_path); try crt_file.full_object_path.root_dir.handle.copyFile( crt_file.full_object_path.sub_path, coff.base.emit.root_dir.handle, compiler_rt_sub_path, .{}, ); } } pub fn getNavVAddr( coff: *Coff, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index, reloc_info: link.File.RelocInfo, ) !u64 { const zcu = pt.zcu; const ip = &zcu.intern_pool; const nav = ip.getNav(nav_index); log.debug("getNavVAddr {f}({d})", .{ nav.fqn.fmt(ip), nav_index }); const sym_index = if (nav.getExtern(ip)) |e| try coff.getGlobalSymbol(nav.name.toSlice(ip), e.lib_name.toSlice(ip)) else coff.getAtom(try coff.getOrCreateAtomForNav(nav_index)).getSymbolIndex().?; const atom_index = coff.getAtomIndexForSymbol(.{ .sym_index = reloc_info.parent.atom_index, .file = null, }).?; const target = SymbolWithLoc{ .sym_index = sym_index, .file = null }; try coff.addRelocation(atom_index, .{ .type = .direct, .target = target, .offset = @as(u32, @intCast(reloc_info.offset)), .addend = reloc_info.addend, .pcrel = false, .length = 3, }); try coff.addBaseRelocation(atom_index, @as(u32, @intCast(reloc_info.offset))); return 0; } pub fn lowerUav( coff: *Coff, pt: Zcu.PerThread, uav: InternPool.Index, explicit_alignment: InternPool.Alignment, src_loc: Zcu.LazySrcLoc, ) !codegen.SymbolResult { const zcu = pt.zcu; const gpa = zcu.gpa; const val = Value.fromInterned(uav); const uav_alignment = switch (explicit_alignment) { .none => val.typeOf(zcu).abiAlignment(zcu), else => explicit_alignment, }; if (coff.uavs.get(uav)) |metadata| { const atom = coff.getAtom(metadata.atom); const existing_addr = atom.getSymbol(coff).value; if (uav_alignment.check(existing_addr)) return .{ .sym_index = atom.getSymbolIndex().? }; } var name_buf: [32]u8 = undefined; const name = std.fmt.bufPrint(&name_buf, "__anon_{d}", .{ @intFromEnum(uav), }) catch unreachable; const res = coff.lowerConst( pt, name, val, uav_alignment, coff.rdata_section_index.?, src_loc, ) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => |e| return .{ .fail = try Zcu.ErrorMsg.create( gpa, src_loc, "lowerAnonDecl failed with error: {s}", .{@errorName(e)}, ) }, }; const atom_index = switch (res) { .ok => |atom_index| atom_index, .fail => |em| return .{ .fail = em }, }; try coff.uavs.put(gpa, uav, .{ .atom = atom_index, .section = coff.rdata_section_index.?, }); return .{ .sym_index = coff.getAtom(atom_index).getSymbolIndex().? }; } pub fn getUavVAddr( coff: *Coff, uav: InternPool.Index, reloc_info: link.File.RelocInfo, ) !u64 { const this_atom_index = coff.uavs.get(uav).?.atom; const sym_index = coff.getAtom(this_atom_index).getSymbolIndex().?; const atom_index = coff.getAtomIndexForSymbol(.{ .sym_index = reloc_info.parent.atom_index, .file = null, }).?; const target = SymbolWithLoc{ .sym_index = sym_index, .file = null }; try coff.addRelocation(atom_index, .{ .type = .direct, .target = target, .offset = @as(u32, @intCast(reloc_info.offset)), .addend = reloc_info.addend, .pcrel = false, .length = 3, }); try coff.addBaseRelocation(atom_index, @as(u32, @intCast(reloc_info.offset))); return 0; } pub fn getGlobalSymbol(coff: *Coff, name: []const u8, lib_name_name: ?[]const u8) !u32 { const gop = try coff.getOrPutGlobalPtr(name); const global_index = coff.getGlobalIndex(name).?; if (gop.found_existing) { return global_index; } const sym_index = try coff.allocateSymbol(); const sym_loc = SymbolWithLoc{ .sym_index = sym_index, .file = null }; gop.value_ptr.* = sym_loc; const gpa = coff.base.comp.gpa; const sym = coff.getSymbolPtr(sym_loc); try coff.setSymbolName(sym, name); sym.storage_class = .EXTERNAL; if (lib_name_name) |lib_name| { // We repurpose the 'value' of the Symbol struct to store an offset into // temporary string table where we will store the library name hint. sym.value = try coff.temp_strtab.insert(gpa, lib_name); } try coff.unresolved.putNoClobber(gpa, global_index, true); return global_index; } pub fn updateLineNumber(coff: *Coff, pt: Zcu.PerThread, ti_id: InternPool.TrackedInst.Index) !void { _ = coff; _ = pt; _ = ti_id; log.debug("TODO implement updateLineNumber", .{}); } /// TODO: note if we need to rewrite base relocations by dirtying any of the entries in the global table /// TODO: note that .ABSOLUTE is used as padding within each block; we could use this fact to do /// incremental updates and writes into the table instead of doing it all at once fn writeBaseRelocations(coff: *Coff) !void { const gpa = coff.base.comp.gpa; var page_table = std.AutoHashMap(u32, std.ArrayList(coff_util.BaseRelocation)).init(gpa); defer { var it = page_table.valueIterator(); while (it.next()) |inner| { inner.deinit(); } page_table.deinit(); } { var it = coff.base_relocs.iterator(); while (it.next()) |entry| { const atom_index = entry.key_ptr.*; const atom = coff.getAtom(atom_index); const sym = atom.getSymbol(coff); const offsets = entry.value_ptr.*; for (offsets.items) |offset| { const rva = sym.value + offset; const page = mem.alignBackward(u32, rva, coff.page_size); const gop = try page_table.getOrPut(page); if (!gop.found_existing) { gop.value_ptr.* = std.ArrayList(coff_util.BaseRelocation).init(gpa); } try gop.value_ptr.append(.{ .offset = @as(u12, @intCast(rva - page)), .type = .DIR64, }); } } { const header = &coff.sections.items(.header)[coff.got_section_index.?]; for (coff.got_table.entries.items, 0..) |entry, index| { if (!coff.got_table.lookup.contains(entry)) continue; const sym = coff.getSymbol(entry); if (sym.section_number == .UNDEFINED) continue; const rva = @as(u32, @intCast(header.virtual_address + index * coff.ptr_width.size())); const page = mem.alignBackward(u32, rva, coff.page_size); const gop = try page_table.getOrPut(page); if (!gop.found_existing) { gop.value_ptr.* = std.ArrayList(coff_util.BaseRelocation).init(gpa); } try gop.value_ptr.append(.{ .offset = @as(u12, @intCast(rva - page)), .type = .DIR64, }); } } } // Sort pages by address. var pages = try std.ArrayList(u32).initCapacity(gpa, page_table.count()); defer pages.deinit(); { var it = page_table.keyIterator(); while (it.next()) |page| { pages.appendAssumeCapacity(page.*); } } mem.sort(u32, pages.items, {}, std.sort.asc(u32)); var buffer = std.ArrayList(u8).init(gpa); defer buffer.deinit(); for (pages.items) |page| { const entries = page_table.getPtr(page).?; // Pad to required 4byte alignment if (!mem.isAlignedGeneric( usize, entries.items.len * @sizeOf(coff_util.BaseRelocation), @sizeOf(u32), )) { try entries.append(.{ .offset = 0, .type = .ABSOLUTE, }); } const block_size = @as( u32, @intCast(entries.items.len * @sizeOf(coff_util.BaseRelocation) + @sizeOf(coff_util.BaseRelocationDirectoryEntry)), ); try buffer.ensureUnusedCapacity(block_size); buffer.appendSliceAssumeCapacity(mem.asBytes(&coff_util.BaseRelocationDirectoryEntry{ .page_rva = page, .block_size = block_size, })); buffer.appendSliceAssumeCapacity(mem.sliceAsBytes(entries.items)); } const header = &coff.sections.items(.header)[coff.reloc_section_index.?]; const needed_size = @as(u32, @intCast(buffer.items.len)); try coff.growSection(coff.reloc_section_index.?, needed_size); try coff.pwriteAll(buffer.items, header.pointer_to_raw_data); coff.data_directories[@intFromEnum(coff_util.DirectoryEntry.BASERELOC)] = .{ .virtual_address = header.virtual_address, .size = needed_size, }; } fn writeImportTables(coff: *Coff) !void { if (coff.idata_section_index == null) return; if (!coff.imports_count_dirty) return; const gpa = coff.base.comp.gpa; const ext = ".dll"; const header = &coff.sections.items(.header)[coff.idata_section_index.?]; // Calculate needed size var iat_size: u32 = 0; var dir_table_size: u32 = @sizeOf(coff_util.ImportDirectoryEntry); // sentinel var lookup_table_size: u32 = 0; var names_table_size: u32 = 0; var dll_names_size: u32 = 0; for (coff.import_tables.keys(), 0..) |off, i| { const lib_name = coff.temp_strtab.getAssumeExists(off); const itable = coff.import_tables.values()[i]; iat_size += itable.size() + 8; dir_table_size += @sizeOf(coff_util.ImportDirectoryEntry); lookup_table_size += @as(u32, @intCast(itable.entries.items.len + 1)) * @sizeOf(coff_util.ImportLookupEntry64.ByName); for (itable.entries.items) |entry| { const sym_name = coff.getSymbolName(entry); names_table_size += 2 + mem.alignForward(u32, @as(u32, @intCast(sym_name.len + 1)), 2); } dll_names_size += @as(u32, @intCast(lib_name.len + ext.len + 1)); } const needed_size = iat_size + dir_table_size + lookup_table_size + names_table_size + dll_names_size; try coff.growSection(coff.idata_section_index.?, needed_size); // Do the actual writes var buffer = std.ArrayList(u8).init(gpa); defer buffer.deinit(); try buffer.ensureTotalCapacityPrecise(needed_size); buffer.resize(needed_size) catch unreachable; const dir_header_size = @sizeOf(coff_util.ImportDirectoryEntry); const lookup_entry_size = @sizeOf(coff_util.ImportLookupEntry64.ByName); var iat_offset: u32 = 0; var dir_table_offset = iat_size; var lookup_table_offset = dir_table_offset + dir_table_size; var names_table_offset = lookup_table_offset + lookup_table_size; var dll_names_offset = names_table_offset + names_table_size; for (coff.import_tables.keys(), 0..) |off, i| { const lib_name = coff.temp_strtab.getAssumeExists(off); const itable = coff.import_tables.values()[i]; // Lookup table header const lookup_header = coff_util.ImportDirectoryEntry{ .import_lookup_table_rva = header.virtual_address + lookup_table_offset, .time_date_stamp = 0, .forwarder_chain = 0, .name_rva = header.virtual_address + dll_names_offset, .import_address_table_rva = header.virtual_address + iat_offset, }; @memcpy(buffer.items[dir_table_offset..][0..@sizeOf(coff_util.ImportDirectoryEntry)], mem.asBytes(&lookup_header)); dir_table_offset += dir_header_size; for (itable.entries.items) |entry| { const import_name = coff.getSymbolName(entry); // IAT and lookup table entry const lookup = coff_util.ImportLookupEntry64.ByName{ .name_table_rva = @as(u31, @intCast(header.virtual_address + names_table_offset)) }; @memcpy( buffer.items[iat_offset..][0..@sizeOf(coff_util.ImportLookupEntry64.ByName)], mem.asBytes(&lookup), ); iat_offset += lookup_entry_size; @memcpy( buffer.items[lookup_table_offset..][0..@sizeOf(coff_util.ImportLookupEntry64.ByName)], mem.asBytes(&lookup), ); lookup_table_offset += lookup_entry_size; // Names table entry mem.writeInt(u16, buffer.items[names_table_offset..][0..2], 0, .little); // Hint set to 0 until we learn how to parse DLLs names_table_offset += 2; @memcpy(buffer.items[names_table_offset..][0..import_name.len], import_name); names_table_offset += @as(u32, @intCast(import_name.len)); buffer.items[names_table_offset] = 0; names_table_offset += 1; if (!mem.isAlignedGeneric(usize, names_table_offset, @sizeOf(u16))) { buffer.items[names_table_offset] = 0; names_table_offset += 1; } } // IAT sentinel mem.writeInt(u64, buffer.items[iat_offset..][0..lookup_entry_size], 0, .little); iat_offset += 8; // Lookup table sentinel @memcpy( buffer.items[lookup_table_offset..][0..@sizeOf(coff_util.ImportLookupEntry64.ByName)], mem.asBytes(&coff_util.ImportLookupEntry64.ByName{ .name_table_rva = 0 }), ); lookup_table_offset += lookup_entry_size; // DLL name @memcpy(buffer.items[dll_names_offset..][0..lib_name.len], lib_name); dll_names_offset += @as(u32, @intCast(lib_name.len)); @memcpy(buffer.items[dll_names_offset..][0..ext.len], ext); dll_names_offset += @as(u32, @intCast(ext.len)); buffer.items[dll_names_offset] = 0; dll_names_offset += 1; } // Sentinel const lookup_header = coff_util.ImportDirectoryEntry{ .import_lookup_table_rva = 0, .time_date_stamp = 0, .forwarder_chain = 0, .name_rva = 0, .import_address_table_rva = 0, }; @memcpy( buffer.items[dir_table_offset..][0..@sizeOf(coff_util.ImportDirectoryEntry)], mem.asBytes(&lookup_header), ); dir_table_offset += dir_header_size; assert(dll_names_offset == needed_size); try coff.pwriteAll(buffer.items, header.pointer_to_raw_data); coff.data_directories[@intFromEnum(coff_util.DirectoryEntry.IMPORT)] = .{ .virtual_address = header.virtual_address + iat_size, .size = dir_table_size, }; coff.data_directories[@intFromEnum(coff_util.DirectoryEntry.IAT)] = .{ .virtual_address = header.virtual_address, .size = iat_size, }; coff.imports_count_dirty = false; } fn writeStrtab(coff: *Coff) !void { if (coff.strtab_offset == null) return; const comp = coff.base.comp; const gpa = comp.gpa; const diags = &comp.link_diags; const allocated_size = coff.allocatedSize(coff.strtab_offset.?); const needed_size: u32 = @intCast(coff.strtab.buffer.items.len); if (needed_size > allocated_size) { coff.strtab_offset = null; coff.strtab_offset = @intCast(coff.findFreeSpace(needed_size, @alignOf(u32))); } log.debug("writing strtab from 0x{x} to 0x{x}", .{ coff.strtab_offset.?, coff.strtab_offset.? + needed_size }); var buffer = std.ArrayList(u8).init(gpa); defer buffer.deinit(); try buffer.ensureTotalCapacityPrecise(needed_size); buffer.appendSliceAssumeCapacity(coff.strtab.buffer.items); // Here, we do a trick in that we do not commit the size of the strtab to strtab buffer, instead // we write the length of the strtab to a temporary buffer that goes to file. mem.writeInt(u32, buffer.items[0..4], @as(u32, @intCast(coff.strtab.buffer.items.len)), .little); coff.pwriteAll(buffer.items, coff.strtab_offset.?) catch |err| { return diags.fail("failed to write: {s}", .{@errorName(err)}); }; } fn writeSectionHeaders(coff: *Coff) !void { const offset = coff.getSectionHeadersOffset(); try coff.pwriteAll(mem.sliceAsBytes(coff.sections.items(.header)), offset); } fn writeDataDirectoriesHeaders(coff: *Coff) !void { const offset = coff.getDataDirectoryHeadersOffset(); try coff.pwriteAll(mem.sliceAsBytes(&coff.data_directories), offset); } fn writeHeader(coff: *Coff) !void { const target = &coff.base.comp.root_mod.resolved_target.result; const gpa = coff.base.comp.gpa; var buffer = std.ArrayList(u8).init(gpa); defer buffer.deinit(); const writer = buffer.writer(); try buffer.ensureTotalCapacity(coff.getSizeOfHeaders()); writer.writeAll(&msdos_stub) catch unreachable; mem.writeInt(u32, buffer.items[0x3c..][0..4], msdos_stub.len, .little); writer.writeAll("PE\x00\x00") catch unreachable; var flags = coff_util.CoffHeaderFlags{ .EXECUTABLE_IMAGE = 1, .DEBUG_STRIPPED = 1, // TODO }; switch (coff.ptr_width) { .p32 => flags.@"32BIT_MACHINE" = 1, .p64 => flags.LARGE_ADDRESS_AWARE = 1, } if (coff.base.comp.config.output_mode == .Lib and coff.base.comp.config.link_mode == .dynamic) { flags.DLL = 1; } const timestamp = if (coff.repro) 0 else std.time.timestamp(); const size_of_optional_header = @as(u16, @intCast(coff.getOptionalHeaderSize() + coff.getDataDirectoryHeadersSize())); var coff_header = coff_util.CoffHeader{ .machine = target.toCoffMachine(), .number_of_sections = @as(u16, @intCast(coff.sections.slice().len)), // TODO what if we prune a section .time_date_stamp = @as(u32, @truncate(@as(u64, @bitCast(timestamp)))), .pointer_to_symbol_table = coff.strtab_offset orelse 0, .number_of_symbols = 0, .size_of_optional_header = size_of_optional_header, .flags = flags, }; writer.writeAll(mem.asBytes(&coff_header)) catch unreachable; const dll_flags: coff_util.DllFlags = .{ .HIGH_ENTROPY_VA = 1, // TODO do we want to permit non-PIE builds at all? .DYNAMIC_BASE = 1, .TERMINAL_SERVER_AWARE = 1, // We are not a legacy app .NX_COMPAT = 1, // We are compatible with Data Execution Prevention }; const subsystem: coff_util.Subsystem = .WINDOWS_CUI; const size_of_image: u32 = coff.getSizeOfImage(); const size_of_headers: u32 = mem.alignForward(u32, coff.getSizeOfHeaders(), default_file_alignment); const base_of_code = coff.sections.get(coff.text_section_index.?).header.virtual_address; const base_of_data = coff.sections.get(coff.data_section_index.?).header.virtual_address; var size_of_code: u32 = 0; var size_of_initialized_data: u32 = 0; var size_of_uninitialized_data: u32 = 0; for (coff.sections.items(.header)) |header| { if (header.flags.CNT_CODE == 1) { size_of_code += header.size_of_raw_data; } if (header.flags.CNT_INITIALIZED_DATA == 1) { size_of_initialized_data += header.size_of_raw_data; } if (header.flags.CNT_UNINITIALIZED_DATA == 1) { size_of_uninitialized_data += header.size_of_raw_data; } } switch (coff.ptr_width) { .p32 => { var opt_header = coff_util.OptionalHeaderPE32{ .magic = coff_util.IMAGE_NT_OPTIONAL_HDR32_MAGIC, .major_linker_version = 0, .minor_linker_version = 0, .size_of_code = size_of_code, .size_of_initialized_data = size_of_initialized_data, .size_of_uninitialized_data = size_of_uninitialized_data, .address_of_entry_point = coff.entry_addr orelse 0, .base_of_code = base_of_code, .base_of_data = base_of_data, .image_base = @intCast(coff.image_base), .section_alignment = coff.page_size, .file_alignment = default_file_alignment, .major_operating_system_version = 6, .minor_operating_system_version = 0, .major_image_version = 0, .minor_image_version = 0, .major_subsystem_version = @intCast(coff.major_subsystem_version), .minor_subsystem_version = @intCast(coff.minor_subsystem_version), .win32_version_value = 0, .size_of_image = size_of_image, .size_of_headers = size_of_headers, .checksum = 0, .subsystem = subsystem, .dll_flags = dll_flags, .size_of_stack_reserve = default_size_of_stack_reserve, .size_of_stack_commit = default_size_of_stack_commit, .size_of_heap_reserve = default_size_of_heap_reserve, .size_of_heap_commit = default_size_of_heap_commit, .loader_flags = 0, .number_of_rva_and_sizes = @intCast(coff.data_directories.len), }; writer.writeAll(mem.asBytes(&opt_header)) catch unreachable; }, .p64 => { var opt_header = coff_util.OptionalHeaderPE64{ .magic = coff_util.IMAGE_NT_OPTIONAL_HDR64_MAGIC, .major_linker_version = 0, .minor_linker_version = 0, .size_of_code = size_of_code, .size_of_initialized_data = size_of_initialized_data, .size_of_uninitialized_data = size_of_uninitialized_data, .address_of_entry_point = coff.entry_addr orelse 0, .base_of_code = base_of_code, .image_base = coff.image_base, .section_alignment = coff.page_size, .file_alignment = default_file_alignment, .major_operating_system_version = 6, .minor_operating_system_version = 0, .major_image_version = 0, .minor_image_version = 0, .major_subsystem_version = coff.major_subsystem_version, .minor_subsystem_version = coff.minor_subsystem_version, .win32_version_value = 0, .size_of_image = size_of_image, .size_of_headers = size_of_headers, .checksum = 0, .subsystem = subsystem, .dll_flags = dll_flags, .size_of_stack_reserve = default_size_of_stack_reserve, .size_of_stack_commit = default_size_of_stack_commit, .size_of_heap_reserve = default_size_of_heap_reserve, .size_of_heap_commit = default_size_of_heap_commit, .loader_flags = 0, .number_of_rva_and_sizes = @intCast(coff.data_directories.len), }; writer.writeAll(mem.asBytes(&opt_header)) catch unreachable; }, } try coff.pwriteAll(buffer.items, 0); } pub fn padToIdeal(actual_size: anytype) @TypeOf(actual_size) { return actual_size +| (actual_size / ideal_factor); } fn detectAllocCollision(coff: *Coff, start: u32, size: u32) ?u32 { const headers_size = @max(coff.getSizeOfHeaders(), coff.page_size); if (start < headers_size) return headers_size; const end = start + padToIdeal(size); if (coff.strtab_offset) |off| { const tight_size = @as(u32, @intCast(coff.strtab.buffer.items.len)); const increased_size = padToIdeal(tight_size); const test_end = off + increased_size; if (end > off and start < test_end) { return test_end; } } for (coff.sections.items(.header)) |header| { const tight_size = header.size_of_raw_data; const increased_size = padToIdeal(tight_size); const test_end = header.pointer_to_raw_data + increased_size; if (end > header.pointer_to_raw_data and start < test_end) { return test_end; } } return null; } fn allocatedSize(coff: *Coff, start: u32) u32 { if (start == 0) return 0; var min_pos: u32 = std.math.maxInt(u32); if (coff.strtab_offset) |off| { if (off > start and off < min_pos) min_pos = off; } for (coff.sections.items(.header)) |header| { if (header.pointer_to_raw_data <= start) continue; if (header.pointer_to_raw_data < min_pos) min_pos = header.pointer_to_raw_data; } return min_pos - start; } fn findFreeSpace(coff: *Coff, object_size: u32, min_alignment: u32) u32 { var start: u32 = 0; while (coff.detectAllocCollision(start, object_size)) |item_end| { start = mem.alignForward(u32, item_end, min_alignment); } return start; } fn allocatedVirtualSize(coff: *Coff, start: u32) u32 { if (start == 0) return 0; var min_pos: u32 = std.math.maxInt(u32); for (coff.sections.items(.header)) |header| { if (header.virtual_address <= start) continue; if (header.virtual_address < min_pos) min_pos = header.virtual_address; } return min_pos - start; } fn getSizeOfHeaders(coff: Coff) u32 { const msdos_hdr_size = msdos_stub.len + 4; return @as(u32, @intCast(msdos_hdr_size + @sizeOf(coff_util.CoffHeader) + coff.getOptionalHeaderSize() + coff.getDataDirectoryHeadersSize() + coff.getSectionHeadersSize())); } fn getOptionalHeaderSize(coff: Coff) u32 { return switch (coff.ptr_width) { .p32 => @as(u32, @intCast(@sizeOf(coff_util.OptionalHeaderPE32))), .p64 => @as(u32, @intCast(@sizeOf(coff_util.OptionalHeaderPE64))), }; } fn getDataDirectoryHeadersSize(coff: Coff) u32 { return @as(u32, @intCast(coff.data_directories.len * @sizeOf(coff_util.ImageDataDirectory))); } fn getSectionHeadersSize(coff: Coff) u32 { return @as(u32, @intCast(coff.sections.slice().len * @sizeOf(coff_util.SectionHeader))); } fn getDataDirectoryHeadersOffset(coff: Coff) u32 { const msdos_hdr_size = msdos_stub.len + 4; return @as(u32, @intCast(msdos_hdr_size + @sizeOf(coff_util.CoffHeader) + coff.getOptionalHeaderSize())); } fn getSectionHeadersOffset(coff: Coff) u32 { return coff.getDataDirectoryHeadersOffset() + coff.getDataDirectoryHeadersSize(); } fn getSizeOfImage(coff: Coff) u32 { var image_size: u32 = mem.alignForward(u32, coff.getSizeOfHeaders(), coff.page_size); for (coff.sections.items(.header)) |header| { image_size += mem.alignForward(u32, header.virtual_size, coff.page_size); } return image_size; } /// Returns symbol location corresponding to the set entrypoint (if any). pub fn getEntryPoint(coff: Coff) ?SymbolWithLoc { const comp = coff.base.comp; // TODO This is incomplete. // The entry symbol name depends on the subsystem as well as the set of // public symbol names from linked objects. // See LinkerDriver::findDefaultEntry from the LLD project for the flow chart. const entry_name = switch (coff.entry) { .disabled => return null, .default => switch (comp.config.output_mode) { .Exe => "wWinMainCRTStartup", .Obj, .Lib => return null, }, .enabled => "wWinMainCRTStartup", .named => |name| name, }; const global_index = coff.resolver.get(entry_name) orelse return null; return coff.globals.items[global_index]; } /// Returns pointer-to-symbol described by `sym_loc` descriptor. pub fn getSymbolPtr(coff: *Coff, sym_loc: SymbolWithLoc) *coff_util.Symbol { assert(sym_loc.file == null); // TODO linking object files return &coff.locals.items[sym_loc.sym_index]; } /// Returns symbol described by `sym_loc` descriptor. pub fn getSymbol(coff: *const Coff, sym_loc: SymbolWithLoc) *const coff_util.Symbol { assert(sym_loc.file == null); // TODO linking object files return &coff.locals.items[sym_loc.sym_index]; } /// Returns name of the symbol described by `sym_loc` descriptor. pub fn getSymbolName(coff: *const Coff, sym_loc: SymbolWithLoc) []const u8 { assert(sym_loc.file == null); // TODO linking object files const sym = coff.getSymbol(sym_loc); const offset = sym.getNameOffset() orelse return sym.getName().?; return coff.strtab.get(offset).?; } /// Returns pointer to the global entry for `name` if one exists. pub fn getGlobalPtr(coff: *Coff, name: []const u8) ?*SymbolWithLoc { const global_index = coff.resolver.get(name) orelse return null; return &coff.globals.items[global_index]; } /// Returns the global entry for `name` if one exists. pub fn getGlobal(coff: *const Coff, name: []const u8) ?SymbolWithLoc { const global_index = coff.resolver.get(name) orelse return null; return coff.globals.items[global_index]; } /// Returns the index of the global entry for `name` if one exists. pub fn getGlobalIndex(coff: *const Coff, name: []const u8) ?u32 { return coff.resolver.get(name); } /// Returns global entry at `index`. pub fn getGlobalByIndex(coff: *const Coff, index: u32) SymbolWithLoc { assert(index < coff.globals.items.len); return coff.globals.items[index]; } const GetOrPutGlobalPtrResult = struct { found_existing: bool, value_ptr: *SymbolWithLoc, }; /// Return pointer to the global entry for `name` if one exists. /// Puts a new global entry for `name` if one doesn't exist, and /// returns a pointer to it. pub fn getOrPutGlobalPtr(coff: *Coff, name: []const u8) !GetOrPutGlobalPtrResult { if (coff.getGlobalPtr(name)) |ptr| { return GetOrPutGlobalPtrResult{ .found_existing = true, .value_ptr = ptr }; } const gpa = coff.base.comp.gpa; const global_index = try coff.allocateGlobal(); const global_name = try gpa.dupe(u8, name); _ = try coff.resolver.put(gpa, global_name, global_index); const ptr = &coff.globals.items[global_index]; return GetOrPutGlobalPtrResult{ .found_existing = false, .value_ptr = ptr }; } pub fn getAtom(coff: *const Coff, atom_index: Atom.Index) Atom { assert(atom_index < coff.atoms.items.len); return coff.atoms.items[atom_index]; } pub fn getAtomPtr(coff: *Coff, atom_index: Atom.Index) *Atom { assert(atom_index < coff.atoms.items.len); return &coff.atoms.items[atom_index]; } /// Returns atom if there is an atom referenced by the symbol described by `sym_loc` descriptor. /// Returns null on failure. pub fn getAtomIndexForSymbol(coff: *const Coff, sym_loc: SymbolWithLoc) ?Atom.Index { assert(sym_loc.file == null); // TODO linking with object files return coff.atom_by_index_table.get(sym_loc.sym_index); } fn setSectionName(coff: *Coff, header: *coff_util.SectionHeader, name: []const u8) !void { if (name.len <= 8) { @memcpy(header.name[0..name.len], name); @memset(header.name[name.len..], 0); return; } const gpa = coff.base.comp.gpa; const offset = try coff.strtab.insert(gpa, name); const name_offset = fmt.bufPrint(&header.name, "/{d}", .{offset}) catch unreachable; @memset(header.name[name_offset.len..], 0); } fn getSectionName(coff: *const Coff, header: *const coff_util.SectionHeader) []const u8 { if (header.getName()) |name| { return name; } const offset = header.getNameOffset().?; return coff.strtab.get(offset).?; } fn setSymbolName(coff: *Coff, symbol: *coff_util.Symbol, name: []const u8) !void { if (name.len <= 8) { @memcpy(symbol.name[0..name.len], name); @memset(symbol.name[name.len..], 0); return; } const gpa = coff.base.comp.gpa; const offset = try coff.strtab.insert(gpa, name); @memset(symbol.name[0..4], 0); mem.writeInt(u32, symbol.name[4..8], offset, .little); } fn logSymAttributes(sym: *const coff_util.Symbol, buf: *[4]u8) []const u8 { @memset(buf[0..4], '_'); switch (sym.section_number) { .UNDEFINED => { buf[3] = 'u'; switch (sym.storage_class) { .EXTERNAL => buf[1] = 'e', .WEAK_EXTERNAL => buf[1] = 'w', .NULL => {}, else => unreachable, } }, .ABSOLUTE => unreachable, // handle ABSOLUTE .DEBUG => unreachable, else => { buf[0] = 's'; switch (sym.storage_class) { .EXTERNAL => buf[1] = 'e', .WEAK_EXTERNAL => buf[1] = 'w', .NULL => {}, else => unreachable, } }, } return buf[0..]; } fn logSymtab(coff: *Coff) void { var buf: [4]u8 = undefined; log.debug("symtab:", .{}); log.debug(" object(null)", .{}); for (coff.locals.items, 0..) |*sym, sym_id| { const where = if (sym.section_number == .UNDEFINED) "ord" else "sect"; const def_index: u16 = switch (sym.section_number) { .UNDEFINED => 0, // TODO .ABSOLUTE => unreachable, // TODO .DEBUG => unreachable, // TODO else => @intFromEnum(sym.section_number), }; log.debug(" %{d}: {s} @{x} in {s}({d}), {s}", .{ sym_id, coff.getSymbolName(.{ .sym_index = @as(u32, @intCast(sym_id)), .file = null }), sym.value, where, def_index, logSymAttributes(sym, &buf), }); } log.debug("globals table:", .{}); for (coff.globals.items) |sym_loc| { const sym_name = coff.getSymbolName(sym_loc); log.debug(" {s} => %{d} in object({?d})", .{ sym_name, sym_loc.sym_index, sym_loc.file }); } log.debug("GOT entries:", .{}); log.debug("{f}", .{coff.got_table}); } fn logSections(coff: *Coff) void { log.debug("sections:", .{}); for (coff.sections.items(.header)) |*header| { log.debug(" {s}: VM({x}, {x}) FILE({x}, {x})", .{ coff.getSectionName(header), header.virtual_address, header.virtual_address + header.virtual_size, header.pointer_to_raw_data, header.pointer_to_raw_data + header.size_of_raw_data, }); } } fn logImportTables(coff: *const Coff) void { log.debug("import tables:", .{}); for (coff.import_tables.keys(), 0..) |off, i| { const itable = coff.import_tables.values()[i]; log.debug("{f}", .{itable.fmtDebug(.{ .coff = coff, .index = i, .name_off = off, })}); } } pub const Atom = struct { /// Each decl always gets a local symbol with the fully qualified name. /// The vaddr and size are found here directly. /// The file offset is found by computing the vaddr offset from the section vaddr /// the symbol references, and adding that to the file offset of the section. /// If this field is 0, it means the codegen size = 0 and there is no symbol or /// offset table entry. sym_index: u32, /// null means symbol defined by Zig source. file: ?u32, /// Size of the atom size: u32, /// Points to the previous and next neighbors, based on the `text_offset`. /// This can be used to find, for example, the capacity of this `Atom`. prev_index: ?Index, next_index: ?Index, const Index = u32; pub fn getSymbolIndex(atom: Atom) ?u32 { if (atom.sym_index == 0) return null; return atom.sym_index; } /// Returns symbol referencing this atom. fn getSymbol(atom: Atom, coff: *const Coff) *const coff_util.Symbol { const sym_index = atom.getSymbolIndex().?; return coff.getSymbol(.{ .sym_index = sym_index, .file = atom.file, }); } /// Returns pointer-to-symbol referencing this atom. fn getSymbolPtr(atom: Atom, coff: *Coff) *coff_util.Symbol { const sym_index = atom.getSymbolIndex().?; return coff.getSymbolPtr(.{ .sym_index = sym_index, .file = atom.file, }); } fn getSymbolWithLoc(atom: Atom) SymbolWithLoc { const sym_index = atom.getSymbolIndex().?; return .{ .sym_index = sym_index, .file = atom.file }; } /// Returns the name of this atom. fn getName(atom: Atom, coff: *const Coff) []const u8 { const sym_index = atom.getSymbolIndex().?; return coff.getSymbolName(.{ .sym_index = sym_index, .file = atom.file, }); } /// Returns how much room there is to grow in virtual address space. fn capacity(atom: Atom, coff: *const Coff) u32 { const atom_sym = atom.getSymbol(coff); if (atom.next_index) |next_index| { const next = coff.getAtom(next_index); const next_sym = next.getSymbol(coff); return next_sym.value - atom_sym.value; } else { // We are the last atom. // The capacity is limited only by virtual address space. return std.math.maxInt(u32) - atom_sym.value; } } fn freeListEligible(atom: Atom, coff: *const Coff) bool { // No need to keep a free list node for the last atom. const next_index = atom.next_index orelse return false; const next = coff.getAtom(next_index); const atom_sym = atom.getSymbol(coff); const next_sym = next.getSymbol(coff); const cap = next_sym.value - atom_sym.value; const ideal_cap = padToIdeal(atom.size); if (cap <= ideal_cap) return false; const surplus = cap - ideal_cap; return surplus >= min_text_capacity; } }; pub const Relocation = struct { type: enum { // x86, x86_64 /// RIP-relative displacement to a GOT pointer got, /// RIP-relative displacement to an import pointer import, // aarch64 /// PC-relative distance to target page in GOT section got_page, /// Offset to a GOT pointer relative to the start of a page in GOT section got_pageoff, /// PC-relative distance to target page in a section (e.g., .rdata) page, /// Offset to a pointer relative to the start of a page in a section (e.g., .rdata) pageoff, /// PC-relative distance to target page in a import section import_page, /// Offset to a pointer relative to the start of a page in an import section (e.g., .rdata) import_pageoff, // common /// Absolute pointer value direct, }, target: SymbolWithLoc, offset: u32, addend: u32, pcrel: bool, length: u2, dirty: bool = true, /// Returns true if and only if the reloc can be resolved. fn isResolvable(reloc: Relocation, coff: *Coff) bool { _ = reloc.getTargetAddress(coff) orelse return false; return true; } fn isGotIndirection(reloc: Relocation) bool { return switch (reloc.type) { .got, .got_page, .got_pageoff => true, else => false, }; } /// Returns address of the target if any. fn getTargetAddress(reloc: Relocation, coff: *const Coff) ?u32 { switch (reloc.type) { .got, .got_page, .got_pageoff => { const got_index = coff.got_table.lookup.get(reloc.target) orelse return null; const header = coff.sections.items(.header)[coff.got_section_index.?]; return header.virtual_address + got_index * coff.ptr_width.size(); }, .import, .import_page, .import_pageoff => { const sym = coff.getSymbol(reloc.target); const index = coff.import_tables.getIndex(sym.value) orelse return null; const itab = coff.import_tables.values()[index]; return itab.getImportAddress(reloc.target, .{ .coff = coff, .index = index, .name_off = sym.value, }); }, else => { const target_atom_index = coff.getAtomIndexForSymbol(reloc.target) orelse return null; const target_atom = coff.getAtom(target_atom_index); return target_atom.getSymbol(coff).value; }, } } fn resolve(reloc: Relocation, atom_index: Atom.Index, code: []u8, image_base: u64, coff: *Coff) void { const atom = coff.getAtom(atom_index); const source_sym = atom.getSymbol(coff); const source_vaddr = source_sym.value + reloc.offset; const target_vaddr = reloc.getTargetAddress(coff).?; // Oops, you didn't check if the relocation can be resolved with isResolvable(). const target_vaddr_with_addend = target_vaddr + reloc.addend; log.debug(" ({x}: [() => 0x{x} ({s})) ({s}) ", .{ source_vaddr, target_vaddr_with_addend, coff.getSymbolName(reloc.target), @tagName(reloc.type), }); const ctx: Context = .{ .source_vaddr = source_vaddr, .target_vaddr = target_vaddr_with_addend, .image_base = image_base, .code = code, .ptr_width = coff.ptr_width, }; const target = &coff.base.comp.root_mod.resolved_target.result; switch (target.cpu.arch) { .aarch64 => reloc.resolveAarch64(ctx), .x86, .x86_64 => reloc.resolveX86(ctx), else => unreachable, // unhandled target architecture } } const Context = struct { source_vaddr: u32, target_vaddr: u32, image_base: u64, code: []u8, ptr_width: PtrWidth, }; fn resolveAarch64(reloc: Relocation, ctx: Context) void { var buffer = ctx.code[reloc.offset..]; switch (reloc.type) { .got_page, .import_page, .page => { const source_page = @as(i32, @intCast(ctx.source_vaddr >> 12)); const target_page = @as(i32, @intCast(ctx.target_vaddr >> 12)); const pages = @as(u21, @bitCast(@as(i21, @intCast(target_page - source_page)))); var inst = aarch64_util.Instruction{ .pc_relative_address = mem.bytesToValue(@FieldType( aarch64_util.Instruction, @tagName(aarch64_util.Instruction.pc_relative_address), ), buffer[0..4]), }; inst.pc_relative_address.immhi = @as(u19, @truncate(pages >> 2)); inst.pc_relative_address.immlo = @as(u2, @truncate(pages)); mem.writeInt(u32, buffer[0..4], inst.toU32(), .little); }, .got_pageoff, .import_pageoff, .pageoff => { assert(!reloc.pcrel); const narrowed = @as(u12, @truncate(@as(u64, @intCast(ctx.target_vaddr)))); if (isArithmeticOp(buffer[0..4])) { var inst = aarch64_util.Instruction{ .add_subtract_immediate = mem.bytesToValue(@FieldType( aarch64_util.Instruction, @tagName(aarch64_util.Instruction.add_subtract_immediate), ), buffer[0..4]), }; inst.add_subtract_immediate.imm12 = narrowed; mem.writeInt(u32, buffer[0..4], inst.toU32(), .little); } else { var inst = aarch64_util.Instruction{ .load_store_register = mem.bytesToValue(@FieldType( aarch64_util.Instruction, @tagName(aarch64_util.Instruction.load_store_register), ), buffer[0..4]), }; const offset: u12 = blk: { if (inst.load_store_register.size == 0) { if (inst.load_store_register.v == 1) { // 128-bit SIMD is scaled by 16. break :blk @divExact(narrowed, 16); } // Otherwise, 8-bit SIMD or ldrb. break :blk narrowed; } else { const denom: u4 = math.powi(u4, 2, inst.load_store_register.size) catch unreachable; break :blk @divExact(narrowed, denom); } }; inst.load_store_register.offset = offset; mem.writeInt(u32, buffer[0..4], inst.toU32(), .little); } }, .direct => { assert(!reloc.pcrel); switch (reloc.length) { 2 => mem.writeInt( u32, buffer[0..4], @as(u32, @truncate(ctx.target_vaddr + ctx.image_base)), .little, ), 3 => mem.writeInt(u64, buffer[0..8], ctx.target_vaddr + ctx.image_base, .little), else => unreachable, } }, .got => unreachable, .import => unreachable, } } fn resolveX86(reloc: Relocation, ctx: Context) void { var buffer = ctx.code[reloc.offset..]; switch (reloc.type) { .got_page => unreachable, .got_pageoff => unreachable, .page => unreachable, .pageoff => unreachable, .import_page => unreachable, .import_pageoff => unreachable, .got, .import => { assert(reloc.pcrel); const disp = @as(i32, @intCast(ctx.target_vaddr)) - @as(i32, @intCast(ctx.source_vaddr)) - 4; mem.writeInt(i32, buffer[0..4], disp, .little); }, .direct => { if (reloc.pcrel) { const disp = @as(i32, @intCast(ctx.target_vaddr)) - @as(i32, @intCast(ctx.source_vaddr)) - 4; mem.writeInt(i32, buffer[0..4], disp, .little); } else switch (ctx.ptr_width) { .p32 => mem.writeInt(u32, buffer[0..4], @as(u32, @intCast(ctx.target_vaddr + ctx.image_base)), .little), .p64 => switch (reloc.length) { 2 => mem.writeInt(u32, buffer[0..4], @as(u32, @truncate(ctx.target_vaddr + ctx.image_base)), .little), 3 => mem.writeInt(u64, buffer[0..8], ctx.target_vaddr + ctx.image_base, .little), else => unreachable, }, } }, } } fn isArithmeticOp(inst: *const [4]u8) bool { const group_decode = @as(u5, @truncate(inst[3])); return ((group_decode >> 2) == 4); } }; pub fn addRelocation(coff: *Coff, atom_index: Atom.Index, reloc: Relocation) !void { const comp = coff.base.comp; const gpa = comp.gpa; log.debug(" (adding reloc of type {s} to target %{d})", .{ @tagName(reloc.type), reloc.target.sym_index }); const gop = try coff.relocs.getOrPut(gpa, atom_index); if (!gop.found_existing) { gop.value_ptr.* = .{}; } try gop.value_ptr.append(gpa, reloc); } fn addBaseRelocation(coff: *Coff, atom_index: Atom.Index, offset: u32) !void { const comp = coff.base.comp; const gpa = comp.gpa; log.debug(" (adding base relocation at offset 0x{x} in %{d})", .{ offset, coff.getAtom(atom_index).getSymbolIndex().?, }); const gop = try coff.base_relocs.getOrPut(gpa, atom_index); if (!gop.found_existing) { gop.value_ptr.* = .{}; } try gop.value_ptr.append(gpa, offset); } fn freeRelocations(coff: *Coff, atom_index: Atom.Index) void { const comp = coff.base.comp; const gpa = comp.gpa; var removed_relocs = coff.relocs.fetchOrderedRemove(atom_index); if (removed_relocs) |*relocs| relocs.value.deinit(gpa); var removed_base_relocs = coff.base_relocs.fetchOrderedRemove(atom_index); if (removed_base_relocs) |*base_relocs| base_relocs.value.deinit(gpa); } /// Represents an import table in the .idata section where each contained pointer /// is to a symbol from the same DLL. /// /// The layout of .idata section is as follows: /// /// --- ADDR1 : IAT (all import tables concatenated together) /// ptr /// ptr /// 0 sentinel /// ptr /// 0 sentinel /// --- ADDR2: headers /// ImportDirectoryEntry header /// ImportDirectoryEntry header /// sentinel /// --- ADDR2: lookup tables /// Lookup table /// 0 sentinel /// Lookup table /// 0 sentinel /// --- ADDR3: name hint tables /// hint-symname /// hint-symname /// --- ADDR4: DLL names /// DLL#1 name /// DLL#2 name /// --- END const ImportTable = struct { entries: std.ArrayListUnmanaged(SymbolWithLoc) = .empty, free_list: std.ArrayListUnmanaged(u32) = .empty, lookup: std.AutoHashMapUnmanaged(SymbolWithLoc, u32) = .empty, fn deinit(itab: *ImportTable, allocator: Allocator) void { itab.entries.deinit(allocator); itab.free_list.deinit(allocator); itab.lookup.deinit(allocator); } /// Size of the import table does not include the sentinel. fn size(itab: ImportTable) u32 { return @as(u32, @intCast(itab.entries.items.len)) * @sizeOf(u64); } fn addImport(itab: *ImportTable, allocator: Allocator, target: SymbolWithLoc) !ImportIndex { try itab.entries.ensureUnusedCapacity(allocator, 1); const index: u32 = blk: { if (itab.free_list.pop()) |index| { log.debug(" (reusing import entry index {d})", .{index}); break :blk index; } else { log.debug(" (allocating import entry at index {d})", .{itab.entries.items.len}); const index = @as(u32, @intCast(itab.entries.items.len)); _ = itab.entries.addOneAssumeCapacity(); break :blk index; } }; itab.entries.items[index] = target; try itab.lookup.putNoClobber(allocator, target, index); return index; } const Context = struct { coff: *const Coff, /// Index of this ImportTable in a global list of all tables. /// This is required in order to calculate the base vaddr of this ImportTable. index: usize, /// Offset into the string interning table of the DLL this ImportTable corresponds to. name_off: u32, }; fn getBaseAddress(ctx: Context) u32 { const header = ctx.coff.sections.items(.header)[ctx.coff.idata_section_index.?]; var addr = header.virtual_address; for (ctx.coff.import_tables.values(), 0..) |other_itab, i| { if (ctx.index == i) break; addr += @as(u32, @intCast(other_itab.entries.items.len * @sizeOf(u64))) + 8; } return addr; } fn getImportAddress(itab: *const ImportTable, target: SymbolWithLoc, ctx: Context) ?u32 { const index = itab.lookup.get(target) orelse return null; const base_vaddr = getBaseAddress(ctx); return base_vaddr + index * @sizeOf(u64); } const Format = struct { itab: ImportTable, ctx: Context, fn default(f: Format, writer: *std.io.Writer) std.io.Writer.Error!void { const lib_name = f.ctx.coff.temp_strtab.getAssumeExists(f.ctx.name_off); const base_vaddr = getBaseAddress(f.ctx); try writer.print("IAT({s}.dll) @{x}:", .{ lib_name, base_vaddr }); for (f.itab.entries.items, 0..) |entry, i| { try writer.print("\n {d}@{?x} => {s}", .{ i, f.itab.getImportAddress(entry, f.ctx), f.ctx.coff.getSymbolName(entry), }); } } }; fn fmtDebug(itab: ImportTable, ctx: Context) fmt.Formatter(Format, Format.default) { return .{ .data = .{ .itab = itab, .ctx = ctx } }; } const ImportIndex = u32; }; fn pwriteAll(coff: *Coff, bytes: []const u8, offset: u64) error{LinkFailure}!void { const comp = coff.base.comp; const diags = &comp.link_diags; coff.base.file.?.pwriteAll(bytes, offset) catch |err| { return diags.fail("failed to write: {s}", .{@errorName(err)}); }; } const Coff = @This(); const std = @import("std"); const build_options = @import("build_options"); const builtin = @import("builtin"); const assert = std.debug.assert; const coff_util = std.coff; const fmt = std.fmt; const fs = std.fs; const log = std.log.scoped(.link); const math = std.math; const mem = std.mem; const Allocator = std.mem.Allocator; const Path = std.Build.Cache.Path; const Directory = std.Build.Cache.Directory; const Cache = std.Build.Cache; const aarch64_util = @import("../arch/aarch64/bits.zig"); const allocPrint = std.fmt.allocPrint; const codegen = @import("../codegen.zig"); const link = @import("../link.zig"); const target_util = @import("../target.zig"); const trace = @import("../tracy.zig").trace; const Compilation = @import("../Compilation.zig"); const Zcu = @import("../Zcu.zig"); const InternPool = @import("../InternPool.zig"); const TableSection = @import("table_section.zig").TableSection; const StringTable = @import("StringTable.zig"); const Type = @import("../Type.zig"); const Value = @import("../Value.zig"); const AnalUnit = InternPool.AnalUnit; const dev = @import("../dev.zig"); /// This is the start of a Portable Executable (PE) file. /// It starts with a MS-DOS header followed by a MS-DOS stub program. /// This data does not change so we include it as follows in all binaries. /// /// In this context, /// A "paragraph" is 16 bytes. /// A "page" is 512 bytes. /// A "long" is 4 bytes. /// A "word" is 2 bytes. const msdos_stub: [120]u8 = .{ 'M', 'Z', // Magic number. Stands for Mark Zbikowski (designer of the MS-DOS executable format). 0x78, 0x00, // Number of bytes in the last page. This matches the size of this entire MS-DOS stub. 0x01, 0x00, // Number of pages. 0x00, 0x00, // Number of entries in the relocation table. 0x04, 0x00, // The number of paragraphs taken up by the header. 4 * 16 = 64, which matches the header size (all bytes before the MS-DOS stub program). 0x00, 0x00, // The number of paragraphs required by the program. 0x00, 0x00, // The number of paragraphs requested by the program. 0x00, 0x00, // Initial value for SS (relocatable segment address). 0x00, 0x00, // Initial value for SP. 0x00, 0x00, // Checksum. 0x00, 0x00, // Initial value for IP. 0x00, 0x00, // Initial value for CS (relocatable segment address). 0x40, 0x00, // Absolute offset to relocation table. 64 matches the header size (all bytes before the MS-DOS stub program). 0x00, 0x00, // Overlay number. Zero means this is the main executable. } // Reserved words. ++ .{ 0x00, 0x00 } ** 4 // OEM-related fields. ++ .{ 0x00, 0x00, // OEM identifier. 0x00, 0x00, // OEM information. } // Reserved words. ++ .{ 0x00, 0x00 } ** 10 // Address of the PE header (a long). This matches the size of this entire MS-DOS stub, so that's the address of what's after this MS-DOS stub. ++ .{ 0x78, 0x00, 0x00, 0x00 } // What follows is a 16-bit x86 MS-DOS program of 7 instructions that prints the bytes after these instructions and then exits. ++ .{ // Set the value of the data segment to the same value as the code segment. 0x0e, // push cs 0x1f, // pop ds // Set the DX register to the address of the message. // If you count all bytes of these 7 instructions you get 14, so that's the address of what's after these instructions. 0xba, 14, 0x00, // mov dx, 14 // Set AH to the system call code for printing a message. 0xb4, 0x09, // mov ah, 0x09 // Perform the system call to print the message. 0xcd, 0x21, // int 0x21 // Set AH to 0x4c which is the system call code for exiting, and set AL to 0x01 which is the exit code. 0xb8, 0x01, 0x4c, // mov ax, 0x4c01 // Peform the system call to exit the program with exit code 1. 0xcd, 0x21, // int 0x21 } // Message to print. ++ "This program cannot be run in DOS mode.".* // Message terminators. ++ .{ '$', // We do not pass a length to the print system call; the string is terminated by this character. 0x00, 0x00, // Terminating zero bytes. };