authorgravatar for kcbanner@gmail.comCasey Banner <kcbanner@gmail.com> 2026-06-05 01:55:34-04:00
committergravatar for kcbanner@gmail.comCasey Banner <kcbanner@gmail.com> 2026-06-23 00:22:42-04:00
loged42120d8b739794e884d924f92ef684325a4e34
tree7e51e043223e5cfbab57d62aa09523b66c27c78e
parentb6192adfb22da64973c961297b60a74a5b6c9b23

Coff: Report undefined symbols

- Report each undefined symbol (once per unique instance), up to a maximum of 4 - WIP on figuring out updating location relocs in inputs sections

1 files changed, 285 insertions(+), 82 deletions(-)

src/link/Coff.zig+285-82
......@@ -737,7 +737,7 @@ pub const Symbol = struct {
737737 value: union {
738738 /// For generated symbols, this is their size
739739 size: u32,
740 /// For globals from input sections, this is the offset within the input section
740 /// For symbols from input sections, this is the offset within the input section
741741 input_offset: u32,
742742 },
743743 /// Relocations contained within this symbol
......@@ -1322,11 +1322,15 @@ pub fn deinit(coff: *Coff) void {
13221322 const gpa = coff.base.comp.gpa;
13231323 coff.mf.deinit(gpa);
13241324 coff.nodes.deinit(gpa);
1325 coff.pending_members.deinit(gpa);
1326 coff.lib_string_table.deinit(gpa);
13251327 coff.long_names_table.entries.deinit(gpa);
13261328 coff.import_table.entries.deinit(gpa);
13271329 coff.export_table.entries.deinit(gpa);
13281330 coff.symbol_table.strings.deinit(gpa);
13291331 coff.symbol_table.pending.deinit(gpa);
1332 coff.inputs.deinit(gpa);
1333 coff.input_sections.deinit(gpa);
13301334 coff.strings.deinit(gpa);
13311335 coff.string_bytes.deinit(gpa);
13321336 coff.section_table.deinit(gpa);
......@@ -2145,6 +2149,13 @@ fn initSymbolAssumeCapacity(coff: *Coff) !Symbol.Index {
21452149 return si;
21462150}
21472151
2152fn initInputSectionSymbol(coff: *Coff, sym: *Symbol, section_si: Symbol.Index, value: u32) void {
2153 const section_sym = section_si.get(coff);
2154 sym.ni = section_sym.ni;
2155 sym.value = .{ .input_offset = value };
2156 sym.section_number = section_sym.section_number;
2157}
2158
21482159fn getOrPutString(coff: *Coff, string: []const u8) !String {
21492160 try coff.ensureUnusedStringCapacity(string.len);
21502161 return coff.getOrPutStringAssumeCapacity(string);
......@@ -2240,7 +2251,12 @@ fn getOrPutGlobalSymbol(
22402251}
22412252
22422253pub fn globalSymbol(coff: *Coff, opts: GlobalOptions) !Symbol.Index {
2243 return (try coff.getOrPutGlobalSymbol(opts)).value_ptr.*;
2254 const gop = try coff.getOrPutGlobalSymbol(opts);
2255 if (gop.found_existing) {
2256 // TODO: Need to know if this is an export or extern, add to opts
2257 }
2258
2259 return gop.value_ptr.*;
22442260}
22452261
22462262pub fn pendingSymbolTableEntry(coff: *Coff, si: Symbol.Index) !void {
......@@ -2657,6 +2673,10 @@ fn flushInputSection(coff: *Coff, isi: Node.InputSectionIndex) !void {
26572673 if (try nw.interface.sendFileAll(&fr, .limited(@intCast(file_loc.size))) != file_loc.size)
26582674 return error.EndOfStream;
26592675 si.applyLocationRelocs(coff);
2676
2677 // TODO: Problem is that if the sym is first seen as undef, it's si is in the range of the section
2678 // that wants that symbol. but when the section that contains it is moved, the iteration doesn't see
2679 // that symbol.
26602680}
26612681
26622682fn addSection(coff: *Coff, name: String, flags: std.coff.SectionHeader.Flags) !Symbol.Index {
......@@ -3267,7 +3287,7 @@ fn loadObject(
32673287 if (section.header.flags.LNK_REMOVE or
32683288 section.header.flags.MEM_DISCARDABLE)
32693289 {
3270 // TODO: Merge .debug$* sections and output to PDB
3290 // TODO: Convert .debug$* sections into PDB
32713291 continue;
32723292 }
32733293
......@@ -3340,8 +3360,6 @@ fn loadObject(
33403360 // This will be necessary if we do the equivalent of /Gy for compiler-rt
33413361 return diags.failParse(path, "TODO handle COMDAT sections in input objects", .{});
33423362
3343 log.debug("loadInputSection({s})", .{section.name.toSlice(coff)});
3344
33453363 const parent_osmi = try coff.objectSectionMapIndex(
33463364 section.name,
33473365 coff.mf.flags.block_size,
......@@ -3372,6 +3390,7 @@ fn loadObject(
33723390 },
33733391 };
33743392
3393 log.debug("loadInputSection({s}) = {d}@{d}", .{ section.name.toSlice(coff), section.si, sym.section_number });
33753394 coff.synth_prog_node.increaseEstimatedTotalItems(1);
33763395 }
33773396
......@@ -3428,89 +3447,141 @@ fn loadObject(
34283447 const name = std.mem.sliceTo(if (std.mem.eql(u8, symbol.name[0..4], "\x00\x00\x00\x00")) name: {
34293448 const index = std.mem.readInt(u32, symbol.name[4..], target_endian);
34303449 if (index >= string_table.len)
3431 return diags.failParse(path, "bad string offset for symbol {d}", .{symbol_ix});
3450 return diags.failParse(path, "bad string offset for symbol 0x{x}", .{symbol_ix});
34323451 break :name string_table[index..];
34333452 } else &symbol.name, 0);
34343453
3435 const si = symbols.addOneAssumeCapacity();
3436 si.* = .null;
3437
3438 switch (symbol.section_number) {
3439 .UNDEFINED, .ABSOLUTE, .DEBUG => continue,
3440 else => switch (symbol.storage_class) {
3441 .STATIC => if (symbol.value == 0 and symbol.type == std.coff.SymType{
3442 .complex_type = .NULL,
3443 .base_type = .NULL,
3444 }) {
3445 if (symbol.number_of_aux_symbols != 1)
3446 return diags.failParse(path, "invalid number of aux symbols for section {d}: {d}", .{
3447 symbol_ix,
3448 symbol.number_of_aux_symbols,
3449 });
3450
3451 var section_def: std.coff.SectionDefinition = undefined;
3452 @memcpy(std.mem.asBytes(&section_def)[0..symbol_size], try r.peek(symbol_size));
3453 if (target_endian != native_endian)
3454 std.mem.byteSwapAllFields(std.coff.SectionDefinition, &section_def);
3455
3456 // TODO: Extract the COMDAT section info
3454 const si_slice = symbols.addManyAsSliceAssumeCapacity(1 + symbol.number_of_aux_symbols);
3455 @memset(si_slice, .null);
34573456
3458 if (section_def.number > sections.len)
3459 return diags.failParse(
3460 path,
3461 "section symbol for '{s}' contained an out of bounds section number: {d}",
3462 .{ name, section_def.number },
3463 );
3464
3465 // It's valid for this to not match the symbol's section number (ie. .drectve sets this)
3466 if (section_def.number == 0)
3467 continue;
3457 defer log.debug("loadInputSymbol({s}, 0x{x}) = {d}@{d}", .{
3458 name,
3459 symbol.value,
3460 si_slice[0],
3461 if (si_slice[0] == .null) .UNDEFINED else si_slice[0].get(coff).section_number,
3462 });
34683463
3469 const section = &sections[section_def.number - 1];
3470 if (section_def.number_of_relocations != section.header.number_of_relocations)
3471 return diags.failParse(
3472 path,
3473 "section symbol for '{s}' relocation count did not match section header: {d} vs {d}",
3474 .{ name, section_def.number_of_relocations, section.header.number_of_relocations },
3475 );
3464 if (is_archive) {
3465 if (symbol.storage_class == .EXTERNAL)
3466 try coff.ensureMemberSymbol(mi, coff.getOrPutStringAssumeCapacity(name));
34763467
3477 if (section_def.number_of_linenumbers != section.header.number_of_linenumbers)
3478 return diags.failParse(
3479 path,
3480 "section symbol for '{s}' line number count did not match section header: {d} vs {d}",
3481 .{ name, section_def.number_of_linenumbers, section.header.number_of_linenumbers },
3482 );
3468 continue;
3469 }
34833470
3484 si.* = section.si;
3485 continue;
3471 switch (symbol.storage_class) {
3472 .STATIC, .LABEL => |storage_class| switch (symbol.section_number) {
3473 .UNDEFINED, .DEBUG, .ABSOLUTE => {
3474 // TODO: Do we need to do anything with @feat.00?
3475 // https://llvm.org/doxygen/namespacellvm_1_1COFF.html#aeffa16735e18df727a173beaf748c392
3476 },
3477 else => |sn| {
3478 // Section symbol
3479 if (storage_class == .STATIC and
3480 symbol.value == 0 and
3481 symbol.type == std.coff.SymType{
3482 .complex_type = .NULL,
3483 .base_type = .NULL,
3484 } and
3485 symbol.number_of_aux_symbols > 0)
3486 {
3487 if (symbol.number_of_aux_symbols > 1)
3488 return diags.failParse(path, "invalid number of aux symbols for section 0x{x}: {d}", .{
3489 symbol_ix,
3490 symbol.number_of_aux_symbols,
3491 });
3492
3493 var section_def: std.coff.SectionDefinition = undefined;
3494 @memcpy(std.mem.asBytes(&section_def)[0..symbol_size], try r.peek(symbol_size));
3495 if (target_endian != native_endian)
3496 std.mem.byteSwapAllFields(std.coff.SectionDefinition, &section_def);
3497
3498 // TODO: Extract the COMDAT section info
3499
3500 if (section_def.number > sections.len)
3501 return diags.failParse(
3502 path,
3503 "section symbol for '{s}' contained an out of bounds section number: 0x{x}",
3504 .{ name, section_def.number },
3505 );
3506
3507 // It's valid for this to not match the symbol's section number (ie. .drectve sets this)
3508 if (section_def.number == 0)
3509 continue;
3510
3511 const section = &sections[section_def.number - 1];
3512 if (section_def.number_of_relocations != section.header.number_of_relocations)
3513 return diags.failParse(
3514 path,
3515 "section symbol for '{s}' relocation count did not match section header: {d} vs {d}",
3516 .{ name, section_def.number_of_relocations, section.header.number_of_relocations },
3517 );
3518
3519 if (section_def.number_of_linenumbers != section.header.number_of_linenumbers)
3520 return diags.failParse(
3521 path,
3522 "section symbol for '{s}' line number count did not match section header: {d} vs {d}",
3523 .{ name, section_def.number_of_linenumbers, section.header.number_of_linenumbers },
3524 );
3525
3526 @memset(si_slice, section.si);
3527 } else {
3528 try coff.symbols.ensureUnusedCapacity(gpa, 1);
3529 si_slice[0] = coff.addSymbolAssumeCapacity();
3530 const sym = si_slice[0].get(coff);
3531 coff.initInputSectionSymbol(sym, sections[@intCast(@intFromEnum(sn) - 1)].si, symbol.value);
3532 }
34863533 },
3487 .EXTERNAL => {},
3488 else => continue,
34893534 },
3535 .EXTERNAL => switch (symbol.section_number) {
3536 .ABSOLUTE => return diags.failParse(
3537 path,
3538 "TODO unhandled external absolute symbol: '{s}'",
3539 .{name},
3540 ),
3541 .DEBUG => return diags.failParse(
3542 path,
3543 "unexpected external symbol in DEBUG section: '{s}'",
3544 .{name},
3545 ),
3546 else => |sn| {
3547 const global_gop = try coff.getOrPutGlobalSymbol(.{ .name = name });
3548 si_slice[0] = global_gop.value_ptr.*;
3549
3550 const sym = si_slice[0].get(coff);
3551 if (sn != .UNDEFINED) {
3552 if (global_gop.found_existing and sym.ni != .none) {
3553 // TODO: Need corresponding logic later if we try to make a global already defined by an input
3554
3555 var err = try diags.addErrorWithNotes(2);
3556 try err.addMsg("multiple definitions of '{s}'", .{name});
3557 switch (coff.getNode(sym.ni)) {
3558 .input_section => |isi| {
3559 const other_ii = isi.input(coff);
3560 err.addNote("first seen in input '{f}{f}'", .{
3561 other_ii.path(coff).fmtEscapeString(),
3562 fmtArchiveNameString(other_ii.archiveName(coff)),
3563 });
3564 },
3565 .nav, .uav => err.addNote("first seen in module '{s}'", .{
3566 comp.zcu.?.root_mod.fully_qualified_name,
3567 }),
3568 else => unreachable,
3569 }
3570 err.addNote("defined again in input '{f}'", .{path});
3571 return error.LinkFailure;
3572 }
3573
3574 // TODO: Here if we *were* undefined we want to associate this symbol now with this section for
3575 // the flushMoved iteration
3576
3577 coff.initInputSectionSymbol(sym, sections[@intCast(@intFromEnum(sn) - 1)].si, symbol.value);
3578 } else if (!global_gop.found_existing) {
3579 sym.value = .{ .size = symbol.value };
3580 }
3581 },
3582 },
3583 else => {},
34903584 }
3491
3492 if (is_archive) {
3493 try coff.ensureMemberSymbol(mi, coff.getOrPutStringAssumeCapacity(name));
3494 continue;
3495 }
3496
3497 // Section numbers are 1-based here
3498 if (@intFromEnum(symbol.section_number) <= 0 or @intFromEnum(symbol.section_number) > sections.len)
3499 return diags.failParse(path, "bad section number {d} for '{s}'", .{ symbol.section_number, name });
3500
3501 const global_gop = try coff.getOrPutGlobalSymbol(.{ .name = name });
3502 // TODO: Support weak symbols
3503 if (global_gop.found_existing)
3504 return diags.failParse(path, "multiple definitions of '{s}'", .{name});
3505 si.* = global_gop.value_ptr.*;
3506
3507 const section = sections[@intCast(@intFromEnum(symbol.section_number) - 1)];
3508 const section_sym = section.si.get(coff);
3509
3510 const sym = si.get(coff);
3511 sym.ni = section_sym.ni;
3512 sym.value = .{ .input_offset = symbol.value };
3513 sym.section_number = section_sym.section_number;
35143585 }
35153586
35163587 if (coff.symbols.items.len > first_si) {
......@@ -3539,16 +3610,17 @@ fn loadObject(
35393610 if (reloc.symbol_table_index >= symbols.items.len)
35403611 return diags.failParse(
35413612 path,
3542 "relocation {d} in section '{s}' targets invalid symbol index {d}",
3613 "relocation 0x{x} in section '{s}' targets invalid symbol index 0x{x}",
35433614 .{ reloc_i, section.name.toSlice(coff), reloc.symbol_table_index },
35443615 );
35453616
3617 assert(symbols.items[reloc.symbol_table_index] != .null);
35463618 try coff.addReloc(
35473619 section.si,
35483620 reloc.virtual_address - section.header.virtual_address,
35493621 symbols.items[reloc.symbol_table_index],
35503622 .pending,
3551 @bitCast(reloc.type), // TODO: Checks on this cast?
3623 @bitCast(reloc.type),
35523624 );
35533625 }
35543626 }
......@@ -3602,6 +3674,8 @@ fn loadDll(coff: *Coff, path: std.Build.Cache.Path, fr: *Io.File.Reader) !void {
36023674pub fn prelink(coff: *Coff, prog_node: std.Progress.Node) link.Error!void {
36033675 _ = coff;
36043676 _ = prog_node;
3677
3678 log.debug("prelink()", .{});
36053679}
36063680
36073681pub fn updateNav(coff: *Coff, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) !void {
......@@ -3888,6 +3962,126 @@ fn flushImplib(
38883962 try file_writer.interface.flush();
38893963}
38903964
3965fn reportUndefs(coff: *Coff, tid: Zcu.PerThread.Id) !void {
3966 const comp = coff.base.comp;
3967 const gpa = comp.gpa;
3968 const max_notes = 4;
3969
3970 var undef_indices: std.ArrayListUnmanaged(u32) = .empty;
3971 for (coff.relocs.items, 0..) |reloc, reloc_i| {
3972 const target_sym = reloc.target.get(coff);
3973 switch (target_sym.ni) {
3974 .none => {
3975 assert(target_sym.gmi != .none);
3976 (try undef_indices.addOne(gpa)).* = @intCast(reloc_i);
3977 },
3978 else => continue,
3979 }
3980 }
3981
3982 if (undef_indices.items.len == 0) return;
3983
3984 const undefLessThan = struct {
3985 fn lessThan(ctx: *const Coff, lhs: u32, rhs: u32) bool {
3986 const reloc_l = &ctx.relocs.items[lhs];
3987 const reloc_r = &ctx.relocs.items[rhs];
3988 if (reloc_l.target == reloc_r.target)
3989 return @intFromEnum(reloc_l.loc) < @intFromEnum(reloc_r.loc)
3990 else
3991 return @intFromEnum(reloc_l.target) < @intFromEnum(reloc_r.target);
3992 }
3993 }.lessThan;
3994
3995 std.mem.sortUnstable(u32, undef_indices.items, coff, undefLessThan);
3996
3997 var start_i: usize = 0;
3998 var num_unique_references: usize = 1;
3999 for (undef_indices.items[0..], 0..) |reloc_i, i| {
4000 const target = coff.relocs.items[undef_indices.items[start_i]].target;
4001 if (target != coff.relocs.items[reloc_i].target or i == undef_indices.items.len - 1) {
4002 defer {
4003 start_i = i;
4004 num_unique_references = 1;
4005 }
4006
4007 const num_references = i - start_i;
4008 const num_notes =
4009 @min(max_notes, num_unique_references) +
4010 @intFromBool(num_unique_references > max_notes);
4011
4012 var err = try comp.link_diags.addErrorWithNotes(num_notes);
4013 const target_sym = target.get(coff);
4014 try err.addMsg("undefined symbol: {s}", .{target_sym.gmi.globalName(coff).name.toSlice(coff)});
4015
4016 var prev_loc_si: Symbol.Index = .null;
4017 for (undef_indices.items[start_i..][0..num_references]) |reference_i| {
4018 if (err.note_slot == num_notes) break;
4019
4020 const loc_si = coff.relocs.items[reference_i].loc;
4021 if (loc_si == prev_loc_si) continue;
4022 defer prev_loc_si = loc_si;
4023
4024 const loc_sym = loc_si.get(coff);
4025 switch (coff.getNode(loc_sym.ni)) {
4026 .input_section => |isi| {
4027 const other_ii = isi.input(coff);
4028 if (loc_sym.gmi == .none) {
4029 // TODO: We could report the name here if we interned it in loadObject
4030 err.addNote("referenced internally by input '{f}{f}'", .{
4031 other_ii.path(coff).fmtEscapeString(),
4032 fmtArchiveNameString(other_ii.archiveName(coff)),
4033 });
4034 } else {
4035 err.addNote("referenced by input symbol '{s}' from '{f}{f}'", .{
4036 loc_sym.gmi.globalName(coff).name.toSlice(coff),
4037 other_ii.path(coff).fmtEscapeString(),
4038 fmtArchiveNameString(other_ii.archiveName(coff)),
4039 });
4040 }
4041 },
4042 .global => |gmi| err.addNote("referenced by '{s}' in module '{s}'", .{
4043 gmi.globalName(coff).name.toSlice(coff),
4044 comp.zcu.?.root_mod.fully_qualified_name,
4045 }),
4046 inline .nav,
4047 .uav,
4048 .lazy_code,
4049 .lazy_const_data,
4050 => |val, tag| {
4051 err.addNote("referenced by '{f}'", .{
4052 format: switch (tag) {
4053 .nav => {
4054 const ip = &comp.zcu.?.intern_pool;
4055 break :format ip.getNav(val.navIndex(coff)).fqn.fmt(ip);
4056 },
4057 .uav => Value.fromInterned(val.uavValue(coff)).fmtValue(.{
4058 .zcu = coff.base.comp.zcu.?,
4059 .tid = tid,
4060 }),
4061 inline .lazy_code, .lazy_const_data => Type.fromInterned(val.lazySymbol(coff).ty).fmt(.{
4062 .zcu = coff.base.comp.zcu.?,
4063 .tid = tid,
4064 }),
4065 else => unreachable,
4066 },
4067 });
4068 },
4069 else => unreachable,
4070 }
4071 }
4072
4073 if (num_unique_references > max_notes)
4074 err.addNote("referenced {d} more times", .{num_references - max_notes});
4075 } else if (i != start_i and
4076 coff.relocs.items[undef_indices.items[i - 1]].loc != coff.relocs.items[undef_indices.items[i]].loc)
4077 {
4078 num_unique_references += 1;
4079 }
4080 }
4081
4082 return error.LinkFailure;
4083}
4084
38914085pub fn flush(
38924086 coff: *Coff,
38934087 arena: std.mem.Allocator,
......@@ -3897,6 +4091,7 @@ pub fn flush(
38974091 _ = arena;
38984092 _ = prog_node;
38994093 while (try coff.idle(tid)) {}
4094 try coff.reportUndefs(tid);
39004095
39014096 const comp = coff.base.comp;
39024097
......@@ -4132,7 +4327,7 @@ fn idleProgNode(
41324327 break :name std.fmt.bufPrint(&name, "{f}{f} {s}", .{
41334328 ii.path(coff).fmtEscapeString(),
41344329 fmtArchiveNameString(ii.archiveName(coff)),
4135 isi.symbol(coff).get(coff).section_number.name(coff).toSlice(coff),
4330 coff.getNode(isi.symbol(coff).node(coff).parent(&coff.mf)).object_section.name(coff).toSlice(coff),
41364331 }) catch &name;
41374332 },
41384333 .global => |gmi| gmi.globalName(coff).name.toSlice(coff),
......@@ -4386,6 +4581,10 @@ fn flushGlobal(coff: *Coff, gmi: Node.GlobalMapIndex) !void {
43864581 coff.nodes.appendAssumeCapacity(.{ .global = gmi });
43874582 sym.rva = coff.computeNodeRva(sym.ni);
43884583 si.applyLocationRelocs(coff);
4584 } else {
4585
4586 // TODO: If no .ni, report symbol not found - or should it be right when it's added as a global if we don't know about it?
4587
43894588 }
43904589}
43914590
......@@ -4505,6 +4704,10 @@ fn flushMoved(coff: *Coff, ni: MappedFile.Node.Index) !void {
45054704 const ii = isi.input(coff);
45064705 var si = ii.firstSymbol(coff);
45074706 const last_si = ii.lastSymbol(coff);
4707
4708 // TODO: This iteration doesn't visit symbols that were added first
4709 // in the range of another section (as undef).
4710
45084711 while (@intFromEnum(si) <= @intFromEnum(last_si)) : (si = si.next()) {
45094712 if (si.get(coff).ni != ni) continue;
45104713 si.flushMoved(coff);
......@@ -5065,7 +5268,7 @@ pub fn printNode(
50655268 try w.print("({f}{f}, {s})", .{
50665269 ii.path(coff).fmtEscapeString(),
50675270 fmtArchiveNameString(ii.archiveName(coff)),
5068 isi.symbol(coff).get(coff).section_number.name(coff).toSlice(coff),
5271 coff.getNode(isi.symbol(coff).node(coff).parent(&coff.mf)).object_section.name(coff).toSlice(coff),
50695272 });
50705273 },
50715274 .import_lookup_table,