| #!/bin/bash |
| # Copyright (c) 2011 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| # Script to generate minidump symbols in the format required by |
| # minidump_stackwalk to dump stack information. |
| # |
| # NOTE: This script must be run from the chromeos build chroot environment. |
| # |
| |
| SCRIPT_ROOT=$(dirname $(readlink -f "$0")) |
| . "${SCRIPT_ROOT}/common.sh" || { echo "Unable to load common.sh"; exit 1; } |
| |
| # Script must be run inside the chroot |
| restart_in_chroot_if_needed "$@" |
| |
| get_default_board |
| |
| # Flags |
| DEFINE_string board "$DEFAULT_BOARD" "The board to build packages for." |
| DEFINE_string minidump_symbol_root "" \ |
| "Symbol root (defaults to /usr/lib/debug/breakpad for board)" |
| DEFINE_boolean verbose ${FLAGS_FALSE} "Be verbose." |
| |
| DUMP_SYMS="dump_syms" |
| DUMP_SYMS32="dump_syms.32" |
| |
| ERROR_COUNT=0 |
| |
| debug() { |
| if [ ${FLAGS_verbose} -eq ${FLAGS_TRUE} ]; then |
| info "$@" |
| fi |
| } |
| |
| # Each job sets this on their own; we declare it |
| # globally so the exit trap can always see the last |
| # setting of it w/in a job worker. |
| SYM_FILE= |
| JOB_FILE= |
| NOTIFIED= |
| |
| # The master process sets these up, which each worker |
| # than uses for communicating once they've finished. |
| CONTROL_PIPE= |
| CONTROL_PIPE_FD= |
| |
| _worker_finished() { |
| if [ -z "${NOTIFIED}" ]; then |
| debug "Sending notification of $BASHPID ${1-1}" |
| echo "$BASHPID ${1-1}" > /dev/fd/${CONTROL_PIPE_FD} |
| NOTIFIED=1 |
| fi |
| } |
| |
| _cleanup_worker() { |
| rm -f "${SYM_FILE}" "${ERR_FILE}" |
| _worker_finished 1 |
| } |
| |
| _cleanup_master() { |
| set +eu |
| rm -f "${CONTROL_PIPE}" |
| if [ ${#JOBS_ARRAY[@]} != 0 ]; then |
| kill -s SIGINT "${!JOBS_ARRAY[@]}" &> /dev/null |
| wait |
| # Clear the array. |
| JOBS_ARRAY=( ) |
| fi |
| } |
| |
| declare -A JOBS_ARRAY |
| |
| finish_job() { |
| local finished result |
| read -r -u ${CONTROL_PIPE_FD} finished result |
| # Bash doesn't allow for zombies, but tell it to clean up its intenral |
| # bookkeeping. Note bash can be buggy here- if a new process has slipped |
| # into that pid, bash doesn't use its internal accounting first, and |
| # can throw an error; doesn't matter, thus this form. |
| ! wait ${finished} &> /dev/null |
| if [ "${result-1}" -ne "0" ]; then |
| : $(( ++ERROR_COUNT )) |
| fi |
| # Bit of a hack, but it works well enough. |
| debug "finished ${finished} with result ${result-1}" |
| unset JOBS_ARRAY[${finished}] |
| } |
| |
| run_job() { |
| local debug_file=${1} text_file=${2} newpid |
| |
| if [ ${#JOBS_ARRAY[@]} -ge ${NUM_JOBS} ]; then |
| # Reclaim a spot. |
| finish_job |
| fi |
| |
| dump_file "${debug_file}" "${text_file}" & |
| newpid=$! |
| debug "Started ${debug_file} ${text_file} at ${newpid}" |
| JOBS_ARRAY[$newpid]=1 |
| } |
| |
| # Given path to a debug file, return its text file |
| get_text_for_debug() { |
| local debug_file=$1 |
| local text_dir=$(dirname "${debug_file#$DEBUG_ROOT}") |
| local text_path=${SYSROOT}${text_dir}/$(basename "${debug_file}" .debug) |
| echo ${text_path} |
| } |
| |
| # Given path to a text file, return its debug file |
| get_debug_for_text() { |
| local text_file=$1 |
| local text_path=${text_file#${SYSROOT}} |
| local debug_path=${DEBUG_ROOT}${text_path}.debug |
| echo ${debug_path} |
| } |
| |
| # Returns true if the file given is a 32-bit ELF file. |
| is_32b_elf() { |
| local elf="$1" |
| file "${elf}" | grep -q "ELF 32-bit" |
| } |
| |
| # Dump given debug and text file. Returns 1 if any errors, even |
| # if they can be ignored, but only sets ERROR_COUNT if the error should not |
| # be ignored (and we should not proceed to upload). |
| dump_file() { |
| trap '_cleanup_worker; exit 1' INT TERM |
| trap _cleanup_worker EXIT |
| local debug_file="$1" |
| local text_file="$2" |
| local debug_directory="$(dirname "${debug_file}")" |
| local dump_syms_prog="${DUMP_SYMS}" |
| # 32-bit dump_syms must be used to dump a 32-bit ELF file |
| if is_32b_elf "${text_file}"; then |
| dump_syms_prog="${DUMP_SYMS32}" |
| debug "Using ${dump_syms_prog} for 32-bit file ${text_file}" |
| fi |
| SYM_FILE=$(mktemp -t "breakpad.sym.XXXXXX") |
| # Dump symbols as root in order to read all files. |
| if ! sudo "${dump_syms_prog}" "${text_file}" "${debug_directory}" \ |
| > "${SYM_FILE}" 2> /dev/null; then |
| # Try dumping just the main file to get public-only symbols. |
| ERR_FILE=$(mktemp -t "breakpad.err.XXXXXX") |
| if ! sudo "${dump_syms_prog}" "${text_file}" > "${SYM_FILE}" \ |
| 2> "${ERR_FILE}"; then |
| # A lot of files (like kernel files) contain no debug information, do |
| # not consider such occurrences as errors. |
| if grep -q "file contains no debugging information" "${ERR_FILE}"; then |
| warn "No symbols found for ${text_file}" |
| _worker_finished 0 |
| exit 0 |
| fi |
| error "Unable to dump symbols for ${text_file}:" |
| error "$(<"${ERR_FILE}")" |
| exit 1 |
| else |
| warn "File ${text_file} did not have debug info, using linkage symbols" |
| fi |
| fi |
| local file_id=$(head -1 ${SYM_FILE} | cut -d' ' -f4) |
| local module_name=$(head -1 ${SYM_FILE} | cut -d' ' -f5) |
| # Show file upload success and symbol info for easier lookup |
| debug "Dumped symbols from ${text_file} for ${module_name}|${file_id}." |
| # Sanity check: if we've created the same named file in the /usr/lib/debug |
| # directory during the src_compile stage of an ebuild, verify our sym file |
| # is the same. |
| local installed_sym="${DEBUG_ROOT}"/$(basename "${text_file}").sym |
| if [ -e "${installed_sym}" ]; then |
| if ! cmp --quiet "${installed_sym}" "${SYM_FILE}"; then |
| error "${installed_sym} differ from current sym file:" |
| error "$(diff "${installed_sym}" "${SYM_FILE}")" |
| : $(( ++ERROR_COUNT )) |
| exit 1 |
| fi |
| fi |
| |
| local container_dir="${FLAGS_minidump_symbol_root}/${module_name}/${file_id}" |
| sudo mkdir -p "${container_dir}" |
| sudo mv "${SYM_FILE}" "${container_dir}/${module_name}.sym" |
| _worker_finished 0 |
| exit 0 |
| } |
| |
| # Convert the given debug file. No return value. |
| process_file() { |
| local debug_file="$1" |
| local text_file="$(get_text_for_debug ${debug_file})" |
| if [ -h "${debug_file}" ]; then |
| # Don't follow symbolic links. In particular, we don't want to bother |
| # with the *.debug links in the "debug/.build-id/" directory. |
| debug "Skipping symbolic link: ${debug_file}" |
| return 0 |
| fi |
| if [ "${text_file##*.}" == "ko" ]; then |
| # Skip kernel objects. We can't use their symbols and they sometimes |
| # have objects with empty text sections which trigger errors in dump_sym. |
| debug "Skipping kernel object: ${text_file}" |
| return 0 |
| fi |
| if [ ! -f "${text_file}" ]; then |
| # Allow files to not exist, for instance if they are in the INSTALL_MASK. |
| warn "Binary does not exist: ${text_file}" |
| return 0 |
| elif [ ! -r "${text_file}" ]; then |
| # Allow files to not be readable, for instance setuid programs like passwd |
| warn "Binary is not user readable: ${text_file}" |
| return 0 |
| fi |
| |
| run_job "${debug_file}" "${text_file}" |
| } |
| |
| main() { |
| |
| # Parse command line |
| FLAGS_HELP="usage: $0 [flags] [<files...>]" |
| FLAGS "$@" || exit 1 |
| eval set -- "${FLAGS_ARGV}" |
| |
| switch_to_strict_mode |
| |
| [ -n "$FLAGS_board" ] || die_notrace "--board is required." |
| |
| SYSROOT="/build/${FLAGS_board}" |
| |
| if [[ -z "${FLAGS_minidump_symbol_root}" ]]; then |
| FLAGS_minidump_symbol_root="${SYSROOT}/usr/lib/debug/breakpad" |
| fi |
| |
| info "Writing minidump symbols to ${FLAGS_minidump_symbol_root}" |
| |
| DEBUG_ROOT="${SYSROOT}/usr/lib/debug" |
| sudo rm -rf "${FLAGS_minidump_symbol_root}" |
| |
| # Open our control pipe. |
| trap '_cleanup_master; exit 1' INT TERM |
| trap _cleanup_master EXIT |
| CONTROL_PIPE=$(mktemp -t "breakpad.fifo.XXXXXX") |
| rm "${CONTROL_PIPE}" |
| mkfifo "${CONTROL_PIPE}" |
| exec {CONTROL_PIPE_FD}<>${CONTROL_PIPE} |
| |
| # We require our stderr (which error/info/warn go through) to be a |
| # pipe for atomic write reasons; thus if it isn't, abuse cat to make it |
| # so. |
| if [ ! -p /dev/stderr ]; then |
| debug "Replacing stderr with a cat process for pipe requirements..." |
| exec 2> >(cat 1>&2) |
| fi |
| |
| if [ -z "${FLAGS_ARGV}" ]; then |
| # Sort on size; we want to start the largest first since it's likely going |
| # to be the chrome binary (which can take 98% of the runtime when we're |
| # running with parallelization for 6 or higher). |
| for debug_file in $(find "${DEBUG_ROOT}" -name \*.debug \ |
| -type f -exec stat -c '%s %n' {} + | sort -gr | cut -d' ' -f2-); do |
| process_file "${debug_file}" |
| done |
| else |
| for either_file in ${FLAGS_ARGV}; do |
| either_file=${either_file#\'} |
| either_file=${either_file%\'} |
| if [ ! -h "${either_file}" -a ! -f "${either_file}" ]; then |
| error "Specified file ${either_file} does not exist" |
| : $(( ++ERROR_COUNT )) |
| continue |
| fi |
| if [ "${either_file##*.}" == "debug" ]; then |
| debug_file="${either_file}" |
| else |
| debug_file="$(get_debug_for_text ${either_file})" |
| fi |
| process_file "${debug_file}" |
| done |
| fi |
| |
| while [[ ${#JOBS_ARRAY[@]} != 0 ]]; do |
| finish_job |
| done |
| |
| local size=$(sudo find "${FLAGS_minidump_symbol_root}" \ |
| -type f -name '*.sym' -exec du -b {} + | \ |
| awk '{t += $1} END {print t}') |
| info "Generated ${size:-0}B of unique debug information" |
| |
| if [[ ${ERROR_COUNT} == 0 ]]; then |
| return 0 |
| fi |
| die_notrace "Encountered ${ERROR_COUNT} problems" |
| } |
| |
| main "$@" |