github.com/cilium/ebpf@v0.16.0/cmd/bpf2go/main.go (about)

     1  package main
     2  
     3  import (
     4  	"errors"
     5  	"flag"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"regexp"
    12  	"slices"
    13  	"sort"
    14  	"strings"
    15  
    16  	"github.com/cilium/ebpf"
    17  	"github.com/cilium/ebpf/btf"
    18  	"github.com/cilium/ebpf/cmd/bpf2go/gen"
    19  )
    20  
    21  const helpText = `Usage: %[1]s [options] <ident> <source file> [-- <C flags>]
    22  
    23  ident is used as the stem of all generated Go types and functions, and
    24  must be a valid Go identifier.
    25  
    26  source is a single C file that is compiled using the specified compiler
    27  (usually some version of clang).
    28  
    29  You can pass options to the compiler by appending them after a '--' argument
    30  or by supplying -cflags. Flags passed as arguments take precedence
    31  over flags passed via -cflags. Additionally, the program expands quotation
    32  marks in -cflags. This means that -cflags 'foo "bar baz"' is passed to the
    33  compiler as two arguments "foo" and "bar baz".
    34  
    35  The program expects GOPACKAGE to be set in the environment, and should be invoked
    36  via go generate. The generated files are written to the current directory.
    37  
    38  Some options take defaults from the environment. Variable name is mentioned
    39  next to the respective option.
    40  
    41  Options:
    42  
    43  `
    44  
    45  func run(stdout io.Writer, args []string) (err error) {
    46  	b2g, err := newB2G(stdout, args)
    47  	switch {
    48  	case err == nil:
    49  		return b2g.convertAll()
    50  	case errors.Is(err, flag.ErrHelp):
    51  		return nil
    52  	default:
    53  		return err
    54  	}
    55  }
    56  
    57  type bpf2go struct {
    58  	stdout io.Writer
    59  	// Absolute path to a .c file.
    60  	sourceFile string
    61  	// Absolute path to a directory where .go are written
    62  	outputDir string
    63  	// Alternative output stem. If empty, identStem is used.
    64  	outputStem string
    65  	// Valid go package name.
    66  	pkg string
    67  	// Valid go identifier.
    68  	identStem string
    69  	// Targets to build for.
    70  	targetArches map[gen.Target]gen.GoArches
    71  	// C compiler.
    72  	cc string
    73  	// Command used to strip DWARF.
    74  	strip            string
    75  	disableStripping bool
    76  	// C flags passed to the compiler.
    77  	cFlags          []string
    78  	skipGlobalTypes bool
    79  	// C types to include in the generated output.
    80  	cTypes cTypes
    81  	// Build tags to be included in the output.
    82  	tags buildTags
    83  	// Base directory of the Makefile. Enables outputting make-style dependencies
    84  	// in .d files.
    85  	makeBase string
    86  }
    87  
    88  func newB2G(stdout io.Writer, args []string) (*bpf2go, error) {
    89  	b2g := &bpf2go{
    90  		stdout: stdout,
    91  	}
    92  
    93  	fs := flag.NewFlagSet("bpf2go", flag.ContinueOnError)
    94  	fs.StringVar(&b2g.cc, "cc", getEnv("BPF2GO_CC", "clang"),
    95  		"`binary` used to compile C to BPF ($BPF2GO_CC)")
    96  	fs.StringVar(&b2g.strip, "strip", getEnv("BPF2GO_STRIP", ""),
    97  		"`binary` used to strip DWARF from compiled BPF ($BPF2GO_STRIP)")
    98  	fs.BoolVar(&b2g.disableStripping, "no-strip", false, "disable stripping of DWARF")
    99  	flagCFlags := fs.String("cflags", getEnv("BPF2GO_CFLAGS", ""),
   100  		"flags passed to the compiler, may contain quoted arguments ($BPF2GO_CFLAGS)")
   101  	fs.Var(&b2g.tags, "tags", "Comma-separated list of Go build tags to include in generated files")
   102  	flagTarget := fs.String("target", "bpfel,bpfeb", "clang target(s) to compile for (comma separated)")
   103  	fs.StringVar(&b2g.makeBase, "makebase", getEnv("BPF2GO_MAKEBASE", ""),
   104  		"write make compatible depinfo files relative to `directory` ($BPF2GO_MAKEBASE)")
   105  	fs.Var(&b2g.cTypes, "type", "`Name` of a type to generate a Go declaration for, may be repeated")
   106  	fs.BoolVar(&b2g.skipGlobalTypes, "no-global-types", false, "Skip generating types for map keys and values, etc.")
   107  	fs.StringVar(&b2g.outputStem, "output-stem", "", "alternative stem for names of generated files (defaults to ident)")
   108  	outDir := fs.String("output-dir", "", "target directory of generated files (defaults to current directory)")
   109  	outPkg := fs.String("go-package", "", "package for output go file (default as ENV GOPACKAGE)")
   110  	fs.SetOutput(b2g.stdout)
   111  	fs.Usage = func() {
   112  		fmt.Fprintf(fs.Output(), helpText, fs.Name())
   113  		fs.PrintDefaults()
   114  		fmt.Fprintln(fs.Output())
   115  		printTargets(fs.Output())
   116  	}
   117  	if err := fs.Parse(args); err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	if *outDir == "" {
   122  		var err error
   123  		if *outDir, err = os.Getwd(); err != nil {
   124  			return nil, err
   125  		}
   126  	}
   127  	b2g.outputDir = *outDir
   128  
   129  	if *outPkg == "" {
   130  		*outPkg = os.Getenv(gopackageEnv)
   131  	}
   132  	b2g.pkg = *outPkg
   133  
   134  	if b2g.pkg == "" {
   135  		return nil, errors.New("missing package, you should either set the go-package flag or the GOPACKAGE env")
   136  	}
   137  
   138  	if b2g.cc == "" {
   139  		return nil, errors.New("no compiler specified")
   140  	}
   141  
   142  	args, cFlags := splitCFlagsFromArgs(fs.Args())
   143  
   144  	if *flagCFlags != "" {
   145  		splitCFlags, err := splitArguments(*flagCFlags)
   146  		if err != nil {
   147  			return nil, err
   148  		}
   149  
   150  		// Command line arguments take precedence over C flags
   151  		// from the flag.
   152  		cFlags = append(splitCFlags, cFlags...)
   153  	}
   154  
   155  	for _, cFlag := range cFlags {
   156  		if strings.HasPrefix(cFlag, "-M") {
   157  			return nil, fmt.Errorf("use -makebase instead of %q", cFlag)
   158  		}
   159  	}
   160  
   161  	b2g.cFlags = cFlags[:len(cFlags):len(cFlags)]
   162  
   163  	if len(args) < 2 {
   164  		return nil, errors.New("expected at least two arguments")
   165  	}
   166  
   167  	b2g.identStem = args[0]
   168  
   169  	sourceFile, err := filepath.Abs(args[1])
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  	b2g.sourceFile = sourceFile
   174  
   175  	if b2g.makeBase != "" {
   176  		b2g.makeBase, err = filepath.Abs(b2g.makeBase)
   177  		if err != nil {
   178  			return nil, err
   179  		}
   180  	}
   181  
   182  	if b2g.outputStem != "" && strings.ContainsRune(b2g.outputStem, filepath.Separator) {
   183  		return nil, fmt.Errorf("-output-stem %q must not contain path separation characters", b2g.outputStem)
   184  	}
   185  
   186  	targetArches := make(map[gen.Target]gen.GoArches)
   187  	for _, tgt := range strings.Split(*flagTarget, ",") {
   188  		target, goarches, err := gen.FindTarget(tgt)
   189  		if err != nil {
   190  			if errors.Is(err, gen.ErrInvalidTarget) {
   191  				printTargets(b2g.stdout)
   192  				fmt.Fprintln(b2g.stdout)
   193  			}
   194  			return nil, err
   195  		}
   196  
   197  		targetArches[target] = goarches
   198  	}
   199  
   200  	if len(targetArches) == 0 {
   201  		return nil, fmt.Errorf("no targets specified")
   202  	}
   203  	b2g.targetArches = targetArches
   204  
   205  	// Try to find a suitable llvm-strip, possibly with a version suffix derived
   206  	// from the clang binary.
   207  	if b2g.strip == "" {
   208  		b2g.strip = "llvm-strip"
   209  		if strings.HasPrefix(b2g.cc, "clang") {
   210  			b2g.strip += strings.TrimPrefix(b2g.cc, "clang")
   211  		}
   212  	}
   213  
   214  	return b2g, nil
   215  }
   216  
   217  // cTypes collects the C type names a user wants to generate Go types for.
   218  //
   219  // Names are guaranteed to be unique, and only a subset of names is accepted so
   220  // that we may extend the flag syntax in the future.
   221  type cTypes []string
   222  
   223  var _ flag.Value = (*cTypes)(nil)
   224  
   225  func (ct *cTypes) String() string {
   226  	if ct == nil {
   227  		return "[]"
   228  	}
   229  	return fmt.Sprint(*ct)
   230  }
   231  
   232  const validCTypeChars = `[a-z0-9_]`
   233  
   234  var reValidCType = regexp.MustCompile(`(?i)^` + validCTypeChars + `+$`)
   235  
   236  func (ct *cTypes) Set(value string) error {
   237  	if !reValidCType.MatchString(value) {
   238  		return fmt.Errorf("%q contains characters outside of %s", value, validCTypeChars)
   239  	}
   240  
   241  	i := sort.SearchStrings(*ct, value)
   242  	if i >= len(*ct) {
   243  		*ct = append(*ct, value)
   244  		return nil
   245  	}
   246  
   247  	if (*ct)[i] == value {
   248  		return fmt.Errorf("duplicate type %q", value)
   249  	}
   250  
   251  	*ct = append((*ct)[:i], append([]string{value}, (*ct)[i:]...)...)
   252  	return nil
   253  }
   254  
   255  func getEnv(key, defaultVal string) string {
   256  	if val, ok := os.LookupEnv(key); ok {
   257  		return val
   258  	}
   259  	return defaultVal
   260  }
   261  
   262  func (b2g *bpf2go) convertAll() (err error) {
   263  	if _, err := os.Stat(b2g.sourceFile); os.IsNotExist(err) {
   264  		return fmt.Errorf("file %s doesn't exist", b2g.sourceFile)
   265  	} else if err != nil {
   266  		return err
   267  	}
   268  
   269  	if !b2g.disableStripping {
   270  		b2g.strip, err = exec.LookPath(b2g.strip)
   271  		if err != nil {
   272  			return err
   273  		}
   274  	}
   275  
   276  	for target, arches := range b2g.targetArches {
   277  		if err := b2g.convert(target, arches); err != nil {
   278  			return err
   279  		}
   280  	}
   281  
   282  	return nil
   283  }
   284  
   285  func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) {
   286  	removeOnError := func(f *os.File) {
   287  		if err != nil {
   288  			os.Remove(f.Name())
   289  		}
   290  		f.Close()
   291  	}
   292  
   293  	outputStem := b2g.outputStem
   294  	if outputStem == "" {
   295  		outputStem = strings.ToLower(b2g.identStem)
   296  	}
   297  
   298  	stem := fmt.Sprintf("%s_%s", outputStem, tgt.Suffix())
   299  
   300  	absOutPath, err := filepath.Abs(b2g.outputDir)
   301  	if err != nil {
   302  		return err
   303  	}
   304  
   305  	objFileName := filepath.Join(absOutPath, stem+".o")
   306  
   307  	cwd, err := os.Getwd()
   308  	if err != nil {
   309  		return err
   310  	}
   311  
   312  	archConstraint := goarches.Constraint()
   313  	constraints := andConstraints(archConstraint, b2g.tags.Expr)
   314  
   315  	if err := b2g.removeOldOutputFiles(outputStem, tgt); err != nil {
   316  		return fmt.Errorf("remove obsolete output: %w", err)
   317  	}
   318  
   319  	var depInput *os.File
   320  	cFlags := slices.Clone(b2g.cFlags)
   321  	if b2g.makeBase != "" {
   322  		depInput, err = os.CreateTemp("", "bpf2go")
   323  		if err != nil {
   324  			return err
   325  		}
   326  		defer depInput.Close()
   327  		defer os.Remove(depInput.Name())
   328  
   329  		cFlags = append(cFlags,
   330  			// Output dependency information.
   331  			"-MD",
   332  			// Create phony targets so that deleting a dependency doesn't
   333  			// break the build.
   334  			"-MP",
   335  			// Write it to temporary file
   336  			"-MF"+depInput.Name(),
   337  		)
   338  	}
   339  
   340  	err = gen.Compile(gen.CompileArgs{
   341  		CC:               b2g.cc,
   342  		Strip:            b2g.strip,
   343  		DisableStripping: b2g.disableStripping,
   344  		Flags:            cFlags,
   345  		Target:           tgt,
   346  		Workdir:          cwd,
   347  		Source:           b2g.sourceFile,
   348  		Dest:             objFileName,
   349  	})
   350  	if err != nil {
   351  		return fmt.Errorf("compile: %w", err)
   352  	}
   353  
   354  	fmt.Fprintln(b2g.stdout, "Compiled", objFileName)
   355  
   356  	if !b2g.disableStripping {
   357  		fmt.Fprintln(b2g.stdout, "Stripped", objFileName)
   358  	}
   359  
   360  	spec, err := ebpf.LoadCollectionSpec(objFileName)
   361  	if err != nil {
   362  		return fmt.Errorf("can't load BPF from ELF: %s", err)
   363  	}
   364  
   365  	var maps []string
   366  	for name := range spec.Maps {
   367  		// Skip .rodata, .data, .bss, etc. sections
   368  		if !strings.HasPrefix(name, ".") {
   369  			maps = append(maps, name)
   370  		}
   371  	}
   372  
   373  	var programs []string
   374  	for name := range spec.Programs {
   375  		programs = append(programs, name)
   376  	}
   377  
   378  	types, err := collectCTypes(spec.Types, b2g.cTypes)
   379  	if err != nil {
   380  		return fmt.Errorf("collect C types: %w", err)
   381  	}
   382  
   383  	if !b2g.skipGlobalTypes {
   384  		types = append(types, gen.CollectGlobalTypes(spec)...)
   385  	}
   386  
   387  	// Write out generated go
   388  	goFileName := filepath.Join(absOutPath, stem+".go")
   389  	goFile, err := os.Create(goFileName)
   390  	if err != nil {
   391  		return err
   392  	}
   393  	defer removeOnError(goFile)
   394  
   395  	err = gen.Generate(gen.GenerateArgs{
   396  		Package:     b2g.pkg,
   397  		Stem:        b2g.identStem,
   398  		Constraints: constraints,
   399  		Maps:        maps,
   400  		Programs:    programs,
   401  		Types:       types,
   402  		ObjectFile:  filepath.Base(objFileName),
   403  		Output:      goFile,
   404  	})
   405  	if err != nil {
   406  		return fmt.Errorf("can't write %s: %s", goFileName, err)
   407  	}
   408  
   409  	fmt.Fprintln(b2g.stdout, "Wrote", goFileName)
   410  
   411  	if b2g.makeBase == "" {
   412  		return
   413  	}
   414  
   415  	deps, err := parseDependencies(cwd, depInput)
   416  	if err != nil {
   417  		return fmt.Errorf("can't read dependency information: %s", err)
   418  	}
   419  
   420  	depFileName := goFileName + ".d"
   421  	depOutput, err := os.Create(depFileName)
   422  	if err != nil {
   423  		return fmt.Errorf("write make dependencies: %w", err)
   424  	}
   425  	defer depOutput.Close()
   426  
   427  	// There is always at least a dependency for the main file.
   428  	deps[0].file = goFileName
   429  	if err := adjustDependencies(depOutput, b2g.makeBase, deps); err != nil {
   430  		return fmt.Errorf("can't adjust dependency information: %s", err)
   431  	}
   432  
   433  	fmt.Fprintln(b2g.stdout, "Wrote", depFileName)
   434  	return nil
   435  }
   436  
   437  // removeOldOutputFiles removes output files generated by an old naming scheme.
   438  //
   439  // In the old scheme some linux targets were interpreted as build constraints
   440  // by the go toolchain.
   441  func (b2g *bpf2go) removeOldOutputFiles(outputStem string, tgt gen.Target) error {
   442  	suffix := tgt.ObsoleteSuffix()
   443  	if suffix == "" {
   444  		return nil
   445  	}
   446  
   447  	stem := fmt.Sprintf("%s_%s", outputStem, suffix)
   448  	for _, ext := range []string{".o", ".go"} {
   449  		filename := filepath.Join(b2g.outputDir, stem+ext)
   450  
   451  		if err := os.Remove(filename); errors.Is(err, os.ErrNotExist) {
   452  			continue
   453  		} else if err != nil {
   454  			return err
   455  		}
   456  
   457  		fmt.Fprintln(b2g.stdout, "Removed obsolete", filename)
   458  	}
   459  
   460  	return nil
   461  }
   462  
   463  func printTargets(w io.Writer) {
   464  	var arches []string
   465  	for goarch, archTarget := range gen.TargetsByGoArch() {
   466  		if archTarget.IsGeneric() {
   467  			continue
   468  		}
   469  		arches = append(arches, string(goarch))
   470  	}
   471  	sort.Strings(arches)
   472  
   473  	fmt.Fprint(w, "Supported targets:\n")
   474  	fmt.Fprint(w, "\tbpf\n\tbpfel\n\tbpfeb\n")
   475  	for _, arch := range arches {
   476  		fmt.Fprintf(w, "\t%s\n", arch)
   477  	}
   478  }
   479  
   480  func collectCTypes(types *btf.Spec, names []string) ([]btf.Type, error) {
   481  	var result []btf.Type
   482  	for _, cType := range names {
   483  		typ, err := types.AnyTypeByName(cType)
   484  		if err != nil {
   485  			return nil, err
   486  		}
   487  		result = append(result, typ)
   488  	}
   489  	return result, nil
   490  }
   491  
   492  const gopackageEnv = "GOPACKAGE"
   493  
   494  func main() {
   495  	if err := run(os.Stdout, os.Args[1:]); err != nil {
   496  		fmt.Fprintln(os.Stderr, "Error:", err)
   497  		os.Exit(1)
   498  	}
   499  }