blob: c7d4873f54a59c912d137061c18d30e1f9fa68d2 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2013 The ChromiumOS Authors
Mike Frysinger69cb41d2013-08-11 20:08:19 -04002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Generate minidump symbols for use by the Crash server.
6
7Note: This should be run inside the chroot.
8
9This produces files in the breakpad format required by minidump_stackwalk and
10the crash server to dump stack information.
11
12Basically it scans all the split .debug files in /build/$BOARD/usr/lib/debug/
13and converts them over using the `dump_syms` programs. Those plain text .sym
14files are then stored in /build/$BOARD/usr/lib/debug/breakpad/.
15
Mike Frysinger02e1e072013-11-10 22:11:34 -050016If you want to actually upload things, see upload_symbols.py.
17"""
Mike Frysinger69cb41d2013-08-11 20:08:19 -040018
19import collections
20import ctypes
Chris McDonaldb55b7032021-06-17 16:41:32 -060021import logging
Mike Frysinger69cb41d2013-08-11 20:08:19 -040022import multiprocessing
23import os
Mike Frysinger69cb41d2013-08-11 20:08:19 -040024
Chris McDonaldb55b7032021-06-17 16:41:32 -060025from chromite.cbuildbot import cbuildbot_alerts
Mike Frysinger06a51c82021-04-06 11:39:17 -040026from chromite.lib import build_target_lib
Mike Frysinger69cb41d2013-08-11 20:08:19 -040027from chromite.lib import commandline
28from chromite.lib import cros_build_lib
29from chromite.lib import osutils
30from chromite.lib import parallel
Mike Frysinger96ad3f22014-04-24 23:27:27 -040031from chromite.lib import signals
Alex Klein1809f572021-09-09 11:28:37 -060032from chromite.utils import file_util
Mike Frysinger69cb41d2013-08-11 20:08:19 -040033
Mike Frysinger807d8282022-04-28 22:45:17 -040034
Stephen Boydfc1c8032021-10-06 20:58:37 -070035# Elf files that don't exist but have a split .debug file installed.
36ALLOWED_DEBUG_ONLY_FILES = {
Alex Klein1699fab2022-09-08 08:46:06 -060037 "boot/vmlinux",
Stephen Boydfc1c8032021-10-06 20:58:37 -070038}
Mike Frysinger69cb41d2013-08-11 20:08:19 -040039
Alex Klein1699fab2022-09-08 08:46:06 -060040SymbolHeader = collections.namedtuple(
41 "SymbolHeader",
42 (
43 "cpu",
44 "id",
45 "name",
46 "os",
47 ),
48)
Mike Frysinger69cb41d2013-08-11 20:08:19 -040049
50
51def ReadSymsHeader(sym_file):
Alex Klein1699fab2022-09-08 08:46:06 -060052 """Parse the header of the symbol file
Mike Frysinger69cb41d2013-08-11 20:08:19 -040053
Alex Klein1699fab2022-09-08 08:46:06 -060054 The first line of the syms file will read like:
55 MODULE Linux arm F4F6FA6CCBDEF455039C8DE869C8A2F40 blkid
Mike Frysinger69cb41d2013-08-11 20:08:19 -040056
Alex Klein1699fab2022-09-08 08:46:06 -060057 https://code.google.com/p/google-breakpad/wiki/SymbolFiles
Mike Frysinger69cb41d2013-08-11 20:08:19 -040058
Alex Klein1699fab2022-09-08 08:46:06 -060059 Args:
60 sym_file: The symbol file to parse
Mike Frysinger1a736a82013-12-12 01:50:59 -050061
Alex Klein1699fab2022-09-08 08:46:06 -060062 Returns:
63 A SymbolHeader object
Mike Frysinger1a736a82013-12-12 01:50:59 -050064
Alex Klein1699fab2022-09-08 08:46:06 -060065 Raises:
66 ValueError if the first line of |sym_file| is invalid
67 """
68 with file_util.Open(sym_file, "rb") as f:
69 header = f.readline().decode("utf-8").split()
Mike Frysinger69cb41d2013-08-11 20:08:19 -040070
Alex Klein1699fab2022-09-08 08:46:06 -060071 if header[0] != "MODULE" or len(header) != 5:
72 raise ValueError("header of sym file is invalid")
Mike Frysinger50cedd32014-02-09 23:03:18 -050073
Alex Klein1699fab2022-09-08 08:46:06 -060074 return SymbolHeader(
75 os=header[1], cpu=header[2], id=header[3], name=header[4]
76 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -040077
78
Alex Klein1699fab2022-09-08 08:46:06 -060079def GenerateBreakpadSymbol(
80 elf_file,
81 debug_file=None,
82 breakpad_dir=None,
83 strip_cfi=False,
84 num_errors=None,
85 dump_syms_cmd="dump_syms",
86):
87 """Generate the symbols for |elf_file| using |debug_file|
Mike Frysinger69cb41d2013-08-11 20:08:19 -040088
Alex Klein1699fab2022-09-08 08:46:06 -060089 Args:
90 elf_file: The file to dump symbols for
91 debug_file: Split debug file to use for symbol information
92 breakpad_dir: The dir to store the output symbol file in
93 strip_cfi: Do not generate CFI data
94 num_errors: An object to update with the error count (needs a .value member)
95 dump_syms_cmd: Command to use for dumping symbols.
Mike Frysinger1a736a82013-12-12 01:50:59 -050096
Alex Klein1699fab2022-09-08 08:46:06 -060097 Returns:
98 The name of symbol file written out on success, or the failure count.
99 """
100 assert breakpad_dir
101 if num_errors is None:
102 num_errors = ctypes.c_int()
103 debug_file_only = not os.path.exists(elf_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400104
Alex Klein1699fab2022-09-08 08:46:06 -0600105 cmd_base = [dump_syms_cmd, "-v"]
106 if strip_cfi:
107 cmd_base += ["-c"]
108 # Some files will not be readable by non-root (e.g. set*id /bin/su).
109 needs_sudo = not os.access(elf_file, os.R_OK)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400110
Alex Klein1699fab2022-09-08 08:46:06 -0600111 def _DumpIt(cmd_args):
112 if needs_sudo:
113 run_command = cros_build_lib.sudo_run
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400114 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600115 run_command = cros_build_lib.run
116 return run_command(
117 cmd_base + cmd_args,
118 stderr=True,
119 stdout=temp.name,
120 check=False,
121 debug_level=logging.DEBUG,
122 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400123
Alex Klein1699fab2022-09-08 08:46:06 -0600124 def _CrashCheck(result, file_or_files, msg):
125 if result.returncode:
126 cbuildbot_alerts.PrintBuildbotStepWarnings()
127 if result.returncode < 0:
128 logging.warning(
129 "dump_syms %s crashed with %s; %s",
130 file_or_files,
131 signals.StrSignal(-result.returncode),
132 msg,
133 )
134 else:
135 logging.warning(
136 "dump_syms %s returned %d; %s",
137 file_or_files,
138 result.returncode,
139 msg,
140 )
141 logging.warning("output:\n%s", result.stderr.decode("utf-8"))
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400142
Alex Klein1699fab2022-09-08 08:46:06 -0600143 osutils.SafeMakedirs(breakpad_dir)
144 with cros_build_lib.UnbufferedNamedTemporaryFile(
145 dir=breakpad_dir, delete=False
146 ) as temp:
147 if debug_file:
148 # Try to dump the symbols using the debug file like normal.
149 if debug_file_only:
150 cmd_args = [debug_file]
151 file_or_files = debug_file
152 else:
153 cmd_args = [elf_file, os.path.dirname(debug_file)]
154 file_or_files = [elf_file, debug_file]
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400155
Alex Klein1699fab2022-09-08 08:46:06 -0600156 result = _DumpIt(cmd_args)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400157
Alex Klein1699fab2022-09-08 08:46:06 -0600158 if result.returncode:
159 # Sometimes dump_syms can crash because there's too much info.
160 # Try dumping and stripping the extended stuff out. At least
161 # this way we'll get the extended symbols. https://crbug.com/266064
162 _CrashCheck(result, file_or_files, "retrying w/out CFI")
163 cmd_args = ["-c", "-r"] + cmd_args
164 result = _DumpIt(cmd_args)
165 _CrashCheck(result, file_or_files, "retrying w/out debug")
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700166
Alex Klein1699fab2022-09-08 08:46:06 -0600167 basic_dump = result.returncode
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700168 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600169 basic_dump = True
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700170
Alex Klein1699fab2022-09-08 08:46:06 -0600171 if basic_dump:
172 # If that didn't work (no debug, or dump_syms still failed), try
173 # dumping just the file itself directly.
174 result = _DumpIt([elf_file])
175 if result.returncode:
176 # A lot of files (like kernel files) contain no debug information,
177 # do not consider such occurrences as errors.
178 cbuildbot_alerts.PrintBuildbotStepWarnings()
179 if b"file contains no debugging information" in result.stderr:
180 logging.warning("dump_syms failed; giving up entirely.")
181 logging.warning("No symbols found for %s", elf_file)
182 else:
183 num_errors.value += 1
184 _CrashCheck(result, elf_file, "giving up entirely")
185 os.unlink(temp.name)
186 return num_errors.value
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700187
Alex Klein1699fab2022-09-08 08:46:06 -0600188 # Move the dumped symbol file to the right place:
189 # /build/$BOARD/usr/lib/debug/breakpad/<module-name>/<id>/<module-name>.sym
190 header = ReadSymsHeader(temp)
191 logging.info("Dumped %s as %s : %s", elf_file, header.name, header.id)
192 sym_file = os.path.join(
193 breakpad_dir, header.name, header.id, header.name + ".sym"
194 )
195 osutils.SafeMakedirs(os.path.dirname(sym_file))
196 os.rename(temp.name, sym_file)
197 os.chmod(sym_file, 0o644)
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700198
Alex Klein1699fab2022-09-08 08:46:06 -0600199 return sym_file
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400200
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700201
Alex Klein1699fab2022-09-08 08:46:06 -0600202def GenerateBreakpadSymbols(
203 board,
204 breakpad_dir=None,
205 strip_cfi=False,
206 generate_count=None,
207 sysroot=None,
208 num_processes=None,
209 clean_breakpad=False,
210 exclude_dirs=(),
211 file_list=None,
212):
213 """Generate symbols for this board.
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700214
Alex Klein1699fab2022-09-08 08:46:06 -0600215 If |file_list| is None, symbols are generated for all executables, otherwise
216 only for the files included in |file_list|.
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700217
Alex Klein1699fab2022-09-08 08:46:06 -0600218 TODO(build):
219 This should be merged with buildbot_commands.GenerateBreakpadSymbols()
220 once we rewrite cros_generate_breakpad_symbols in python.
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400221
Alex Klein1699fab2022-09-08 08:46:06 -0600222 Args:
223 board: The board whose symbols we wish to generate
224 breakpad_dir: The full path to the breakpad directory where symbols live
225 strip_cfi: Do not generate CFI data
226 generate_count: If set, only generate this many symbols (meant for testing)
227 sysroot: The root where to find the corresponding ELFs
228 num_processes: Number of jobs to run in parallel
229 clean_breakpad: Should we `rm -rf` the breakpad output dir first; note: we
230 do not do any locking, so do not run more than one in parallel when True
231 exclude_dirs: List of dirs (relative to |sysroot|) to not search
232 file_list: Only generate symbols for files in this list. Each file must be a
233 full path (including |sysroot| prefix).
234 TODO(build): Support paths w/o |sysroot|.
235
236 Returns:
237 The number of errors that were encountered.
238 """
239 if sysroot is None:
240 sysroot = build_target_lib.get_default_sysroot_path(board)
241 if breakpad_dir is None:
242 breakpad_dir = FindBreakpadDir(board, sysroot=sysroot)
243 if clean_breakpad:
244 logging.info("cleaning out %s first", breakpad_dir)
245 osutils.RmDir(breakpad_dir, ignore_missing=True, sudo=True)
246 # Make sure non-root can write out symbols as needed.
247 osutils.SafeMakedirs(breakpad_dir, sudo=True)
248 if not os.access(breakpad_dir, os.W_OK):
249 cros_build_lib.sudo_run(["chown", "-R", str(os.getuid()), breakpad_dir])
250 debug_dir = FindDebugDir(board, sysroot=sysroot)
251 exclude_paths = [os.path.join(debug_dir, x) for x in exclude_dirs]
252 if file_list is None:
253 file_list = []
254 file_filter = dict.fromkeys([os.path.normpath(x) for x in file_list], False)
255
256 logging.info("generating breakpad symbols using %s", debug_dir)
257
258 # Let's locate all the debug_files and elfs first along with the debug file
259 # sizes. This way we can start processing the largest files first in parallel
260 # with the small ones.
261 # If |file_list| was given, ignore all other files.
262 targets = []
263 for root, dirs, files in os.walk(debug_dir):
264 if root in exclude_paths:
265 logging.info("Skipping excluded dir %s", root)
266 del dirs[:]
267 continue
268
269 for debug_file in files:
270 debug_file = os.path.join(root, debug_file)
271 # Turn /build/$BOARD/usr/lib/debug/sbin/foo.debug into
272 # /build/$BOARD/sbin/foo.
273 elf_file = os.path.join(
274 sysroot, debug_file[len(debug_dir) + 1 : -6]
275 )
276
277 if file_filter:
278 if elf_file in file_filter:
279 file_filter[elf_file] = True
280 elif debug_file in file_filter:
281 file_filter[debug_file] = True
282 else:
283 continue
284
285 # Filter out files based on common issues with the debug file.
286 if not debug_file.endswith(".debug"):
287 continue
288
289 elif os.path.islink(debug_file):
290 # The build-id stuff is common enough to filter out by default.
291 if "/.build-id/" in debug_file:
292 msg = logging.debug
293 else:
294 msg = logging.warning
295 msg("Skipping symbolic link %s", debug_file)
296 continue
297
298 # Filter out files based on common issues with the elf file.
299 elf_path = os.path.relpath(elf_file, sysroot)
300 debug_only = elf_path in ALLOWED_DEBUG_ONLY_FILES
301 if not os.path.exists(elf_file) and not debug_only:
302 # Sometimes we filter out programs from /usr/bin but leave behind
303 # the .debug file.
304 logging.warning("Skipping missing %s", elf_file)
305 continue
306
307 targets.append((os.path.getsize(debug_file), elf_file, debug_file))
308
309 bg_errors = parallel.WrapMultiprocessing(multiprocessing.Value, "i")
310 if file_filter:
311 files_not_found = [x for x, found in file_filter.items() if not found]
312 bg_errors.value += len(files_not_found)
313 if files_not_found:
314 logging.error("Failed to find requested files: %s", files_not_found)
315
316 # Now start generating symbols for the discovered elfs.
317 with parallel.BackgroundTaskRunner(
318 GenerateBreakpadSymbol,
319 breakpad_dir=breakpad_dir,
320 strip_cfi=strip_cfi,
321 num_errors=bg_errors,
322 processes=num_processes,
323 ) as queue:
324 for _, elf_file, debug_file in sorted(targets, reverse=True):
325 if generate_count == 0:
326 break
327
328 queue.put([elf_file, debug_file])
329 if generate_count is not None:
330 generate_count -= 1
331 if generate_count == 0:
332 break
333
334 return bg_errors.value
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400335
336
Mike Frysinger3f571af2016-08-31 23:56:53 -0400337def FindDebugDir(board, sysroot=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600338 """Given a |board|, return the path to the split debug dir for it"""
339 if sysroot is None:
340 sysroot = build_target_lib.get_default_sysroot_path(board)
341 return os.path.join(sysroot, "usr", "lib", "debug")
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400342
343
Mike Frysinger3f571af2016-08-31 23:56:53 -0400344def FindBreakpadDir(board, sysroot=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600345 """Given a |board|, return the path to the breakpad dir for it"""
346 return os.path.join(FindDebugDir(board, sysroot=sysroot), "breakpad")
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400347
348
349def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -0600350 parser = commandline.ArgumentParser(description=__doc__)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400351
Alex Klein1699fab2022-09-08 08:46:06 -0600352 parser.add_argument(
353 "--board", default=None, help="board to generate symbols for"
354 )
355 parser.add_argument(
356 "--breakpad_root",
357 type="path",
358 default=None,
359 help="root output directory for breakpad symbols",
360 )
361 parser.add_argument(
362 "--sysroot",
363 type="path",
364 default=None,
365 help="root input directory for files",
366 )
367 parser.add_argument(
368 "--exclude-dir",
369 type=str,
370 action="append",
371 default=[],
372 help="directory (relative to |board| root) to not search",
373 )
374 parser.add_argument(
375 "--generate-count",
376 type=int,
377 default=None,
378 help="only generate # number of symbols",
379 )
380 parser.add_argument(
381 "--noclean",
382 dest="clean",
383 action="store_false",
384 default=True,
385 help="do not clean out breakpad dir before running",
386 )
387 parser.add_argument(
388 "--jobs", type=int, default=None, help="limit number of parallel jobs"
389 )
390 parser.add_argument(
391 "--strip_cfi",
392 action="store_true",
393 default=False,
394 help="do not generate CFI data (pass -c to dump_syms)",
395 )
396 parser.add_argument(
397 "file_list",
398 nargs="*",
399 default=None,
400 help="generate symbols for only these files "
401 "(e.g. /build/$BOARD/usr/bin/foo)",
402 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400403
Alex Klein1699fab2022-09-08 08:46:06 -0600404 opts = parser.parse_args(argv)
405 opts.Freeze()
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400406
Alex Klein1699fab2022-09-08 08:46:06 -0600407 if opts.board is None and opts.sysroot is None:
408 cros_build_lib.Die("--board or --sysroot is required")
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400409
Alex Klein1699fab2022-09-08 08:46:06 -0600410 ret = GenerateBreakpadSymbols(
411 opts.board,
412 breakpad_dir=opts.breakpad_root,
413 strip_cfi=opts.strip_cfi,
414 generate_count=opts.generate_count,
415 sysroot=opts.sysroot,
416 num_processes=opts.jobs,
417 clean_breakpad=opts.clean,
418 exclude_dirs=opts.exclude_dir,
419 file_list=opts.file_list,
420 )
421 if ret:
422 logging.error("encountered %i problem(s)", ret)
423 # Since exit(status) gets masked, clamp it to 1 so we don't inadvertently
424 # return 0 in case we are a multiple of the mask.
425 ret = 1
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400426
Alex Klein1699fab2022-09-08 08:46:06 -0600427 return ret