From 71806d4456edb31b77744a5964bf51acb70d8cee Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Tue, 18 Aug 2026 18:29:32 +0800 Subject: [PATCH] cmd/compile: preserve file-backed data in LLVM IR --- src/cmd/compile/internal/ssa/llvmdata.go | 79 ++++++++++++++++--- src/cmd/compile/internal/ssa/llvmtypeddata.go | 6 +- src/cmd/compile/internal/ssa/ssa2llvm_test.go | 70 ++++++++++++++++ test/llvm_file_backed_embed.go | 61 ++++++++++++++ test/llvm_tests.json | 1 + 5 files changed, 206 insertions(+), 11 deletions(-) create mode 100644 test/llvm_file_backed_embed.go diff --git a/src/cmd/compile/internal/ssa/llvmdata.go b/src/cmd/compile/internal/ssa/llvmdata.go index 3cdb622def25a9..6e5cb794646211 100644 --- a/src/cmd/compile/internal/ssa/llvmdata.go +++ b/src/cmd/compile/internal/ssa/llvmdata.go @@ -12,6 +12,8 @@ import ( "encoding/hex" "encoding/json" "fmt" + "io" + "os" "sort" "strconv" "strings" @@ -498,13 +500,13 @@ func llvmDataSymbolKindSupported(kind objabi.SymKind) bool { } } -func llvmDataType(s *obj.LSym) llvm.Type { - fields := llvmDataFields(s, nil, nil) +func llvmDataType(s *obj.LSym, contents []byte) llvm.Type { + fields := llvmDataFields(s, contents, nil, nil) return GlobalCtxt.ConstStruct(fields, true).Type() } -func llvmDataInitializer(s *obj.LSym, globals map[*obj.LSym]llvm.Value, data map[*obj.LSym]bool) llvm.Value { - fields := llvmDataFields(s, globals, data) +func llvmDataInitializer(s *obj.LSym, contents []byte, globals map[*obj.LSym]llvm.Value, data map[*obj.LSym]bool) llvm.Value { + fields := llvmDataFields(s, contents, globals, data) return GlobalCtxt.ConstStruct(fields, true) } @@ -512,13 +514,17 @@ func llvmDataInitializer(s *obj.LSym, globals map[*obj.LSym]llvm.Value, data map // frontend's exact layout while relocation slots retain their LLVM constants. // Using a packed aggregate is necessary because descriptors contain 32-bit // offsets adjacent to pointer-sized fields. -func llvmDataFields(s *obj.LSym, globals map[*obj.LSym]llvm.Value, data map[*obj.LSym]bool) []llvm.Value { - dataSize := int(s.Size) - if dataSize < len(s.P) { - dataSize = len(s.P) +func llvmDataFields(s *obj.LSym, contents []byte, globals map[*obj.LSym]llvm.Value, data map[*obj.LSym]bool) []llvm.Value { + dataSize64 := s.Size + if dataSize64 < int64(len(contents)) { + dataSize64 = int64(len(contents)) } + if dataSize64 < 0 || uint64(dataSize64) > uint64(^uint(0)>>1) { + base.Fatalf("invalid LLVM data symbol size %d for %s", dataSize64, s.Name) + } + dataSize := int(dataSize64) bytes := make([]byte, dataSize) - copy(bytes, s.P) + copy(bytes, contents) relocs := llvmDataStorageRelocs(s) sort.Slice(relocs, func(i, j int) bool { return relocs[i].Off < relocs[j].Off }) @@ -549,6 +555,61 @@ func llvmDataFields(s *obj.LSym, globals map[*obj.LSym]llvm.Value, data map[*obj return fields } +func (l *llvmDataLowerer) dataBytes(s *obj.LSym) []byte { + if s.File() == nil { + return s.P + } + if data, ok := l.fileData[s]; ok { + return data + } + data, err := readLLVMFileData(s) + if err != nil { + base.Fatalf("reading file-backed LLVM data symbol %s: %v", s.Name, err) + } + l.fileData[s] = data + return data +} + +// readLLVMFileData materializes the same file-backed LSym bytes that the +// native Go object writer streams into its object. LLVM constants require the +// complete initializer in memory, so validate both metadata sizes and the +// actual EOF while reading it once for the lowerer's type and initializer. +func readLLVMFileData(s *obj.LSym) ([]byte, error) { + file := s.File() + if file == nil { + return s.P, nil + } + if s.P != nil { + return nil, fmt.Errorf("file-backed symbol also has %d inline bytes", len(s.P)) + } + if file.Size != s.Size { + return nil, fmt.Errorf("file metadata length %d does not match symbol size %d", file.Size, s.Size) + } + if file.Size < 0 || uint64(file.Size) > uint64(^uint(0)>>1) { + return nil, fmt.Errorf("invalid file length %d", file.Size) + } + f, err := os.Open(file.Name) + if err != nil { + return nil, err + } + defer f.Close() + + data := make([]byte, int(file.Size)) + if _, err := io.ReadFull(f, data); err != nil { + return nil, fmt.Errorf("copy %s: expected %d bytes: %w", file.Name, file.Size, err) + } + var extra [1]byte + n, err := io.ReadFull(f, extra[:]) + switch { + case n == 0 && err == io.EOF: + return data, nil + case err == nil: + return nil, fmt.Errorf("copy %s: file is longer than expected %d bytes", file.Name, file.Size) + default: + return nil, fmt.Errorf("copy %s after %d bytes: %w", file.Name, file.Size, err) + } +} + func llvmDataBytes(b []byte) llvm.Value { return GlobalCtxt.ConstString(string(b), false) } diff --git a/src/cmd/compile/internal/ssa/llvmtypeddata.go b/src/cmd/compile/internal/ssa/llvmtypeddata.go index 165c46de1677d7..8cd081feeba08a 100644 --- a/src/cmd/compile/internal/ssa/llvmtypeddata.go +++ b/src/cmd/compile/internal/ssa/llvmtypeddata.go @@ -25,6 +25,7 @@ type llvmDataLowerer struct { externalRoots map[*obj.LSym]bool lowered map[*obj.LSym]bool values map[*obj.LSym]llvm.Value + fileData map[*obj.LSym][]byte anonymousCount int runtimeTypes map[*types.Type]llvm.Type descriptorTypes map[*obj.LSym]llvm.Type @@ -38,6 +39,7 @@ func newLLVMDataLowerer(data map[*obj.LSym]bool) *llvmDataLowerer { externalRoots: make(map[*obj.LSym]bool), lowered: make(map[*obj.LSym]bool), values: make(map[*obj.LSym]llvm.Value), + fileData: make(map[*obj.LSym][]byte), runtimeTypes: make(map[*types.Type]llvm.Type), descriptorTypes: make(map[*obj.LSym]llvm.Type), namedRuntimeType: make(map[*types.Type]bool), @@ -51,7 +53,7 @@ func (l *llvmDataLowerer) dataType(s *obj.LSym) llvm.Type { if s.ItabInfo() != nil { return l.itabType(s) } - return llvmDataType(s) + return llvmDataType(s, l.dataBytes(s)) } func (l *llvmDataLowerer) dataInitializer(s *obj.LSym, globals map[*obj.LSym]llvm.Value) llvm.Value { @@ -61,7 +63,7 @@ func (l *llvmDataLowerer) dataInitializer(s *obj.LSym, globals map[*obj.LSym]llv if s.ItabInfo() != nil { return l.itabInitializer(s, globals) } - return llvmDataInitializer(s, globals, l.data) + return llvmDataInitializer(s, l.dataBytes(s), globals, l.data) } func llvmDescriptorGoType(s *obj.LSym) *types.Type { diff --git a/src/cmd/compile/internal/ssa/ssa2llvm_test.go b/src/cmd/compile/internal/ssa/ssa2llvm_test.go index 33c20fffe7a554..4b30ba9cd6c3b0 100644 --- a/src/cmd/compile/internal/ssa/ssa2llvm_test.go +++ b/src/cmd/compile/internal/ssa/ssa2llvm_test.go @@ -7,6 +7,8 @@ package ssa import ( + "bytes" + "os" "strings" "testing" @@ -182,6 +184,74 @@ func TestLLVMGoObjCompilerUsedOnlyKeepsExternalDataRoots(t *testing.T) { } } +func TestLLVMFileBackedDataLowering(t *testing.T) { + payload := append(bytes.Repeat([]byte("file-backed-data-"), 80), []byte("GOALLC_FILE_END")...) + path := t.TempDir() + "/payload" + if err := os.WriteFile(path, payload, 0o600); err != nil { + t.Fatal(err) + } + + s := &obj.LSym{Name: "test.file.backed", Type: objabi.SRODATA, Size: int64(len(payload))} + file := s.NewFileInfo() + file.Name = path + file.Size = int64(len(payload)) + lowerer := newLLVMDataLowerer(map[*obj.LSym]bool{s: true}) + + module := GlobalCtxt.NewModule("file_backed_data") + t.Cleanup(module.Dispose) + g := llvm.AddGlobal(module, lowerer.dataType(s), s.Name) + g.SetInitializer(lowerer.dataInitializer(s, map[*obj.LSym]llvm.Value{s: g})) + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("LLVM verifier rejected file-backed data: %v\n%s", err, module.String()) + } + + ir := module.String() + if strings.Contains(ir, "zeroinitializer") { + t.Fatalf("file-backed LLVM data was lowered as zeros:\n%s", ir) + } + if !strings.Contains(ir, "GOALLC_FILE_END") { + t.Fatalf("file-backed LLVM data lost its payload:\n%s", ir) + } +} + +func TestReadLLVMFileDataRejectsLengthMismatches(t *testing.T) { + path := t.TempDir() + "/payload" + if err := os.WriteFile(path, []byte("12345"), 0o600); err != nil { + t.Fatal(err) + } + + newSymbol := func(symbolSize, fileSize int64) *obj.LSym { + s := &obj.LSym{Name: "test.file.backed", Type: objabi.SRODATA, Size: symbolSize} + file := s.NewFileInfo() + file.Name = path + file.Size = fileSize + return s + } + for _, tc := range []struct { + name string + symbolSize int64 + fileSize int64 + want string + }{ + {name: "metadata", symbolSize: 5, fileSize: 4, want: "metadata length 4 does not match symbol size 5"}, + {name: "short", symbolSize: 6, fileSize: 6, want: "expected 6 bytes"}, + {name: "long", symbolSize: 4, fileSize: 4, want: "longer than expected 4 bytes"}, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := readLLVMFileData(newSymbol(tc.symbolSize, tc.fileSize)) + if err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("readLLVMFileData error = %v, want substring %q", err, tc.want) + } + }) + } + + s := newSymbol(5, 5) + s.P = []byte("12345") + if _, err := readLLVMFileData(s); err == nil || !strings.Contains(err.Error(), "also has 5 inline bytes") { + t.Fatalf("readLLVMFileData inline/file conflict error = %v", err) + } +} + func TestLLVMUntypedABI0FunctionAddressCreatesFunctionDeclaration(t *testing.T) { oldModule := CurrentModule oldLowerer := currentLLVMDataLowerer diff --git a/test/llvm_file_backed_embed.go b/test/llvm_file_backed_embed.go new file mode 100644 index 00000000000000..8fbb05dd47087b --- /dev/null +++ b/test/llvm_file_backed_embed.go @@ -0,0 +1,61 @@ +// run + +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import _ "embed" + +//go:embed llvm_file_backed_embed.go +var llvmFileBackedSource string + +//go:embed llvm_file_backed_embed.go +var llvmFileBackedBytes []byte + +const llvmFileBackedSentinel = "GOALLC_FILE_BACKED_EMBED_SENTINEL" + +func containsFileBackedSentinel(data, sentinel string) bool { + for i := 0; i+len(sentinel) <= len(data); i++ { + if data[i:i+len(sentinel)] == sentinel { + return true + } + } + return false +} + +func main() { + if len(llvmFileBackedSource) <= 1024 { + panic("fixture did not use a file-backed linker symbol") + } + if !containsFileBackedSentinel(llvmFileBackedSource, llvmFileBackedSentinel) { + panic("file-backed linker symbol lost its contents") + } + if len(llvmFileBackedBytes) != len(llvmFileBackedSource) || !containsFileBackedSentinel(string(llvmFileBackedBytes), llvmFileBackedSentinel) { + panic("writable file-backed linker symbol lost its contents") + } + original := llvmFileBackedSource[0] + llvmFileBackedBytes[0] ^= 0xff + if llvmFileBackedBytes[0] == original || llvmFileBackedSource[0] != original { + panic("writable file-backed linker symbol did not retain separate storage") + } +} + +// Keep this checked-in fixture larger than staticdata.fileStringSym's 1 KiB +// in-memory threshold. The embedded file is this source itself, so testdir's +// ordinary Go command path supplies a real embed configuration while the test +// remains a single-file run recipe. The padding is deliberately readable and +// stable: it is part of the compiler input and lets the runtime assertion +// distinguish a correctly materialized LLVM constant from a zero initializer. +// +// LLVM file-backed data padding 01: abcdefghijklmnopqrstuvwxyz0123456789 +// LLVM file-backed data padding 02: abcdefghijklmnopqrstuvwxyz0123456789 +// LLVM file-backed data padding 03: abcdefghijklmnopqrstuvwxyz0123456789 +// LLVM file-backed data padding 04: abcdefghijklmnopqrstuvwxyz0123456789 +// LLVM file-backed data padding 05: abcdefghijklmnopqrstuvwxyz0123456789 +// LLVM file-backed data padding 06: abcdefghijklmnopqrstuvwxyz0123456789 +// LLVM file-backed data padding 07: abcdefghijklmnopqrstuvwxyz0123456789 +// LLVM file-backed data padding 08: abcdefghijklmnopqrstuvwxyz0123456789 +// LLVM file-backed data padding 09: abcdefghijklmnopqrstuvwxyz0123456789 +// LLVM file-backed data padding 10: abcdefghijklmnopqrstuvwxyz0123456789 diff --git a/test/llvm_tests.json b/test/llvm_tests.json index 0642a9880bac4b..3130e098ce12e5 100644 --- a/test/llvm_tests.json +++ b/test/llvm_tests.json @@ -166,6 +166,7 @@ "llvm_range_statepoint_gc.go": "wide array range derives each element from a relocatable base across repeated statepoints and stack growth", "llvm_memeq.go": "raw memory equality for true, false, different-length, and offset substring inputs", "llvm_move.go": "overlap-safe, aligned, and runtime-helper memory moves", + "llvm_file_backed_embed.go": "large string and byte-slice go:embed data preserve file-backed LSym bytes and storage semantics through LLVM GoObj linking and execution", "llvm_private_string.go": "frontend private string constant materialization, GoObj linking, and execution", "llvm_zero_sized_symbols.go": "zero-value strings and zero-sized interface data through GoObj linking and execution", "llvm_slicemask.go": "slice start, middle, and zero-capacity end boundary semantics",