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 }