| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| // TODO(kjharland): change crypto/sha1 to a safer hash algorithm. sha256 or sha2, etc. |
| "archive/tar" |
| "bytes" |
| "compress/gzip" |
| "context" |
| "crypto/sha1" |
| "encoding/hex" |
| "flag" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "log" |
| "os" |
| "os/exec" |
| "path" |
| "path/filepath" |
| "strings" |
| |
| "fuchsia.googlesource.com/tools/breakpad" |
| "fuchsia.googlesource.com/tools/elflib" |
| ) |
| |
| const usage = `usage: dump_breakpad_symbols [options] file1 file2 ... fileN |
| |
| Dumps symbol data from a collection of IDs files. IDs files are generated as |
| part of the build and contain a number of newline-separate records which have |
| the syntax: |
| |
| <hash-value> <absolute-path> |
| |
| This command does not care about <hash-value>. <absolute-path> is the path to a |
| binary generated as part of the Fuchsia build. This command collects every |
| <absolute-path> from each of file1, file2 ... fileN and dumps symbol data for |
| the binaries at each of those paths. Duplicate paths are skipped. |
| |
| The output is a collection of symbol files, one for each binary, using an |
| arbitrary naming scheme to ensure that every output file name is unique. |
| |
| Example invocation: |
| |
| $ dump_breakpad_symbols \ |
| -out-dir=/path/to/output/ \ |
| -dump-syms-path=/path/to/breakpad/dump_syms \ |
| -summary-file=/path/to/summary \ |
| /path/to/ids1.txt |
| ` |
| |
| // The default module name for modules that don't have a soname, e.g., executables and |
| // loadable modules. This allows us to use the same module name at runtime as sonames are |
| // the only names that are guaranteed to be available at build and run times. This value |
| // must be kept in sync with what Crashpad uses at run time for symbol resolution to work |
| // properly. |
| const defaultModuleName = "<_>" |
| |
| // Command line flag values |
| var ( |
| depFilepath string |
| dumpSymsPath string |
| outdir string |
| tarFilepath string |
| ) |
| |
| // ExecDumpSyms runs the beakpad `dump_syms` command and returns the output. |
| type ExecDumpSyms = func(args []string) ([]byte, error) |
| |
| // CreateFile returns an io.ReadWriteCloser for the file at the given path. |
| type CreateFile = func(path string) (io.ReadWriteCloser, error) |
| |
| func init() { |
| flag.Usage = func() { |
| fmt.Fprint(os.Stderr, usage) |
| flag.PrintDefaults() |
| os.Exit(0) |
| } |
| |
| // First set the flags ... |
| flag.StringVar(&outdir, "out-dir", "", |
| "The directory where symbol output should be written") |
| flag.StringVar(&dumpSymsPath, "dump-syms-path", "", |
| "Path to the breakpad tools `dump_syms` executable") |
| flag.StringVar(&depFilepath, "depfile", "", |
| "Path to the ninja depfile to generate. The file has the single line: "+ |
| "`OUTPUT: INPUT1 INPUT2 ...` where OUTPUT is the value of -summary-file "+ |
| "and INPUTX is the ids file in the same order it was provided on the "+ |
| "command line. -summary-file must be provided with this flag. "+ |
| "See `gn help depfile` for more information on depfiles.") |
| flag.StringVar(&tarFilepath, "tar-file", "", |
| "Path to the tarball that contains all Breakpad symbol files generated "+ |
| " by the build") |
| } |
| |
| func main() { |
| flag.Parse() |
| if err := execute(context.Background()); err != nil { |
| log.Fatal(err) |
| } |
| } |
| |
| func execute(ctx context.Context) error { |
| // Callback to run breakpad `dump_syms` command. |
| execDumpSyms := func(args []string) ([]byte, error) { |
| return exec.Command(dumpSymsPath, args...).Output() |
| } |
| |
| // Callback to create new files. |
| createFile := func(path string) (io.ReadWriteCloser, error) { |
| return os.Create(path) |
| } |
| // Open the input files for reading. In practice there are very few files, |
| // so it's fine to open them all at once. |
| var inputReaders []io.Reader |
| inputPaths := flag.Args() |
| for _, path := range inputPaths { |
| file, err := os.Open(path) |
| if err != nil { |
| return fmt.Errorf("failed to open %s: %v\n", path, err) |
| } |
| defer file.Close() |
| inputReaders = append(inputReaders, file) |
| } |
| |
| // Process the IDsFiles. |
| summary := processIdsFiles(inputReaders, outdir, execDumpSyms, createFile) |
| |
| // Write the Ninja dep file. |
| depfile := depfile{outputPath: tarFilepath, inputPaths: inputPaths} |
| depfd, err := os.Create(depFilepath) |
| if err != nil { |
| return fmt.Errorf("failed to create file %q: %v", depFilepath, err) |
| } |
| n, err := depfile.WriteTo(depfd) |
| if err != nil { |
| return fmt.Errorf("failed to write Ninja dep file %q: %v", depFilepath, err) |
| } |
| if n == 0 { |
| return fmt.Errorf("wrote 0 bytes to %q", depFilepath) |
| } |
| |
| // Create and open tarball. |
| tarFile, err := os.Create(tarFilepath) |
| if err != nil { |
| return fmt.Errorf("could not create file %s: %v", tarFilepath, err) |
| } |
| defer tarFile.Close() |
| |
| // Collect Breakpad symbol files generated by the build to a tarball. |
| if err := writeTarball(tarFile, summary, outdir); err != nil { |
| return fmt.Errorf("failed to generate tarball %s: %v", tarFilepath, err) |
| } |
| return nil |
| } |
| |
| // processIdsFiles dumps symbol data for each executable in a set of ids files. |
| func processIdsFiles(idsFiles []io.Reader, outdir string, execDumpSyms ExecDumpSyms, createFile CreateFile) map[string]string { |
| // Binary paths we've already seen. Duplicates are skipped. |
| visited := make(map[string]bool) |
| binaryToSymbolFile := make(map[string]string) |
| |
| // Iterate through the given set of filepaths. |
| for _, idsFile := range idsFiles { |
| // Extract the paths to each binary from the IDs file. |
| binaries, err := elflib.ReadIDsFile(idsFile) |
| if err != nil { |
| fmt.Fprintln(os.Stderr, err) |
| continue |
| } |
| |
| // Generate the symbol file for each binary. |
| for _, bin := range binaries { |
| binaryPath := bin.Filepath |
| |
| // Check whether we've seen this path already. Skip if so. |
| if _, ok := visited[binaryPath]; ok { |
| continue |
| } |
| // Record that we've seen this binary path. |
| visited[binaryPath] = true |
| |
| symbolFilepath, err := generateSymbolFile(binaryPath, createFile, execDumpSyms) |
| if err != nil { |
| log.Println(err) |
| continue |
| } |
| |
| // Record the mapping in the summary. |
| binaryToSymbolFile[binaryPath] = symbolFilepath |
| } |
| } |
| |
| return binaryToSymbolFile |
| } |
| |
| func generateSymbolFile(path string, createFile CreateFile, execDumpSyms ExecDumpSyms) (outputPath string, err error) { |
| outputPath = createSymbolFilepath(outdir, path) |
| output, err := execDumpSyms([]string{path}) |
| if err != nil { |
| return "", fmt.Errorf("failed to generate symbol data for %s: %v", path, err) |
| } |
| symbolFile, err := breakpad.ParseSymbolFile(bytes.NewReader(output)) |
| if err != nil { |
| return "", fmt.Errorf("failed to read dump_syms output: %v", err) |
| } |
| // Ensure the module name is either the soname (for shared libraries) or the default |
| // value (for executables and loadable modules). |
| fd, err := os.Open(path) |
| if err != nil { |
| return "", fmt.Errorf("failed to open %q: %v", path, err) |
| } |
| defer fd.Close() |
| soname, err := elflib.GetSoName(path, fd) |
| if err != nil { |
| return "", fmt.Errorf("failed to read soname from %q: %v", path, err) |
| } |
| if soname == "" { |
| symbolFile.ModuleSection.ModuleName = defaultModuleName |
| } else { |
| symbolFile.ModuleSection.ModuleName = soname |
| } |
| |
| // Ensure the module section specifies this is a Fuchsia binary instead of Linux |
| // binary, which is the default for the dump_syms tool. |
| symbolFile.ModuleSection.OS = "Fuchsia" |
| symbolFd, err := createFile(outputPath) |
| if err != nil { |
| return "", fmt.Errorf("failed to create symbol file %s: %v", outputPath, err) |
| } |
| defer fd.Close() |
| if _, err := symbolFile.WriteTo(symbolFd); err != nil { |
| return "", fmt.Errorf("failed to write symbol file %s: %v", outputPath, err) |
| } |
| return outputPath, nil |
| } |
| |
| // Writes the given symbol file data to the given writer after massaging the data. |
| func writeSymbolFile(w io.Writer, symbolData []byte) error { |
| // Many Fuchsia binaries are built as "something.elf", but then packaged as |
| // just "something". In the ids.txt file, the name still includes the ".elf" |
| // extension, which dump_syms emits into the .sym file, and the crash server |
| // uses as part of the lookup. The binary name and this value written to |
| // the .sym file must match, so if the first header line ends in ".elf" |
| // strip it off. This line usually looks something like: |
| // MODULE Linux x86_64 094B63014248508BA0636AD3AC3E81D10 sysconf.elf |
| lines := strings.SplitN(string(symbolData), "\n", 2) |
| if len(lines) != 2 { |
| return fmt.Errorf("got <2 lines in symbol data") |
| } |
| |
| // Make sure the first line is not empty. |
| lines[0] = strings.TrimSpace(lines[0]) |
| if lines[0] == "" { |
| return fmt.Errorf("unexpected blank first line in symbol data") |
| } |
| |
| // Strip .elf from header if it exists. |
| if strings.HasSuffix(lines[0], ".elf") { |
| lines[0] = strings.TrimSuffix(lines[0], ".elf") |
| // Join the new lines of the symbol data. |
| symbolData = []byte(strings.Join(lines, "\n")) |
| } |
| |
| // Write the symbol file. |
| _, err := w.Write(symbolData) |
| return err |
| } |
| |
| // Creates the absolute path to the symbol file for the given binary. |
| // |
| // The returned path is generated as a subpath of parentDir. |
| func createSymbolFilepath(parentDir string, binaryPath string) string { |
| // Create the symbole file basename as a hash of the path to the binary. |
| // This ensures that filenames are unique within the output directory. |
| hash := sha1.New() |
| n, err := hash.Write([]byte(binaryPath)) |
| if err != nil { |
| panic(err) |
| } |
| if n == 0 { |
| // Empty text should never be passed to this function and likely signifies |
| // an error in the input file. Panic here as well. |
| panic("0 bytes written for hash of input text '" + binaryPath + "'") |
| } |
| basename := hex.EncodeToString(hash.Sum(nil)) + ".sym" |
| |
| // Generate the filepath as an subdirectory of the given parent directory. |
| absPath, err := filepath.Abs(path.Join(parentDir, basename)) |
| if err != nil { |
| // Panic because if this fails once it's likely to keep failing. |
| panic(fmt.Sprintf("failed to get path to symbol file for %s: %v", binaryPath, err)) |
| } |
| |
| return absPath |
| } |
| |
| func writeTarball(w io.Writer, summary map[string]string, parentDir string) error { |
| gw := gzip.NewWriter(w) |
| defer gw.Close() |
| // Create a tarWriter to perform the tarring task. |
| tw := tar.NewWriter(gw) |
| defer tw.Close() |
| |
| // Iterate through the summary file, writting each symbol file to the tarball buffer. |
| for _, file := range summary { |
| data, err := ioutil.ReadFile(file) |
| if err != nil { |
| return fmt.Errorf("failed to read the symbol data file %s: %v", file, err) |
| } |
| |
| // Add a file with its relative path instead of absolute path to the tarball. |
| rel, err := filepath.Rel(parentDir, file) |
| if err != nil { |
| return fmt.Errorf("failed to get the relative path from %s to %s: %v", parentDir, file, err) |
| } |
| hdr := &tar.Header{ |
| Name: rel, |
| Mode: 0600, |
| Size: int64(len(data)), |
| } |
| if err := tw.WriteHeader(hdr); err != nil { |
| return fmt.Errorf("failed to write header for tarball: %v", err) |
| } |
| if _, err := tw.Write([]byte(data)); err != nil { |
| return fmt.Errorf("failed to write contents for tarball: %v", err) |
| } |
| } |
| |
| return nil |
| } |