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 }