blob: 43b413b996adc88a8395f4866db7f02537919848 [file] [log] [blame]
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# 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
16If you want to actually upload things, see upload_symbols.py."""
17
18import collections
19import ctypes
20import logging
21import multiprocessing
22import os
23import tempfile
24
25from chromite.lib import commandline
26from chromite.lib import cros_build_lib
27from chromite.lib import osutils
28from chromite.lib import parallel
29
30
31SymbolHeader = collections.namedtuple('SymbolHeader',
32 ('cpu', 'id', 'name', 'os',))
33
34
35def ReadSymsHeader(sym_file):
36 """Parse the header of the symbol file
37
38 The first line of the syms file will read like:
39 MODULE Linux arm F4F6FA6CCBDEF455039C8DE869C8A2F40 blkid
40
41 https://code.google.com/p/google-breakpad/wiki/SymbolFiles
42
43 Args:
44 sym_file: The symbol file to parse
45 Returns:
46 A SymbolHeader object
47 Raises:
48 ValueError if the first line of |sym_file| is invalid
49 """
50 read_it = lambda x: x.readline().split()
51 if isinstance(sym_file, basestring):
52 with open(sym_file, 'r') as f:
53 header = read_it(f)
54 else:
55 header = read_it(sym_file)
56
57 if header[0] != 'MODULE' or len(header) != 5:
58 raise ValueError('header of sym file is invalid')
59 return SymbolHeader(os=header[1], cpu=header[2], id=header[3], name=header[4])
60
61
62def GenerateBreakpadSymbol(elf_file, debug_file=None, breakpad_dir=None,
63 board=None, strip_cfi=False, num_errors=None):
64 """Generate the symbols for |elf_file| using |debug_file|
65
66 Args:
67 elf_file: The file to dump symbols for
68 debug_file: Split debug file to use for symbol information
69 breakpad_dir: The dir to store the output symbol file in
70 board: If |breakpad_dir| is not specified, use |board| to find it
71 strip_cfi: Do not generate CFI data
72 num_errors: An object to update with the error count (needs a .value member)
73 Returns:
74 The number of errors that were encountered.
75 """
76 if breakpad_dir is None:
77 breakpad_dir = FindBreakpadDir(board)
78 if num_errors is None:
79 num_errors = ctypes.c_int()
80
81 cmd_base = ['dump_syms']
82 if strip_cfi:
83 cmd_base += ['-c']
84 # Some files will not be readable by non-root (e.g. set*id /bin/su).
85 needs_sudo = not os.access(elf_file, os.R_OK)
86
87 def _DumpIt(cmd_args):
88 if needs_sudo:
89 run_command = cros_build_lib.SudoRunCommand
90 else:
91 run_command = cros_build_lib.RunCommand
92 return run_command(
93 cmd_base + cmd_args, redirect_stderr=True, log_stdout_to_file=temp.name,
94 error_code_ok=True, debug_level=logging.DEBUG)
95
96 def _CrashCheck(ret, msg):
97 if ret < 0:
98 cros_build_lib.PrintBuildbotStepWarnings()
99 cros_build_lib.Warning('dump_syms crashed with %s; %s',
100 osutils.StrSignal(-ret), msg)
101
102 osutils.SafeMakedirs(breakpad_dir)
103 with tempfile.NamedTemporaryFile(dir=breakpad_dir, bufsize=0) as temp:
104 if debug_file:
105 # Try to dump the symbols using the debug file like normal.
106 cmd_args = [elf_file, os.path.dirname(debug_file)]
107 result = _DumpIt(cmd_args)
108
109 if result.returncode:
110 # Sometimes dump_syms can crash because there's too much info.
111 # Try dumping and stripping the extended stuff out. At least
112 # this way we'll get the extended symbols. http://crbug.com/266064
113 _CrashCheck(result.returncode, 'retrying w/out CFI')
114 cmd_args = ['-c', '-r'] + cmd_args
115 result = _DumpIt(cmd_args)
116 _CrashCheck(result.returncode, 'retrying w/out debug')
117
118 basic_dump = result.returncode
119 else:
120 basic_dump = True
121
122 if basic_dump:
123 # If that didn't work (no debug, or dump_syms still failed), try
124 # dumping just the file itself directly.
125 result = _DumpIt([elf_file])
126 if result.returncode:
127 # A lot of files (like kernel files) contain no debug information,
128 # do not consider such occurrences as errors.
129 cros_build_lib.PrintBuildbotStepWarnings()
130 _CrashCheck(result.returncode, 'giving up entirely')
131 if 'file contains no debugging information' in result.error:
132 cros_build_lib.Warning('no symbols found for %s', elf_file)
133 else:
134 num_errors.value += 1
135 cros_build_lib.Error('dumping symbols for %s failed:\n%s',
136 elf_file, result.error)
137 return num_errors.value
138
139 # Move the dumped symbol file to the right place:
140 # /build/$BOARD/usr/lib/debug/breakpad/<module-name>/<id>/<module-name>.sym
141 header = ReadSymsHeader(temp)
142 cros_build_lib.Info('Dumped %s as %s : %s', elf_file, header.name,
143 header.id)
144 sym_file = os.path.join(breakpad_dir, header.name, header.id,
145 header.name + '.sym')
146 osutils.SafeMakedirs(os.path.dirname(sym_file))
147 os.rename(temp.name, sym_file)
Mike Frysinger60ec1012013-10-21 00:11:10 -0400148 os.chmod(sym_file, 0o644)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400149 temp.delete = False
150
151 return num_errors.value
152
153
154def GenerateBreakpadSymbols(board, breakpad_dir=None, strip_cfi=False,
Mike Frysingeref9ab2f2013-08-26 22:16:00 -0400155 generate_count=None, sysroot=None,
Shawn Nematbakhsh2c169cb2013-10-29 16:23:58 -0700156 num_processes=None, clean_breakpad=False,
157 exclude_dirs=()):
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400158 """Generate all the symbols for this board
159
160 TODO(build):
161 This should be merged with buildbot_commands.GenerateBreakpadSymbols()
162 once we rewrite cros_generate_breakpad_symbols in python.
163
164 Args:
165 board: The board whose symbols we wish to generate
166 breakpad_dir: The full path to the breakpad directory where symbols live
167 strip_cfi: Do not generate CFI data
168 generate_count: If set, only generate this many symbols (meant for testing)
169 sysroot: The root where to find the corresponding ELFs
Mike Frysingeref9ab2f2013-08-26 22:16:00 -0400170 num_processes: Number of jobs to run in parallel
Mike Frysinger9a628bb2013-10-24 15:51:37 -0400171 clean_breakpad: Should we `rm -rf` the breakpad output dir first; note: we
172 do not do any locking, so do not run more than one in parallel when True
Shawn Nematbakhsh2c169cb2013-10-29 16:23:58 -0700173 exclude_dirs: List of dirs (relative to |sysroot|) to not search
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400174 Returns:
175 The number of errors that were encountered.
176 """
177 if breakpad_dir is None:
178 breakpad_dir = FindBreakpadDir(board)
179 if sysroot is None:
180 sysroot = os.path.join('/build', board)
Mike Frysinger9a628bb2013-10-24 15:51:37 -0400181 if clean_breakpad:
182 cros_build_lib.Info('cleaning out %s first', breakpad_dir)
183 osutils.RmDir(breakpad_dir, ignore_missing=True, sudo=True)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400184 # Make sure non-root can write out symbols as needed.
185 osutils.SafeMakedirs(breakpad_dir, sudo=True)
186 if not os.access(breakpad_dir, os.W_OK):
187 cros_build_lib.SudoRunCommand(['chown', '-R', str(os.getuid()),
188 breakpad_dir])
189 debug_dir = FindDebugDir(board)
Shawn Nematbakhsh2c169cb2013-10-29 16:23:58 -0700190 exclude_paths = [os.path.join(debug_dir, x) for x in exclude_dirs]
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400191
192 cros_build_lib.Info('generating all breakpad symbol files using %s',
193 debug_dir)
194
195 # Let's locate all the debug_files first and their size. This way we can
196 # start processing the largest files first in parallel with the small ones.
197 debug_files = []
Shawn Nematbakhsh2c169cb2013-10-29 16:23:58 -0700198 for root, dirs, files in os.walk(debug_dir):
199 if root in exclude_paths:
200 cros_build_lib.Info('Skipping excluded dir %s', root)
201 del dirs[:]
202 continue
203
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400204 for debug_file in files:
205 debug_file = os.path.join(root, debug_file)
206 if debug_file.endswith('.ko.debug'):
207 cros_build_lib.Debug('Skipping kernel module %s', debug_file)
208 elif debug_file.endswith('.debug'):
209 if os.path.islink(debug_file):
210 # The build-id stuff is common enough to filter out by default.
211 if '/.build-id/' in debug_file:
212 msg = cros_build_lib.Debug
213 else:
214 msg = cros_build_lib.Warning
215 msg('Skipping symbolic link %s', debug_file)
216 else:
217 debug_files.append((os.path.getsize(debug_file), debug_file))
218
219 # Now start generating symbols for all the inputs.
220 bg_errors = multiprocessing.Value('i')
221 with parallel.BackgroundTaskRunner(GenerateBreakpadSymbol,
222 breakpad_dir=breakpad_dir, board=board,
223 strip_cfi=strip_cfi,
Mike Frysingeref9ab2f2013-08-26 22:16:00 -0400224 num_errors=bg_errors,
225 processes=num_processes) as queue:
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400226 for _, debug_file in sorted(debug_files, reverse=True):
227 if generate_count == 0:
228 break
229
230 # Turn /build/$BOARD/usr/lib/debug/sbin/foo.debug into
231 # /build/$BOARD/sbin/foo.
232 elf_file = os.path.join(sysroot, debug_file[len(debug_dir) + 1:-6])
233 if not os.path.exists(elf_file):
234 # Sometimes we filter out programs from /usr/bin but leave behind
235 # the .debug file.
236 cros_build_lib.Warning('Skipping missing %s', elf_file)
237 continue
238
239 queue.put([elf_file, debug_file])
240 if generate_count is not None:
241 generate_count -= 1
242 if generate_count == 0:
243 break
244
245 return bg_errors.value
246
247
248def FindDebugDir(board):
249 """Given a |board|, return the path to the split debug dir for it"""
250 return os.path.join('/build', board, 'usr', 'lib', 'debug')
251
252
253def FindBreakpadDir(board):
254 """Given a |board|, return the path to the breakpad dir for it"""
255 return os.path.join(FindDebugDir(board), 'breakpad')
256
257
258def main(argv):
259 parser = commandline.ArgumentParser(description=__doc__)
260
261 parser.add_argument('--board', default=None,
262 help='board to generate symbols for')
263 parser.add_argument('--breakpad_root', type='path', default=None,
264 help='root directory for breakpad symbols')
Shawn Nematbakhsh2c169cb2013-10-29 16:23:58 -0700265 parser.add_argument('--exclude-dir', type=str, action='append',
266 default=[],
267 help='directory (relative to |board| root) to not search')
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400268 parser.add_argument('--generate-count', type=int, default=None,
269 help='only generate # number of symbols')
Mike Frysinger9a628bb2013-10-24 15:51:37 -0400270 parser.add_argument('--noclean', dest='clean', action='store_false',
271 default=True,
272 help='do not clean out breakpad dir before running')
Mike Frysingeref9ab2f2013-08-26 22:16:00 -0400273 parser.add_argument('--jobs', type=int, default=None,
274 help='limit number of parallel jobs')
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400275 parser.add_argument('--strip_cfi', action='store_true', default=False,
276 help='do not generate CFI data (pass -c to dump_syms)')
277
278 opts = parser.parse_args(argv)
279
280 if opts.board is None:
281 cros_build_lib.Die('--board is required')
282
283 ret = GenerateBreakpadSymbols(opts.board, breakpad_dir=opts.breakpad_root,
284 strip_cfi=opts.strip_cfi,
Mike Frysingeref9ab2f2013-08-26 22:16:00 -0400285 generate_count=opts.generate_count,
Mike Frysinger9a628bb2013-10-24 15:51:37 -0400286 num_processes=opts.jobs,
Shawn Nematbakhsh2c169cb2013-10-29 16:23:58 -0700287 clean_breakpad=opts.clean,
288 exclude_dirs=opts.exclude_dir)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400289 if ret:
290 cros_build_lib.Error('encountered %i problem(s)', ret)
291 # Since exit(status) gets masked, clamp it to 1 so we don't inadvertently
292 # return 0 in case we are a multiple of the mask.
293 ret = 1
294
295 return ret