blob: 4c443e52099e370b93be1e5f946a91437cdf0c81 [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
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
21import logging
22import multiprocessing
23import os
24import tempfile
25
26from chromite.lib import commandline
27from chromite.lib import cros_build_lib
28from chromite.lib import osutils
29from chromite.lib import parallel
30
31
32SymbolHeader = collections.namedtuple('SymbolHeader',
33 ('cpu', 'id', 'name', 'os',))
34
35
36def ReadSymsHeader(sym_file):
37 """Parse the header of the symbol file
38
39 The first line of the syms file will read like:
40 MODULE Linux arm F4F6FA6CCBDEF455039C8DE869C8A2F40 blkid
41
42 https://code.google.com/p/google-breakpad/wiki/SymbolFiles
43
44 Args:
45 sym_file: The symbol file to parse
46 Returns:
47 A SymbolHeader object
48 Raises:
49 ValueError if the first line of |sym_file| is invalid
50 """
51 read_it = lambda x: x.readline().split()
52 if isinstance(sym_file, basestring):
53 with open(sym_file, 'r') as f:
54 header = read_it(f)
55 else:
56 header = read_it(sym_file)
57
58 if header[0] != 'MODULE' or len(header) != 5:
59 raise ValueError('header of sym file is invalid')
60 return SymbolHeader(os=header[1], cpu=header[2], id=header[3], name=header[4])
61
62
63def GenerateBreakpadSymbol(elf_file, debug_file=None, breakpad_dir=None,
64 board=None, strip_cfi=False, num_errors=None):
65 """Generate the symbols for |elf_file| using |debug_file|
66
67 Args:
68 elf_file: The file to dump symbols for
69 debug_file: Split debug file to use for symbol information
70 breakpad_dir: The dir to store the output symbol file in
71 board: If |breakpad_dir| is not specified, use |board| to find it
72 strip_cfi: Do not generate CFI data
73 num_errors: An object to update with the error count (needs a .value member)
74 Returns:
75 The number of errors that were encountered.
76 """
77 if breakpad_dir is None:
78 breakpad_dir = FindBreakpadDir(board)
79 if num_errors is None:
80 num_errors = ctypes.c_int()
81
82 cmd_base = ['dump_syms']
83 if strip_cfi:
84 cmd_base += ['-c']
85 # Some files will not be readable by non-root (e.g. set*id /bin/su).
86 needs_sudo = not os.access(elf_file, os.R_OK)
87
88 def _DumpIt(cmd_args):
89 if needs_sudo:
90 run_command = cros_build_lib.SudoRunCommand
91 else:
92 run_command = cros_build_lib.RunCommand
93 return run_command(
94 cmd_base + cmd_args, redirect_stderr=True, log_stdout_to_file=temp.name,
95 error_code_ok=True, debug_level=logging.DEBUG)
96
97 def _CrashCheck(ret, msg):
98 if ret < 0:
99 cros_build_lib.PrintBuildbotStepWarnings()
100 cros_build_lib.Warning('dump_syms crashed with %s; %s',
101 osutils.StrSignal(-ret), msg)
102
103 osutils.SafeMakedirs(breakpad_dir)
104 with tempfile.NamedTemporaryFile(dir=breakpad_dir, bufsize=0) as temp:
105 if debug_file:
106 # Try to dump the symbols using the debug file like normal.
107 cmd_args = [elf_file, os.path.dirname(debug_file)]
108 result = _DumpIt(cmd_args)
109
110 if result.returncode:
111 # Sometimes dump_syms can crash because there's too much info.
112 # Try dumping and stripping the extended stuff out. At least
113 # this way we'll get the extended symbols. http://crbug.com/266064
114 _CrashCheck(result.returncode, 'retrying w/out CFI')
115 cmd_args = ['-c', '-r'] + cmd_args
116 result = _DumpIt(cmd_args)
117 _CrashCheck(result.returncode, 'retrying w/out debug')
118
119 basic_dump = result.returncode
120 else:
121 basic_dump = True
122
123 if basic_dump:
124 # If that didn't work (no debug, or dump_syms still failed), try
125 # dumping just the file itself directly.
126 result = _DumpIt([elf_file])
127 if result.returncode:
128 # A lot of files (like kernel files) contain no debug information,
129 # do not consider such occurrences as errors.
130 cros_build_lib.PrintBuildbotStepWarnings()
131 _CrashCheck(result.returncode, 'giving up entirely')
132 if 'file contains no debugging information' in result.error:
133 cros_build_lib.Warning('no symbols found for %s', elf_file)
134 else:
135 num_errors.value += 1
136 cros_build_lib.Error('dumping symbols for %s failed:\n%s',
137 elf_file, result.error)
138 return num_errors.value
139
140 # Move the dumped symbol file to the right place:
141 # /build/$BOARD/usr/lib/debug/breakpad/<module-name>/<id>/<module-name>.sym
142 header = ReadSymsHeader(temp)
143 cros_build_lib.Info('Dumped %s as %s : %s', elf_file, header.name,
144 header.id)
145 sym_file = os.path.join(breakpad_dir, header.name, header.id,
146 header.name + '.sym')
147 osutils.SafeMakedirs(os.path.dirname(sym_file))
148 os.rename(temp.name, sym_file)
Mike Frysinger60ec1012013-10-21 00:11:10 -0400149 os.chmod(sym_file, 0o644)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400150 temp.delete = False
151
152 return num_errors.value
153
154
155def GenerateBreakpadSymbols(board, breakpad_dir=None, strip_cfi=False,
Mike Frysingeref9ab2f2013-08-26 22:16:00 -0400156 generate_count=None, sysroot=None,
Shawn Nematbakhsh2c169cb2013-10-29 16:23:58 -0700157 num_processes=None, clean_breakpad=False,
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700158 exclude_dirs=(), file_list=None):
159 """Generate symbols for this board.
160
161 If |file_list| is None, symbols are generated for all executables, otherwise
162 only for the files included in |file_list|.
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400163
164 TODO(build):
165 This should be merged with buildbot_commands.GenerateBreakpadSymbols()
166 once we rewrite cros_generate_breakpad_symbols in python.
167
168 Args:
169 board: The board whose symbols we wish to generate
170 breakpad_dir: The full path to the breakpad directory where symbols live
171 strip_cfi: Do not generate CFI data
172 generate_count: If set, only generate this many symbols (meant for testing)
173 sysroot: The root where to find the corresponding ELFs
Mike Frysingeref9ab2f2013-08-26 22:16:00 -0400174 num_processes: Number of jobs to run in parallel
Mike Frysinger9a628bb2013-10-24 15:51:37 -0400175 clean_breakpad: Should we `rm -rf` the breakpad output dir first; note: we
176 do not do any locking, so do not run more than one in parallel when True
Shawn Nematbakhsh2c169cb2013-10-29 16:23:58 -0700177 exclude_dirs: List of dirs (relative to |sysroot|) to not search
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700178 file_list: Only generate symbols for files in this list. Each file must be a
179 full path (including |sysroot| prefix).
180 TODO(build): Support paths w/o |sysroot|.
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400181 Returns:
182 The number of errors that were encountered.
183 """
184 if breakpad_dir is None:
185 breakpad_dir = FindBreakpadDir(board)
186 if sysroot is None:
187 sysroot = os.path.join('/build', board)
Mike Frysinger9a628bb2013-10-24 15:51:37 -0400188 if clean_breakpad:
189 cros_build_lib.Info('cleaning out %s first', breakpad_dir)
190 osutils.RmDir(breakpad_dir, ignore_missing=True, sudo=True)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400191 # Make sure non-root can write out symbols as needed.
192 osutils.SafeMakedirs(breakpad_dir, sudo=True)
193 if not os.access(breakpad_dir, os.W_OK):
194 cros_build_lib.SudoRunCommand(['chown', '-R', str(os.getuid()),
195 breakpad_dir])
196 debug_dir = FindDebugDir(board)
Shawn Nematbakhsh2c169cb2013-10-29 16:23:58 -0700197 exclude_paths = [os.path.join(debug_dir, x) for x in exclude_dirs]
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700198 if file_list is None:
199 file_list = []
200 file_filter = dict.fromkeys([os.path.normpath(x) for x in file_list], False)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400201
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700202 cros_build_lib.Info('generating breakpad symbols using %s', debug_dir)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400203
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700204 # Let's locate all the debug_files and elfs first along with the debug file
205 # sizes. This way we can start processing the largest files first in parallel
206 # with the small ones.
207 # If |file_list| was given, ignore all other files.
208 targets = []
Shawn Nematbakhsh2c169cb2013-10-29 16:23:58 -0700209 for root, dirs, files in os.walk(debug_dir):
210 if root in exclude_paths:
211 cros_build_lib.Info('Skipping excluded dir %s', root)
212 del dirs[:]
213 continue
214
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400215 for debug_file in files:
216 debug_file = os.path.join(root, debug_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400217 # Turn /build/$BOARD/usr/lib/debug/sbin/foo.debug into
218 # /build/$BOARD/sbin/foo.
219 elf_file = os.path.join(sysroot, debug_file[len(debug_dir) + 1:-6])
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700220
221 if file_filter:
222 if elf_file in file_filter:
223 file_filter[elf_file] = True
224 elif debug_file in file_filter:
225 file_filter[debug_file] = True
226 else:
227 continue
228
229 # Filter out files based on common issues with the debug file.
230 if not debug_file.endswith('.debug'):
231 continue
232
233 elif debug_file.endswith('.ko.debug'):
234 cros_build_lib.Debug('Skipping kernel module %s', debug_file)
235 continue
236
237 elif os.path.islink(debug_file):
238 # The build-id stuff is common enough to filter out by default.
239 if '/.build-id/' in debug_file:
240 msg = cros_build_lib.Debug
241 else:
242 msg = cros_build_lib.Warning
243 msg('Skipping symbolic link %s', debug_file)
244 continue
245
246 # Filter out files based on common issues with the elf file.
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400247 if not os.path.exists(elf_file):
248 # Sometimes we filter out programs from /usr/bin but leave behind
249 # the .debug file.
250 cros_build_lib.Warning('Skipping missing %s', elf_file)
251 continue
252
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700253 targets.append((os.path.getsize(debug_file), elf_file, debug_file))
254
255 bg_errors = multiprocessing.Value('i')
256 if file_filter:
257 files_not_found = [x for x, found in file_filter.iteritems() if not found]
258 bg_errors.value += len(files_not_found)
259 if files_not_found:
260 cros_build_lib.Error('Failed to find requested files: %s',
261 files_not_found)
262
263 # Now start generating symbols for the discovered elfs.
264 with parallel.BackgroundTaskRunner(GenerateBreakpadSymbol,
265 breakpad_dir=breakpad_dir, board=board,
266 strip_cfi=strip_cfi,
267 num_errors=bg_errors,
268 processes=num_processes) as queue:
269 for _, elf_file, debug_file in sorted(targets, reverse=True):
270 if generate_count == 0:
271 break
272
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400273 queue.put([elf_file, debug_file])
274 if generate_count is not None:
275 generate_count -= 1
276 if generate_count == 0:
277 break
278
279 return bg_errors.value
280
281
282def FindDebugDir(board):
283 """Given a |board|, return the path to the split debug dir for it"""
284 return os.path.join('/build', board, 'usr', 'lib', 'debug')
285
286
287def FindBreakpadDir(board):
288 """Given a |board|, return the path to the breakpad dir for it"""
289 return os.path.join(FindDebugDir(board), 'breakpad')
290
291
292def main(argv):
293 parser = commandline.ArgumentParser(description=__doc__)
294
295 parser.add_argument('--board', default=None,
296 help='board to generate symbols for')
297 parser.add_argument('--breakpad_root', type='path', default=None,
298 help='root directory for breakpad symbols')
Shawn Nematbakhsh2c169cb2013-10-29 16:23:58 -0700299 parser.add_argument('--exclude-dir', type=str, action='append',
300 default=[],
301 help='directory (relative to |board| root) to not search')
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400302 parser.add_argument('--generate-count', type=int, default=None,
303 help='only generate # number of symbols')
Mike Frysinger9a628bb2013-10-24 15:51:37 -0400304 parser.add_argument('--noclean', dest='clean', action='store_false',
305 default=True,
306 help='do not clean out breakpad dir before running')
Mike Frysingeref9ab2f2013-08-26 22:16:00 -0400307 parser.add_argument('--jobs', type=int, default=None,
308 help='limit number of parallel jobs')
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400309 parser.add_argument('--strip_cfi', action='store_true', default=False,
310 help='do not generate CFI data (pass -c to dump_syms)')
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700311 parser.add_argument('file_list', nargs='*', default=None,
312 help='generate symbols for only these files '
313 '(e.g. /build/$BOARD/usr/bin/foo)')
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400314
315 opts = parser.parse_args(argv)
316
317 if opts.board is None:
318 cros_build_lib.Die('--board is required')
319
320 ret = GenerateBreakpadSymbols(opts.board, breakpad_dir=opts.breakpad_root,
321 strip_cfi=opts.strip_cfi,
Mike Frysingeref9ab2f2013-08-26 22:16:00 -0400322 generate_count=opts.generate_count,
Mike Frysinger9a628bb2013-10-24 15:51:37 -0400323 num_processes=opts.jobs,
Shawn Nematbakhsh2c169cb2013-10-29 16:23:58 -0700324 clean_breakpad=opts.clean,
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700325 exclude_dirs=opts.exclude_dir,
326 file_list=opts.file_list)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400327 if ret:
328 cros_build_lib.Error('encountered %i problem(s)', ret)
329 # Since exit(status) gets masked, clamp it to 1 so we don't inadvertently
330 # return 0 in case we are a multiple of the mask.
331 ret = 1
332
333 return ret