Skip to content

Instantly share code, notes, and snippets.

@toonetown
Last active August 8, 2022 22:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save toonetown/618507f37da728bbfe3f3e3490c8a550 to your computer and use it in GitHub Desktop.
Save toonetown/618507f37da728bbfe3f3e3490c8a550 to your computer and use it in GitHub Desktop.
A script which will help decode macOS dumps, logs, and panics
#!/bin/bash
###########
# A script which will help to decode crashes, spindumps, and samples
###########
# Helper functions
function do_jq {
_J="$(which jq 2>/dev/null)" || {
echo "You must install jq (brew install jq) to use the script" >&2; return 1;
}; "${_J}" "$@"
}
function do_ips2crash {
_I="$(which ips2crash 2>/dev/null)" || {
echo "You must install ips2crash (brew install toonetown/extras/ips2crash --head) to use the script" >&2; return 1;
}; "${_I}" "$@"
}
function make_absolute { cd "$(dirname "${1}")"; echo "$(pwd)/$(basename "${1}")"; }
function strip_trailing_slash { echo "${1}" | sed -e 's/\/$//g'; }
function exit_usage { cat << EOF
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! ${1}
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Usage: ${0} <TARGET_FILE> <PATH_TO_BINARY|--extract-only> [PATH_TO_KDK]
Note: You must place the dSYM file in the same directory as the binary file is located
If you pass \`--extract-only\` to a kernel panic, it will decode the panic data and output it instead.
When doing extraction-only, the path is not needed.
The path to the KDK is only needed for decoding kernel panics. KDKs are downloaded from
https://developer.apple.com/downloads/ and when installed are placed locally in /Library/Developer/KDKs.
If you do not specify a path to the KDK, the script will attempt to locate the KDK corresponding to the
macOS version in the target file.
Environment Variables:
XCODE Set to the path to Xcode for use in decoding kernel panics. Defaults to /Applications/Xcode-10.3.app
ATOS Set to the path to atos for use in decoding userspace panics. Defaults to the version in your PATH
EOF
exit 1
}
# Check our target file
TARGET_FILE="$(make_absolute "${1}")"
[ -f "${TARGET_FILE}" ] || { exit_usage "Missing target file ${1}"; }
# Check the binary file
if [ "${2}" == "--extract-only" ]; then
EXTRACT_ONLY="yes"
else
BINARY_PATH="$(strip_trailing_slash "$(make_absolute "${2}")")"
BINARY_PLIST="${BINARY_PATH}/Contents/Info.plist"
if [ -f "${BINARY_PLIST}" ]; then
BINARY_ID="$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${BINARY_PLIST}" 2>/dev/null)"
BINARY_VER="$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "${BINARY_PLIST}" 2>/dev/null)"
BINARY_EXE="$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "${BINARY_PLIST}" 2>/dev/null)"
BINARY_TYPE="$(/usr/libexec/PlistBuddy -c "Print :CFBundlePackageType" "${BINARY_PLIST}" 2>/dev/null)"
BINARY_BIN="${BINARY_PATH}/Contents/MacOS/${BINARY_EXE}"
else
BINARY_ID="$(basename "${BINARY_PATH}")"
BINARY_VER="0"
BINARY_EXE="${BINARY_ID}"
BINARY_TYPE="exe"
BINARY_BIN="${BINARY_PATH}"
fi
[ -f "${BINARY_BIN}" ] || { exit_usage "Invalid or missing binary path ${2}"; }
# Check the dSYM
DSYM_PATH="${BINARY_PATH}.dSYM"
[ -d "${DSYM_PATH}" ] || { exit_usage "Missing dSYM at ${DSYM_PATH}"; }
EXTRACT_ONLY="no"
fi
# Check if we have data to extract
CRASH_DATA="$(cat "${TARGET_FILE}")"
echo "${CRASH_DATA}" | grep -q macOSPanicString && {
CRASH_DATA="$(echo "${CRASH_DATA}" | grep macOSPanicString | do_jq -r .macOSPanicString)"
BINARY_TYPE="KEXT"
OS_VER="$(echo "${CRASH_DATA}" | grep -a1 'Mac OS version:' 2>/dev/null | tail -n1)"
[ -n "${OS_VER}" ] || { exit_usage "Invalid panic file ${1}"; }
}
echo "${CRASH_DATA}" | do_jq -r '.incident | select(.)' 2>/dev/null | grep -q '.' && {
CRASH_DATA="$(do_ips2crash "${TARGET_FILE}")"
}
if [ "${EXTRACT_ONLY}" == "yes" ]; then
echo "${CRASH_DATA}"
exit 0
fi
# Check if we are a KEXT or a regular bundle
if [ "${BINARY_TYPE}" == "KEXT" ]; then
OS_VER="$(echo "${CRASH_DATA}" | grep -a1 'Mac OS version:' 2>/dev/null | tail -n1)"
[ -n "${OS_VER}" ] || { exit_usage "Invalid panic file ${1}"; }
if [ "${EXTRACT_ONLY}" == "yes" ]; then
echo "${CRASH_DATA}"
exit 0
fi
# Point to the right xcode
: ${XCODE:="/Applications/Xcode-10.3.app"}
LLDB="${XCODE}/Contents/Developer/usr/bin/lldb"
[ -x "${LLDB}" ] || { exit_usage "Missing Xcode version 10.3 - set XCODE environment variable"; }
# Check the provided KDK (or find one)
if [ -d "${3}" ]; then
KDK_PATH="$(strip_trailing_slash "$(make_absolute "${3}")")"
else
KDK_PATH="$(ls -d "/Library/Developer/KDKs/"*"_${OS_VER}.kdk" 2>/dev/null)"
[ -d "${KDK_PATH}" ] || { exit_usage "Could not find KDK for OS Version ${OS_VER}"; }
fi
KDK_KERN="${KDK_PATH}/System/Library/Kernels/kernel"
KDK_SCRIPT="${KDK_KERN}.dSYM/Contents/Resources/Python/kernel.py"
[ -f "${KDK_KERN}" -a -f "${KDK_SCRIPT}" ] || { exit_usage "Invalid or missing KDK path ${3}"; }
LOAD_ADDR="$(echo "${CRASH_DATA}" | sed -nE "s/^ +${BINARY_ID}\(${BINARY_VER}\).*@(0x[a-f0-9]+)->0x[a-f0-9]+$/\1/p")"
[ -n "${LOAD_ADDR}" ] || { exit_usage "Could not find ${BINARY_ID}(${BINARY_VER}) load address in ${TARGET_FILE}"; }
echo "========================="
echo "Symbolicate: ${TARGET_FILE}"
echo " with: ${KDK_PATH}"
echo " image: ${BINARY_PATH}"
echo " symbols: ${DSYM_PATH}"
echo "-------------------------"
COMMANDS=()
COMMANDS+=("command script import \"${KDK_SCRIPT}\"")
COMMANDS+=("settings set target.load-script-from-symbol-file true")
COMMANDS+=("addkext -F \"${BINARY_BIN}\" ${LOAD_ADDR}")
for i in $(echo "${CRASH_DATA}" | sed -nE 's/^0x[a-f0-9]+ : (0x[a-f0-9]+).*$/\1/p'); do
COMMANDS+=("image lookup -a ${i}")
done
for i in "${COMMANDS[@]}"; do echo "${i}"; done | PATH="/usr/bin:${PATH}" "${LLDB}" "${KDK_KERN}"
else
# This is a userspace app we want to symbolicate
LOAD_ADDR="$(echo "${CRASH_DATA}" | \
sed -nE "s/^ +(0x[0-9a-f]+) - +0x[0-9a-f]+ +\+?(${BINARY_ID}|${BINARY_EXE}).*[ \(](${BINARY_VER}|0)\).*$/\1/p")"
[ -n "${LOAD_ADDR}" ] || { exit_usage "Could not find ${BINARY_ID}(${BINARY_VER}) load address in ${TARGET_FILE}"; }
: ${ATOS:="$(which atos)"}
[ -x "${ATOS}" ] || { exit_usage "Missing atos - set ATOS environment variable"; }
# Parse crash dump first
ADDRS=(); MATCH=""
for i in $(echo "${CRASH_DATA}" | \
sed -nE "s/^[0-9]+[[:space:]]+(${BINARY_ID}|${BINARY_EXE})[[:space:]]+(0x[0-9a-f]+) ${LOAD_ADDR} \+ .*$/\2/p" | \
sort -u); do
ADDRS+=("${i}"); MATCH="([0-9]+[[:space:]]+(${BINARY_ID}|${BINARY_EXE})[[:space:]]+)"
done
# Parse sample
[ -n "${MATCH}" ] || {
for i in $(echo "${CRASH_DATA}" | \
sed -nE "s/^.*\?\?\? +\(in ${BINARY_ID}\) +load address ${LOAD_ADDR} \+ .*\[(0x[a-f0-9]+)\].*$/\1/p" | \
sort -u); do
ADDRS+=("${i}"); MATCH="(.*)\?\?\? +\(in ${BINARY_ID}\) +load address ${LOAD_ADDR} \+ .*\["
done
}
# Parse spindump
[ -n "${MATCH}" ] || {
for i in $(echo "${CRASH_DATA}" | \
sed -nE "s/^.*\?\?\? +\(${BINARY_ID} \+.*\[(0x[a-f0-9]+)\].*$/\1/p" | \
sort -u); do
ADDRS+=("${i}"); MATCH="(.*)\?\?\? +\(${BINARY_ID} \+.*\["
done
}
[ -n "${MATCH}" ] || { exit_usage "Invalid target file ${TARGET_FILE}"; }
# Symbolicate
echo "========================="
echo "Symbolicate: ${TARGET_FILE}"
echo " image: ${BINARY_PATH}"
echo " symbols: ${DSYM_PATH}"
echo "-------------------------"
TMPFILE="${TMPDIR:-/tmp}/sym.$$"
trap 'rm -f "${TMPFILE}"' EXIT
while read -r LINE; do
A="${ADDRS[$(($(echo "${LINE}" | cut -d':' -f1)-1))]}"
L="$(echo "${LINE}" | cut -d':' -f2-)"
echo "s/^${MATCH}${A}.*$/\1${L}/g" | sed -e 's/&/\\\&/g' >> ${TMPFILE}
done << EOF
$(${ATOS} -o "${BINARY_BIN}" -l ${LOAD_ADDR} ${ADDRS[@]} | grep -n '.')
EOF
echo "${CRASH_DATA}" | sed -f "${TMPFILE}" -E
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment