Demystifying Unity iOS Crash Logs
Someone who never ported Unity game to mobile (iOS / Android) can tell that it’s pretty easy. If you think so, you are very wrong. When running i.e. WebPlayer version of your game it allows you throwing exceptions that are not caught and sometimes the game runs without any crash, only in the log file you can read note about it. Mobile version is less forgiving. Unhandled exception will crash your app.
Reading iOS crash logs helps a lot while debugging, but if they are still mystery for you, you can read great article about them here: Demystifying iOS Application Crash Logs. The problem is, that sometimes Unity creates crash on your iOS device that will not tell you much:
... 6 mygame 0x002328f4 ___lldb_unnamed_function7016$$mygame + 268 7 mygame 0x0122597c ___lldb_unnamed_function93440$$mygame + 200 8 mygame 0x01964224 ___lldb_unnamed_function136404$$mygame + 2152 9 mygame 0x01a06274 ___lldb_unnamed_function138276$$mygame + 132 10 mygame 0x015a08cc MonoBehaviour::InvokeMethodOrCoroutineChecked(ScriptingMethod*, MonoObject*, MonoException**) (MonoUtility.h:452) 11 mygame 0x015a0918 MonoBehaviour::InvokeMethodOrCoroutineChecked(ScriptingMethod*, MonoObject*) (MonoBehaviour.cpp:975) ...
It will tell you nothing because of ___lldb_unnamed_function (sometimes there is only memory address if automatic symbolication went totally crazy). Instead of this unnamed function you would like to see name of your method in your C# scripts. The problem is wrong calculation of method address by default Xcode symbolication tool (more details about these calculations you can find here). To ease you life I created a script that will symbolicate crash log for you. All you need is the archive from which you created your binary, name of the application and crash log itself.
Usage is as follows:
symbolicate_crash.sh <archive> <app_name> <crash_log> [<output_file>]
<output_file> is optional, if you will not specify it, script will symbolicate crash log in file with the name same as the crash log, but with additional postfix. Instead of <crash_log> you can put directory that contains crashes and all of them will be symbolicated one after another.
Here is the usage example:
symbolicate_crash.sh "Unity-iPhone 3-20-14 4.17 PM.xcarchive/" mygame "mygame 3-20-14 5-11 PM.crash"
Now the crash log will look more user friendly:
... 6 mygame 0x002328f4 m_Facebook_IOSFacebook_OnRequestComplete_string (in mygame) + 268 7 mygame 0x0122597c m_wrapper_runtime_invoke_object_runtime_invoke_dynamic_intptr_intptr_intptr_intptr (in mygame) + 200 8 mygame 0x01964224 mono_jit_runtime_invoke (in mygame) + 2152 9 mygame 0x01a06274 mono_runtime_invoke (in mygame) + 132 10 mygame 0x015a08cc MonoBehaviour::InvokeMethodOrCoroutineChecked(ScriptingMethod*, MonoObject*, MonoException**) (in mygame) (MonoUtility.h:452) 11 mygame 0x015a0918 MonoBehaviour::InvokeMethodOrCoroutineChecked(ScriptingMethod*, MonoObject*) (in mygame) (MonoBehaviour.cpp:975) ...
I have a dream that one day I rewrite this script in some more readable form switching to python maybe, but for now it should solve at least some of your Unity iOS crash log problems.
Happy bugs hunting!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
function usage() | |
{ | |
echo "" | |
echo "Usage:" | |
echo "$0 <archive> <app_name> <crashlog> [<output_file>]" | |
echo "or" | |
echo "$0 <archive> <app_name> <crashlog_dir> [<output_dir>]" | |
echo "" | |
} | |
function symbolicate() | |
{ | |
crash_log_local=$1 | |
output_file_local=$2 | |
if [ -e "${output_file_local}" ]; then | |
echo "already symbolicated: ${crash_log_local}" | |
return | |
fi | |
echo "processing crash log: ${crash_log_local}" | |
exec 2<&– | |
unnamed_functions=$(${symbolicatecrash_app} "${crash_log_local}" "${dsym}") | |
load_address_line=$(echo -e "${unnamed_functions}" | grep -A1 "Binary Images:" | grep -v "Binary Images:") | |
load_address=$(echo $load_address_line | awk '{print $1;}') | |
slide_output=$(otool -arch ${architecture} -l "${app}" | grep -B 3 -A 8 -m 2 "__TEXT" | grep vmaddr) | |
search_string="* ${app_name} *" | |
all_lines=$(cat "${crash_log_local}" | wc -l | sed -e 's/^ *//' -e 's/ *$//') | |
counter=0 | |
echo -n "" > ${output_file_local} | |
symbolicated_string="" | |
while read -r line | |
do | |
counter=$((counter + 1)) | |
pct=$(bc <<< "scale=2; (100*$counter/$all_lines)") | |
echo -ne \\r"processing line: ${counter}/${all_lines} (${pct}%)" | |
if [[ "$line" == $search_string ]] | |
then | |
IFS=', ' read -a array <<< "$line" | |
IFS=', ' read -a slide_array <<< "$slide_output" | |
slide=${slide_array[1]} | |
stack_address=${array[2]} | |
let symbol_address="${slide}+${stack_address}–${load_address}" | |
final=$(printf "%x\n" ${symbol_address}) | |
named_function=$(atos -arch ${architecture} -o "${app}" ${final} | tail -1) | |
new_line=$(fill_spaces "${array[0]}" 4) | |
new_line="${new_line}"$(fill_spaces "${array[1]}" 30) | |
new_line="${new_line}"$(printf \\t) | |
new_line="${new_line}"$(fill_spaces "$stack_address" 11) | |
symbolicated_string="${new_line}"" ${named_function}" | |
else | |
symbolicated_string="${line}" | |
fi | |
echo "$symbolicated_string" >> "${output_file_local}" | |
done < <(echo -e "$unnamed_functions") | |
echo "" | |
exec 2>&1 | |
} | |
if [ "$#" -ne 4 ] && [ "$#" -ne 3 ]; then | |
usage | |
exit 0 | |
fi | |
function fill_spaces() | |
{ | |
word=$1 | |
max_length=$2 | |
word_length=${#word} | |
let spaces_count="${max_length}–${word_length}" | |
printf ${word} | |
awk "BEGIN{for(c=0;c<${spaces_count};c++) printf \" \";}" | |
} | |
export -f fill_spaces | |
export DEVELOPER_DIR=$(xcode-select -print-path) | |
export symbolicatecrash_app=$(find $DEVELOPER_DIR -name symbolicatecrash -type f) | |
export architecture="armv7" | |
output_postfix="_symbolicated.txt" | |
if [ "$#" -eq 4 ]; then | |
archive="${1}" | |
app_name="${2}" | |
crash_log="${3}" | |
output_file="${4}" | |
elif [ "$#" -eq 3 ]; then | |
archive="${1}" | |
app_name="${2}" | |
crash_log="${3}" | |
if [ -d "$crash_log" ]; then | |
output_file="${3}" | |
elif [ -f "$crash_log" ]; then | |
output_file="${3}${output_postfix}" | |
fi | |
fi | |
app="${archive}/Products/Applications/${app_name}.app/${app_name}" | |
dsym="${archive}/dSYMs/${app_name}.app.dSYM" | |
if [ -d "$crash_log" ] && [ -d "$output_file" ]; then | |
for i in $crash_log/*.crash; do | |
if [ -f "$i" ]; then | |
symbolicate "$i" "${i}${output_postfix}" | |
fi | |
done | |
elif [ -f "$crash_log" ]; then | |
symbolicate "$crash_log" "$output_file" | |
else | |
usage | |
exit 0 | |
fi | |
echo "Done" |
Zbyhoo, this script looks like it could be a lifesaver as we have a live product experiencing crashes, but I’m having some trouble getting it to work for us. See my post here and any thoughts on how I might be able to use it would be greatly appreciated:
http://forum.unity3d.com/threads/unity-and-ios-crash-logs.221865/
I’ve made a mistake, correct usage is the one in example, and should be:
symbolicate_crash.sh archive app_name crashlog
Already fixed, thanks!
If you replace some of the script with the following, then you shouldn’t need to hard code anything like it is now!
export xcodepath=$(xcode-select -print-path)
export symbolicatecrash_app=$(find $xcodepath -name symbolicatecrash -type f)
export DEVELOPER_DIR=`xcode-select -print-path`
The replacement for the symbolicatecrash comes from the following url: https://github.com/robovm/robovm/wiki/Symbolicate-a-crash-reports and seems to work just fine, as long as the following command returns something:
find `xcode-select -print-path` -name symbolicatecrash -type f
I’ve had a couple of machines where Xcode 6 has been installed, but it doesn’t have the symbolicatecrash tool anywhere in Xcode. I have other machines where it works just fine. It’s usually the older machines where multiple versions of Xcode have been installed before Xcode 6. The ones that usually don’t return anything have only had Xcode 6 installed.
The DEVELOPER_DIR is from the same article and I’ve not had that work on any machine, so it should be really solid!
Thanks for sharing your script! It’s really awesome! Hopefully, this will make it more resilient for future changes from Apple!
Thanks again!
Mike
Thanks a lot for your improvements! I already applied them to my scripts.
Also, found out where the symbolicatecrash is for Xcode 6, as per https://stackoverflow.com/questions/25769775/symbolicatecrash-from-xcode-6-gm/26148021#26148021. In short, it is at:
/Applications/Xcode.app/Contents/SharedFrameworks/DTDeviceKitBase.framework/Versions/A/Resources/symbolicatecrash
Just found this this, so thought I’d share!
That seemed to cut off the path, so here it is again:
/Applications/Xcode.app/Contents/SharedFrameworks/DTDeviceKitBase.framework/Versions/A/Resources/symbolicatecrash
😀
ok, that still didn’t seem to work, so …
/Applications/Xcode.app/Contents/SharedFrameworks/DTDeviceKitBase.framework
/Versions/A/Resources/symbolicatecrash
just append the two lines together and you should be good!
One more chime in that will allow whether you’re using Xcode 5 or Xcode 6 work correctly, which, btw, allows my machine which wasn’t working to work.
If you put the following in (replacing as necessary), it does it up quite well:
export DEVELOPER_DIR=$(xcode-select -print-path)
export DEVDIR=${DEVELOPER_DIR%/*}
export symbolicatecrash_app=$(find $DEVDIR -name symbolicatecrash -type f)
the second statement basically drops off the last directory from the path and should give you something like /Applications/Xcode.app/Contents, which seems to work fine with the find for symbolicatecrash and might take just a little more time to find, but …