Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions internal/build/collect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
}

Expand Down
23 changes: 17 additions & 6 deletions internal/build/link_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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)
}
113 changes: 105 additions & 8 deletions internal/build/link_options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package build

import (
"bytes"
"debug/elf"
"os"
"os/exec"
Expand All @@ -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"
)

Expand All @@ -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) {
Expand All @@ -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
Expand All @@ -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}}},
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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}},
Expand Down Expand Up @@ -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 {
Expand Down
64 changes: 54 additions & 10 deletions internal/crosscompile/crosscompile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

targetDebugInfoPolicy is only reached via UseTarget, and Use() routes every wasm/wasi target name to use() instead (crosscompile.go:753). All four wasm-ld target JSONs (wasm, wasm-unknown, wasip1, wasip2) start with those prefixes, so the wasm-ld branch here is never exercised by a real build — only by TestTargetDebugInfoPolicy. Not a bug, but worth a one-line comment noting that wasm/wasi are handled by use() and that this branch is currently test-only.

Separately, any unrecognized (linker, llvmTarget) combination silently falls through to DebugInfoUnavailable, which disables DWARF entirely for that target (via !CanRetain() in shouldEmitDebugInfo). Documenting that intentional fail-to-omit default would help the next person adding a target with a different linker.

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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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{
Expand All @@ -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 != "" {
Expand Down
Loading
Loading