diff --git a/internal/build/build.go b/internal/build/build.go index 6db6ae9085..1c8588f89f 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -1609,9 +1609,7 @@ func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose buildArgs = append(buildArgs, linuxExportDynamicArgs(ctx)...) } - if shouldEmitDebugInfo(ctx.buildConf, &ctx.crossCompile) { - buildArgs = append(buildArgs, "-gdwarf-4") - } + buildArgs = append(buildArgs, dwarfPreserveLinkerArgs(ctx.buildConf, &ctx.crossCompile)...) if ctx.buildConf.GenLL { var compiledObjFiles []string diff --git a/internal/build/collect_test.go b/internal/build/collect_test.go index cc9785518f..31cbf531aa 100644 --- a/internal/build/collect_test.go +++ b/internal/build/collect_test.go @@ -232,7 +232,7 @@ func TestCollectFingerprintIncludesEmitDWARF(t *testing.T) { } targetWithoutDWARF := newPkg() - if err := newContext(LinkOptions{}, crosscompile.DebugInfoPolicy{AlwaysOmit: true}).collectFingerprint(targetWithoutDWARF); err != nil { + if err := newContext(LinkOptions{}, crosscompile.DebugInfoPolicy{Capability: crosscompile.DebugInfoUnavailable}).collectFingerprint(targetWithoutDWARF); err != nil { t.Fatal(err) } if withDWARF.Fingerprint == targetWithoutDWARF.Fingerprint { @@ -243,7 +243,7 @@ func TestCollectFingerprintIncludesEmitDWARF(t *testing.T) { t.Fatal(err) } if targetData.Common != nil && targetData.Common.EmitDWARF { - t.Fatalf("always-omit target manifest unexpectedly contains EMIT_DWARF=true:\n%s", targetWithoutDWARF.Manifest) + t.Fatalf("target without DWARF support unexpectedly contains EMIT_DWARF=true:\n%s", targetWithoutDWARF.Manifest) } } diff --git a/internal/build/link_options.go b/internal/build/link_options.go index 1a7c0140d8..dcc851eb18 100644 --- a/internal/build/link_options.go +++ b/internal/build/link_options.go @@ -76,10 +76,10 @@ func omitDWARFRequested(conf *Config) bool { } // effectiveOmitDWARF combines command intent with the selected toolchain's -// baseline behavior. Some fixed-target linkers always omit DWARF, so LLGo -// should avoid generating debug metadata that cannot reach the artifact. +// capability. LLGo avoids generating debug metadata when the linked output +// format cannot retain it. func effectiveOmitDWARF(conf *Config, target *crosscompile.Export) bool { - return omitDWARFRequested(conf) || target.DebugInfo.AlwaysOmit + return omitDWARFRequested(conf) || !target.DebugInfo.CanRetain() } // shouldEmitDebugInfo reports whether this compilation should produce DWARF. @@ -100,13 +100,13 @@ func validateLinkOptions(conf *Config, target *crosscompile.Export) error { if err := conf.LinkOptions.validate(); err != nil { return err } - if conf.LinkOptions.DWARF == DWARFPreserve && target.DebugInfo.AlwaysOmit { + if conf.LinkOptions.DWARF == DWARFPreserve && !target.DebugInfo.CanRetain() { return fmt.Errorf("preserving DWARF is not supported by the selected target linker") } if !omitDWARFRequested(conf) { return nil } - if target.DebugInfo.AlwaysOmit { + if !target.DebugInfo.CanRetain() { return nil } if len(target.DebugInfo.OmitLinkFlags) == 0 { @@ -120,7 +120,7 @@ func validateLinkOptions(conf *Config, target *crosscompile.Export) error { // earlier; this handles debug sections in native and prebuilt inputs without // rewriting the linked binary afterward. func dwarfLinkerArgs(conf *Config, target *crosscompile.Export) []string { - if target.DebugInfo.AlwaysOmit || !effectiveOmitDWARF(conf, target) { + if !target.DebugInfo.CanRetain() || !effectiveOmitDWARF(conf, target) { return nil } // c-archive has no final native link step. Omitting generated DWARF is @@ -130,3 +130,14 @@ func dwarfLinkerArgs(conf *Config, target *crosscompile.Export) []string { } return slices.Clone(target.DebugInfo.OmitLinkFlags) } + +// dwarfPreserveLinkerArgs returns options needed while linking a debug +// artifact. Direct linker invocations retain input DWARF without a preserve +// flag, while clang-driver invocations need -gdwarf-4; the invocation path, +// rather than the linker executable name, determines the option. +func dwarfPreserveLinkerArgs(conf *Config, target *crosscompile.Export) []string { + if !shouldEmitDebugInfo(conf, target) { + return nil + } + return slices.Clone(target.DebugInfo.PreserveLinkFlags) +} diff --git a/internal/build/link_options_test.go b/internal/build/link_options_test.go index b903487dfe..d5ec1254b1 100644 --- a/internal/build/link_options_test.go +++ b/internal/build/link_options_test.go @@ -19,6 +19,7 @@ package build import ( + "bytes" "debug/elf" "os" "os/exec" @@ -29,6 +30,7 @@ import ( "testing" "github.com/goplus/llgo/internal/crosscompile" + "github.com/goplus/llgo/internal/firmware" "github.com/goplus/llgo/xtool/env/llvm" ) @@ -48,7 +50,9 @@ func TestDwarfLinkerArgs(t *testing.T) { {name: "w", conf: Config{LinkOptions: LinkOptions{DWARF: DWARFOmit}}, target: configurableDebugInfo(), want: []string{"-Wl,-S"}}, {name: "s implies w", conf: Config{LinkOptions: LinkOptions{OmitSymbolTable: true}}, target: configurableDebugInfo(), want: []string{"-Wl,-S"}}, {name: "explicit w false", conf: Config{LinkOptions: LinkOptions{OmitSymbolTable: true, DWARF: DWARFPreserve}}}, - {name: "target linker already suppresses DWARF", conf: Config{Target: "rp2040", LinkOptions: LinkOptions{DWARF: DWARFOmit}}, target: alwaysOmitDebugInfo()}, + {name: "fixed target w", conf: Config{Target: "rp2040", BuildMode: BuildModeExe, LinkOptions: LinkOptions{DWARF: DWARFOmit}}, target: targetDebugInfo(), want: []string{"-S"}}, + {name: "fixed target w false", conf: Config{Target: "rp2040", BuildMode: BuildModeExe, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}, target: targetDebugInfo()}, + {name: "target without DWARF support", conf: Config{Target: "rp2040", LinkOptions: LinkOptions{DWARF: DWARFOmit}}, target: unavailableDebugInfo()}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -59,6 +63,27 @@ func TestDwarfLinkerArgs(t *testing.T) { } } +func TestDwarfPreserveLinkerArgs(t *testing.T) { + tests := []struct { + name string + conf Config + target crosscompile.Export + want []string + }{ + {name: "native preserve", conf: Config{Mode: ModeBuild, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}, target: configurableDebugInfo(), want: []string{"-gdwarf-4"}}, + {name: "native omit", conf: Config{Mode: ModeBuild, LinkOptions: LinkOptions{DWARF: DWARFOmit}}, target: configurableDebugInfo()}, + {name: "direct target preserve", conf: Config{Mode: ModeBuild, Target: "rp2040", LinkOptions: LinkOptions{DWARF: DWARFPreserve}}, target: targetDebugInfo()}, + {name: "unsupported target", conf: Config{Mode: ModeBuild, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}, target: unavailableDebugInfo()}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := dwarfPreserveLinkerArgs(&tt.conf, &tt.target); !reflect.DeepEqual(got, tt.want) { + t.Fatalf("dwarfPreserveLinkerArgs() = %v, want %v", got, tt.want) + } + }) + } +} + func TestEffectiveOmitDWARF(t *testing.T) { tests := []struct { name string @@ -71,7 +96,7 @@ func TestEffectiveOmitDWARF(t *testing.T) { {name: "safe default c-shared", conf: Config{BuildMode: BuildModeCShared, OmitDWARFByDefault: true}, want: true}, {name: "safe default c-archive", conf: Config{BuildMode: BuildModeCArchive, OmitDWARFByDefault: true}, want: true}, {name: "requested", conf: Config{LinkOptions: LinkOptions{DWARF: DWARFOmit}}, want: true}, - {name: "target baseline", target: alwaysOmitDebugInfo(), want: true}, + {name: "target baseline", target: unavailableDebugInfo(), want: true}, {name: "explicit preserve", conf: Config{LinkOptions: LinkOptions{DWARF: DWARFPreserve}}}, {name: "explicit preserve overrides safe default", conf: Config{OmitDWARFByDefault: true, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}}, } @@ -100,7 +125,7 @@ func TestShouldEmitDebugInfo(t *testing.T) { {name: "linked s w false", conf: Config{Mode: ModeBuild, LinkOptions: LinkOptions{OmitSymbolTable: true, DWARF: DWARFPreserve}}, want: true}, {name: "generation default", conf: Config{Mode: ModeGen}}, {name: "generation requested", conf: Config{Mode: ModeGen, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}, want: true}, - {name: "target always omits", conf: Config{Mode: ModeBuild}, target: alwaysOmitDebugInfo()}, + {name: "target without DWARF support", conf: Config{Mode: ModeBuild}, target: unavailableDebugInfo()}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -130,8 +155,10 @@ func TestValidateLinkOptions(t *testing.T) { {name: "c-archive omit", conf: Config{Goos: "linux", BuildMode: BuildModeCArchive, LinkOptions: w}, target: configurableDebugInfo()}, {name: "c-shared preserve", conf: Config{Goos: "linux", BuildMode: BuildModeCShared, LinkOptions: wFalse}, target: configurableDebugInfo()}, {name: "c-archive preserve", conf: Config{Goos: "linux", BuildMode: BuildModeCArchive, LinkOptions: wFalse}, target: configurableDebugInfo()}, - {name: "fixed target omit", conf: Config{Target: "rp2040", Goos: "linux", BuildMode: BuildModeExe, LinkOptions: w}, target: alwaysOmitDebugInfo()}, - {name: "fixed target explicit DWARF", conf: Config{Target: "rp2040", Goos: "linux", BuildMode: BuildModeExe, LinkOptions: wFalse}, target: alwaysOmitDebugInfo(), wantErr: true}, + {name: "target without DWARF support omit", conf: Config{Target: "custom", Goos: "linux", BuildMode: BuildModeExe, LinkOptions: w}, target: unavailableDebugInfo()}, + {name: "target without DWARF support preserve", conf: Config{Target: "custom", Goos: "linux", BuildMode: BuildModeExe, LinkOptions: wFalse}, target: unavailableDebugInfo(), wantErr: true}, + {name: "fixed target omit", conf: Config{Target: "rp2040", Goos: "linux", BuildMode: BuildModeExe, LinkOptions: w}, target: targetDebugInfo(), wantErr: false}, + {name: "fixed target preserve", conf: Config{Target: "rp2040", Goos: "linux", BuildMode: BuildModeExe, LinkOptions: wFalse}, target: targetDebugInfo()}, {name: "configurable WASI omit", conf: Config{Target: "wasi", Goos: "wasip1", BuildMode: BuildModeExe, LinkOptions: w}, target: configurableDebugInfo()}, {name: "configurable WASI preserve", conf: Config{Target: "wasi", Goos: "wasip1", BuildMode: BuildModeExe, LinkOptions: wFalse}, target: configurableDebugInfo()}, {name: "no omission", conf: Config{Goos: "windows", BuildMode: BuildModeExe, LinkOptions: wFalse}}, @@ -182,12 +209,82 @@ func TestDwarfLinkerArgsSuppressNativeInputDWARF(t *testing.T) { } } +func TestTargetDWARFDoesNotChangeLoadableELF(t *testing.T) { + dir := t.TempDir() + source := filepath.Join(dir, "main.c") + object := filepath.Join(dir, "main.o") + debugELF := filepath.Join(dir, "debug.elf") + strippedELF := filepath.Join(dir, "stripped.elf") + debugBin := filepath.Join(dir, "debug.bin") + strippedBin := filepath.Join(dir, "stripped.bin") + if err := os.WriteFile(source, []byte("volatile int value = 41; void Reset_Handler(void) { value++; for (;;) {} }\n"), 0o644); err != nil { + t.Fatal(err) + } + clang, err := exec.LookPath("clang") + if err != nil { + t.Fatal(err) + } + if out, err := exec.Command(clang, "--target=thumbv7em-none-unknown-eabi", "-g", "-ffreestanding", "-fno-unwind-tables", "-fno-asynchronous-unwind-tables", "-c", "-o", object, source).CombinedOutput(); err != nil { + t.Fatalf("compile Cortex-M DWARF fixture: %v\n%s", err, out) + } + linker, err := exec.LookPath("ld.lld") + if err != nil { + t.Fatal(err) + } + repoRoot, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + t.Fatal(err) + } + linkerScript := filepath.Join(repoRoot, "targets", "lm3s6965.ld") + link := func(path string, opts LinkOptions) { + t.Helper() + conf := &Config{Target: "cortex-m", BuildMode: BuildModeExe, LinkOptions: opts} + target := targetDebugInfo() + args := append(dwarfLinkerArgs(conf, &target), "-T", linkerScript, "-L", repoRoot, "-o", path, object) + if out, err := exec.Command(linker, args...).CombinedOutput(); err != nil { + t.Fatalf("link Cortex-M fixture: %v\n%s", err, out) + } + } + link(debugELF, LinkOptions{DWARF: DWARFPreserve}) + link(strippedELF, LinkOptions{DWARF: DWARFOmit}) + if !elfHasDebugInfo(t, debugELF) { + t.Fatal("preserved Cortex-M ELF has no debug information") + } + if elfHasDebugInfo(t, strippedELF) { + t.Fatal("omitted Cortex-M ELF still has debug information") + } + if err := firmware.ConvertFormats("", "", map[string]string{"out": debugELF, "bin": debugBin}); err != nil { + t.Fatal(err) + } + if err := firmware.ConvertFormats("", "", map[string]string{"out": strippedELF, "bin": strippedBin}); err != nil { + t.Fatal(err) + } + debugImage, err := os.ReadFile(debugBin) + if err != nil { + t.Fatal(err) + } + strippedImage, err := os.ReadFile(strippedBin) + if err != nil { + t.Fatal(err) + } + if len(debugImage) == 0 || !bytes.Equal(debugImage, strippedImage) { + t.Fatal("DWARF changed Cortex-M flashed bytes") + } +} + func configurableDebugInfo() crosscompile.Export { - return crosscompile.Export{DebugInfo: crosscompile.DebugInfoPolicy{OmitLinkFlags: []string{"-Wl,-S"}}} + return crosscompile.Export{DebugInfo: crosscompile.DebugInfoPolicy{ + PreserveLinkFlags: []string{"-gdwarf-4"}, + OmitLinkFlags: []string{"-Wl,-S"}, + }} +} + +func targetDebugInfo() crosscompile.Export { + return crosscompile.Export{DebugInfo: crosscompile.DebugInfoPolicy{OmitLinkFlags: []string{"-S"}}} } -func alwaysOmitDebugInfo() crosscompile.Export { - return crosscompile.Export{DebugInfo: crosscompile.DebugInfoPolicy{AlwaysOmit: true}} +func unavailableDebugInfo() crosscompile.Export { + return crosscompile.Export{DebugInfo: crosscompile.DebugInfoPolicy{Capability: crosscompile.DebugInfoUnavailable}} } func elfHasDebugInfo(t *testing.T, path string) bool { diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index c017c7e5f4..7b25668995 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -47,21 +47,65 @@ type Export struct { Device flash.Device // Device configuration for flashing/debugging } +// DebugInfoCapability describes whether the selected linker and its linked +// output format can retain DWARF. Deployment formats such as bin, hex, and uf2 +// are derived later and do not affect this capability. +type DebugInfoCapability uint8 + +const ( + DebugInfoRetainable DebugInfoCapability = iota + DebugInfoUnavailable +) + // DebugInfoPolicy describes how a selected linker handles debug information. // Build orchestration consumes this typed capability instead of inferring it // from a target name or linker executable. type DebugInfoPolicy struct { - AlwaysOmit bool - OmitLinkFlags []string + Capability DebugInfoCapability + PreserveLinkFlags []string + OmitLinkFlags []string +} + +func (p DebugInfoPolicy) CanRetain() bool { + return p.Capability == DebugInfoRetainable +} + +// driverDebugInfoPolicy describes a link performed through a clang-compatible +// driver. The driver needs an explicit DWARF version while its linker flag +// spelling uses -Wl,-S. +func driverDebugInfoPolicy() DebugInfoPolicy { + return DebugInfoPolicy{ + PreserveLinkFlags: []string{"-gdwarf-4"}, + OmitLinkFlags: []string{"-Wl,-S"}, + } } func nativeDebugInfoPolicy(goos string) DebugInfoPolicy { - switch goos { - case "darwin", "linux": - return DebugInfoPolicy{OmitLinkFlags: []string{"-Wl,-S"}} - default: - return DebugInfoPolicy{} + policy := driverDebugInfoPolicy() + if goos != "darwin" && goos != "linux" { + // The driver still accepts the preserve flag, but these native targets + // do not have a supported omit-DWARF spelling. + policy.OmitLinkFlags = nil + } + return policy +} + +func targetDebugInfoPolicy(linker, llvmTarget string) DebugInfoPolicy { + switch linker { + case "ld.lld": + if !strings.HasPrefix(llvmTarget, "wasm") { + return DebugInfoPolicy{OmitLinkFlags: []string{"-S"}} + } + case "wasm-ld": + // Use routes wasm/wasi names through the clang-driver path. Keep this + // direct wasm-ld policy for target-policy callers and tests. + if strings.HasPrefix(llvmTarget, "wasm") { + return DebugInfoPolicy{OmitLinkFlags: []string{"-S"}} + } } + // Unknown linker/target pairs intentionally fail closed: without a known + // retention policy, generated DWARF must not be promised to callers. + return DebugInfoPolicy{Capability: DebugInfoUnavailable} } // URLs and configuration that can be overridden for testing @@ -333,7 +377,7 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool, level optlevel.Le if goarch != "wasm" { return } - export.DebugInfo.OmitLinkFlags = []string{"-Wl,-S"} + export.DebugInfo = driverDebugInfoPolicy() // Configure based on GOOS switch goos { @@ -504,7 +548,7 @@ func UseTarget(targetName string, level optlevel.Level, ltoMode lto.Mode) (expor export.BinaryFormat = config.BinaryFormat export.FormatDetail = config.FormatDetail() export.Emulator = config.Emulator - export.DebugInfo.AlwaysOmit = true + export.DebugInfo = targetDebugInfoPolicy(config.Linker, config.LLVMTarget) // Set flashing/debugging configuration export.Device = flash.Device{ @@ -531,7 +575,7 @@ func UseTarget(targetName string, level optlevel.Level, ltoMode lto.Mode) (expor // Convert LLVMTarget, CPU, Features to CCFLAGS/LDFLAGS // ICF off for Go pc-identity semantics (see the non-cross flags above). - ldflags := []string{"-S", "--icf=none"} + ldflags := []string{"--icf=none"} ccflags := []string{level.Flag()} cflags := []string{"-Wno-override-module", "-Qunused-arguments", "-Wno-unused-command-line-argument"} if config.LLVMTarget != "" { diff --git a/internal/crosscompile/crosscompile_test.go b/internal/crosscompile/crosscompile_test.go index ea89a9596a..7bbe5c5628 100644 --- a/internal/crosscompile/crosscompile_test.go +++ b/internal/crosscompile/crosscompile_test.go @@ -248,11 +248,14 @@ func TestUseTarget(t *testing.T) { if err != nil { t.Fatalf("Unexpected error for target %s: %v", tc.targetName, err) } - if !export.DebugInfo.AlwaysOmit { - t.Fatalf("target %s debug-info policy = %+v, want AlwaysOmit", tc.targetName, export.DebugInfo) + if !export.DebugInfo.CanRetain() { + t.Fatalf("target %s debug-info policy = %+v, want retainable ELF DWARF", tc.targetName, export.DebugInfo) } - if !slices.Contains(export.LDFLAGS, "-S") { - t.Fatalf("target %s declares AlwaysOmit without linker -S: %v", tc.targetName, export.LDFLAGS) + if slices.Contains(export.LDFLAGS, "-S") { + t.Fatalf("target %s unconditionally strips DWARF: %v", tc.targetName, export.LDFLAGS) + } + if !slices.Equal(export.DebugInfo.OmitLinkFlags, []string{"-S"}) { + t.Fatalf("target %s debug omission flags = %v, want [-S]", tc.targetName, export.DebugInfo.OmitLinkFlags) } // Check if LLVM target is in CCFLAGS @@ -344,7 +347,9 @@ func TestUseWithTarget(t *testing.T) { t.Error("Expected LDFLAGS to be set for native build") } wantDebugInfo := nativeDebugInfoPolicy(runtime.GOOS) - if export.DebugInfo.AlwaysOmit != wantDebugInfo.AlwaysOmit || !slices.Equal(export.DebugInfo.OmitLinkFlags, wantDebugInfo.OmitLinkFlags) { + if export.DebugInfo.Capability != wantDebugInfo.Capability || + !slices.Equal(export.DebugInfo.PreserveLinkFlags, wantDebugInfo.PreserveLinkFlags) || + !slices.Equal(export.DebugInfo.OmitLinkFlags, wantDebugInfo.OmitLinkFlags) { t.Fatalf("native debug-info policy = %+v, want %+v", export.DebugInfo, wantDebugInfo) } } @@ -362,7 +367,9 @@ func TestNativeDebugInfoPolicy(t *testing.T) { for _, tt := range tests { t.Run(tt.goos, func(t *testing.T) { policy := nativeDebugInfoPolicy(tt.goos) - got := !policy.AlwaysOmit && slices.Equal(policy.OmitLinkFlags, []string{"-Wl,-S"}) + got := policy.CanRetain() && + slices.Equal(policy.PreserveLinkFlags, []string{"-gdwarf-4"}) && + slices.Equal(policy.OmitLinkFlags, []string{"-Wl,-S"}) if got != tt.supported { t.Fatalf("nativeDebugInfoPolicy(%q) = %+v, supported = %v", tt.goos, policy, got) } @@ -370,6 +377,30 @@ func TestNativeDebugInfoPolicy(t *testing.T) { } } +func TestTargetDebugInfoPolicy(t *testing.T) { + tests := []struct { + name string + linker string + target string + canRetain bool + omitFlags []string + }{ + {name: "ELF lld", linker: "ld.lld", target: "thumbv7em-none-unknown-eabi", canRetain: true, omitFlags: []string{"-S"}}, + {name: "Wasm lld", linker: "wasm-ld", target: "wasm32-unknown-unknown", canRetain: true, omitFlags: []string{"-S"}}, + {name: "mismatched ELF linker", linker: "ld.lld", target: "wasm32-unknown-unknown"}, + {name: "mismatched Wasm linker", linker: "wasm-ld", target: "thumbv7em-none-unknown-eabi"}, + {name: "unknown linker", linker: "custom-ld", target: "thumbv7em-none-unknown-eabi"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := targetDebugInfoPolicy(tt.linker, tt.target) + if got.CanRetain() != tt.canRetain || !slices.Equal(got.OmitLinkFlags, tt.omitFlags) { + t.Fatalf("targetDebugInfoPolicy(%q, %q) = %+v, want retain=%v flags=%v", tt.linker, tt.target, got, tt.canRetain, tt.omitFlags) + } + }) + } +} + func TestOptimizationFlagPlacement(t *testing.T) { export, err := UseTarget("rp2040", optlevel.Oz, lto.Off) if err != nil {