radlink: implement /NOENTRY

This commit is contained in:
Nikita Smith authored and Ryan Fleury committed 2026-09-21 09:08:05 -07:00
1 parent 4663d1a89a
commit 5b646344eb
5 files changed
+234 -49

No files matched your search

+43 -43
View File
@@ -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)
+10 -6
View File
@@ -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);
}
+10
View File
@@ -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;
+2
View File
@@ -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 --------------------------------------------------------
+169
View File
@@ -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){