diff --git a/cmd/bpf2go/gen/link.go b/cmd/bpf2go/gen/link.go new file mode 100644 index 000000000..0dadb1704 --- /dev/null +++ b/cmd/bpf2go/gen/link.go @@ -0,0 +1,34 @@ +//go:build !windows + +package gen + +import ( + "fmt" + "os" + "os/exec" +) + +// LinkArgs specifies the arguments for linking multiple BPF object files together. +type LinkArgs struct { + // Destination object file name + Dest string + // Source object files to link together + Sources []string +} + +// Link combines multiple BPF object files into a single object file. +func Link(args LinkArgs) error { + if len(args.Sources) == 0 { + return fmt.Errorf("no source files to link") + } + + cmd := exec.Command("bpftool", "gen", "object", args.Dest) + cmd.Args = append(cmd.Args, args.Sources...) + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("bpftool gen object returned error: %w", err) + } + + return nil +} diff --git a/cmd/bpf2go/gen/link_test.go b/cmd/bpf2go/gen/link_test.go new file mode 100644 index 000000000..10f2cc420 --- /dev/null +++ b/cmd/bpf2go/gen/link_test.go @@ -0,0 +1,72 @@ +//go:build !windows + +package gen + +import ( + "os" + "path/filepath" + "testing" + + "github.com/cilium/ebpf/internal/testutils" +) + +const ( + func1 = `__attribute__((section("socket"), used)) int func1() { return 1; }` + func2 = `__attribute__((section("socket"), used)) int func2() { return 2; }` +) + +func TestLink(t *testing.T) { + if testing.Short() { + t.SkipNow() + } + + dir := t.TempDir() + mustWriteFile(t, dir, "func1.c", func1) + mustWriteFile(t, dir, "func2.c", func2) + + // Compile first object + obj1 := filepath.Join(dir, "func1.o") + err := Compile(CompileArgs{ + CC: testutils.ClangBin(t), + DisableStripping: true, + Workdir: dir, + Source: filepath.Join(dir, "func1.c"), + Dest: obj1, + }) + if err != nil { + t.Fatal("Can't compile func1:", err) + } + + // Compile second object + obj2 := filepath.Join(dir, "func2.o") + err = Compile(CompileArgs{ + CC: testutils.ClangBin(t), + DisableStripping: true, + Workdir: dir, + Source: filepath.Join(dir, "func2.c"), + Dest: obj2, + }) + if err != nil { + t.Fatal("Can't compile func2:", err) + } + + // Link both objects + linked := filepath.Join(dir, "linked.o") + err = Link(LinkArgs{ + Dest: linked, + Sources: []string{obj1, obj2}, + }) + if err != nil { + t.Fatal("Can't link objects:", err) + } + + // Verify the linked file exists and has content + stat, err := os.Stat(linked) + if err != nil { + t.Fatal("Can't stat linked file:", err) + } + + if stat.Size() == 0 { + t.Error("Linked file is empty") + } +} diff --git a/cmd/bpf2go/main.go b/cmd/bpf2go/main.go index 3732333f6..adc489819 100644 --- a/cmd/bpf2go/main.go +++ b/cmd/bpf2go/main.go @@ -21,13 +21,14 @@ import ( "github.com/cilium/ebpf/cmd/bpf2go/gen" ) -const helpText = `Usage: %[1]s [options] [-- ] +const helpText = `Usage: %[1]s [options] [-- ] ident is used as the stem of all generated Go types and functions, and must be a valid Go identifier. -source is a single C file that is compiled using the specified compiler -(usually some version of clang). +source files are C files that are compiled using the specified compiler +(usually some version of clang) and linked together into a single +BPF program. You can pass options to the compiler by appending them after a '--' argument or by supplying -cflags. Flags passed as arguments take precedence @@ -60,8 +61,8 @@ func run(stdout io.Writer, args []string) (err error) { type bpf2go struct { stdout io.Writer verbose bool - // Absolute path to a .c file. - sourceFile string + // Absolute paths to .c files. + sourceFiles []string // Absolute path to a directory where .go are written outputDir string // Alternative output stem. If empty, identStem is used. @@ -186,13 +187,18 @@ func newB2G(stdout io.Writer, args []string) (*bpf2go, error) { b2g.identStem = args[0] - sourceFile, err := filepath.Abs(args[1]) - if err != nil { - return nil, err + sourceFiles := args[1:] + b2g.sourceFiles = make([]string, len(sourceFiles)) + for i, source := range sourceFiles { + absPath, err := filepath.Abs(source) + if err != nil { + return nil, fmt.Errorf("convert source file to absolute path: %w", err) + } + b2g.sourceFiles[i] = absPath } - b2g.sourceFile = sourceFile if b2g.makeBase != "" { + var err error b2g.makeBase, err = filepath.Abs(b2g.makeBase) if err != nil { return nil, err @@ -298,10 +304,13 @@ func getBool(key string, defaultVal bool) bool { } func (b2g *bpf2go) convertAll() (err error) { - if _, err := os.Stat(b2g.sourceFile); os.IsNotExist(err) { - return fmt.Errorf("file %s doesn't exist", b2g.sourceFile) - } else if err != nil { - return err + // Check all source files exist + for _, source := range b2g.sourceFiles { + if _, err := os.Stat(source); os.IsNotExist(err) { + return fmt.Errorf("file %s doesn't exist", source) + } else if err != nil { + return err + } } if !b2g.disableStripping { @@ -320,6 +329,55 @@ func (b2g *bpf2go) convertAll() (err error) { return nil } +// compileOne compiles a single source file and returns any dependencies found during compilation. +func (b2g *bpf2go) compileOne(tgt gen.Target, cwd, source, objFileName string) (deps []dependency, err error) { + var depInput *os.File + cFlags := slices.Clone(b2g.cFlags) + if b2g.makeBase != "" { + depInput, err = os.CreateTemp("", "bpf2go") + if err != nil { + return nil, err + } + defer depInput.Close() + defer os.Remove(depInput.Name()) + + cFlags = append(cFlags, + // Output dependency information. + "-MD", + // Create phony targets so that deleting a dependency doesn't + // break the build. + "-MP", + // Write it to temporary file + "-MF"+depInput.Name(), + ) + } + + // Compile to final object file name + err = gen.Compile(gen.CompileArgs{ + CC: b2g.cc, + Strip: b2g.strip, + DisableStripping: b2g.disableStripping, + Flags: cFlags, + Target: tgt, + Workdir: cwd, + Source: source, + Dest: objFileName, + }) + if err != nil { + return nil, fmt.Errorf("compile %s: %w", source, err) + } + + // Parse dependencies if enabled + if b2g.makeBase != "" { + deps, err = parseDependencies(cwd, depInput) + if err != nil { + return nil, fmt.Errorf("parse dependencies for %s: %w", source, err) + } + } + + return deps, nil +} + func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) { removeOnError := func(f *os.File) { if err != nil { @@ -341,6 +399,7 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) { } objFileName := filepath.Join(absOutPath, stem+".o") + goFileName := filepath.Join(absOutPath, stem+".go") cwd, err := os.Getwd() if err != nil { @@ -354,39 +413,48 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) { return fmt.Errorf("remove obsolete output: %w", err) } - var depInput *os.File - cFlags := slices.Clone(b2g.cFlags) - if b2g.makeBase != "" { - depInput, err = os.CreateTemp("", "bpf2go") + // Compile each source file + var allDeps []dependency + var tmpObjFileNames []string + for _, source := range b2g.sourceFiles { + // Determine the target object file name + var targetObjFileName string + if len(b2g.sourceFiles) > 1 { + // For multiple source files, use a temporary file + tmpObj, err := os.CreateTemp("", filepath.Base(source)) + if err != nil { + return fmt.Errorf("create temporary object file: %w", err) + } + tmpObj.Close() + defer os.Remove(tmpObj.Name()) + targetObjFileName = tmpObj.Name() + tmpObjFileNames = append(tmpObjFileNames, targetObjFileName) + } else { + // For single source file, use the final object file name + targetObjFileName = objFileName + } + + deps, err := b2g.compileOne(tgt, cwd, source, targetObjFileName) if err != nil { return err } - defer depInput.Close() - defer os.Remove(depInput.Name()) - cFlags = append(cFlags, - // Output dependency information. - "-MD", - // Create phony targets so that deleting a dependency doesn't - // break the build. - "-MP", - // Write it to temporary file - "-MF"+depInput.Name(), - ) + if len(deps) > 0 { + // There is always at least a dependency for the main file. + deps[0].file = goFileName + allDeps = append(allDeps, deps...) + } } - err = gen.Compile(gen.CompileArgs{ - CC: b2g.cc, - Strip: b2g.strip, - DisableStripping: b2g.disableStripping, - Flags: cFlags, - Target: tgt, - Workdir: cwd, - Source: b2g.sourceFile, - Dest: objFileName, - }) - if err != nil { - return fmt.Errorf("compile: %w", err) + // If we have multiple object files, link them together + if len(tmpObjFileNames) > 1 { + err = gen.Link(gen.LinkArgs{ + Dest: objFileName, + Sources: tmpObjFileNames, + }) + if err != nil { + return fmt.Errorf("link object files: %w", err) + } } if b2g.disableStripping { @@ -428,7 +496,6 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) { } // Write out generated go - goFileName := filepath.Join(absOutPath, stem+".go") goFile, err := os.Create(goFileName) if err != nil { return err @@ -456,9 +523,10 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) { return } - deps, err := parseDependencies(cwd, depInput) - if err != nil { - return fmt.Errorf("can't read dependency information: %s", err) + // Merge dependencies if we have multiple source files + var finalDeps []dependency + if len(allDeps) > 0 { + finalDeps = mergeDependencies(allDeps) } depFileName := goFileName + ".d" @@ -468,9 +536,7 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) { } defer depOutput.Close() - // There is always at least a dependency for the main file. - deps[0].file = goFileName - if err := adjustDependencies(depOutput, b2g.makeBase, deps); err != nil { + if err := adjustDependencies(depOutput, b2g.makeBase, finalDeps); err != nil { return fmt.Errorf("can't adjust dependency information: %s", err) } diff --git a/cmd/bpf2go/main_test.go b/cmd/bpf2go/main_test.go index a7c960585..d3d23d979 100644 --- a/cmd/bpf2go/main_test.go +++ b/cmd/bpf2go/main_test.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "testing" @@ -159,7 +160,7 @@ func TestConvertGOARCH(t *testing.T) { identStem: "test", cc: testutils.ClangBin(t), disableStripping: true, - sourceFile: tmp + "/test.c", + sourceFiles: []string{tmp + "/test.c"}, outputDir: tmp, } @@ -406,6 +407,98 @@ func TestParseArgs(t *testing.T) { }) } +func TestMultipleSourceFiles(t *testing.T) { + modRoot, err := filepath.Abs("../..") + if err != nil { + t.Fatal("Can't get module root:", err) + } + + if _, err := os.Stat(filepath.Join(modRoot, "go.mod")); os.IsNotExist(err) { + t.Fatal("No go.mod file in", modRoot) + } + + // bpftool appears to support the endianness of the machine it is running on. + //Determine native endianness based on GOARCH + var target string + switch runtime.GOARCH { + case "amd64", "arm64", "riscv64": + target = "bpfel" // little-endian + case "s390x", "ppc64": + target = "bpfeb" // big-endian + default: + t.Fatalf("Unsupported architecture: %s", runtime.GOARCH) + } + + dir := t.TempDir() + + // Create two source files with different functions + mustWriteFile(t, dir, "func1.c", `__attribute__((section("socket"), used)) int func1() { return 1; }`) + mustWriteFile(t, dir, "func2.c", `__attribute__((section("socket"), used)) int func2() { return 2; }`) + + // Set up module directory + modDir := t.TempDir() + execInModule := func(name string, args ...string) { + t.Helper() + cmd := exec.Command(name, args...) + cmd.Dir = modDir + if out, err := cmd.CombinedOutput(); err != nil { + if out := string(out); out != "" { + t.Log(out) + } + t.Fatalf("Can't execute %s: %v", name, args) + } + } + + // Initialize module + execInModule("go", "mod", "init", "bpf2go-test") + execInModule("go", "mod", "edit", + fmt.Sprintf("-require=%s@v0.0.0", internal.CurrentModule), + fmt.Sprintf("-replace=%s=%s", internal.CurrentModule, modRoot), + ) + + // Run bpf2go with both source files + err = run(io.Discard, []string{ + "-go-package", "main", + "-output-dir", modDir, + "-cc", testutils.ClangBin(t), + "-target", target, + "bar", + filepath.Join(dir, "func1.c"), + filepath.Join(dir, "func2.c"), + }) + + if err != nil { + t.Fatal("Can't run bpf2go with multiple source files:", err) + } + + // Create a main.go that uses both functions + mustWriteFile(t, modDir, "main.go", + ` +package main + +func main() { + var obj barObjects + println(obj.Func1) + println(obj.Func2) +}`) + + // Test compilation for the native architecture + goBuild := exec.Command("go", "build", "-mod=mod", "-o", "/dev/null") + goBuild.Dir = modDir + goBuild.Env = append(os.Environ(), + "GOOS=linux", + "GOPROXY=off", + "GOSUMDB=off", + ) + out, err := goBuild.CombinedOutput() + if err != nil { + if out := string(out); out != "" { + t.Log(out) + } + t.Error("Can't compile package:", err) + } +} + func mustWriteFile(tb testing.TB, dir, name, contents string) { tb.Helper() tmpFile := filepath.Join(dir, name) diff --git a/cmd/bpf2go/makedep.go b/cmd/bpf2go/makedep.go index b5d34b7c4..ccb6d6780 100644 --- a/cmd/bpf2go/makedep.go +++ b/cmd/bpf2go/makedep.go @@ -106,3 +106,48 @@ func parseDependencies(baseDir string, in io.Reader) ([]dependency, error) { } return deps, nil } + +// mergeDependencies combines multiple dependency slices into one, merging prerequisites +// for files that appear in multiple slices. +func mergeDependencies(depsSlices ...[]dependency) []dependency { + // Map to track merged dependencies by file + merged := make(map[string][]string) + + // Process each slice of dependencies + for _, deps := range depsSlices { + for _, dep := range deps { + // If we've seen this file before, merge prerequisites + if existing, ok := merged[dep.file]; ok { + // Combine prerequisites, avoiding duplicates + prereqs := make(map[string]struct{}) + for _, p := range existing { + prereqs[p] = struct{}{} + } + for _, p := range dep.prerequisites { + prereqs[p] = struct{}{} + } + + // Convert back to slice + merged[dep.file] = make([]string, 0, len(prereqs)) + for p := range prereqs { + merged[dep.file] = append(merged[dep.file], p) + } + } else { + // First time seeing this file, just copy prerequisites + merged[dep.file] = make([]string, len(dep.prerequisites)) + copy(merged[dep.file], dep.prerequisites) + } + } + } + + // Convert map back to slice + result := make([]dependency, 0, len(merged)) + for file, prereqs := range merged { + result = append(result, dependency{ + file: file, + prerequisites: prereqs, + }) + } + + return result +} diff --git a/cmd/bpf2go/makedep_test.go b/cmd/bpf2go/makedep_test.go index 5f879883b..de99f12c8 100644 --- a/cmd/bpf2go/makedep_test.go +++ b/cmd/bpf2go/makedep_test.go @@ -5,6 +5,7 @@ package main import ( "bytes" "reflect" + "sort" "strings" "testing" ) @@ -59,3 +60,34 @@ nothing: t.Error("Output doesn't match") } } + +func TestMergeDependencies(t *testing.T) { + deps1 := []dependency{ + {"/foo/main.c", []string{"/foo/bar.h", "/foo/baz.h"}}, + {"/foo/other.c", []string{"/foo/bar.h"}}, + } + + deps2 := []dependency{ + {"/foo/main.c", []string{"/foo/qux.h"}}, + {"/foo/third.c", []string{"/foo/bar.h"}}, + } + + merged := mergeDependencies(deps1, deps2) + + // Sort merged dependencies for stable comparison + sort.Slice(merged, func(i, j int) bool { + return merged[i].file < merged[j].file + }) + + want := []dependency{ + {"/foo/main.c", []string{"/foo/bar.h", "/foo/baz.h", "/foo/qux.h"}}, + {"/foo/other.c", []string{"/foo/bar.h"}}, + {"/foo/third.c", []string{"/foo/bar.h"}}, + } + + if !reflect.DeepEqual(merged, want) { + t.Logf("Have: %#v", merged) + t.Logf("Want: %#v", want) + t.Error("Merged dependencies don't match") + } +}