From 5b646344eb1cdd9d69a35f6b60de7135dffa660d Mon Sep 17 00:00:00 2001 From: Nikita Smith Date: Tue, 15 Sep 2026 18:35:22 -0700 Subject: [PATCH] radlink: implement /NOENTRY --- src/coff/coff_parse.c | 86 ++++++++-------- src/linker/lnk.c | 16 +-- src/linker/lnk_config.c | 10 ++ src/linker/lnk_config.h | 2 + src/linker/tests/linker_tests.c | 169 ++++++++++++++++++++++++++++++++ 5 files changed, 234 insertions(+), 49 deletions(-) diff --git a/src/coff/coff_parse.c b/src/coff/coff_parse.c index c2d129f7..b5848cc8 100644 --- a/src/coff/coff_parse.c +++ b/src/coff/coff_parse.c @@ -166,20 +166,20 @@ coff_section_header_array_from_name(Arena *arena, String8 string_table, COFF_Sec } -internal COFF_ParsedSymbol -coff_parse_symbol32(String8 string_table, COFF_Symbol32 *sym32) -{ - COFF_ParsedSymbol result = coff_parse_symbol32_no_name(sym32); - result.name = coff_read_symbol_name(string_table, &sym32->name); - return result; -} - -internal force_inline COFF_ParsedSymbol -coff_parse_symbol32_no_name(COFF_Symbol32 *sym32) -{ - COFF_ParsedSymbol result = {0}; - result.value = sym32->value; - result.section_number = sym32->section_number; +internal COFF_ParsedSymbol +coff_parse_symbol32(String8 string_table, COFF_Symbol32 *sym32) +{ + COFF_ParsedSymbol result = coff_parse_symbol32_no_name(sym32); + result.name = coff_read_symbol_name(string_table, &sym32->name); + return result; +} + +internal force_inline COFF_ParsedSymbol +coff_parse_symbol32_no_name(COFF_Symbol32 *sym32) +{ + COFF_ParsedSymbol result = {0}; + result.value = sym32->value; + result.section_number = sym32->section_number; result.type = sym32->type; result.storage_class = sym32->storage_class; result.aux_symbol_count = sym32->aux_symbol_count; @@ -187,19 +187,19 @@ coff_parse_symbol32_no_name(COFF_Symbol32 *sym32) return result; } -internal COFF_ParsedSymbol -coff_parse_symbol16(String8 string_table, COFF_Symbol16 *sym16) -{ - COFF_ParsedSymbol result = coff_parse_symbol16_no_name(sym16); - result.name = coff_read_symbol_name(string_table, &sym16->name); - return result; -} - -internal force_inline COFF_ParsedSymbol -coff_parse_symbol16_no_name(COFF_Symbol16 *sym16) -{ - COFF_ParsedSymbol result = {0}; - result.value = sym16->value; +internal COFF_ParsedSymbol +coff_parse_symbol16(String8 string_table, COFF_Symbol16 *sym16) +{ + COFF_ParsedSymbol result = coff_parse_symbol16_no_name(sym16); + result.name = coff_read_symbol_name(string_table, &sym16->name); + return result; +} + +internal force_inline COFF_ParsedSymbol +coff_parse_symbol16_no_name(COFF_Symbol16 *sym16) +{ + COFF_ParsedSymbol result = {0}; + result.value = sym16->value; if (sym16->section_number == COFF_Symbol_DebugSection16) { result.section_number = COFF_Symbol_DebugSection32; } else if (sym16->section_number == COFF_Symbol_AbsSection16) { @@ -214,8 +214,8 @@ coff_parse_symbol16_no_name(COFF_Symbol16 *sym16) return result; } -internal COFF_ParsedSymbol -coff_parse_symbol(COFF_FileHeaderInfo header, String8 string_table, String8 symbol_table, U32 symbol_idx) +internal COFF_ParsedSymbol +coff_parse_symbol(COFF_FileHeaderInfo header, String8 string_table, String8 symbol_table, U32 symbol_idx) { COFF_ParsedSymbol symbol; if (header.is_big_obj) { @@ -223,20 +223,20 @@ coff_parse_symbol(COFF_FileHeaderInfo header, String8 string_table, String8 symb } else { symbol = coff_parse_symbol16(string_table, (COFF_Symbol16 *)symbol_table.str + symbol_idx); } - return symbol; -} - -internal force_inline COFF_ParsedSymbol -coff_parse_symbol_no_name(COFF_FileHeaderInfo header, String8 symbol_table, U32 symbol_idx) -{ - COFF_ParsedSymbol symbol; - if (header.is_big_obj) { - symbol = coff_parse_symbol32_no_name((COFF_Symbol32 *)symbol_table.str + symbol_idx); - } else { - symbol = coff_parse_symbol16_no_name((COFF_Symbol16 *)symbol_table.str + symbol_idx); - } - return symbol; -} + return symbol; +} + +internal force_inline COFF_ParsedSymbol +coff_parse_symbol_no_name(COFF_FileHeaderInfo header, String8 symbol_table, U32 symbol_idx) +{ + COFF_ParsedSymbol symbol; + if (header.is_big_obj) { + symbol = coff_parse_symbol32_no_name((COFF_Symbol32 *)symbol_table.str + symbol_idx); + } else { + symbol = coff_parse_symbol16_no_name((COFF_Symbol16 *)symbol_table.str + symbol_idx); + } + return symbol; +} internal COFF_Symbol32Array coff_symbol_array_from_data_16(Arena *arena, String8 raw_coff, U64 symbol_array_off, U64 symbol_count) diff --git a/src/linker/lnk.c b/src/linker/lnk.c index 0d908965..6afd95d5 100644 --- a/src/linker/lnk.c +++ b/src/linker/lnk.c @@ -1366,13 +1366,13 @@ lnk_load_inputs(TP_Context *tp, TP_Arena *arena, LNK_Config *config, LNK_Inputer // load new libs lnk_load_libs(tp, arena, config, inputer, link); - // resolve entry point + // resolve entry point and subsystem if (link->try_to_resolve_entry_point) { B32 is_entry_point_name_inferred = config->entry_point_name.size == 0; // loop over all possible subsystems and entry point names and pick // subsystem that has a defined entry point symbol - if (config->entry_point_name.size == 0) { + if (config->no_entry == 0 && config->entry_point_name.size == 0) { PE_WindowsSubsystem subsys_first = config->subsystem; PE_WindowsSubsystem subsys_last = config->subsystem == PE_WindowsSubsystem_UNKNOWN ? PE_WindowsSubsystem_COUNT : config->subsystem+1; LNK_Symbol *entry_point_symbol = 0; @@ -1391,7 +1391,7 @@ lnk_load_inputs(TP_Context *tp, TP_Arena *arena, LNK_Config *config, LNK_Inputer } // search for entry point in libs - if (config->entry_point_name.size == 0 && config->subsystem != PE_WindowsSubsystem_UNKNOWN) { + if (config->no_entry == 0 && config->entry_point_name.size == 0 && config->subsystem != PE_WindowsSubsystem_UNKNOWN) { String8Array entry_points = pe_get_entry_point_names(config->machine, config->subsystem, config->file_characteristics); for EachIndex(entry_idx, entry_points.count) { for (LNK_LibNode *lib_n = link->libs.first; lib_n != 0; lib_n = lib_n->next) { @@ -1427,7 +1427,9 @@ lnk_load_inputs(TP_Context *tp, TP_Arena *arena, LNK_Config *config, LNK_Inputer } // generate undefined symbol for entry point - lnk_include_symbol(config, config->entry_point_name, 0); + if (config->entry_point_name.size) { + lnk_include_symbol(config, config->entry_point_name, 0); + } // do we have a subsystem? if (config->subsystem != PE_WindowsSubsystem_UNKNOWN) { @@ -2271,7 +2273,7 @@ lnk_link_image(TP_Context *tp, TP_Arena *arena, LNK_Config *config, LNK_Inputer // // was entry point resolved? // - if (config->entry_point_name.size == 0 || link->try_to_resolve_entry_point) { + if (config->no_entry == 0 && (config->entry_point_name.size == 0 || link->try_to_resolve_entry_point)) { String8 machine_str = coff_string_from_machine_type(config->machine); String8 subsystem_str = pe_string_from_subsystem(config->subsystem); String8Array entry_points = pe_get_entry_point_names(config->machine, config->subsystem, config->file_characteristics); @@ -5635,7 +5637,7 @@ lnk_build_win32_header(Arena *arena, LNK_SymbolTable *symtab, LNK_Config *config // // entry point // - { + if (config->no_entry == 0) { Temp scratch = scratch_begin(&arena, 1); COFF_SectionHeader **section_table = push_array(arena, COFF_SectionHeader *, coff_section_table_count + 1); @@ -7076,6 +7078,8 @@ entry_point(CmdLine *cmdline) case LNK_BootMode_TypeServer: lnk_run_type_server(tp, tp_arena, config); break; } + // Stop workers before tearing down linker state. + tp_release(tp); lnk_log_end(); scratch_end(scratch); } diff --git a/src/linker/lnk_config.c b/src/linker/lnk_config.c index f44b4f1e..d5bd26ec 100644 --- a/src/linker/lnk_config.c +++ b/src/linker/lnk_config.c @@ -50,6 +50,7 @@ global LNK_CmdSwitch g_cmd_switch_map[] = { LNK_CmdSwitch_Natvis, 0, LNK_CmdValueKind_Scalar, "NATVIS", ":FILENAME", "NATVIS to embed in the PDB." }, { LNK_CmdSwitch_NoDefaultLib, 1, LNK_CmdValueKind_Scalar, "NODEFAULTLIB", ":LIBNAME", "Ignore a /DEFAULTLIB." }, { LNK_CmdSwitch_NoDefaultLib, 0, LNK_CmdValueKind_Scalar, "NOD", ":LIBNAME", "Alias for /NODEFAULTLIB." }, + { LNK_CmdSwitch_NoEntry, 0, LNK_CmdValueKind_Null, "NOENTRY", "", "Link to a DLL without an entry point." }, { LNK_CmdSwitch_NoExp, 0, LNK_CmdValueKind_Null, "NOEXP", "", "No support." }, { LNK_CmdSwitch_NoImpLib, 0, LNK_CmdValueKind_Null, "NOIMPLIB", "", "Do not create the import library." }, { LNK_CmdSwitch_NxCompat, 0, LNK_CmdValueKind_Scalar, "NXCOMPAT", "[:NO]", "Image is compatible with data execution prevention." }, @@ -1362,6 +1363,7 @@ lnk_apply_cmd_option_to_config(LNK_Config *config, String8 cmd_name, String8 val } config->entry_point_name = new_entry_point_name; + config->no_entry = 0; } break; case LNK_CmdSwitch_Export: { @@ -1701,6 +1703,10 @@ lnk_apply_cmd_option_to_config(LNK_Config *config, String8 cmd_name, String8 val } } break; + case LNK_CmdSwitch_NoEntry: { + config->no_entry = 1; + } break; + case LNK_CmdSwitch_NoExp: { config->build_exp = 0; } break; @@ -3190,6 +3196,10 @@ lnk_config_init(U64 argc, char **argv) lnk_apply_write_temp_files(arena, config); } + if (config->no_entry && (config->file_characteristics & PE_ImageFileCharacteristic_DLL) == 0) { + lnk_error_cmd_switch(LNK_Error_IncomatibleCmdOptions, 0, LNK_CmdSwitch_NoEntry, "cannot link current image without an entry point; please use /DLL"); + } + scratch_end(scratch); ProfEnd(); return config; diff --git a/src/linker/lnk_config.h b/src/linker/lnk_config.h index 5ad6bb20..d4f025a5 100644 --- a/src/linker/lnk_config.h +++ b/src/linker/lnk_config.h @@ -82,6 +82,7 @@ typedef enum LNK_CmdSwitch_Merge, LNK_CmdSwitch_Natvis, LNK_CmdSwitch_NoDefaultLib, + LNK_CmdSwitch_NoEntry, LNK_CmdSwitch_NoExp, LNK_CmdSwitch_NoImpLib, LNK_CmdSwitch_NxCompat, @@ -425,6 +426,7 @@ typedef struct LNK_Config LNK_SwitchState type_server; LNK_SwitchState sort_imports; LNK_SwitchState llvm_addrsig; + B32 no_entry; } LNK_Config; // --- MSVC Error Codes -------------------------------------------------------- diff --git a/src/linker/tests/linker_tests.c b/src/linker/tests/linker_tests.c index 6dffdbb8..42cd74fd 100644 --- a/src/linker/tests/linker_tests.c +++ b/src/linker/tests/linker_tests.c @@ -637,6 +637,175 @@ TEST(simple_link_test) T_Ok(opt->loader_flags == 0); } +TEST(entry_point) +{ + // Give the user entry, CRT startup, and custom entry distinct offsets so that + // the PE header tells us which symbol was selected, including CRT redirection. + struct { + char *user_entry; + char *crt_entry; + } entries[] = { + { "main", "mainCRTStartup" }, + { "wmain", "wmainCRTStartup" }, + { "WinMain", "WinMainCRTStartup" }, + { "wWinMain", "wWinMainCRTStartup" }, + { "DllMain", "_DllMainCRTStartup" }, + }; + for EachElement(i, entries) { + T_COFF_DefObj obj = { + .machine = T_COFF_DefSetMachine(X64), + .sections = (T_COFF_DefSection[]){ + { "text", ".text", str8_lit_comp("\xc3\xc3\xc3"), .flags = "rx:code@1" }, + {0} + }, + .symbols = (T_COFF_DefSymbol[]){ + T_COFF_DefSymbol_ExternFunc(entries[i].user_entry, "text", 0), + T_COFF_DefSymbol_ExternFunc(entries[i].crt_entry, "text", 1), + T_COFF_DefSymbol_ExternFunc("custom_entry", "text", 2), + {0} + } + }; + T_Ok(t_write_def_obj((char *)str8f(arena, "%s.obj", entries[i].user_entry).str, obj)); + T_Ok(t_write_def_lib((char *)str8f(arena, "%s.lib", entries[i].user_entry).str, (T_COFF_DefLib){ + .members = (T_COFF_DefLibMember[]){ + { .type = T_COFF_DefLibMember_Obj, .obj = obj }, + {0} + } + })); + } + + // Keep a real input section even when no startup object is needed. + T_Ok(t_write_def_obj("data.obj", (T_COFF_DefObj){ + .sections = (T_COFF_DefSection[]){ + { "data", ".data", str8_lit("data"), .flags = "rw:data" }, + {0} + } + })); + T_Ok(t_write_def_obj("crt.obj", (T_COFF_DefObj){ + .sections = (T_COFF_DefSection[]){ + { "text", ".text", str8_lit_comp("\xc3"), .flags = "rx:code@1" }, + {0} + }, + .symbols = (T_COFF_DefSymbol[]){ + T_COFF_DefSymbol_ExternFunc("mainCRTStartup", "text", 0), + {0} + } + })); + + // /NOENTRY must not extract this member just to resolve DLL startup. Without /NOENTRY, + // the missing dependency proves that the member was extracted. + T_Ok(t_write_def_lib("bad_startup.lib", (T_COFF_DefLib){ + .members = (T_COFF_DefLibMember[]){ + { .type = T_COFF_DefLibMember_Obj, .obj = { + .sections = (T_COFF_DefSection[]){ + { "text", ".text", str8_lit_comp("\xe8\0\0\0\0\xc3"), .flags = "rx:code@1", + .relocs = (T_COFF_DefReloc[]){ + T_COFF_DefReloc(X64_Rel32, 1, "missing_dependency"), + {0} + } + }, + {0} + }, + .symbols = (T_COFF_DefSymbol[]){ + T_COFF_DefSymbol_ExternFunc("_DllMainCRTStartup", "text", 0), + T_COFF_DefSymbol_UndefFunc("missing_dependency"), + {0} + } + } }, + {0} + } + })); + + struct { + char *options; + char *inputs; + PE_WindowsSubsystem subsystem; + S32 entry_off; // -1 means no entry point. + LNK_ErrorCode error; + } cases[] = { + // Explicit /ENTRY bypasses CRT redirection and overrides default startup. + { "/subsystem:console /entry:custom_entry", "main.obj", PE_WindowsSubsystem_WINDOWS_CUI, 2 }, + { "/subsystem:console /entry:main", "main.obj", PE_WindowsSubsystem_WINDOWS_CUI, 0 }, + { "/entry:main", "main.obj", PE_WindowsSubsystem_WINDOWS_CUI, 0 }, + { "/dll /entry:custom_entry", "DllMain.obj", PE_WindowsSubsystem_WINDOWS_GUI, 2 }, + { "/subsystem:console /entry:custom_entry", "main.lib", PE_WindowsSubsystem_WINDOWS_CUI, 2 }, + + // Infer the subsystem and redirect each user entry to its CRT startup. + { "", "main.obj", PE_WindowsSubsystem_WINDOWS_CUI, 1 }, + { "", "wmain.obj", PE_WindowsSubsystem_WINDOWS_CUI, 1 }, + { "", "WinMain.obj", PE_WindowsSubsystem_WINDOWS_GUI, 1 }, + { "", "wWinMain.obj", PE_WindowsSubsystem_WINDOWS_GUI, 1 }, + { "/subsystem:console", "main.obj", PE_WindowsSubsystem_WINDOWS_CUI, 1 }, + { "/subsystem:console", "wmain.obj", PE_WindowsSubsystem_WINDOWS_CUI, 1 }, + { "/subsystem:windows", "WinMain.obj", PE_WindowsSubsystem_WINDOWS_GUI, 1 }, + { "/subsystem:windows", "wWinMain.obj", PE_WindowsSubsystem_WINDOWS_GUI, 1 }, + { "/subsystem:console", "crt.obj", PE_WindowsSubsystem_WINDOWS_CUI, 0 }, + + // Resolve startup from a library, or use the default DLL entry/subsystem. + { "/subsystem:console", "main.lib", PE_WindowsSubsystem_WINDOWS_CUI, 1 }, + { "/subsystem:console /entry:wmainCRTStartup", "wmain.lib", PE_WindowsSubsystem_WINDOWS_CUI, 1 }, + { "/subsystem:windows", "WinMain.lib", PE_WindowsSubsystem_WINDOWS_GUI, 1 }, + { "/subsystem:windows /entry:wWinMainCRTStartup", "wWinMain.lib", PE_WindowsSubsystem_WINDOWS_GUI, 1 }, + { "/dll", "DllMain.obj", PE_WindowsSubsystem_WINDOWS_GUI, 1 }, + { "/dll", "DllMain.lib", PE_WindowsSubsystem_WINDOWS_GUI, 1 }, + { "/dll /subsystem:console", "DllMain.obj", PE_WindowsSubsystem_WINDOWS_CUI, 1 }, + + // /NOENTRY leaves a zero RVA even when default startup symbols are available. + { "/dll /noentry", "", PE_WindowsSubsystem_WINDOWS_GUI, -1 }, + { "/noentry /dll", "", PE_WindowsSubsystem_WINDOWS_GUI, -1 }, + { "/dll /noentry", "DllMain.obj", PE_WindowsSubsystem_WINDOWS_GUI, -1 }, + { "/dll /noentry", "DllMain.lib", PE_WindowsSubsystem_WINDOWS_GUI, -1 }, + { "/dll /noentry", "bad_startup.lib", PE_WindowsSubsystem_WINDOWS_GUI, -1 }, + { "/dll /noentry /subsystem:console", "", PE_WindowsSubsystem_WINDOWS_CUI, -1 }, + + // Match LINK: the last /ENTRY or /NOENTRY wins, including undefined entries. + { "/dll /entry:custom_entry /noentry", "DllMain.obj", PE_WindowsSubsystem_WINDOWS_GUI, -1 }, + { "/dll /noentry /entry:custom_entry", "DllMain.obj", PE_WindowsSubsystem_WINDOWS_GUI, 2 }, + { "/dll /entry:missing_entry /noentry", "", PE_WindowsSubsystem_WINDOWS_GUI, -1, LNK_Error_UnresolvedSymbol }, + { "/dll /noentry /entry:missing_entry", "", 0, -1, LNK_Error_UnresolvedSymbol }, + { "/noentry /subsystem:console /entry:main", "main.obj", PE_WindowsSubsystem_WINDOWS_CUI, 0 }, + + // Missing startup, unresolved explicit entry, and invalid /NOENTRY usage. + { "/subsystem:console", "", 0, 0, LNK_Error_EntryPoint }, + { "/dll", "", 0, 0, LNK_Error_EntryPoint }, + { "/subsystem:console /entry:missing_entry", "main.obj", 0, 0, LNK_Error_UnresolvedSymbol }, + { "/dll", "bad_startup.lib", 0, 0, LNK_Error_UnresolvedSymbol }, + { "/noentry /subsystem:console", "", 0, 0, LNK_Error_IncomatibleCmdOptions }, + { "/subsystem:console /entry:main /noentry", "main.obj", 0, 0, LNK_Error_IncomatibleCmdOptions }, + }; + + for EachElement(i, cases) { + String8 out_name = str8f(arena, "entry_%u.dll", i); + test_outf("entry configuration %u: %s data.obj %s\n", i, cases[i].options, cases[i].inputs); + T_Ok(t_invoke_linkerf("/machine:x64 /nod /manifest:no /opt:ref /opt:noicf /out:%S %s data.obj %s", out_name, cases[i].options, cases[i].inputs)); + + if (cases[i].error) { + T_Ok(g_last_exit_code != 0); + + if (t_id_linker() == Linker_radlink) { + T_Ok(g_last_exit_code == cases[i].error); + } + } else { + T_Ok(g_last_exit_code == 0); + + String8 image = t_read_file(arena, out_name); + PE_BinInfo pe = pe_bin_info_from_data(arena, image); + T_Ok(pe.arch == Arch_x64); + T_Ok(pe.subsystem == cases[i].subsystem); + + if (cases[i].entry_off < 0) { + T_Ok(pe.entry_point == 0); + } else { + COFF_SectionHeader *sections = (COFF_SectionHeader *)str8_substr(image, pe.section_table_range).str; + String8 strings = str8_substr(image, pe.string_table_range); + COFF_SectionHeader *text = coff_section_header_from_name(strings, sections, pe.section_count, str8_lit(".text")); + T_Ok(text); + T_Ok(pe.entry_point == text->voff + cases[i].entry_off); + } + } + } +} + TEST(map) { T_Ok(t_write_def_obj("map.obj", (T_COFF_DefObj){