From 2afb78e49877621993ebeb4441a3e3ba1d73c8cd Mon Sep 17 00:00:00 2001 From: Nikita Smith Date: Wed, 16 Sep 2026 15:12:15 -0700 Subject: [PATCH] radlink: resolve alternate-name fallbacks through prevailing weak symbols Follow prevailing weak definitions instead of synthetic anti-dependency placeholders. Resolve all weak symbols before publishing results so anti-dependency chaining rules are independent of worker order. --- src/linker/lnk.c | 23 +-- src/linker/lnk_symbol_table.c | 147 +++++++++----- src/linker/tests/linker_tests.c | 339 ++++++++++++++++++++++++++------ 3 files changed, 387 insertions(+), 122 deletions(-) diff --git a/src/linker/lnk.c b/src/linker/lnk.c index 6afd95d5..ad666b0d 100644 --- a/src/linker/lnk.c +++ b/src/linker/lnk.c @@ -1154,14 +1154,14 @@ internal LNK_Link * lnk_link_init(TP_Arena *arena, LNK_Config *config) { LNK_Link *link = push_array(arena->v[0], LNK_Link, 1); - link->arena = arena_alloc(.name = "LINK"); - link->last_symbol_input = &link->objs.first; - link->last_include = &config->include_symbol_list.first; + link->arena = arena_alloc(.name = "LINK"); + link->last_symbol_input = &link->objs.first; + link->last_include = &config->include_symbol_list.first; link->last_func_override_alt_name = &config->alt_name_list.first; - link->last_default_lib = &config->input_default_lib_list.first; - link->last_obj_lib = &config->input_obj_lib_list.first; - link->last_cmd_lib = &config->input_list[LNK_Input_Lib].first; - link->try_to_resolve_entry_point = 1; + link->last_default_lib = &config->input_default_lib_list.first; + link->last_obj_lib = &config->input_obj_lib_list.first; + link->last_cmd_lib = &config->input_list[LNK_Input_Lib].first; + link->try_to_resolve_entry_point = 1; return link; } @@ -1924,14 +1924,13 @@ lnk_link_inputs(TP_Context *tp, search_anti_deps = 0; // replace undefined symbols that have an alternate name with a weak symbol - for (LNK_AltNameNode *alt_name_n = config->alt_name_list.first; alt_name_n != 0; alt_name_n = alt_name_n->next) { + for EachNode(alt_name_n, LNK_AltNameNode, config->alt_name_list.first) { LNK_SymbolHashTrie *symbol_ht = lnk_symbol_table_search_(symtab, alt_name_n->v.from); if (symbol_ht) { COFF_SymbolValueInterpType interp = lnk_interp_from_symbol(symbol_ht->symbol); if (interp == COFF_SymbolValueInterp_Undefined) { - // clear out slot so weak symbol can replace undefined symbol (general rule is - // weak symbol is not allowed to replace undefined) - LNK_Symbol *undef_symbol = symbol_ht->symbol; + // /ALTERNATENAME explicitly installs a fallback for an unresolved name. + // Normally, an undefined external prevails over a SERACH_LIBRARY weak symbol. symbol_ht->symbol = 0; // make obj with alternamte name symbol @@ -1945,10 +1944,12 @@ lnk_link_inputs(TP_Context *tp, coff_obj_writer_release(&obj_writer); } + // input synthetic object LNK_Obj *obj_with_alt_name = alt_name_n->v.obj; String8 obj_with_alt_name_path = obj_with_alt_name ? obj_with_alt_name->path : str8_lit("RADLINK"); lnk_inputer_push_obj_linkgen(inputer, obj_with_alt_name ? obj_with_alt_name->link_member : 0, obj_with_alt_name_path, alt_name_obj_data); + // request another round of anti-dependency symbols search search_anti_deps = 1; } } diff --git a/src/linker/lnk_symbol_table.c b/src/linker/lnk_symbol_table.c index 9fd5e51e..d0578332 100644 --- a/src/linker/lnk_symbol_table.c +++ b/src/linker/lnk_symbol_table.c @@ -37,6 +37,12 @@ lnk_make_symbol(Arena *arena, String8 name, LNK_Obj *obj, U32 symbol_idx, LNK_Sy return symbol; } +internal B32 +lnk_obj_symbol_ref_match(LNK_ObjSymbolRef a, LNK_ObjSymbolRef b) +{ + return MemoryMatchStruct(&a, &b); +} + internal LNK_ObjSymbolRefNode * lnk_last_ref_from_symbol(LNK_Symbol *symbol) { @@ -717,69 +723,92 @@ lnk_resolve_weak_symbol(LNK_SymbolTable *symtab, LNK_ObjSymbolRef symbol, LNK_Ob { Temp scratch = scratch_begin(0,0); - B32 is_resolved = 0; - - struct S { struct S *next; LNK_ObjSymbolRef symbol; B32 is_anti_dep; }; + struct S { struct S *next; LNK_ObjSymbolRef symbol; }; struct S *sf = 0, *sl = 0; - LNK_ObjSymbolRef current_symbol = symbol; + LNK_ObjSymbolRef current_symbol = symbol; + B32 followed_fallback = 0; + B32 is_resolved = 0; for (;;) { - // guard against self-referencing weak symbols - struct S *was_visited = 0; - for (struct S *s = sf; s != 0; s = s->next) { - if (MemoryCompare(&s->symbol, ¤t_symbol, sizeof(LNK_ObjSymbolRef)) == 0) { was_visited = s; break; } - } - if (was_visited) { - String8List chain = {0}; - for (struct S *s = sf; s != 0; s = s->next) { - COFF_ParsedSymbol s_parsed = lnk_parsed_symbol_from_coff_symbol_idx(s->symbol.obj, s->symbol.symbol_idx); - str8_list_pushf(scratch.arena, &chain, "\t%S Symbol %S (No. %#x) =>", s->symbol.obj->path, s_parsed.name, s->symbol.symbol_idx); + // guard against cyclic weak symbols + { + struct S *was_visited = 0; + for EachNode(s, struct S, sf) { + if (s->symbol.obj == current_symbol.obj && s->symbol.symbol_idx == current_symbol.symbol_idx) { was_visited = s; break; } } - COFF_ParsedSymbol symbol_parsed = lnk_parsed_symbol_from_coff_symbol_idx(symbol.obj, symbol.symbol_idx); - str8_list_pushf(scratch.arena, &chain, "\t%S Symbol %S (No. %#x)", sf->symbol.obj->path, symbol_parsed.name, sf->symbol.symbol_idx); + if (was_visited) { + String8List chain = {0}; + for EachNode(s, struct S, sf) { + COFF_ParsedSymbol s_parsed = lnk_parsed_symbol_from_coff_symbol_idx(s->symbol.obj, s->symbol.symbol_idx); + str8_list_pushf(scratch.arena, &chain, "\t%S Symbol %S (No. %#x) =>", s->symbol.obj->path, s_parsed.name, s->symbol.symbol_idx); + } + COFF_ParsedSymbol symbol_parsed = lnk_parsed_symbol_from_coff_symbol_idx(symbol.obj, symbol.symbol_idx); + str8_list_pushf(scratch.arena, &chain, "\t%S Symbol %S (No. %#x)", sf->symbol.obj->path, symbol_parsed.name, sf->symbol.symbol_idx); - String8 chain_string = str8_list_join(scratch.arena, &chain, &(StringJoin){ .sep = str8_lit("\n") }); - lnk_error_obj(LNK_Error_WeakCycle, symbol.obj, "unable to resolve cyclic symbol %S; ref chain:\n%S", symbol_parsed.name, chain_string); + String8 chain_string = str8_list_join(scratch.arena, &chain, &(StringJoin){ .sep = str8_lit("\n") }); + lnk_error_obj(LNK_Error_WeakCycle, symbol.obj, "unable to resolve cyclic symbol %S; ref chain:\n%S", symbol_parsed.name, chain_string); - goto exit; + goto exit; + } } COFF_ParsedSymbol current_parsed = lnk_parsed_symbol_from_coff_symbol_idx_no_name(current_symbol.obj, current_symbol.symbol_idx); COFF_SymbolValueInterpType current_interp = coff_interp_symbol(current_parsed.section_number, current_parsed.value, current_parsed.storage_class); + if (current_interp == COFF_SymbolValueInterp_Weak) { // record visited symbol - struct S *s = push_array(scratch.arena, struct S, 1); - s->symbol = current_symbol; - SLLQueuePush(sf, sl, s); + { + struct S *s = push_array(scratch.arena, struct S, 1); + s->symbol = current_symbol; + SLLQueuePush(sf, sl, s); + } - // does weak symbol have a definition? - LNK_Symbol *defn_symbol = lnk_symbol_table_search(symtab, lnk_symbol_name_from_coff_symbol_idx(current_symbol.obj, current_symbol.symbol_idx)); - COFF_ParsedSymbol defn_parsed = lnk_parsed_from_symbol(defn_symbol); - COFF_SymbolValueInterpType defn_interp = coff_interp_symbol(defn_parsed.section_number, defn_parsed.value, defn_parsed.storage_class); - if (defn_interp != COFF_SymbolValueInterp_Weak) { - current_symbol = lnk_ref_from_symbol(defn_symbol); - break; + // follow the leader symbol even when it is another weak symbol + { + LNK_Symbol *leader_symbol = lnk_symbol_table_search(symtab, lnk_symbol_name_from_coff_symbol_idx(current_symbol.obj, current_symbol.symbol_idx)); + if (leader_symbol) { + LNK_ObjSymbolRef leader_ref = lnk_ref_from_symbol(leader_symbol); + if ( ! lnk_obj_symbol_ref_match(leader_ref, current_symbol)) { + current_symbol = leader_ref; + continue; + } + } } COFF_SymbolWeakExt *weak_ext = coff_parse_weak_tag(current_parsed, current_symbol.obj->coff.header.is_big_obj); + // + // An anti-dependency can resolve its own direct fallback, but cannot be + // used as a link in another alias's chain. Test the prevailing record's + // kind, before any weak symbols have been flattened to their defaults. + // + // Given this chain: + // foo [WeakAlias] -> bar [AntiDependency] -> qwe [Defined] + // + // * A relocation referencing 'foo' cannot follow through 'bar'; 'foo' remains unresolved. + // * A relocation referencing 'bar' directly resolves to 'qwe'. The relocation reference itself is not an alias-chain step. + // + if (followed_fallback && weak_ext->characteristics == COFF_WeakExt_AntiDependency) { break; } + // no definition -- fallback to default symbol COFF_ParsedSymbol tag_parsed = lnk_parsed_symbol_from_coff_symbol_idx_no_name(current_symbol.obj, weak_ext->tag_index); COFF_SymbolValueInterpType tag_interp = coff_interp_symbol(tag_parsed.section_number, tag_parsed.value, tag_parsed.storage_class); current_symbol = (LNK_ObjSymbolRef){ .obj = current_symbol.obj, .symbol_idx = weak_ext->tag_index }; + followed_fallback = 1; if (weak_ext->characteristics == COFF_WeakExt_AntiDependency) { if (tag_interp == COFF_SymbolValueInterp_Undefined || tag_interp == COFF_SymbolValueInterp_Weak) { LNK_Symbol *dep_symbol = lnk_symbol_table_search(symtab, lnk_symbol_name_from_coff_symbol_idx(current_symbol.obj, weak_ext->tag_index)); - tag_interp = lnk_interp_from_symbol(dep_symbol); + if (dep_symbol) { tag_interp = lnk_interp_from_symbol(dep_symbol); } } if (tag_interp == COFF_SymbolValueInterp_Weak) { break; } } } else if (current_interp == COFF_SymbolValueInterp_Undefined) { - LNK_Symbol *defn_symbol = lnk_symbol_table_search(symtab, lnk_symbol_name_from_coff_symbol_idx(current_symbol.obj, current_symbol.symbol_idx)); - COFF_SymbolValueInterpType defn_interp = lnk_interp_from_symbol(defn_symbol); + LNK_Symbol *defn_symbol = lnk_symbol_table_search(symtab, lnk_symbol_name_from_coff_symbol_idx(current_symbol.obj, current_symbol.symbol_idx)); + if (defn_symbol == 0) { break; } - // unresolved undefined symbol + // unresolved symbol + COFF_SymbolValueInterpType defn_interp = lnk_interp_from_symbol(defn_symbol); if (defn_interp == COFF_SymbolValueInterp_Undefined) { break; } // follow symbol definition @@ -846,6 +875,19 @@ lnk_resolve_symbol(LNK_SymbolTable *symtab, LNK_ObjSymbolRef symbol, LNK_ObjSymb internal THREAD_POOL_TASK_FUNC(lnk_replace_weak_with_default_symbol_task) { + Temp scratch = scratch_begin(0,0); + + struct Resolution { + struct Resolution *next; + LNK_Symbol *symbol; + LNK_ObjSymbolRef target; + B32 resolved; + }; + struct Resolution *first = 0, *last = 0; + + // Resolve against a stable symbol table. Updating the symbol table in this loop + // would make alias/anti-dependency semantics depend on traversal order and + // allow other workers to read symbol records while they are being rewritten. LNK_SymbolTable *symtab = raw_task; for EachNode(c, LNK_SymbolHashTrieChunk, symtab->search_chunks[task_id].first) { for EachIndex(i, c->count) { @@ -853,25 +895,38 @@ THREAD_POOL_TASK_FUNC(lnk_replace_weak_with_default_symbol_task) LNK_ObjSymbolRef symbol_ref = lnk_ref_from_symbol(symbol); COFF_ParsedSymbol symbol_parsed = lnk_parsed_symbol_from_coff_symbol_idx_no_name(symbol_ref.obj, symbol_ref.symbol_idx); COFF_SymbolValueInterpType symbol_interp = coff_interp_from_parsed_symbol(symbol_parsed); + if (symbol_interp == COFF_SymbolValueInterp_Weak) { - LNK_ObjSymbolRef resolve = {0}; - if (lnk_resolve_weak_symbol(symtab, symbol_ref, &resolve)) { - COFF_ParsedSymbol resolve_parsed = lnk_parsed_symbol_from_coff_symbol_idx_no_name(resolve.obj, resolve.symbol_idx); + struct Resolution *r = push_array(scratch.arena, struct Resolution, 1); + r->symbol = symbol; + SLLQueuePush(first, last, r); + + if (lnk_resolve_weak_symbol(symtab, symbol_ref, &r->target)) { + COFF_ParsedSymbol resolve_parsed = lnk_parsed_symbol_from_coff_symbol_idx_no_name(r->target.obj, r->target.symbol_idx); COFF_SymbolValueInterpType resolve_interp = coff_interp_from_parsed_symbol(resolve_parsed); - if (resolve_interp == COFF_SymbolValueInterp_Weak) { - U64 primary_idx = lnk_obj_primary_symbol_idx_from_coff_symbol_idx(symbol_ref.obj, symbol_ref.symbol_idx); - symbol_ref.obj->coff.symbols.section_numbers[primary_idx] = COFF_Symbol_UndefinedSection; - symbol_ref.obj->coff.symbols.values [primary_idx] = 0; - symbol_ref.obj->coff.symbols.storage_classes[primary_idx] = COFF_SymStorageClass_External; - lnk_symbol_set_search_type(symbol, LNK_SymbolSearch_Undefined); - } else { - symbol->first_ref->v = resolve; - lnk_symbol_set_search_type(symbol, LNK_SymbolSearch_Null); - } + r->resolved = resolve_interp != COFF_SymbolValueInterp_Weak && resolve_interp != COFF_SymbolValueInterp_Undefined; } } } } + barrier_wait(tp->barrier); + + for EachNode(r, struct Resolution, first) { + LNK_Symbol *symbol = r->symbol; + if (r->resolved) { + symbol->first_ref->v = r->target; + lnk_symbol_set_search_type(symbol, LNK_SymbolSearch_Null); + } else { + LNK_ObjSymbolRef symbol_ref = lnk_ref_from_symbol(symbol); + U64 primary_idx = lnk_obj_primary_symbol_idx_from_coff_symbol_idx(symbol_ref.obj, symbol_ref.symbol_idx); + symbol_ref.obj->coff.symbols.section_numbers[primary_idx] = COFF_Symbol_UndefinedSection; + symbol_ref.obj->coff.symbols.values [primary_idx] = 0; + symbol_ref.obj->coff.symbols.storage_classes[primary_idx] = COFF_SymStorageClass_External; + lnk_symbol_set_search_type(symbol, LNK_SymbolSearch_Undefined); + } + } + + scratch_end(scratch); } internal void diff --git a/src/linker/tests/linker_tests.c b/src/linker/tests/linker_tests.c index 42cd74fd..8622aec9 100644 --- a/src/linker/tests/linker_tests.c +++ b/src/linker/tests/linker_tests.c @@ -4786,99 +4786,308 @@ TEST(sect_align) TEST(alt_name) { - T_Ok(t_write_def_obj("test.obj", (T_COFF_DefObj){ - .machine = T_COFF_DefSetMachine(X64), + // Inspect an RVA relocation instead of merely accepting a successful link. + // Separate sections and nonzero symbol offsets distinguish the selected target + // without depending on a particular linker's section ordering or image layout. + T_COFF_DefObj target = { + .machine = T_COFF_DefSetMachine(X64), + .path = str8_lit("target.obj"), .sections = (T_COFF_DefSection[]){ - { "data", ".data", str8_lit("test"), .flags = "rw:data" }, + { "data", ".target", str8_lit("pad!TEST"), .flags = "rw:data@4" }, {0} }, .symbols = (T_COFF_DefSymbol[]){ - T_COFF_DefSymbol_Extern("test", "data", 0), + T_COFF_DefSymbol_Extern("test", "data", 4), {0} } - })); + }; - T_Ok(t_write_def_obj("foo.obj", (T_COFF_DefObj){ - .machine = T_COFF_DefSetMachine(X64), + T_COFF_DefObj primary = { + .machine = T_COFF_DefSetMachine(X64), + .path = str8_lit("foo.obj"), .sections = (T_COFF_DefSection[]){ - { "data", ".data", str8_lit("foo"), .flags = "rw:data" }, + { "data", ".primary", str8_lit("pad!REAL"), .flags = "rw:data@4" }, {0} }, .symbols = (T_COFF_DefSymbol[]){ - T_COFF_DefSymbol_Extern("foo", "data", 0), + T_COFF_DefSymbol_Extern("foo", "data", 4), + {0} + } + }; + + T_COFF_DefObj weak = { + .machine = T_COFF_DefSetMachine(X64), + .path = str8_lit("weak.obj"), + .symbols = (T_COFF_DefSymbol[]){ + T_COFF_DefSymbol_Undef("test"), + T_COFF_DefSymbol_Weak("altsym", COFF_WeakExt_SearchAlias, "test"), + T_COFF_DefSymbol_AbsExtern("alias_anchor", 1), + {0} + } + }; + + T_Ok(t_write_def_obj("target.obj", target)); + T_Ok(t_write_def_obj("foo.obj", primary)); + T_Ok(t_write_def_obj("weak.obj", weak)); + T_Ok(t_write_entry_obj()); + T_Ok(t_write_def_obj("ref.obj", (T_COFF_DefObj){ + .machine = T_COFF_DefSetMachine(X64), + .sections = (T_COFF_DefSection[]){ + { "probe", ".probe", str8_lit_comp("\0\0\0\0"), .flags = "rw:data@4", + .relocs = (T_COFF_DefReloc[]){ T_COFF_DefReloc(X64_Addr32Nb, 0, "foo"), {0} } }, + {0} + }, + .symbols = (T_COFF_DefSymbol[]){ T_COFF_DefSymbol_Undef("foo"), {0} } + })); + + // Use ADDR32 for the absolute target: LINK rejects absolute ADDR32NB fixups. + T_Ok(t_write_def_obj("ref_absolute.obj", (T_COFF_DefObj){ + .machine = T_COFF_DefSetMachine(X64), + .sections = (T_COFF_DefSection[]){ + { "probe", ".probe", str8_lit_comp("\0\0\0\0"), .flags = "rw:data@4", + .relocs = (T_COFF_DefReloc[]){ T_COFF_DefReloc(X64_Addr32, 0, "foo"), {0} } }, + {0} + }, + .symbols = (T_COFF_DefSymbol[]){ T_COFF_DefSymbol_Undef("foo"), {0} } + })); + + // Same-object weak target, as in LLVM's alternatename-alias.s. + T_Ok(t_write_def_obj("weak_local.obj", (T_COFF_DefObj){ + .machine = T_COFF_DefSetMachine(X64), + .sections = target.sections, + .symbols = (T_COFF_DefSymbol[]){ + T_COFF_DefSymbol_Extern("test", "data", 4), + T_COFF_DefSymbol_Weak("altsym", COFF_WeakExt_SearchAlias, "test"), {0} } })); - - T_Ok(t_write_def_obj("entry.obj", (T_COFF_DefObj){ - .machine = T_COFF_DefSetMachine(X64), + // A local default is identified by its object-local tag, not its spelling. + T_Ok(t_write_def_obj("weak_static.obj", (T_COFF_DefObj){ + .machine = T_COFF_DefSetMachine(X64), + .sections = target.sections, + .symbols = (T_COFF_DefSymbol[]){ + T_COFF_DefSymbol_Static("local_target", "data", 4), + T_COFF_DefSymbol_Weak("altsym", COFF_WeakExt_SearchAlias, "local_target"), + {0} + } + })); + T_Ok(t_write_def_obj("global_target.obj", (T_COFF_DefObj){ + .machine = T_COFF_DefSetMachine(X64), + .sections = primary.sections, + .symbols = (T_COFF_DefSymbol[]){ T_COFF_DefSymbol_Extern("local_target", "data", 4), {0} } + })); + T_Ok(t_write_def_obj("override.obj", (T_COFF_DefObj){ + .machine = T_COFF_DefSetMachine(X64), .sections = (T_COFF_DefSection[]){ - { - "text", ".text", - str8_lit_comp( - "\x48\xC7\xC0\x00\x00\x00\x00" // mov rax, $imm - "\xC3" - ), // ret - .flags = "rx:code", - .relocs = (T_COFF_DefReloc[]){ - T_COFF_DefReloc(X64_Addr32Nb, 0, "foo"), - {0} - } + { "data", ".chosen", str8_lit("pad!OVER"), .flags = "rw:data@4" }, {0} + }, + .symbols = (T_COFF_DefSymbol[]){ T_COFF_DefSymbol_Extern("altsym", "data", 4), {0} } + })); + T_Ok(t_write_def_obj("absolute.obj", (T_COFF_DefObj){ + .machine = T_COFF_DefSetMachine(X64), + .symbols = (T_COFF_DefSymbol[]){ T_COFF_DefSymbol_AbsExtern("absolute", 0x1234), {0} } + })); + + struct { + char *path; + char *name; + char *tag; + COFF_WeakExtType type; + } aliases[] = { + { "weak_chain.obj", "altsym", "middle", COFF_WeakExt_SearchAlias }, + { "weak_middle.obj", "middle", "test", COFF_WeakExt_SearchAlias }, + { "anti.obj", "altsym", "test", COFF_WeakExt_AntiDependency }, + { "anti_middle.obj", "middle", "test", COFF_WeakExt_AntiDependency }, + { "missing.obj", "altsym", "missing", COFF_WeakExt_SearchAlias }, + { "cycle_tail.obj", "middle", "altsym", COFF_WeakExt_SearchAlias }, + { "source_weak.obj", "foo", "test", COFF_WeakExt_SearchAlias }, + { "weak_nolib.obj", "altsym", "test", COFF_WeakExt_NoLibrary }, + { "weak_lib.obj", "altsym", "test", COFF_WeakExt_SearchLibrary }, + }; + for EachElement(i, aliases) { + T_Ok(t_write_def_obj(aliases[i].path, (T_COFF_DefObj){ + .machine = T_COFF_DefSetMachine(X64), + .symbols = (T_COFF_DefSymbol[]){ + T_COFF_DefSymbol_Undef(aliases[i].tag), + T_COFF_DefSymbol_Weak(aliases[i].name, aliases[i].type, aliases[i].tag), + {0} + } + })); + } + + T_Ok(t_write_def_obj("directive.obj", (T_COFF_DefObj){ .directives = (char *[]){ "/alternatename:foo=test", 0 } })); + T_Ok(t_write_def_obj("weak_directive.obj", (T_COFF_DefObj){ .directives = (char *[]){ "/alternatename:foo=altsym", 0 } })); + T_Ok(t_write_def_obj("conflict.obj", (T_COFF_DefObj){ .directives = (char *[]){ "/alternatename:foo=other", 0 } })); + T_Ok(t_write_file(str8_lit("alternate.rsp"), str8_lit("/alternatename:foo=test"))); + + T_COFF_DefObj lib_objs[] = { target, primary, weak }; + char *lib_paths[] = { "target.lib", "foo.lib", "weak.lib" }; + for EachElement(i, lib_objs) { + T_Ok(t_write_def_lib(lib_paths[i], (T_COFF_DefLib){ + .emit_second_member = 1, + .members = (T_COFF_DefLibMember[]){ { .type = T_COFF_DefLibMember_Obj, .obj = lib_objs[i] }, {0} } + })); + } + + // Extracting the fallback unnecessarily must fail, even if its data is unused. + T_Ok(t_write_def_lib("poison.lib", (T_COFF_DefLib){ + .emit_second_member = 1, + .members = (T_COFF_DefLibMember[]){ { .type = T_COFF_DefLibMember_Obj, .obj = { + .path = str8_lit("poison.obj"), + .machine = T_COFF_DefSetMachine(X64), + .sections = (T_COFF_DefSection[]){ + { "data", ".poison", str8_lit_comp("\0\0\0\0"), .flags = "rw:data@4", + .relocs = (T_COFF_DefReloc[]){ T_COFF_DefReloc(X64_Addr32Nb, 0, "poison_missing"), {0} } }, + {0} }, - {0} - }, - .symbols = (T_COFF_DefSymbol[]){ - T_COFF_DefSymbol_Extern("entry", "text", 0), - T_COFF_DefSymbol_Undef("foo"), - {0} - } + .symbols = (T_COFF_DefSymbol[]){ + T_COFF_DefSymbol_Extern("test", "data", 0), + T_COFF_DefSymbol_Undef("poison_missing"), + {0} + } + } }, {0} } })); - // basic alternate name test - t_invoke_linkerf("/subsystem:console /entry:entry /out:a.exe /alternatename:foo=test test.obj entry.obj"); - T_Ok(g_last_exit_code == 0); + struct { + char *name; + char *options; + char *inputs; + B32 success; + char *section; // NULL on success means an absolute symbol value. + U32 offset; + LNK_ErrorCode error; // Defaults to UnresolvedSymbol for negative cases. + } cases[] = { + { "direct", "/alternatename:foo=test", "target.obj", 1, ".target", 4 }, + { "identical_duplicate", "/alternatename:foo=test /alternatename:foo=test", "target.obj", 1, ".target", 4 }, + { "absolute", "/alternatename:foo=absolute", "absolute.obj", 1, 0, 0x1234 }, + { "primary_before", "/alternatename:foo=test", "foo.obj target.obj", 1, ".primary", 4 }, + { "primary_after", "/alternatename:foo=test", "target.obj foo.obj", 1, ".primary", 4 }, + { "primary_missing_alt", "/alternatename:foo=missing", "foo.obj", 1, ".primary", 4 }, + { "primary_self", "/alternatename:foo=foo", "foo.obj", 1, ".primary", 4 }, + { "unused_mapping", "/alternatename:unused=missing", "foo.obj", 1, ".primary", 4 }, + { "unused_cycle", "/alternatename:unused=other /alternatename:other=unused", "foo.obj", 1, ".primary", 4 }, - // linker should not chase alt name links - t_invoke_linkerf("/subsystem:console /entry:entry /out:b.exe /alternatename:foo=bar /alternatename:bar=test test.obj entry.obj"); - T_Ok(g_last_exit_code != 0); + // Object-file and response-file directives have the same fallback semantics. + { "directive_before", "", "directive.obj target.obj", 1, ".target", 4 }, + { "directive_after", "", "target.obj directive.obj", 1, ".target", 4 }, + { "directive_duplicate", "/alternatename:foo=test", "directive.obj target.obj", 1, ".target", 4 }, + { "response_file", "@alternate.rsp", "target.obj", 1, ".target", 4 }, - // alt name conflict - t_invoke_linkerf("/subsystem:console /entry:entry /out:c.exe /alternatename:foo=test /alternatename:foo=qwe test.obj entry.obj"); - T_Ok(g_last_exit_code != 0); + // Archive search must prefer the primary name and avoid unused fallbacks. + { "archive_target", "/alternatename:foo=test", "target.lib", 1, ".target", 4 }, + { "archive_primary", "/alternatename:foo=test", "target.obj foo.lib", 1, ".primary", 4 }, + { "archives_before", "/alternatename:foo=test", "foo.lib target.lib", 1, ".primary", 4 }, + { "archives_after", "/alternatename:foo=test", "target.lib foo.lib", 1, ".primary", 4 }, + { "unused_fallback_lib", "/alternatename:foo=test", "foo.obj poison.lib", 1, ".primary", 4 }, + { "unused_mapping_lib", "/alternatename:unused=test", "foo.obj poison.lib", 1, ".primary", 4 }, + { "required_poison_lib", "/alternatename:foo=test", "poison.lib", 0 }, - // syntax error - t_invoke_linkerf("/subsystem:console /entry:entry /out:d.exe /alternatename:foo foo.obj entry.obj"); - T_Ok(g_last_exit_code != 0); - - // syntax error - t_invoke_linkerf("/subsystem:console /entry:entry /out:e.exe /alternatename:foo-oof foo.obj entry.obj"); - T_Ok(g_last_exit_code != 0); + // An ordinary weak alias is a valid target; its prevailing definition wins. + { "weak_local", "/alternatename:foo=altsym", "weak_local.obj", 1, ".target", 4 }, + { "weak_before", "/alternatename:foo=altsym", "weak.obj target.obj", 1, ".target", 4 }, + { "weak_after", "/alternatename:foo=altsym", "target.obj weak.obj", 1, ".target", 4 }, + { "weak_chain_before", "/alternatename:foo=altsym", "weak_chain.obj weak_middle.obj target.obj", 1, ".target", 4 }, + { "weak_chain_after", "/alternatename:foo=altsym", "target.obj weak_middle.obj weak_chain.obj", 1, ".target", 4 }, + { "weak_override_first", "/alternatename:foo=altsym", "override.obj weak.obj target.obj", 1, ".chosen", 4 }, + { "weak_override_last", "/alternatename:foo=altsym", "weak.obj target.obj override.obj", 1, ".chosen", 4 }, + { "weak_source", "/alternatename:foo=missing", "source_weak.obj target.obj", 1, ".target", 4 }, + { "weak_directive", "", "weak_directive.obj weak.obj target.obj", 1, ".target", 4 }, + { "weak_archive", "/alternatename:foo=altsym /include:alias_anchor", "weak.lib target.obj", 1, ".target", 4 }, + { "weak_target_archive", "/alternatename:foo=altsym", "weak.obj target.lib", 1, ".target", 4 }, + { "weak_nolib_target", "/alternatename:foo=altsym", "weak_nolib.obj target.obj", 1, ".target", 4 }, + { "weak_lib_target", "/alternatename:foo=altsym", "weak_lib.obj target.obj", 1, ".target", 4 }, + // LINK rejects a section-defined static weak default; RAD and LLD support it. + { "weak_static_target", "/alternatename:foo=altsym", "weak_static.obj global_target.obj", t_id_linker() != Linker_msvc, ".target", 4 }, - // syntax error - t_invoke_linkerf("/subsystem:console /entry:entry /out:a.exe /alternatename:foo=test=bar foo.obj entry.obj"); - T_Ok(g_last_exit_code != 0); + // altsym is discovered only after both synthetic fallback pairs exist. + { "late_weak_archive", "/alternatename:foo=altsym /alternatename:trigger=alias_anchor /include:trigger", "weak.lib target.obj", 1, ".target", 4 }, - // syntax error - t_invoke_linkerf("/subsystem:console /entry:entry /out:a.exe /alternatename:foo= foo.obj entry.obj"); - T_Ok(g_last_exit_code != 0); + // Unlike an ordinary alias, an anti-dependency cannot be chained through. + { "anti_before", "/alternatename:foo=altsym", "anti.obj target.obj", 0 }, + { "anti_after", "/alternatename:foo=altsym", "target.obj anti.obj", 0 }, + { "nested_anti", "/alternatename:foo=altsym", "weak_chain.obj anti_middle.obj target.obj", 0 }, + { "anti_override_first", "/alternatename:foo=altsym", "override.obj anti.obj target.obj", 1, ".chosen", 4 }, + { "anti_override_last", "/alternatename:foo=altsym", "anti.obj target.obj override.obj", 1, ".chosen", 4 }, - // syntax error - t_invoke_linkerf("/subsystem:console /entry:entry /out:a.exe /alternatename:= foo.obj entry.obj"); - T_Ok(g_last_exit_code != 0); + // Unreferenced intermediate command-line alternate names are not alias chains. + { "command_chain", "/alternatename:foo=bar /alternatename:bar=test", "target.obj", 0 }, + { "command_chain_order", "/alternatename:bar=test /alternatename:foo=bar", "target.obj", 0 }, + { "missing_target", "/alternatename:foo=missing", "target.obj", 0 }, + { "case_sensitive_from", "/alternatename:Foo=test", "target.obj", 0 }, + { "case_sensitive_to", "/alternatename:foo=TEST", "target.obj", 0 }, + { "missing_weak_target", "/alternatename:foo=altsym", "missing.obj", 0 }, + { "undefined_self", "/alternatename:foo=foo", "target.obj", 0 }, + { "command_cycle", "/alternatename:foo=bar /alternatename:bar=foo", "target.obj", 0 }, + { "weak_cycle", "/alternatename:foo=altsym", "weak_chain.obj cycle_tail.obj", 0 }, - // syntax error - t_invoke_linkerf("/subsystem:console /entry:entry /out:a.exe /alternatename: foo.obj entry.obj"); - T_Ok(g_last_exit_code != 0); + // RAD and LLD reject conflicts even for defined sources; LINK keeps the first mapping. + { "conflict", "/alternatename:foo=test /alternatename:foo=other", "target.obj", t_id_linker() == Linker_msvc, ".target", 4, LNK_Error_AlternateNameConflict }, + { "defined_conflict", "/alternatename:foo=test /alternatename:foo=other", "foo.obj", t_id_linker() == Linker_msvc, ".primary", 4, LNK_Error_AlternateNameConflict }, + { "directive_conflict", "/alternatename:foo=test", "conflict.obj target.obj", t_id_linker() == Linker_msvc, ".target", 4, LNK_Error_AlternateNameConflict }, + { "two_directives", "", "directive.obj conflict.obj target.obj", t_id_linker() == Linker_msvc, ".target", 4, LNK_Error_AlternateNameConflict }, + { "missing_equals", "/alternatename:foo", "foo.obj", 0, 0, 0, LNK_Error_Cmdl }, + { "wrong_separator", "/alternatename:foo-oof", "foo.obj", 0, 0, 0, LNK_Error_Cmdl }, + { "empty_target", "/alternatename:foo=", "foo.obj", 0, 0, 0, LNK_Error_Cmdl }, - // TODO: check that RAD Linker prints these warnings + // LINK accepts an empty (unused) source; RAD and LLD diagnose it. + { "empty_source", "/alternatename:=test", "foo.obj", t_id_linker() == Linker_msvc, ".primary", 4, LNK_Error_Cmdl }, + { "empty_both", "/alternatename:=", "foo.obj", 0, 0, 0, LNK_Error_Cmdl }, + { "empty_option", "/alternatename:", "foo.obj", 0, 0, 0, LNK_Error_Cmdl }, + + // LLD and LINK accept the extra '=' with a defined source; RAD rejects it. + { "extra_equals", "/alternatename:foo=test=bar", "foo.obj", t_id_linker() != Linker_radlink, ".primary", 4, LNK_Error_Cmdl }, + }; - // warn about alt name to self alt name? - t_invoke_linkerf("/subsystem:console /entry:entry /out:f.exe /alternatename:foo=foo foo.obj entry.obj"); - T_Ok(g_last_exit_code == 0); + B32 all_ok = 1; + U32 worker_counts[] = { 1, 4 }; + U64 pass_count = t_id_linker() == Linker_radlink ? ArrayCount(worker_counts) : 1; + for EachIndex(pass_idx, pass_count) { + for EachElement(case_idx, cases) { + String8 out_name = str8f(arena, "alt_%s_%u.exe", cases[case_idx].name, worker_counts[pass_idx]); + String8 workers = t_id_linker() == Linker_radlink ? str8f(arena, "/rad_workers:%u", worker_counts[pass_idx]) : str8_zero(); + char *ref_obj = cases[case_idx].success && !cases[case_idx].section ? "ref_absolute.obj" : "ref.obj"; + String8 cmdline = str8f(arena, "/subsystem:console /entry:entry /nodefaultlib /opt:noref,noicf /out:%S %S %s entry.obj %s %s", + out_name, workers, cases[case_idx].options, ref_obj, cases[case_idx].inputs); + test_outf("alt_name %s (workers %u): %S\n", cases[case_idx].name, worker_counts[pass_idx], cmdline); - // warn about alt name to unknown symbol? - t_invoke_linkerf("/subsystem:console /entry:entry /out:g.exe /alternatename:qwe=ewq foo.obj entry.obj"); - T_Ok(g_last_exit_code == 0); + // Bound cycles require a normal exit: a crash/timeout is not an expected failure. + B32 invoked = t_invoke(t_linker_path(), cmdline, 10 * 1000 * 1000); + B32 normal_exit = invoked && g_last_exit_code < 0x80000000 && g_last_exit_code != 999; + B32 case_ok = normal_exit && ((g_last_exit_code == 0) == cases[case_idx].success); + if (case_ok && !cases[case_idx].success && t_id_linker() == Linker_radlink) { + LNK_ErrorCode expected_error = cases[case_idx].error ? cases[case_idx].error : LNK_Error_UnresolvedSymbol; + case_ok = g_last_exit_code == expected_error; + if (!case_ok) { test_outf(" expected error code %u\n", expected_error); } + } + + if (normal_exit && g_last_exit_code == 0 && cases[case_idx].success) { + String8 image = t_read_file(arena, out_name); + PE_BinInfo pe = pe_bin_info_from_data(arena, image); + COFF_SectionHeader *sections = (COFF_SectionHeader *)str8_substr(image, pe.section_table_range).str; + String8 strings = str8_substr(image, pe.string_table_range); + COFF_SectionHeader *probe = coff_section_header_from_name(strings, sections, pe.section_count, str8_lit(".probe")); + U64 expected_rva = cases[case_idx].offset; + + case_ok = pe.arch == Arch_x64 && probe && probe->vsize == sizeof(U32); + if (cases[case_idx].section) { + COFF_SectionHeader *dest = coff_section_header_from_name(strings, sections, pe.section_count, str8_cstring(cases[case_idx].section)); + case_ok &= dest != 0 && dest->vsize >= cases[case_idx].offset + sizeof(U32); + if (dest) { expected_rva += dest->voff; } + } + + if (case_ok) { + String8 data = str8_substr(image, rng_1u64(probe->foff, (U64)probe->foff + sizeof(U32))); + case_ok = data.size == sizeof(U32) && memory_read32(data.str) == expected_rva; + if (!case_ok) { test_outf(" incorrect relocated RVA; expected %#llx\n", expected_rva); } + } + } + + test_outf(" %s: normal exit %u, exit code %llu, expected %s\n", case_ok ? "PASS" : "FAIL", normal_exit, g_last_exit_code, cases[case_idx].success ? "success with correct RVA" : "failure"); + T_Ok(case_ok); + } + } + + T_Ok(all_ok); } TEST(include)