| #!/usr/bin/env python | |
| # Copyright 2016 The Fuchsia Authors | |
| # | |
| # Use of this source code is governed by a MIT-style | |
| # license that can be found in the LICENSE file or at | |
| # https://opensource.org/licenses/MIT | |
| """ | |
| This tool will symbolize a crash from Zircon's crash logger, adding | |
| function names and, if available, source code locations (filenames and | |
| line numbers from debug info). | |
| Example usage #1: | |
| ./scripts/run-zircon -a x86 | ./scripts/symbolize devmgr.elf --build-dir=build-x86 | |
| Example usage #2: | |
| ./scripts/symbolize devmgr.elf --build-dir=build-x86 | |
| <copy and paste output from Zircon> | |
| Example usage #3 (for zircon kernel output): | |
| ./scripts/symbolize --build-dir=build-x86 | |
| <copy and paste output from Zircon> | |
| """ | |
| import argparse | |
| import errno | |
| import os | |
| import re | |
| import subprocess | |
| import sys | |
| SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__)) | |
| PREBUILTS_BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(SCRIPT_DIR), "prebuilt", | |
| "downloads")) | |
| GCC_VERSION = '6.3.0' | |
| name_to_full_path = {} | |
| debug_mode = False | |
| def find_func(find_args, dirname, names): | |
| if find_args["path"] != "": # found something! | |
| return | |
| if dirname.find("sysroot") != -1: | |
| return | |
| for name in names: | |
| if name == find_args["name"]: | |
| find_args["path"] = dirname | |
| return | |
| def find_file_in_build_dir(name, build_dirs): | |
| find_args = {"name": name, "path": ""} | |
| for location in build_dirs: | |
| os.path.walk(location, find_func, find_args) | |
| if find_args["path"] != "": | |
| return os.path.abspath(os.path.join(find_args["path"], name)) | |
| return None | |
| def buildid_to_full_path(buildid, build_dirs): | |
| for build_dir in build_dirs: | |
| id_file_path = os.path.join(build_dir, "ids.txt") | |
| if os.path.exists(id_file_path): | |
| with open(id_file_path) as id_file: | |
| for line in id_file: | |
| id, path = line.split() | |
| if id == buildid: | |
| return path | |
| return None | |
| def find_file_in_boot_manifest(boot_app_name, build_dirs): | |
| manifest_path = find_file_in_build_dir("bootfs.manifest", build_dirs) | |
| if manifest_path: | |
| with open(manifest_path) as manifest_file: | |
| for line in manifest_file: | |
| out_path, in_path = line.rstrip().split("=") | |
| if out_path == boot_app_name: | |
| if in_path.endswith(".strip"): | |
| in_path = in_path[:-len(".strip")] | |
| return in_path | |
| return None | |
| def find_dso_full_path_uncached(dso, exe_name, name_to_buildid, build_dirs): | |
| if dso in name_to_buildid: | |
| found_path = buildid_to_full_path(name_to_buildid[dso], build_dirs) | |
| if found_path: | |
| return found_path | |
| # This can be a bug in the generation of the ids.txt file, report it. | |
| # It's not necessarily a bug though, so for now only report in debug mode. | |
| if debug_mode: | |
| print "WARNING: Unable to find %s in any ids.txt file." % dso | |
| # The name 'app' indicates the real app name is unknown. | |
| # If the process has a name property that will be printed, but | |
| # it has a max of 32 characters so it may be insufficient. | |
| # Crashlogger prefixes such names with "app:" for our benefit. | |
| if dso == "app" or dso.startswith("app:"): | |
| # If an executable was passed on the command-line, try using that | |
| if exe_name: | |
| found_path = find_file_in_build_dir(exe_name, build_dirs) | |
| if found_path: | |
| return found_path | |
| # If this looks like a program in boot fs, consult the manifest | |
| if dso.startswith("app:/boot/"): | |
| boot_app_name = dso[len("app:/boot/"):] | |
| found_path = find_file_in_boot_manifest(boot_app_name, build_dirs) | |
| if found_path: | |
| return found_path | |
| return None | |
| # First, try an exact match for the filename | |
| found_path = find_file_in_build_dir(dso, build_dirs) | |
| if not found_path: | |
| # If that fails, and this file doesn't end with .so, try the executable | |
| # name | |
| if not dso.endswith(".so"): | |
| found_path = find_file_in_build_dir(exe_name, build_dirs) | |
| if not found_path: | |
| # If that still fails and this looks like an absolute path, try the | |
| # last path component | |
| if dso.startswith("/"): | |
| short_name = dso[dso.rfind("/"):] | |
| found_path = find_file_in_build_dir(short_name, build_dirs) | |
| return found_path | |
| def find_dso_full_path(dso, exe_name, name_to_buildid, build_dirs): | |
| if dso in name_to_full_path: | |
| return name_to_full_path[dso] | |
| found_path = find_dso_full_path_uncached(dso, exe_name, name_to_buildid, build_dirs) | |
| if found_path: | |
| name_to_full_path[dso] = found_path | |
| return found_path | |
| def tool_path(arch, tool): | |
| if sys.platform.startswith("linux"): | |
| platform = "Linux" | |
| elif sys.platform.startswith("darwin"): | |
| platform = "Darwin" | |
| else: | |
| raise Exception("Unsupported platform!") | |
| return ("%s/%s-elf-%s-%s-x86_64/bin/%s-elf-%s" % | |
| (PREBUILTS_BASE_DIR, arch, GCC_VERSION, platform, arch, tool)) | |
| def run_tool(arch, tool, *args): | |
| cmd = [tool_path(arch, tool)] + list(args) | |
| if debug_mode: | |
| print "Running: %s" % " ".join(cmd) | |
| try: | |
| output = subprocess.check_output(cmd) | |
| except Exception as e: | |
| print "Calling %s failed: command %s error %s" % (tool, cmd, e) | |
| return False | |
| return output | |
| # Note: addr2line requires hex addresses. | |
| # |addr_as_hex_string| must already be PIE-adjusted. | |
| def run_addr2line(arch, elf_path, addr_as_hex_string): | |
| return run_tool(arch, "addr2line", "-Cipfe", elf_path, addr_as_hex_string) | |
| def get_call_location(arch, elf_path, addr_as_hex_string): | |
| # Subtract 1 to convert from a return address to a call site | |
| # address. (To be more exact, this converts to an address that | |
| # is within the call site instruction.) This adjustment gives | |
| # more correct results in the presence of inlining and | |
| # 'noreturn' functions. (See ZX-842.) | |
| call_addr = "0x%x" % (int(addr_as_hex_string, 16) - 1) | |
| return run_addr2line(arch, elf_path, call_addr) | |
| # On BSD platforms there are cases where writing to stdout can return EAGAIN. | |
| # In that event, retry the line again. This only manifests itself when piping | |
| # qemu's stdout directly to this script. | |
| def writelines(lines): | |
| for line in lines: | |
| writeline(line) | |
| def writeline(line): | |
| while True: | |
| try: | |
| sys.stdout.write(line) | |
| except IOError as e: | |
| if e.errno == errno.EAGAIN: | |
| continue | |
| break | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description=__doc__, | |
| formatter_class=argparse.RawDescriptionHelpFormatter) | |
| parser.add_argument("--file", "-f", nargs="?", type=argparse.FileType("r"), | |
| default=sys.stdin, | |
| help="File to read from, stdin by default") | |
| parser.add_argument("--build-dir", "-b", nargs="*", | |
| help="List of build directories to search instead of" | |
| " the default Fuchsia build directory" | |
| " (out/<variant>-<arch>)") | |
| parser.add_argument("--disassemble", "-d", action="store_true", | |
| help="Show disassembly of each function") | |
| parser.add_argument("--stack_size", "-s", type=int, | |
| default=256*1024, | |
| help="Change the assumed size of the stack (e.g. use 1048576 for ftl or mtl default thread size") | |
| parser.add_argument("--echo", dest="echo", action="store_true", | |
| help="Echo lines of input (on by default)") | |
| parser.add_argument("--no-echo", dest="echo", action="store_false", | |
| help="Don't echo lines of input") | |
| parser.add_argument("--debug", "-D", action="store_true", | |
| help="Print messages for debugging symbolize") | |
| parser.add_argument("app", nargs="?", help="Name of primary application") | |
| parser.set_defaults(echo=True) | |
| args = parser.parse_args() | |
| global debug_mode | |
| debug_mode = args.debug | |
| zircon_build_dir = os.path.join( | |
| os.path.dirname(SCRIPT_DIR), "build-x86") | |
| if not os.path.exists(zircon_build_dir): | |
| zircon_build_dir = os.path.join( | |
| os.path.dirname(SCRIPT_DIR), os.pardir, "out", "build-zircon", "build-x86") | |
| build_dirs = [zircon_build_dir] | |
| if args.build_dir: | |
| build_dirs = args.build_dir + build_dirs | |
| else: | |
| # Put the unstripped path ahead of the stripped one, we want the | |
| # former searched first. This does mean the unstripped directory | |
| # will get searched twice, but relative to the entire search time, | |
| # the addition is small. | |
| # Plus once a file is found its location is cached. | |
| # Plus this is only used as a fallback in case the file isn't found | |
| # in ids.txt. | |
| fuchsia_build_dir = os.path.abspath(os.path.join( | |
| os.path.dirname(SCRIPT_DIR), os.pardir, "out", "debug-x86-64")) | |
| fuchsia_unstripped_dir = os.path.join(fuchsia_build_dir, | |
| "exe.unstripped") | |
| build_dirs = [fuchsia_unstripped_dir, fuchsia_build_dir] + build_dirs | |
| # Parsing vars | |
| arch = "x86_64" | |
| bias = 0 | |
| name_to_buildid = {} | |
| processed_lines = [] | |
| prev_sp = None | |
| prev_frame_num = None | |
| frame_sizes = [] | |
| total_stack_size = 0 | |
| # If True and we see a dso line, start over collecting the list. | |
| done_dso_list = True | |
| # Regex for parsing | |
| # Either nothing, or something like "[00007.268] 00304.00325> " | |
| full_prefix = "^(|\[\d+\.\d+\] \d+\.\d+> )" | |
| btre = re.compile(full_prefix + "bt#(\d+):") | |
| bt_with_offset_re = re.compile(full_prefix + | |
| "bt#(\d+): pc 0x[0-9a-f]+ sp (0x[0-9a-f]+) \(([^,]+),(0x[0-9a-f]+)\)$") | |
| bt_end_re = re.compile(full_prefix + "bt#(\d+): end") | |
| arch_re = re.compile(full_prefix + "arch: ([\\S]+)$") | |
| build_id_re = re.compile(full_prefix + | |
| "dso: id=([0-9a-z]+) base=(0x[0-9a-f]+) name=(.+)$") | |
| disasm_re = re.compile("^ *(0x[0-9a-f]+)( .+)$") | |
| # Zircon backtraces | |
| zircon_crash_re = re.compile(full_prefix + "ZIRCON KERNEL PANIC$") | |
| # TODO(cja): Add ARM to the regex | |
| zircon_pc_re = re.compile("RIP: (0x[0-9a-z]+)") | |
| zircon_bt_re = re.compile(full_prefix + | |
| "bt#(\d+): (\dx[0-9a-fA-F]+)$") | |
| zircon_elf_path = '' | |
| zircon_bt = False | |
| zircon_pc = '' | |
| while True: | |
| line = args.file.readline() | |
| if args.echo and not args.file.isatty(): | |
| writeline(line) | |
| end_of_file = (line == '') | |
| # Strip any trailing carriage return character ("\r"), since | |
| # these appear in the serial console output from QEMU. | |
| line = line.rstrip() | |
| if bt_end_re.match(line) or end_of_file: | |
| if len(processed_lines) != 0: | |
| writeline("\nstart of symbolized stack:\n") | |
| writelines(processed_lines) | |
| writeline("end of symbolized stack\n") | |
| if total_stack_size > args.stack_size - 8*1024: | |
| if total_stack_size >= args.stack_size: | |
| message = "Overflowed stack" | |
| else: | |
| message = "Potentially overflowed stack" | |
| writeline("WARNING: %s (total usage: %d, stack size: %d)\n" % (message, total_stack_size, args.stack_size)) | |
| for frame, size in frame_sizes: | |
| writeline("#%s: %d bytes\n" % (frame, size)) | |
| # Since we don't have the last stack frame, leave some leeway | |
| # when warning about stackoverflow | |
| processed_lines = [] | |
| frames_sizes = [] | |
| total_stack_size = 0 | |
| prev_sp = None | |
| zircon_bt = False | |
| if end_of_file: | |
| break | |
| else: | |
| continue | |
| m = arch_re.match(line) | |
| if m: | |
| arch = m.group(2) | |
| continue | |
| m = build_id_re.match(line) | |
| if m: | |
| if done_dso_list: | |
| name_to_buildid = {} | |
| done_dso_list = False | |
| buildid = m.group(2) | |
| bias = int(m.group(3), 16) | |
| name = m.group(4) | |
| name_to_buildid[name] = buildid | |
| continue | |
| # We didn't see a dso line, so we're done with this list. | |
| # The next time we see one means we're starting a new list. | |
| done_dso_list = True | |
| m = btre.match(line) | |
| if m and not zircon_bt: | |
| frame_num = m.group(2) | |
| m = bt_with_offset_re.match(line) | |
| if m: | |
| sp = int(m.group(3), 16) | |
| if prev_sp is not None: | |
| frame_size = sp - prev_sp | |
| total_stack_size += frame_size | |
| frame_sizes.append((prev_frame_num, frame_size)) | |
| prev_sp = sp | |
| prev_frame_num = frame_num | |
| dso = m.group(4) | |
| off = m.group(5) | |
| dso_full_path = find_dso_full_path( | |
| dso, args.app, name_to_buildid, build_dirs) | |
| if dso_full_path: | |
| call_loc = get_call_location(arch, dso_full_path, off) | |
| if call_loc: | |
| processed_lines.append( | |
| "#%s: %s" % (frame_num, call_loc)) | |
| if args.disassemble: | |
| pc = int(off, 16) | |
| disassembly = run_tool( | |
| arch, "gdb", "--nx", "--batch", "-ex", | |
| "disassemble %#x" % pc, dso_full_path) | |
| if disassembly: | |
| for line in disassembly.splitlines(): | |
| m = disasm_re.match(line) | |
| if m: | |
| timestamp, addr, rest = m.groups() | |
| addr = int(addr, 16) | |
| if addr == pc: | |
| prefix = "=> " | |
| else: | |
| prefix = " " | |
| line = "%s%#.16x%s" % (prefix, bias + addr, rest) | |
| processed_lines.append(line + "\n") | |
| continue | |
| else: | |
| # can't find dso_full_path | |
| processed_lines.append("#%s: unknown, can't find full path for %s\n" % (frame_num, dso)) | |
| else: | |
| # bt_with_offset_re failed | |
| processed_lines.append("#%s: unknown, can't find pc, sp or app/library in line\n" % (frame_num,)) | |
| # Zircon Specific Handling | |
| if zircon_crash_re.search(line): | |
| zircon_elf_path = find_file_in_build_dir("zircon.elf", build_dirs) | |
| if zircon_elf_path: | |
| zircon_bt = True | |
| else: | |
| sys.stderr.write("Symbolize could not find the zircon elf binary. Perhaps you need to build " | |
| "zircon or specify the build directory with -b?\n") | |
| continue | |
| m = zircon_pc_re.search(line) | |
| if m: | |
| zircon_pc = m.group(1) | |
| continue | |
| m = zircon_bt_re.match(line) | |
| if m and zircon_bt: | |
| frame_num = m.group(2) | |
| addr = m.group(3) | |
| # If we saw the instruction pointer for the fault/panic then use it once | |
| if zircon_pc: | |
| prefix = " pc: %s => " % zircon_pc | |
| a2l_out = run_addr2line(arch, zircon_elf_path, zircon_pc) | |
| processed_lines.append(prefix + | |
| a2l_out.replace("(inlined", (" " * len(prefix)) + "(inlined")) | |
| zircon_pc = None | |
| prefix = "bt#%s: %s => " % (frame_num, addr) | |
| call_loc = get_call_location(arch, zircon_elf_path, addr) | |
| # In the case of inlined methods, it is more readable if the | |
| # inlined lines are aligned to be to the right of "=>". | |
| processed_lines.append(prefix + | |
| call_loc.replace("(inlined", (" " * len(prefix)) + "(inlined")) | |
| if __name__ == '__main__': | |
| sys.exit(main()) |