github.com/cilium/ebpf@v0.15.1-0.20240517100537-8079b37aa138/cmd/bpf2go/main.go (about)

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