#!/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" || 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] []" 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 "$@"