From 65991f64123b658c3a2d347c8ec8ac042355516a Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 2 Aug 2026 03:57:06 +0800 Subject: [PATCH 1/4] debug: define the common debugger ABI schema --- internal/debugabi/fixture_test.go | 55 +++++ internal/debugabi/schema.go | 216 ++++++++++++++++++ internal/debugabi/schema_test.go | 121 ++++++++++ internal/debugabi/schema_v1.json | 120 ++++++++++ internal/debugabi/testdata/fixture/main.go | 100 ++++++++ .../testdata/fixture/manifest_v1.json | 43 ++++ 6 files changed, 655 insertions(+) create mode 100644 internal/debugabi/fixture_test.go create mode 100644 internal/debugabi/schema.go create mode 100644 internal/debugabi/schema_test.go create mode 100644 internal/debugabi/schema_v1.json create mode 100644 internal/debugabi/testdata/fixture/main.go create mode 100644 internal/debugabi/testdata/fixture/manifest_v1.json diff --git a/internal/debugabi/fixture_test.go b/internal/debugabi/fixture_test.go new file mode 100644 index 0000000000..c4cb351dbf --- /dev/null +++ b/internal/debugabi/fixture_test.go @@ -0,0 +1,55 @@ +//go:build !llgo + +package debugabi + +import ( + _ "embed" + "encoding/json" + "os" + "strings" + "testing" +) + +type fixtureManifest struct { + SchemaVersion uint8 `json:"schema_version"` + Source string `json:"source"` + Categories []string `json:"categories"` + Breakpoints []struct { + Name string `json:"name"` + Marker string `json:"marker"` + Values map[string]json.RawMessage `json:"values"` + } `json:"breakpoints"` +} + +//go:embed testdata/fixture/manifest_v1.json +var fixtureManifestV1 []byte + +func TestSharedFixtureManifest(t *testing.T) { + var manifest fixtureManifest + if err := json.Unmarshal(fixtureManifestV1, &manifest); err != nil { + t.Fatal(err) + } + if manifest.SchemaVersion != SchemaVersion || manifest.Source != "main.go" { + t.Fatalf("fixture manifest identity = v%d %q", manifest.SchemaVersion, manifest.Source) + } + wantCategories := []string{ + "primitives", "aliases", "named_types", "recursive_types", "aggregates", + "strings", "slices", "maps", "channels", "interfaces", + "functions", "closures", "bound_methods", "goroutines", + } + if strings.Join(manifest.Categories, ",") != strings.Join(wantCategories, ",") { + t.Fatalf("fixture categories = %v", manifest.Categories) + } + source, err := os.ReadFile("testdata/fixture/" + manifest.Source) + if err != nil { + t.Fatal(err) + } + for _, breakpoint := range manifest.Breakpoints { + if breakpoint.Name == "" || len(breakpoint.Values) == 0 { + t.Fatalf("incomplete fixture breakpoint: %+v", breakpoint) + } + if count := strings.Count(string(source), breakpoint.Marker); count != 1 { + t.Errorf("fixture marker %q occurs %d times", breakpoint.Marker, count) + } + } +} diff --git a/internal/debugabi/schema.go b/internal/debugabi/schema.go new file mode 100644 index 0000000000..123f4aa6a2 --- /dev/null +++ b/internal/debugabi/schema.go @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package debugabi defines the debugger-independent LLGo runtime contract. +package debugabi + +import ( + "bytes" + _ "embed" + "encoding/hex" + "encoding/json" + "errors" + "fmt" +) + +const ( + SchemaVersion uint8 = 1 + RuntimeLayoutVersion uint8 = 1 + LLGoABIVersion uint8 = 1 + RecordVersion uint8 = 1 + RecordSize = 16 + + NativeRecordSymbol = "__llgo_debugger_abi_v1" + LegacyMarkerSymbol = "__llgo_debugger_marker_v1" + WasmSectionName = "llgo.debugger" +) + +const recordMagic = "LLGODBG\x00" + +// ByteOrder is the target byte order recorded independently of the fixed +// byte-oriented debugger record encoding. +type ByteOrder uint8 + +const ( + ByteOrderUnknown ByteOrder = iota + ByteOrderLittle + ByteOrderBig +) + +// Record identifies the schema and the target ABI needed by debugger +// frontends. Every field is one byte so the record has the same encoding on +// native targets and in a WebAssembly custom section. +type Record struct { + RecordVersion uint8 + SchemaVersion uint8 + RuntimeLayoutVersion uint8 + LLGoABIVersion uint8 + CABIMode uint8 + PointerSize uint8 + ByteOrder ByteOrder +} + +// NewRecord returns the current debugger ABI record for a target. +func NewRecord(cabiMode, pointerSize uint8, byteOrder ByteOrder) Record { + return Record{ + RecordVersion: RecordVersion, + SchemaVersion: SchemaVersion, + RuntimeLayoutVersion: RuntimeLayoutVersion, + LLGoABIVersion: LLGoABIVersion, + CABIMode: cabiMode, + PointerSize: pointerSize, + ByteOrder: byteOrder, + } +} + +// MarshalBinary serializes the record using the stable v1 byte layout. +func (r Record) MarshalBinary() ([]byte, error) { + if err := r.Validate(); err != nil { + return nil, err + } + out := make([]byte, RecordSize) + copy(out, recordMagic) + out[8] = r.RecordVersion + out[9] = r.SchemaVersion + out[10] = r.RuntimeLayoutVersion + out[11] = r.LLGoABIVersion + out[12] = r.CABIMode + out[13] = r.PointerSize + out[14] = byte(r.ByteOrder) + return out, nil +} + +// ParseRecord validates and decodes a v1 debugger ABI record. +func ParseRecord(raw []byte) (Record, error) { + if len(raw) != RecordSize { + return Record{}, fmt.Errorf("debugger ABI record size is %d, want %d", len(raw), RecordSize) + } + if !bytes.Equal(raw[:len(recordMagic)], []byte(recordMagic)) { + return Record{}, errors.New("invalid debugger ABI record magic") + } + if raw[15] != 0 { + return Record{}, errors.New("debugger ABI record reserved byte is non-zero") + } + r := Record{ + RecordVersion: raw[8], + SchemaVersion: raw[9], + RuntimeLayoutVersion: raw[10], + LLGoABIVersion: raw[11], + CABIMode: raw[12], + PointerSize: raw[13], + ByteOrder: ByteOrder(raw[14]), + } + if err := r.Validate(); err != nil { + return Record{}, err + } + return r, nil +} + +// Validate rejects records that this schema cannot describe. +func (r Record) Validate() error { + if r.RecordVersion != RecordVersion { + return fmt.Errorf("unsupported debugger ABI record version %d", r.RecordVersion) + } + if r.SchemaVersion != SchemaVersion { + return fmt.Errorf("unsupported debugger schema version %d", r.SchemaVersion) + } + if r.RuntimeLayoutVersion != RuntimeLayoutVersion { + return fmt.Errorf("unsupported runtime layout version %d", r.RuntimeLayoutVersion) + } + if r.LLGoABIVersion != LLGoABIVersion { + return fmt.Errorf("unsupported LLGo ABI version %d", r.LLGoABIVersion) + } + if r.CABIMode > 2 { + return fmt.Errorf("invalid C ABI mode %d", r.CABIMode) + } + if r.PointerSize != 4 && r.PointerSize != 8 { + return fmt.Errorf("invalid pointer size %d", r.PointerSize) + } + if r.ByteOrder != ByteOrderLittle && r.ByteOrder != ByteOrderBig { + return fmt.Errorf("invalid byte order %d", r.ByteOrder) + } + return nil +} + +// Schema is the stable, language-neutral description consumed by LLDB, GDB, +// and browser adapters. +type Schema struct { + Contract string `json:"contract"` + SchemaVersion uint8 `json:"schema_version"` + RuntimeLayoutVersion uint8 `json:"runtime_layout_version"` + LLGoABIVersion uint8 `json:"llgo_abi_version"` + Record SchemaRecord `json:"record"` + ByteOrders map[string]string `json:"byte_orders"` + CABIModes map[string]string `json:"cabi_modes"` + RuntimeLayouts map[string]json.RawMessage `json:"runtime_layouts"` +} + +// SchemaRecord describes how frontends locate and decode Record. +type SchemaRecord struct { + Version uint8 `json:"version"` + Size int `json:"size"` + MagicHex string `json:"magic_hex"` + NativeSymbol string `json:"native_symbol"` + LegacySymbols map[string]LegacyContract `json:"legacy_symbols"` + WasmCustomSection string `json:"wasm_custom_section"` + Fields []SchemaField `json:"fields"` +} + +// LegacyContract maps the original symbol-only marker to explicit versions. +type LegacyContract struct { + SchemaVersion uint8 `json:"schema_version"` + RuntimeLayoutVersion uint8 `json:"runtime_layout_version"` + LLGoABIVersion uint8 `json:"llgo_abi_version"` +} + +// SchemaField describes one byte range in Record. +type SchemaField struct { + Name string `json:"name"` + Offset int `json:"offset"` + Size int `json:"size"` +} + +//go:embed schema_v1.json +var schemaV1 []byte + +// SchemaV1 returns an independent copy of the canonical schema document. +func SchemaV1() []byte { + return bytes.Clone(schemaV1) +} + +// ParseSchemaV1 validates the canonical schema metadata and returns it. +func ParseSchemaV1() (Schema, error) { + var schema Schema + if err := json.Unmarshal(schemaV1, &schema); err != nil { + return Schema{}, err + } + magic, err := hex.DecodeString(schema.Record.MagicHex) + if err != nil { + return Schema{}, fmt.Errorf("decode debugger ABI magic: %w", err) + } + if schema.Contract != "llgo.debugger" || + schema.SchemaVersion != SchemaVersion || + schema.RuntimeLayoutVersion != RuntimeLayoutVersion || + schema.LLGoABIVersion != LLGoABIVersion || + schema.Record.Version != RecordVersion || + schema.Record.Size != RecordSize || + !bytes.Equal(magic, []byte(recordMagic)) || + schema.Record.NativeSymbol != NativeRecordSymbol || + schema.Record.WasmCustomSection != WasmSectionName { + return Schema{}, errors.New("canonical debugger schema metadata does not match the v1 contract") + } + return schema, nil +} diff --git a/internal/debugabi/schema_test.go b/internal/debugabi/schema_test.go new file mode 100644 index 0000000000..7a8f6c4144 --- /dev/null +++ b/internal/debugabi/schema_test.go @@ -0,0 +1,121 @@ +//go:build !llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package debugabi + +import ( + "bytes" + "encoding/json" + "strings" + "testing" +) + +func TestRecordRoundTrip(t *testing.T) { + want := NewRecord(2, 8, ByteOrderLittle) + raw, err := want.MarshalBinary() + if err != nil { + t.Fatal(err) + } + if len(raw) != RecordSize || !bytes.Equal(raw[:8], []byte(recordMagic)) { + t.Fatalf("record bytes = %x", raw) + } + got, err := ParseRecord(raw) + if err != nil { + t.Fatal(err) + } + if got != want { + t.Fatalf("ParseRecord() = %+v, want %+v", got, want) + } +} + +func TestRecordValidation(t *testing.T) { + if _, err := (Record{}).MarshalBinary(); err == nil { + t.Fatal("MarshalBinary accepted an empty record") + } + valid, err := NewRecord(2, 4, ByteOrderLittle).MarshalBinary() + if err != nil { + t.Fatal(err) + } + tests := []struct { + name string + raw []byte + want string + }{ + {name: "short", raw: valid[:15], want: "size"}, + {name: "magic", raw: mutateRecord(valid, 0, 0), want: "magic"}, + {name: "record version", raw: mutateRecord(valid, 8, 2), want: "record version"}, + {name: "schema version", raw: mutateRecord(valid, 9, 2), want: "schema version"}, + {name: "runtime layout", raw: mutateRecord(valid, 10, 2), want: "runtime layout"}, + {name: "LLGo ABI", raw: mutateRecord(valid, 11, 2), want: "LLGo ABI"}, + {name: "C ABI", raw: mutateRecord(valid, 12, 3), want: "C ABI"}, + {name: "pointer size", raw: mutateRecord(valid, 13, 16), want: "pointer size"}, + {name: "byte order", raw: mutateRecord(valid, 14, 3), want: "byte order"}, + {name: "reserved", raw: mutateRecord(valid, 15, 1), want: "reserved"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := ParseRecord(tt.raw); err == nil || !strings.Contains(err.Error(), tt.want) { + t.Fatalf("ParseRecord() error = %v, want %q", err, tt.want) + } + }) + } +} + +func mutateRecord(raw []byte, offset int, value byte) []byte { + copy := bytes.Clone(raw) + copy[offset] = value + return copy +} + +func TestSchemaV1Contract(t *testing.T) { + schema, err := ParseSchemaV1() + if err != nil { + t.Fatal(err) + } + legacy, ok := schema.Record.LegacySymbols[LegacyMarkerSymbol] + if !ok || legacy.SchemaVersion != SchemaVersion || + legacy.RuntimeLayoutVersion != RuntimeLayoutVersion || + legacy.LLGoABIVersion != LLGoABIVersion { + t.Fatalf("legacy marker contract = %+v, %v", legacy, ok) + } + if len(schema.Record.Fields) != 8 { + t.Fatalf("record fields = %d, want 8", len(schema.Record.Fields)) + } + layout, ok := schema.RuntimeLayouts["1"] + if !ok { + t.Fatal("runtime layout v1 is missing") + } + var categories map[string]json.RawMessage + if err := json.Unmarshal(layout, &categories); err != nil { + t.Fatal(err) + } + for _, category := range []string{ + "string", "slice", "interface", "runtime_type", "function", + "map", "channel", "goroutine", + } { + if len(categories[category]) == 0 { + t.Errorf("runtime layout is missing %q", category) + } + } + + first := SchemaV1() + first[0] = 0 + if bytes.Equal(first, SchemaV1()) { + t.Fatal("SchemaV1 returned shared mutable storage") + } +} diff --git a/internal/debugabi/schema_v1.json b/internal/debugabi/schema_v1.json new file mode 100644 index 0000000000..1b7ac4bb7a --- /dev/null +++ b/internal/debugabi/schema_v1.json @@ -0,0 +1,120 @@ +{ + "contract": "llgo.debugger", + "schema_version": 1, + "runtime_layout_version": 1, + "llgo_abi_version": 1, + "record": { + "version": 1, + "size": 16, + "magic_hex": "4c4c474f44424700", + "native_symbol": "__llgo_debugger_abi_v1", + "legacy_symbols": { + "__llgo_debugger_marker_v1": { + "schema_version": 1, + "runtime_layout_version": 1, + "llgo_abi_version": 1 + } + }, + "wasm_custom_section": "llgo.debugger", + "fields": [ + {"name": "record_version", "offset": 8, "size": 1}, + {"name": "schema_version", "offset": 9, "size": 1}, + {"name": "runtime_layout_version", "offset": 10, "size": 1}, + {"name": "llgo_abi_version", "offset": 11, "size": 1}, + {"name": "cabi_mode", "offset": 12, "size": 1}, + {"name": "pointer_size", "offset": 13, "size": 1}, + {"name": "byte_order", "offset": 14, "size": 1}, + {"name": "reserved", "offset": 15, "size": 1} + ] + }, + "byte_orders": { + "1": "little", + "2": "big" + }, + "cabi_modes": { + "0": "none", + "1": "cfunc", + "2": "allfunc" + }, + "runtime_layouts": { + "1": { + "string": { + "type_name": "string", + "data": "data", + "length": "len" + }, + "slice": { + "type_pattern": "^\\[\\].+", + "data": "data", + "length": "len", + "capacity": "cap" + }, + "interface": { + "type_pattern": "^interface\\{.*\\}$", + "type": "type", + "data": "data", + "empty_type": "interface{}", + "itab_type": "github.com/goplus/llgo/runtime/internal/runtime.itab", + "itab_concrete_type": "_type" + }, + "runtime_type": { + "type_name": "github.com/goplus/llgo/runtime/abi.Type", + "tflag": "TFlag", + "extra_star_flag": 2, + "string": "Str_" + }, + "function": { + "type_pattern": "^struct\\{\\$f func.*; \\$data unsafe\\.Pointer\\}$", + "code": "$f", + "data": "$data", + "closure_symbol_pattern": "\\$[0-9]+(?:\\$[0-9]+)*$", + "bound_symbol_suffix": "$bound" + }, + "map": { + "type_pattern": "^map\\[.+\\].+$", + "count": "count", + "flags": "flags", + "bucket_bits": "B", + "buckets": "buckets", + "old_buckets": "oldbuckets", + "bucket_tophash": "tophash", + "bucket_keys": "keys", + "bucket_indirect_keys": "indirectkeys", + "bucket_values": "values", + "bucket_indirect_values": "indirectvalues", + "bucket_overflow": "overflow" + }, + "channel": { + "type_pattern": "^(chan |chan<- |<-chan ).+", + "count": "qcount", + "capacity": "dataqsiz", + "buffer": "buf", + "closed": "closed", + "receive_index": "recvx", + "receive_queue": "recvq", + "queue_first": "first", + "waiter_element": "elem" + }, + "goroutine": { + "head_symbol": "github.com/goplus/llgo/runtime/internal/runtime.debuggerAllgV1", + "goroutine_type": "github.com/goplus/llgo/runtime/internal/runtime.g", + "next": "alllink", + "status": "atomicstatus", + "id": "goid", + "parent_id": "parentGoid", + "m": "m", + "m_current_goroutine": "curg", + "m_p": "p", + "m_id": "id", + "m_procid": "procid", + "p_m": "m", + "p_id": "id", + "status_names": { + "1": "runnable", + "2": "running", + "6": "dead" + } + } + } + } +} diff --git a/internal/debugabi/testdata/fixture/main.go b/internal/debugabi/testdata/fixture/main.go new file mode 100644 index 0000000000..2a3d7a96de --- /dev/null +++ b/internal/debugabi/testdata/fixture/main.go @@ -0,0 +1,100 @@ +package main + +type AliasInt = int + +type NamedInt int + +type Node struct { + Value int + Next *Node +} + +type Aggregate struct { + Name string + Values [3]NamedInt + Node *Node +} + +type Greeter interface { + Greet(string) string +} + +type Prefix string + +func (prefix Prefix) Greet(name string) string { + return string(prefix) + name +} + +type Counter struct { + Base int +} + +func (counter *Counter) Add(value int) int { + return counter.Base + value +} + +func addOne(value int) int { + return value + 1 +} + +var GlobalAggregate = Aggregate{ + Name: "global", + Values: [3]NamedInt{1, 2, 3}, + Node: &Node{Value: 5}, +} + +func debuggerFixtures() { + integer := 42 + truth := true + floating := 3.5 + var alias AliasInt = 17 + named := NamedInt(18) + recursive := &Node{Value: 1, Next: &Node{Value: 2}} + aggregate := Aggregate{ + Name: "local", + Values: [3]NamedInt{4, 5, 6}, + Node: recursive, + } + text := "hello 世界" + values := []int{7, 8, 9} + mapping := map[string]int{"answer": 42} + queue := make(chan int, 3) + queue <- 10 + queue <- 11 + var greeter Greeter = Prefix("hello ") + plain := addOne + base := 20 + closure := func(value int) int { return base + value } + counter := &Counter{Base: 30} + bound := counter.Add + plainResult := plain(1) + closureResult := closure(2) + boundResult := bound(3) + interfaceResult := greeter.Greet("LLGo") + mapResult := mapping["answer"] + + ready := make(chan struct{}) + release := make(chan struct{}) + result := make(chan int, 1) + go func() { + close(ready) + <-release + result <- 81 + }() + <-ready + println( + integer, truth, floating, alias, named, recursive, &aggregate, + text, values, mapping, queue, greeter, + plain, closure, bound, + plainResult, closureResult, boundResult, interfaceResult, mapResult, + &GlobalAggregate, + ) // DEBUGGER_BREAK: all_values + close(release) + if got := <-result; got != 81 { + panic("goroutine result mismatch") + } +} + +func main() { + debuggerFixtures() +} diff --git a/internal/debugabi/testdata/fixture/manifest_v1.json b/internal/debugabi/testdata/fixture/manifest_v1.json new file mode 100644 index 0000000000..7457f1ecb9 --- /dev/null +++ b/internal/debugabi/testdata/fixture/manifest_v1.json @@ -0,0 +1,43 @@ +{ + "schema_version": 1, + "source": "main.go", + "categories": [ + "primitives", + "aliases", + "named_types", + "recursive_types", + "aggregates", + "strings", + "slices", + "maps", + "channels", + "interfaces", + "functions", + "closures", + "bound_methods", + "goroutines" + ], + "breakpoints": [ + { + "name": "all_values", + "marker": "DEBUGGER_BREAK: all_values", + "values": { + "integer": 42, + "truth": true, + "floating": 3.5, + "alias": 17, + "named": 18, + "recursive.Value": 1, + "recursive.Next.Value": 2, + "aggregate.Name": "local", + "text": "hello 世界", + "values": [7, 8, 9], + "mapResult": 42, + "plainResult": 2, + "closureResult": 22, + "boundResult": 33, + "interfaceResult": "hello LLGo" + } + } + ] +} From 62182b58f1a63c0656b56e8e171b014def27373b Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 2 Aug 2026 03:57:07 +0800 Subject: [PATCH 2/4] debug: publish debugger ABI records --- internal/build/build.go | 1 + internal/build/debug_artifact_external.go | 33 +++++++++++ .../build/debug_artifact_external_test.go | 55 +++++++++++++++++- internal/debuginfo/builder.go | 35 ++++++++--- internal/debuginfo/builder_test.go | 24 +++++++- internal/wasmdebug/wasmdebug.go | 48 +++++++++++++++ internal/wasmdebug/wasmdebug_test.go | 58 ++++++++++++++++++- ssa/di.go | 10 +++- ssa/target.go | 1 + 9 files changed, 249 insertions(+), 16 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index 058bb7d2d1..b951e78eab 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -450,6 +450,7 @@ func Build(inv Invocation) ([]Package, error) { GOOS: conf.Goos, GOARCH: conf.Goarch, Target: conf.Target, + ABIMode: uint8(conf.AbiMode), OptLevel: conf.OptLevel, } diff --git a/internal/build/debug_artifact_external.go b/internal/build/debug_artifact_external.go index a2e5224051..58953147d9 100644 --- a/internal/build/debug_artifact_external.go +++ b/internal/build/debug_artifact_external.go @@ -22,6 +22,7 @@ import ( "os" "path/filepath" + "github.com/goplus/llgo/internal/debugabi" "github.com/goplus/llgo/internal/wasmdebug" ) @@ -35,6 +36,9 @@ func finalizeDebugArtifact(conf *Config, out *OutFmtDetails, verbose bool) error // path and must not leave a stale optional artifact behind. _ = os.Remove(dwarfSidecarPath(out.Out)) } + if conf.DebugArtifactMode == DebugArtifactEmbedded && conf.Goarch == "wasm" { + return finalizeEmbeddedWasmDebuggerRecord(conf, out) + } return nil } if out.Out == "" { @@ -47,6 +51,12 @@ func finalizeDebugArtifact(conf *Config, out *OutFmtDetails, verbose bool) error if err != nil { return err } + if conf.Goarch == "wasm" { + raw, err = wasmdebug.SetDebuggerRecord(raw, wasmDebuggerRecord(conf)) + if err != nil { + return fmt.Errorf("add WebAssembly debugger ABI record: %w", err) + } + } // external_debug_info stores a URL, not a filesystem path. Keep the // sidecar adjacent to the module and escape its filename for URL lookup. main, err := wasmdebug.Externalize(raw, url.PathEscape(filepath.Base(out.DWARF))) @@ -70,6 +80,29 @@ func finalizeDebugArtifact(conf *Config, out *OutFmtDetails, verbose bool) error return nil } +func finalizeEmbeddedWasmDebuggerRecord(conf *Config, out *OutFmtDetails) error { + if out.Out == "" { + return fmt.Errorf("embedded WebAssembly debugger artifact path is empty") + } + raw, err := os.ReadFile(out.Out) + if err != nil { + return err + } + raw, err = wasmdebug.SetDebuggerRecord(raw, wasmDebuggerRecord(conf)) + if err != nil { + return fmt.Errorf("add WebAssembly debugger ABI record: %w", err) + } + info, err := os.Stat(out.Out) + if err != nil { + return err + } + return writeDebugArtifactFile(out.Out, raw, info.Mode()) +} + +func wasmDebuggerRecord(conf *Config) debugabi.Record { + return debugabi.NewRecord(uint8(conf.AbiMode), 4, debugabi.ByteOrderLittle) +} + func writeDebugArtifactFile(path string, data []byte, mode os.FileMode) (err error) { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err diff --git a/internal/build/debug_artifact_external_test.go b/internal/build/debug_artifact_external_test.go index 61f1712c6e..022914a9a2 100644 --- a/internal/build/debug_artifact_external_test.go +++ b/internal/build/debug_artifact_external_test.go @@ -9,6 +9,7 @@ import ( "path/filepath" "testing" + "github.com/goplus/llgo/internal/debugabi" "github.com/goplus/llgo/internal/wasmdebug" ) @@ -35,7 +36,7 @@ func TestFinalizeExternalWasmDWARF(t *testing.T) { t.Fatal(err) } if err := finalizeDebugArtifact( - &Config{DebugArtifactMode: DebugArtifactExternal}, + &Config{Goarch: "wasm", AbiMode: 2, DebugArtifactMode: DebugArtifactExternal}, &OutFmtDetails{Out: module, DWARF: sidecar}, false, ); err != nil { @@ -45,8 +46,13 @@ func TestFinalizeExternalWasmDWARF(t *testing.T) { if err != nil { t.Fatal(err) } - if !bytes.Equal(debugModule, original) { - t.Fatal("external DWARF sidecar differs from the linked debug module") + wantRecord := debugabi.NewRecord(2, 4, debugabi.ByteOrderLittle) + wantDebugModule, err := wasmdebug.SetDebuggerRecord(original, wantRecord) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(debugModule, wantDebugModule) { + t.Fatal("external DWARF sidecar differs from the recorded debug module") } main, err := os.ReadFile(module) if err != nil { @@ -62,6 +68,30 @@ func TestFinalizeExternalWasmDWARF(t *testing.T) { if err != nil || !ok || url != "app%20debug.wasm" { t.Fatalf("main external URL = %q, %v, %v", url, ok, err) } + for name, contents := range map[string][]byte{"main": main, "sidecar": debugModule} { + if got, ok, err := wasmdebug.DebuggerRecord(contents); err != nil || !ok || got != wantRecord { + t.Fatalf("%s DebuggerRecord = %+v, %v, %v", name, got, ok, err) + } + } + + embedded := filepath.Join(dir, "embedded.wasm") + if err := os.WriteFile(embedded, original, 0o755); err != nil { + t.Fatal(err) + } + if err := finalizeDebugArtifact( + &Config{Goarch: "wasm", AbiMode: 1, DebugArtifactMode: DebugArtifactEmbedded}, + &OutFmtDetails{Out: embedded}, + false, + ); err != nil { + t.Fatal(err) + } + embeddedModule, err := os.ReadFile(embedded) + if err != nil { + t.Fatal(err) + } + if got, ok, err := wasmdebug.DebuggerRecord(embeddedModule); err != nil || !ok || got.CABIMode != 1 { + t.Fatalf("embedded DebuggerRecord = %+v, %v, %v", got, ok, err) + } } func TestFinalizeDebugArtifactValidation(t *testing.T) { @@ -74,6 +104,25 @@ func TestFinalizeDebugArtifactValidation(t *testing.T) { if err := finalizeDebugArtifact(&Config{DebugArtifactMode: DebugArtifactExternal}, &OutFmtDetails{}, false); err == nil { t.Fatal("external mode accepted an empty executable path") } + if err := finalizeDebugArtifact( + &Config{DebugArtifactMode: DebugArtifactExternal}, + &OutFmtDetails{Out: "app.wasm"}, false, + ); err == nil { + t.Fatal("external mode accepted an empty sidecar path") + } + if err := finalizeDebugArtifact( + &Config{Goarch: "wasm", DebugArtifactMode: DebugArtifactEmbedded}, + &OutFmtDetails{}, false, + ); err == nil { + t.Fatal("embedded Wasm mode accepted an empty executable path") + } + missing := filepath.Join(t.TempDir(), "missing.wasm") + if err := finalizeDebugArtifact( + &Config{Goarch: "wasm", DebugArtifactMode: DebugArtifactEmbedded}, + &OutFmtDetails{Out: missing}, false, + ); err == nil { + t.Fatal("embedded Wasm mode accepted a missing executable") + } } func TestFinalizeDebugArtifactRemovesStaleSidecar(t *testing.T) { diff --git a/internal/debuginfo/builder.go b/internal/debuginfo/builder.go index 5dc973f51b..ad0c83f1da 100644 --- a/internal/debuginfo/builder.go +++ b/internal/debuginfo/builder.go @@ -3,8 +3,10 @@ package debuginfo import ( + "fmt" "path/filepath" + "github.com/goplus/llgo/internal/debugabi" "github.com/xgo-dev/llvm" ) @@ -15,15 +17,14 @@ const ( // inspection for DW_LANG_Go. Use its C language path for interoperable // access to LLGo's otherwise Go-shaped DWARF. dwarfSourceLanguageC llvm.DwarfLang = 1 - - debuggerMarkerSymbol = "__llgo_debugger_marker_v1" ) // Config describes properties of the generated debug information. Optimized // reports what the compilation pipeline does; it does not select any pass. type Config struct { - Producer string - Optimized bool + Producer string + Optimized bool + DebuggerRecord debugabi.Record } // Builder is a package-local owner of an LLVM DIBuilder. Finalize must be @@ -54,7 +55,7 @@ func New(module llvm.Module, config Config) *Builder { b.addModuleFlag(8, "PIC Level", 2) b.addModuleFlag(7, "uwtable", 1) b.addModuleFlag(7, "frame-pointer", 1) - b.addDebuggerMarker() + b.addDebuggerMarkers() ctx := module.Context() module.AddNamedMetadataOperand("llvm.ident", ctx.MDNode([]llvm.Metadata{ @@ -63,17 +64,35 @@ func New(module llvm.Module, config Config) *Builder { return b } -func (b *Builder) addDebuggerMarker() { +func (b *Builder) addDebuggerMarkers() { ctx := b.module.Context() i8 := ctx.Int8Type() - marker := llvm.AddGlobal(b.module, i8, debuggerMarkerSymbol) + marker := llvm.AddGlobal(b.module, i8, debugabi.LegacyMarkerSymbol) marker.SetInitializer(llvm.ConstInt(i8, 1, false)) marker.SetGlobalConstant(true) marker.SetLinkage(llvm.LinkOnceODRLinkage) marker.SetVisibility(llvm.HiddenVisibility) ptr := llvm.PointerType(i8, 0) - usedInit := llvm.ConstArray(ptr, []llvm.Value{llvm.ConstBitCast(marker, ptr)}) + retained := []llvm.Value{llvm.ConstBitCast(marker, ptr)} + if b.config.DebuggerRecord.RecordVersion != 0 { + raw, err := b.config.DebuggerRecord.MarshalBinary() + if err != nil { + panic(fmt.Sprintf("debuginfo: invalid debugger ABI record: %v", err)) + } + values := make([]llvm.Value, len(raw)) + for index, value := range raw { + values[index] = llvm.ConstInt(i8, uint64(value), false) + } + initializer := llvm.ConstArray(i8, values) + record := llvm.AddGlobal(b.module, initializer.Type(), debugabi.NativeRecordSymbol) + record.SetInitializer(initializer) + record.SetGlobalConstant(true) + record.SetLinkage(llvm.LinkOnceODRLinkage) + record.SetVisibility(llvm.HiddenVisibility) + retained = append(retained, llvm.ConstBitCast(record, ptr)) + } + usedInit := llvm.ConstArray(ptr, retained) used := llvm.AddGlobal(b.module, usedInit.Type(), "llvm.used") used.SetInitializer(usedInit) used.SetLinkage(llvm.AppendingLinkage) diff --git a/internal/debuginfo/builder_test.go b/internal/debuginfo/builder_test.go index 952a02b267..b2cc11814a 100644 --- a/internal/debuginfo/builder_test.go +++ b/internal/debuginfo/builder_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "github.com/goplus/llgo/internal/debugabi" "github.com/xgo-dev/llvm" ) @@ -16,7 +17,11 @@ func TestBuilderLifecycleAndModuleMetadata(t *testing.T) { module := ctx.NewModule("debug-test") defer module.Dispose() - builder := New(module, Config{Producer: "LLGo", Optimized: true}) + builder := New(module, Config{ + Producer: "LLGo", + Optimized: true, + DebuggerRecord: debugabi.NewRecord(2, 8, debugabi.ByteOrderLittle), + }) cu := builder.CompileUnit("main.go", "/src/example") file := builder.File("/src/example/main.go") if got := builder.File("/src/example/dir/../main.go"); got != file { @@ -54,7 +59,8 @@ func TestBuilderLifecycleAndModuleMetadata(t *testing.T) { `producer: "LLGo"`, `isOptimized: true`, `@__llgo_debugger_marker_v1 = linkonce_odr hidden constant i8 1`, - `@llvm.used = appending global [1 x ptr] [ptr @__llgo_debugger_marker_v1], section "llvm.metadata"`, + `@__llgo_debugger_abi_v1 = linkonce_odr hidden constant [16 x i8]`, + `@llvm.used = appending global [2 x ptr] [ptr @__llgo_debugger_marker_v1, ptr @__llgo_debugger_abi_v1], section "llvm.metadata"`, `!{i32 7, !"Dwarf Version", i32 4}`, } { if !strings.Contains(ir, want) { @@ -63,6 +69,20 @@ func TestBuilderLifecycleAndModuleMetadata(t *testing.T) { } } +func TestBuilderRejectsInvalidDebuggerRecord(t *testing.T) { + ctx := llvm.NewContext() + defer ctx.Dispose() + module := ctx.NewModule("invalid-debugger-record") + defer module.Dispose() + + defer func() { + if recover() == nil { + t.Fatal("New accepted an invalid debugger record") + } + }() + New(module, Config{DebuggerRecord: debugabi.Record{RecordVersion: 2}}) +} + func TestBuilderMetadataOperations(t *testing.T) { ctx := llvm.NewContext() defer ctx.Dispose() diff --git a/internal/wasmdebug/wasmdebug.go b/internal/wasmdebug/wasmdebug.go index 8ed96d1c1e..16fc278525 100644 --- a/internal/wasmdebug/wasmdebug.go +++ b/internal/wasmdebug/wasmdebug.go @@ -24,6 +24,8 @@ import ( "fmt" "strings" "unicode/utf8" + + "github.com/goplus/llgo/internal/debugabi" ) const externalDebugInfo = "external_debug_info" @@ -148,6 +150,52 @@ func HasDWARF(module []byte) (bool, error) { return false, nil } +// SetDebuggerRecord replaces the LLGo debugger ABI custom section with the +// canonical encoding of record. Other custom and standard sections retain +// their original bytes and order. +func SetDebuggerRecord(module []byte, record debugabi.Record) ([]byte, error) { + raw, err := record.MarshalBinary() + if err != nil { + return nil, err + } + sections, err := parse(module) + if err != nil { + return nil, err + } + out := append([]byte(nil), wasmHeader...) + for _, section := range sections { + if section.id == 0 && section.name == debugabi.WasmSectionName { + continue + } + out = append(out, section.raw...) + } + return appendCustomSection(out, debugabi.WasmSectionName, raw), nil +} + +// DebuggerRecord returns the unique LLGo debugger ABI record, if present. +func DebuggerRecord(module []byte) (debugabi.Record, bool, error) { + sections, err := parse(module) + if err != nil { + return debugabi.Record{}, false, err + } + var record debugabi.Record + found := false + for _, section := range sections { + if section.id != 0 || section.name != debugabi.WasmSectionName { + continue + } + if found { + return debugabi.Record{}, false, errors.New("multiple LLGo debugger ABI sections") + } + record, err = debugabi.ParseRecord(section.content) + if err != nil { + return debugabi.Record{}, false, fmt.Errorf("invalid LLGo debugger ABI section: %w", err) + } + found = true + } + return record, found, nil +} + // Externalize removes embedded DWARF custom sections and appends the standard // external_debug_info URL record. The original module is suitable for use as // the sidecar because the convention permits it to retain code and data. diff --git a/internal/wasmdebug/wasmdebug_test.go b/internal/wasmdebug/wasmdebug_test.go index a02692cad1..00622877de 100644 --- a/internal/wasmdebug/wasmdebug_test.go +++ b/internal/wasmdebug/wasmdebug_test.go @@ -4,6 +4,8 @@ import ( "bytes" "strings" "testing" + + "github.com/goplus/llgo/internal/debugabi" ) func appendSection(dst []byte, id byte, payload []byte) []byte { @@ -24,7 +26,11 @@ func debugFixture() []byte { } func TestExternalize(t *testing.T) { - sidecar := debugFixture() + record := debugabi.NewRecord(2, 4, debugabi.ByteOrderLittle) + sidecar, err := SetDebuggerRecord(debugFixture(), record) + if err != nil { + t.Fatal(err) + } url := strings.Repeat("debug-", 24) + ".wasm" main, err := Externalize(sidecar, url) if err != nil { @@ -69,6 +75,12 @@ func TestExternalize(t *testing.T) { if countCustom(mainSections, "producers") != 1 { t.Fatal("externalization removed an unrelated custom section") } + if got, ok, err := DebuggerRecord(main); err != nil || !ok || got != record { + t.Fatalf("main DebuggerRecord = %+v, %v, %v", got, ok, err) + } + if got, ok, err := DebuggerRecord(sidecar); err != nil || !ok || got != record { + t.Fatalf("sidecar DebuggerRecord = %+v, %v, %v", got, ok, err) + } } func countCustom(sections []section, name string) int { @@ -143,3 +155,47 @@ func TestHasDWARFRejectsMalformedCustomSection(t *testing.T) { t.Fatal("HasDWARF accepted a truncated custom-section name") } } + +func TestDebuggerRecordValidation(t *testing.T) { + base := append([]byte(nil), wasmHeader...) + record := debugabi.NewRecord(1, 4, debugabi.ByteOrderLittle) + module, err := SetDebuggerRecord(base, record) + if err != nil { + t.Fatal(err) + } + replaced, err := SetDebuggerRecord(module, debugabi.NewRecord(2, 4, debugabi.ByteOrderLittle)) + if err != nil { + t.Fatal(err) + } + if sections, err := parse(replaced); err != nil || countCustom(sections, debugabi.WasmSectionName) != 1 { + t.Fatalf("replacement custom sections = %v, %v", sections, err) + } + if got, ok, err := DebuggerRecord(replaced); err != nil || !ok || got.CABIMode != 2 { + t.Fatalf("DebuggerRecord = %+v, %v, %v", got, ok, err) + } + if got, ok, err := DebuggerRecord(base); err != nil || ok || got != (debugabi.Record{}) { + t.Fatalf("absent DebuggerRecord = %+v, %v, %v", got, ok, err) + } + + invalid := appendCustomSection(base, debugabi.WasmSectionName, []byte("invalid")) + if _, _, err := DebuggerRecord(invalid); err == nil { + t.Fatal("DebuggerRecord accepted invalid content") + } + valid, err := record.MarshalBinary() + if err != nil { + t.Fatal(err) + } + duplicate := appendCustomSection(appendCustomSection(base, debugabi.WasmSectionName, valid), debugabi.WasmSectionName, valid) + if _, _, err := DebuggerRecord(duplicate); err == nil { + t.Fatal("DebuggerRecord accepted duplicate sections") + } + if _, err := SetDebuggerRecord([]byte("not wasm"), record); err == nil { + t.Fatal("SetDebuggerRecord accepted an invalid module") + } + if _, _, err := DebuggerRecord([]byte("not wasm")); err == nil { + t.Fatal("DebuggerRecord accepted an invalid module") + } + if _, err := SetDebuggerRecord(base, debugabi.Record{}); err == nil { + t.Fatal("SetDebuggerRecord accepted an invalid record") + } +} diff --git a/ssa/di.go b/ssa/di.go index f046d8601a..0422a6ccac 100644 --- a/ssa/di.go +++ b/ssa/di.go @@ -6,6 +6,7 @@ import ( "go/token" "go/types" + "github.com/goplus/llgo/internal/debugabi" "github.com/goplus/llgo/internal/debuginfo" "github.com/xgo-dev/llvm" ) @@ -24,10 +25,15 @@ type aDIBuilder struct { type diBuilder = *aDIBuilder func newDIBuilder(prog Program, pkg Package, positioner Positioner) diBuilder { + byteOrder := debugabi.ByteOrderLittle + if prog.TargetData().ByteOrder() == llvm.BigEndian { + byteOrder = debugabi.ByteOrderBig + } return &aDIBuilder{ di: debuginfo.New(pkg.mod, debuginfo.Config{ - Producer: "LLGo", - Optimized: prog.debugInfoOptimized, + Producer: "LLGo", + Optimized: prog.debugInfoOptimized, + DebuggerRecord: debugabi.NewRecord(prog.Target().ABIMode, uint8(prog.PointerSize()), byteOrder), }), prog: prog, types: make(map[*aType]DIType), diff --git a/ssa/target.go b/ssa/target.go index a352b477fd..b048fc0402 100644 --- a/ssa/target.go +++ b/ssa/target.go @@ -31,6 +31,7 @@ type Target struct { GOARCH string GOARM string // "5", "6", "7" (default) Target string // target name from -target flag (e.g., "esp32", "arm7tdmi", "wasi") + ABIMode uint8 // LLGo C ABI mode recorded for debugger consumers OptLevel optlevel.Level } From 754e096109fa3ee58e77261d92b12fff94282dd4 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 2 Aug 2026 03:57:29 +0800 Subject: [PATCH 3/4] cmd: load the debugger ABI schema in LLDB --- cmd/internal/lldb/lldb.go | 6 + cmd/internal/lldb/lldb_test.go | 9 +- cmd/internal/lldb/llgo_plugin.py | 278 ++++++++++++++++++++++++++++--- cmd/llgo/lldbtest/runtest.sh | 49 +++++- cmd/llgo/lldbtest/test.py | 6 +- 5 files changed, 316 insertions(+), 32 deletions(-) diff --git a/cmd/internal/lldb/lldb.go b/cmd/internal/lldb/lldb.go index 348de22f9d..75f798bd91 100644 --- a/cmd/internal/lldb/lldb.go +++ b/cmd/internal/lldb/lldb.go @@ -30,10 +30,12 @@ import ( "strings" "github.com/goplus/llgo/cmd/internal/base" + "github.com/goplus/llgo/internal/debugabi" "github.com/goplus/llgo/internal/mockable" ) const minimumUpstreamLLDBVersion = 18 +const debuggerSchemaFilename = "llgo_debugger_schema_v1.json" var ( //go:embed llgo_plugin.py @@ -91,6 +93,10 @@ func run(configuredPath string, args []string, stdin io.Reader, stdout, stderr i if err := os.WriteFile(pluginPath, pluginSource, 0600); err != nil { return fmt.Errorf("llgo lldb: write plugin: %w", err) } + schemaPath := filepath.Join(pluginDir, debuggerSchemaFilename) + if err := os.WriteFile(schemaPath, debugabi.SchemaV1(), 0600); err != nil { + return fmt.Errorf("llgo lldb: write debugger schema: %w", err) + } lldbArgs := make([]string, 0, len(args)+2) lldbArgs = append(lldbArgs, "-O", lldbImportCommand(pluginPath)) diff --git a/cmd/internal/lldb/lldb_test.go b/cmd/internal/lldb/lldb_test.go index a4eb66212a..f55fd15607 100644 --- a/cmd/internal/lldb/lldb_test.go +++ b/cmd/internal/lldb/lldb_test.go @@ -117,7 +117,10 @@ func TestRunImportsEmbeddedPluginAndPassesArguments(t *testing.T) { printf '%s\n' "$@" > "$LLGO_LLDB_TEST_CAPTURE" plugin=$(printf '%s\n' "$2" | sed 's/^command script import "//; s/"$//') test -s "$plugin" -grep -q __llgo_debugger_marker_v1 "$plugin" +schema=$(dirname "$plugin")/llgo_debugger_schema_v1.json +test -s "$schema" +grep -q '"contract": "llgo.debugger"' "$schema" +grep -q __llgo_debugger_marker_v1 "$schema" `) var stdout, stderr bytes.Buffer @@ -170,11 +173,13 @@ func TestEmbeddedPluginIdentity(t *testing.T) { source := string(pluginSource) for _, want := range []string{ "__lldb_init_module", - "__llgo_debugger_marker_v1", + "LLGO_DEBUGGER_MARKER_PREFIX", "is_llgo_compiler", "inspect_target", "LLGO_DEBUGGER_SCHEMAS", "LLGO_RUNTIME_LAYOUTS", + "LLGO_DEBUGGER_RECORD_SYMBOL", + "llgo_debugger_schema_v1.json", "string_summary", "slice_summary", "SliceSyntheticProvider", diff --git a/cmd/internal/lldb/llgo_plugin.py b/cmd/internal/lldb/llgo_plugin.py index 843c77cf57..44a6b8304b 100644 --- a/cmd/internal/lldb/llgo_plugin.py +++ b/cmd/internal/lldb/llgo_plugin.py @@ -1,21 +1,73 @@ # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring from dataclasses import dataclass +import json +from pathlib import Path from typing import List, Optional, Dict, Any, Tuple import re import lldb LLGO_DEBUGGER_MARKER_PREFIX = "__llgo_debugger_marker_v" -LLGO_DEBUGGER_SCHEMAS = { - "__llgo_debugger_marker_v1": (1, 1), -} +LLGO_DEBUGGER_SCHEMA_FILENAME = "llgo_debugger_schema_v1.json" LLGO_TYPE_CATEGORY = "LLGo" LLGO_MAX_STRING_SUMMARY_BYTES = 256 LLGO_DEFAULT_MAX_CHILDREN = 256 _TARGET_INFO_CACHE: Dict[Tuple[Any, ...], "LLGoTargetInfo"] = {} +def _load_debugger_schema() -> Tuple[Dict[str, Any], Optional[str]]: + source = Path(__file__).resolve() + candidates = [source.with_name(LLGO_DEBUGGER_SCHEMA_FILENAME)] + if len(source.parents) > 3: + candidates.append( + source.parents[3] / "internal" / "debugabi" / "schema_v1.json") + errors = [] + for path in candidates: + try: + with path.open("r", encoding="utf-8") as schema_file: + schema = json.load(schema_file) + if schema.get("contract") != "llgo.debugger": + raise ValueError("unexpected debugger schema contract") + return schema, None + except (OSError, ValueError, TypeError, json.JSONDecodeError) as error: + errors.append(f"{path}: {error}") + return {}, "; ".join(errors) + + +LLGO_DEBUGGER_SCHEMA, LLGO_DEBUGGER_SCHEMA_ERROR = _load_debugger_schema() +_RECORD_SCHEMA = LLGO_DEBUGGER_SCHEMA.get("record", {}) +LLGO_DEBUGGER_RECORD_SYMBOL = _RECORD_SCHEMA.get("native_symbol", "") +LLGO_DEBUGGER_RECORD_SIZE = int(_RECORD_SCHEMA.get("size", 0)) +try: + LLGO_DEBUGGER_RECORD_MAGIC = bytes.fromhex( + _RECORD_SCHEMA.get("magic_hex", "")) +except ValueError: + LLGO_DEBUGGER_RECORD_MAGIC = b"" +LLGO_DEBUGGER_RECORD_FIELDS = { + field.get("name"): field + for field in _RECORD_SCHEMA.get("fields", []) + if isinstance(field, dict) and field.get("name") +} +LLGO_DEBUGGER_SCHEMAS = { + symbol: ( + int(contract.get("schema_version", 0)), + int(contract.get("runtime_layout_version", 0)), + int(contract.get("llgo_abi_version", 0)), + ) + for symbol, contract in _RECORD_SCHEMA.get("legacy_symbols", {}).items() + if isinstance(contract, dict) +} +LLGO_BYTE_ORDERS = { + int(value): name + for value, name in LLGO_DEBUGGER_SCHEMA.get("byte_orders", {}).items() +} +LLGO_CABI_MODES = { + int(value): name + for value, name in LLGO_DEBUGGER_SCHEMA.get("cabi_modes", {}).items() +} + + @dataclass(frozen=True) class LLGoRuntimeLayout: string_type: str @@ -36,17 +88,39 @@ class LLGoSliceValue: element_size: int -LLGO_RUNTIME_LAYOUTS = { - 1: LLGoRuntimeLayout( - string_type="string", - string_data="data", - string_len="len", - slice_type_pattern=r"^\[\].+", - slice_data="data", - slice_len="len", - slice_cap="cap", - ), -} +def _runtime_layouts() -> Dict[int, LLGoRuntimeLayout]: + layouts = {} + for version, raw in LLGO_DEBUGGER_SCHEMA.get( + "runtime_layouts", {}).items(): + string_layout = raw.get("string", {}) + slice_layout = raw.get("slice", {}) + try: + layouts[int(version)] = LLGoRuntimeLayout( + string_type=string_layout["type_name"], + string_data=string_layout["data"], + string_len=string_layout["length"], + slice_type_pattern=slice_layout["type_pattern"], + slice_data=slice_layout["data"], + slice_len=slice_layout["length"], + slice_cap=slice_layout["capacity"], + ) + except (KeyError, TypeError, ValueError): + continue + return layouts + + +LLGO_RUNTIME_LAYOUTS = _runtime_layouts() + + +@dataclass(frozen=True) +class LLGoDebuggerRecord: + record_version: int + schema_version: int + runtime_layout_version: int + llgo_abi_version: int + cabi_mode: int + pointer_size: int + byte_order: int @dataclass(frozen=True) @@ -57,10 +131,16 @@ class LLGoTargetInfo: triple: str pointer_size: int byte_order: str + record_version: Optional[int] = None + llgo_abi_version: Optional[int] = None + cabi_mode: Optional[int] = None + cabi_name: Optional[str] = None + compatibility_error: Optional[str] = None @property def supported(self) -> bool: - return self.schema_version is not None + return (self.schema_version is not None and + self.compatibility_error is None) def log(*args: Any, **kwargs: Any) -> None: @@ -131,6 +211,91 @@ def _marker_versions(target: lldb.SBTarget) -> Tuple[int, ...]: return tuple(sorted(versions)) +def _sbdata_bytes(data: lldb.SBData, size: int) -> Optional[bytes]: + if not data or not data.IsValid() or data.GetByteSize() < size: + return None + error = lldb.SBError() + raw = bytes(data.GetUnsignedInt8(error, offset) for offset in range(size)) + return raw if error.Success() else None + + +def _read_debugger_record(target: lldb.SBTarget) -> Optional[bytes]: + if not LLGO_DEBUGGER_RECORD_SYMBOL or LLGO_DEBUGGER_RECORD_SIZE <= 0: + return None + + values = target.FindGlobalVariables(LLGO_DEBUGGER_RECORD_SYMBOL, 256) + records = [] + if values and values.IsValid(): + for index in range(values.GetSize()): + raw = _sbdata_bytes( + values.GetValueAtIndex(index).GetData(), + LLGO_DEBUGGER_RECORD_SIZE) + if raw is not None: + records.append(raw) + + if not records: + for module_index in range(target.GetNumModules()): + module = target.GetModuleAtIndex(module_index) + for symbol_index in range(module.GetNumSymbols()): + symbol = module.GetSymbolAtIndex(symbol_index) + if symbol.GetName() != LLGO_DEBUGGER_RECORD_SYMBOL: + continue + error = lldb.SBError() + raw = target.ReadMemory( + symbol.GetStartAddress(), LLGO_DEBUGGER_RECORD_SIZE, error) + if error.Success() and raw is not None: + records.append(bytes(raw)) + + if not records: + return None + first = records[0] + return first if all(record == first for record in records) else b"" + + +def _record_field(raw: bytes, name: str) -> Optional[int]: + field = LLGO_DEBUGGER_RECORD_FIELDS.get(name) + if not isinstance(field, dict): + return None + try: + offset = int(field["offset"]) + size = int(field["size"]) + except (KeyError, TypeError, ValueError): + return None + if offset < 0 or size <= 0 or offset + size > len(raw): + return None + return int.from_bytes(raw[offset:offset + size], "little") + + +def _decode_debugger_record( + raw: bytes) -> Tuple[Optional[LLGoDebuggerRecord], Optional[str]]: + if len(raw) != LLGO_DEBUGGER_RECORD_SIZE: + return None, "conflicting or incorrectly sized native records" + if not LLGO_DEBUGGER_RECORD_MAGIC or not raw.startswith( + LLGO_DEBUGGER_RECORD_MAGIC): + return None, "invalid record magic" + values = { + name: _record_field(raw, name) + for name in ( + "record_version", "schema_version", "runtime_layout_version", + "llgo_abi_version", "cabi_mode", "pointer_size", "byte_order", + "reserved", + ) + } + if any(value is None for value in values.values()): + return None, "record fields do not match the loaded schema" + if values["reserved"] != 0: + return None, "record reserved byte is non-zero" + return LLGoDebuggerRecord( + record_version=values["record_version"], + schema_version=values["schema_version"], + runtime_layout_version=values["runtime_layout_version"], + llgo_abi_version=values["llgo_abi_version"], + cabi_mode=values["cabi_mode"], + pointer_size=values["pointer_size"], + byte_order=values["byte_order"], + ), None + + def _byte_order_name(byte_order: int) -> str: return { lldb.eByteOrderBig: "big", @@ -167,32 +332,100 @@ def inspect_target(target: lldb.SBTarget) -> LLGoTargetInfo: marker_versions = _marker_versions(target) schema_version: Optional[int] = None runtime_layout_version: Optional[int] = None + llgo_abi_version: Optional[int] = None + record_version: Optional[int] = None + cabi_mode: Optional[int] = None + cabi_name: Optional[str] = None + compatibility_error: Optional[str] = None + pointer_size = target.GetAddressByteSize() + byte_order = _byte_order_name(target.GetByteOrder()) + + raw_record = _read_debugger_record(target) + if raw_record is not None: + record, compatibility_error = _decode_debugger_record(raw_record) + if record is not None: + record_version = record.record_version + schema_version = record.schema_version + runtime_layout_version = record.runtime_layout_version + llgo_abi_version = record.llgo_abi_version + cabi_mode = record.cabi_mode + cabi_name = LLGO_CABI_MODES.get(cabi_mode) + record_byte_order = LLGO_BYTE_ORDERS.get(record.byte_order) + expected = ( + int(_RECORD_SCHEMA.get("version", 0)), + int(LLGO_DEBUGGER_SCHEMA.get("schema_version", 0)), + int(LLGO_DEBUGGER_SCHEMA.get("runtime_layout_version", 0)), + int(LLGO_DEBUGGER_SCHEMA.get("llgo_abi_version", 0)), + ) + actual = ( + record.record_version, + record.schema_version, + record.runtime_layout_version, + record.llgo_abi_version, + ) + if actual != expected: + compatibility_error = ( + "unsupported record/schema/runtime/ABI versions " + f"{actual}, want {expected}") + elif cabi_name is None: + compatibility_error = f"unsupported C ABI mode {cabi_mode}" + elif record.pointer_size != pointer_size: + compatibility_error = ( + f"record pointer size {record.pointer_size} does not match " + f"target pointer size {pointer_size}") + elif record_byte_order != byte_order: + compatibility_error = ( + f"record byte order {record_byte_order or record.byte_order} " + f"does not match target byte order {byte_order}") + elif marker_versions and marker_versions != (record.schema_version,): + compatibility_error = ( + f"record schema v{record.schema_version} conflicts with " + f"legacy marker version(s) {marker_versions}") + # Multiple markers are ambiguous: do not select a runtime layout merely # because one of the advertised schema versions happens to be supported. - if len(marker_versions) == 1: + if raw_record is None and len(marker_versions) == 1: candidate = marker_versions[0] - for supported_schema, supported_runtime_layout in ( + for (supported_schema, supported_runtime_layout, + supported_llgo_abi) in ( LLGO_DEBUGGER_SCHEMAS.values()): if candidate == supported_schema: schema_version = supported_schema runtime_layout_version = supported_runtime_layout + llgo_abi_version = supported_llgo_abi break + if ((marker_versions or raw_record is not None) and + not LLGO_DEBUGGER_SCHEMA and compatibility_error is None): + compatibility_error = ( + "debugger schema could not be loaded: " + + (LLGO_DEBUGGER_SCHEMA_ERROR or "unknown error")) + info = LLGoTargetInfo( marker_versions=marker_versions, schema_version=schema_version, runtime_layout_version=runtime_layout_version, triple=target.GetTriple() or "", - pointer_size=target.GetAddressByteSize(), - byte_order=_byte_order_name(target.GetByteOrder()), + pointer_size=pointer_size, + byte_order=byte_order, + record_version=record_version, + llgo_abi_version=llgo_abi_version, + cabi_mode=cabi_mode, + cabi_name=cabi_name, + compatibility_error=compatibility_error, ) _TARGET_INFO_CACHE[cache_key] = info return info def target_status(info: LLGoTargetInfo) -> str: - if not info.marker_versions: + if not info.marker_versions and info.record_version is None: return "Not an LLGo target; raw LLDB debugging remains available." + if info.compatibility_error: + return ( + f"Unsupported LLGo debugger ABI: {info.compatibility_error}; " + "raw LLDB debugging remains available." + ) if not info.supported: versions = ", ".join(f"v{version}" for version in info.marker_versions) @@ -200,9 +433,14 @@ def target_status(info: LLGoTargetInfo) -> str: f"Unsupported LLGo debugger marker version(s): {versions}; " "raw LLDB debugging remains available." ) + abi = (f"LLGo ABI v{info.llgo_abi_version}; " + if info.llgo_abi_version is not None else "") + cabi = (f"C ABI mode {info.cabi_mode} ({info.cabi_name}); " + if info.cabi_mode is not None else "") return ( f"LLGo debugger schema v{info.schema_version} " f"(runtime layout v{info.runtime_layout_version}); " + f"{abi}{cabi}" f"target {info.triple}; pointer size {info.pointer_size}; " f"byte order {info.byte_order}." ) diff --git a/cmd/llgo/lldbtest/runtest.sh b/cmd/llgo/lldbtest/runtest.sh index 1bb0188e08..1731bac533 100755 --- a/cmd/llgo/lldbtest/runtest.sh +++ b/cmd/llgo/lldbtest/runtest.sh @@ -44,6 +44,21 @@ test_tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/llgo-lldbtest.XXXXXX") trap 'rm -rf "$test_tmp_dir"' EXIT result_file="$test_tmp_dir/exit-code" +# LLDB reports Python assertion failures in its output but can still exit with +# status 0 in batch mode. Treat a traceback as a test failure so compatibility +# fallback checks cannot pass silently. +run_checked_lldb() { + local output + if ! output=$("$@" 2>&1); then + printf '%s\n' "$output" + return 1 + fi + printf '%s\n' "$output" + if [[ "$output" == *"Traceback (most recent call last)"* ]]; then + return 1 + fi +} + # Prepare LLDB commands lldb_commands=( "command script import ./test.py" @@ -74,9 +89,9 @@ if [ "$exit_code" -ne 0 ]; then exit "$exit_code" fi -llgo lldb -lldb "$LLDB_PATH" -- --batch "./debug.out" \ - -o 'script info = llgo_plugin.inspect_target(lldb.target); assert info.schema_version == 1 and info.runtime_layout_version == 1 and info.pointer_size == lldb.target.GetAddressByteSize() and info.byte_order != "unknown"' \ - -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); assert result.Succeeded() and "LLGo debugger schema v1 (runtime layout v1)" in result.GetOutput()' \ +run_checked_lldb llgo lldb -lldb "$LLDB_PATH" -- --batch "./debug.out" \ + -o 'script info = llgo_plugin.inspect_target(lldb.target); assert info.schema_version == 1 and info.runtime_layout_version == 1 and info.record_version == 1 and info.llgo_abi_version == 1 and info.cabi_mode == 2 and info.cabi_name == "allfunc" and info.pointer_size == lldb.target.GetAddressByteSize() and info.byte_order != "unknown"' \ + -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); assert result.Succeeded() and "LLGo debugger schema v1 (runtime layout v1); LLGo ABI v1; C ABI mode 2 (allfunc)" in result.GetOutput()' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo vars", result); assert not result.Succeeded() and "requires a stopped process" in result.GetError()' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo print s", result); assert not result.Succeeded() and "requires a stopped process" in result.GetError()' @@ -85,18 +100,18 @@ non_llgo_dir="$test_tmp_dir/non-llgo" mkdir -p "$non_llgo_dir" printf 'typedef struct { const char *data; unsigned long len; } string; string cstring = {"raw", 3}; int main(void) { return 0; }\n' | \ "${CC:-cc}" -x c -g -o "$non_llgo_dir/non-llgo" - -llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/non-llgo" \ +run_checked_lldb llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/non-llgo" \ -o 'script info = llgo_plugin.inspect_target(lldb.target); assert not info.marker_versions and not info.supported' \ - -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() is None and value.GetNumChildren() == 2' \ + -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() in (None, "None") and value.GetNumChildren() == 2' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); assert result.Succeeded() and "Not an LLGo target" in result.GetOutput()' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("p 1+1", result); assert result.Succeeded() and "2" in result.GetOutput()' # An unknown marker must disable only LLGo-specific presentation. printf 'typedef struct { const char *data; unsigned long len; } string; string cstring = {"raw", 3}; __attribute__((used)) int __llgo_debugger_marker_v2 = 2; int main(void) { return 0; }\n' | \ "${CC:-cc}" -x c -g -o "$non_llgo_dir/unsupported-llgo" - -llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/unsupported-llgo" \ +run_checked_lldb llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/unsupported-llgo" \ -o 'script info = llgo_plugin.inspect_target(lldb.target); assert info.marker_versions == (2,) and not info.supported' \ - -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() is None and value.GetNumChildren() == 2' \ + -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() in (None, "None") and value.GetNumChildren() == 2' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); assert result.Succeeded() and "Unsupported LLGo debugger marker version(s): v2" in result.GetOutput()' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo vars", result); assert not result.Succeeded() and "Unsupported LLGo debugger marker version(s): v2" in result.GetError()' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("p 1+1", result); assert result.Succeeded() and "2" in result.GetOutput()' @@ -104,7 +119,23 @@ llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/unsupported-llgo" \ # Multiple marker versions are ambiguous even when one version is supported. printf 'typedef struct { const char *data; unsigned long len; } string; string cstring = {"raw", 3}; __attribute__((used)) int __llgo_debugger_marker_v1 = 1; __attribute__((used)) int __llgo_debugger_marker_v2 = 2; int main(void) { return 0; }\n' | \ "${CC:-cc}" -x c -g -o "$non_llgo_dir/ambiguous-llgo" - -llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/ambiguous-llgo" \ +run_checked_lldb llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/ambiguous-llgo" \ -o 'script info = llgo_plugin.inspect_target(lldb.target); assert info.marker_versions == (1, 2) and not info.supported' \ - -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() is None and value.GetNumChildren() == 2' \ + -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() in (None, "None") and value.GetNumChildren() == 2' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); assert result.Succeeded() and "Unsupported LLGo debugger marker version(s): v1, v2" in result.GetOutput()' + +# A structured record is authoritative and must reject unsupported schemas or +# target-property mismatches without disabling ordinary LLDB commands. +printf 'typedef struct { const char *data; unsigned long len; } string; string cstring = {"raw", 3}; __attribute__((used)) int __llgo_debugger_marker_v1 = 1; __attribute__((used, visibility("hidden"))) unsigned char __llgo_debugger_abi_v1[16] = {0x4c,0x4c,0x47,0x4f,0x44,0x42,0x47,0,1,2,1,1,2,sizeof(void*),1,0}; int main(void) { return 0; }\n' | \ + "${CC:-cc}" -x c -g -o "$non_llgo_dir/unsupported-record" - +run_checked_lldb llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/unsupported-record" \ + -o 'script info = llgo_plugin.inspect_target(lldb.target); assert info.record_version == 1 and info.compatibility_error and not info.supported' \ + -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); assert result.Succeeded() and "Unsupported LLGo debugger ABI" in result.GetOutput() and "unsupported record/schema/runtime/ABI versions" in result.GetOutput()' \ + -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("p 1+1", result); assert result.Succeeded() and "2" in result.GetOutput()' + +printf 'typedef struct { const char *data; unsigned long len; } string; string cstring = {"raw", 3}; __attribute__((used)) int __llgo_debugger_marker_v1 = 1; __attribute__((used, visibility("hidden"))) unsigned char __llgo_debugger_abi_v1[16] = {0x4c,0x4c,0x47,0x4f,0x44,0x42,0x47,0,1,1,1,1,2,4,1,0}; int main(void) { return 0; }\n' | \ + "${CC:-cc}" -x c -g -o "$non_llgo_dir/mismatched-record" - +run_checked_lldb llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/mismatched-record" \ + -o 'script info = llgo_plugin.inspect_target(lldb.target); assert info.compatibility_error and "pointer size" in info.compatibility_error and not info.supported' \ + -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() in (None, "None") and value.GetNumChildren() == 2' \ + -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("p 1+1", result); assert result.Succeeded() and "2" in result.GetOutput()' diff --git a/cmd/llgo/lldbtest/test.py b/cmd/llgo/lldbtest/test.py index fa40a129cb..9b946d832d 100644 --- a/cmd/llgo/lldbtest/test.py +++ b/cmd/llgo/lldbtest/test.py @@ -318,7 +318,11 @@ def setup(self) -> None: if llgo_plugin.inspect_target(self.target) is not target_info: raise LLDBTestException("LLGo target inspection was not cached") if (target_info.schema_version != 1 or - target_info.runtime_layout_version != 1): + target_info.runtime_layout_version != 1 or + target_info.record_version != 1 or + target_info.llgo_abi_version != 1 or + target_info.cabi_mode != 2 or + target_info.cabi_name != "allfunc"): raise LLDBTestException( f"Unexpected LLGo debugger schema: {target_info}") if (target_info.pointer_size != self.target.GetAddressByteSize() or From 2ba269c23fab6061461d19272ca8b212be562936 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 2 Aug 2026 04:05:17 +0800 Subject: [PATCH 4/4] cmd: preserve raw LLDB for unsupported targets --- cmd/internal/lldb/lldb.go | 4 +++- cmd/internal/lldb/lldb_test.go | 5 ++++- cmd/internal/lldb/llgo_plugin.py | 3 ++- cmd/llgo/lldbtest/runtest.sh | 8 ++++---- cmd/llgo/lldbtest/test.py | 5 +++-- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/cmd/internal/lldb/lldb.go b/cmd/internal/lldb/lldb.go index 75f798bd91..dff50ef137 100644 --- a/cmd/internal/lldb/lldb.go +++ b/cmd/internal/lldb/lldb.go @@ -99,7 +99,9 @@ func run(configuredPath string, args []string, stdin io.Reader, stdout, stderr i } lldbArgs := make([]string, 0, len(args)+2) - lldbArgs = append(lldbArgs, "-O", lldbImportCommand(pluginPath)) + // Import after LLDB creates the target so the plugin can enable runtime + // formatters only for binaries that advertise a supported LLGo schema. + lldbArgs = append(lldbArgs, "-o", lldbImportCommand(pluginPath)) lldbArgs = append(lldbArgs, args...) command := exec.Command(path, lldbArgs...) diff --git a/cmd/internal/lldb/lldb_test.go b/cmd/internal/lldb/lldb_test.go index f55fd15607..cb0030c39c 100644 --- a/cmd/internal/lldb/lldb_test.go +++ b/cmd/internal/lldb/lldb_test.go @@ -132,7 +132,10 @@ grep -q __llgo_debugger_marker_v1 "$schema" t.Fatal(err) } got := string(data) - for _, want := range []string{"-O\n", "command script import \"", "--batch\n", "./program\n", "-o\n", "run\n"} { + if !strings.HasPrefix(got, "-o\ncommand script import \"") { + t.Fatalf("LLDB arguments %q do not import the plugin after target creation", got) + } + for _, want := range []string{"--batch\n", "./program\n", "-o\n", "run\n"} { if !strings.Contains(got, want) { t.Fatalf("LLDB arguments %q do not contain %q", got, want) } diff --git a/cmd/internal/lldb/llgo_plugin.py b/cmd/internal/lldb/llgo_plugin.py index 44a6b8304b..f87e71e47d 100644 --- a/cmd/internal/lldb/llgo_plugin.py +++ b/cmd/internal/lldb/llgo_plugin.py @@ -159,7 +159,8 @@ def register_commands(debugger: lldb.SBDebugger) -> None: 'command script add -f llgo_plugin.print_go_expression llgo print') debugger.HandleCommand( 'command script add -f llgo_plugin.print_all_variables llgo vars') - register_type_formatters(debugger) + if inspect_target(debugger.GetSelectedTarget()).supported: + register_type_formatters(debugger) def _type_options(hide_children: bool = False) -> int: diff --git a/cmd/llgo/lldbtest/runtest.sh b/cmd/llgo/lldbtest/runtest.sh index 1731bac533..5d0d5ea4f4 100755 --- a/cmd/llgo/lldbtest/runtest.sh +++ b/cmd/llgo/lldbtest/runtest.sh @@ -102,7 +102,7 @@ printf 'typedef struct { const char *data; unsigned long len; } string; string c "${CC:-cc}" -x c -g -o "$non_llgo_dir/non-llgo" - run_checked_lldb llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/non-llgo" \ -o 'script info = llgo_plugin.inspect_target(lldb.target); assert not info.marker_versions and not info.supported' \ - -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() in (None, "None") and value.GetNumChildren() == 2' \ + -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() is None and value.GetNumChildren() == 2' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); assert result.Succeeded() and "Not an LLGo target" in result.GetOutput()' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("p 1+1", result); assert result.Succeeded() and "2" in result.GetOutput()' @@ -111,7 +111,7 @@ printf 'typedef struct { const char *data; unsigned long len; } string; string c "${CC:-cc}" -x c -g -o "$non_llgo_dir/unsupported-llgo" - run_checked_lldb llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/unsupported-llgo" \ -o 'script info = llgo_plugin.inspect_target(lldb.target); assert info.marker_versions == (2,) and not info.supported' \ - -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() in (None, "None") and value.GetNumChildren() == 2' \ + -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() is None and value.GetNumChildren() == 2' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); assert result.Succeeded() and "Unsupported LLGo debugger marker version(s): v2" in result.GetOutput()' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo vars", result); assert not result.Succeeded() and "Unsupported LLGo debugger marker version(s): v2" in result.GetError()' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("p 1+1", result); assert result.Succeeded() and "2" in result.GetOutput()' @@ -121,7 +121,7 @@ printf 'typedef struct { const char *data; unsigned long len; } string; string c "${CC:-cc}" -x c -g -o "$non_llgo_dir/ambiguous-llgo" - run_checked_lldb llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/ambiguous-llgo" \ -o 'script info = llgo_plugin.inspect_target(lldb.target); assert info.marker_versions == (1, 2) and not info.supported' \ - -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() in (None, "None") and value.GetNumChildren() == 2' \ + -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() is None and value.GetNumChildren() == 2' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); assert result.Succeeded() and "Unsupported LLGo debugger marker version(s): v1, v2" in result.GetOutput()' # A structured record is authoritative and must reject unsupported schemas or @@ -137,5 +137,5 @@ printf 'typedef struct { const char *data; unsigned long len; } string; string c "${CC:-cc}" -x c -g -o "$non_llgo_dir/mismatched-record" - run_checked_lldb llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/mismatched-record" \ -o 'script info = llgo_plugin.inspect_target(lldb.target); assert info.compatibility_error and "pointer size" in info.compatibility_error and not info.supported' \ - -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() in (None, "None") and value.GetNumChildren() == 2' \ + -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() is None and value.GetNumChildren() == 2' \ -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("p 1+1", result); assert result.Succeeded() and "2" in result.GetOutput()' diff --git a/cmd/llgo/lldbtest/test.py b/cmd/llgo/lldbtest/test.py index 9b946d832d..0df9ceb828 100644 --- a/cmd/llgo/lldbtest/test.py +++ b/cmd/llgo/lldbtest/test.py @@ -304,12 +304,13 @@ def __init__(self, executable_path: str, plugin_path: Optional[str] = None) -> N def setup(self) -> None: plugin_path = self.plugin_path or llgo_plugin.__file__ - self.debugger.HandleCommand( - f'command script import "{plugin_path}"') self.target = self.debugger.CreateTarget(self.executable_path) if not self.target: raise LLDBTestException( f"Failed to create target for {self.executable_path}") + self.debugger.SetSelectedTarget(self.target) + self.debugger.HandleCommand( + f'command script import "{plugin_path}"') target_info = llgo_plugin.inspect_target(self.target) if not target_info.supported: