blob: 444adff014aef46a93bd7bac31f77f1111cc5fa4 [file] [log] [blame]
#!/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 "$@"
# 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"
sudo 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
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 "$@"