Unity Android Symbol Table Parsing Crash Log

After we package Unity into an apk in il2cpp + release mode, the internal code will be packaged into a so file. This thing is actually the format of the Linux dynamic link library, which corresponds to the DLL of Windows or the dylib of Mac. It can also be called a symbol table on the android side, because it will store the correspondence between the virtual memory address and the assembly code internally.

After packaging in this way, it will reduce our package size and help our code to protect against plagiarism, but it will also cause us developers to not understand the error message, because these error messages are virtual memory addresses, not The name of the function, so how do we read the error message that occurs online?

In fact, we just said that this so file is not only a dynamic link library, but also saves the mapping relationship between the virtual memory address and the function, so we need to read these error messages and use this dynamic link library.

Let’s talk about how to use this so

Save files during packaging so

Packaging android project, we can choose to export directly as apk or export as gradle project, if we pack the apk, check the packaging setting’Create Symbols.zip ', it will be in the apk directory of the same level to save all the so files

If we choose to export as a gradle project, and the architecture choice is arm64-v8a, then our own code will be packaged as libil2cpp.so because we chose il2cpp, and stored as a dynamic link library in the unityLibrary/src/main/jniLibs/arm64-v8a of the gradle project

Here to explain this path, if familiar with Android development colleagues will be very sensitive to this path, this path is basically the ordinary Android project default dynamic link library path, jniLibs, taken apart is jni

In addition to our own code will be packaged into libil2cpp.so, unity’s own engine code, or the code of third-party packages, will also be packaged into various so files, such as libunity.so, but these files are not in this directory, we have two places to find these so files, one is unityLibrary/src/symbols/arm64-v8a, and the other is in the Unity project/Temp/StagingArea/libs/below, we need to copy them after packaging.

It should be noted here that if you use the editor to manually build, then manually copy it, but if it is packaged through the command line batchmode, you should pay attention that if you want the BuildPlayer method to run and then copy it, you can’t get it, because once the editor is closed, the Temp folder will be deleted.

At this time we need to use the PostProcessBuildAttribute hook, after the Build is completed, add a logic to save before exiting the editor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
using UnityEngine;
using System.Collections;
using UnityEditor.Callbacks;
using UnityEditor;
using System.IO;
using System;
public class PostProcessBuild
{
[PostProcessBuildAttribute()]
public static void OnPostprocessBuild(BuildTarget target, string pathToBuiltProject)
{
if (target BuildTarget.Android)
PostProcessAndroidBuild(pathToBuiltProject);
}

public static void PostProcessAndroidBuild(string pathToBuiltProject)
{
UnityEditor.ScriptingImplementation backend = UnityEditor.PlayerSettings.GetScriptingBackend(UnityEditor.BuildTargetGroup.Android);

if (backend UnityEditor.ScriptingImplementation.IL2CPP)
{
CopyARMSymbols ("armeabi-v7a");
CopyARMSymbols("arm64-v8a");
}
}


private static void CopyARMSymbols(string target)
{
const string libpath = "/Temp/StagingArea/libs/";
string sourcePath = Directory.GetCurrentDirectory() + libpath + $"{target}/";
string desPath = Directory.GetCurrentDirectory() + $"/Build/android/unityLibrary/symbols/{target}";
if (!Directory.Exists(desPath))
{
Directory.CreateDirectory(desPath);
}
CopyDirectory(sourcePath, desPath);
}

private static void CopyDirectory(string sourcePath, string targetPath)
{

if (System.IO.Directory.Exists(sourcePath))
{
string[] files = System.IO.Directory.GetFiles(sourcePath);

// Copy the files and overwrite destination files if they already exist.
foreach (string s in files)
{
// Use static Path methods to extract only the file name from the path.
var fileName = System.IO.Path.GetFileName(s);
var destFile = System.IO.Path.Combine(targetPath, fileName);
Debug.Log($"fileName: {fileName}, destFile: {destFile}");
System.IO.File.Copy(s, destFile, true);
}
}
else
{
Console.WriteLine("Source path does not exist!");
}
}

}

Use the saved so to parse the log

With so, we can parse logs that are all addresses.

Because this is actually the dynamic link library of android, the analysis needs to use the android tool: addr2line. I directly used the android tool installed by unity, and depending on the architecture, I need to use different tools, because mine is arm64-v8a. What I use is aarch64-linux-android-addr2line. The specific path is different for each person’s computer, just find it

We can directly run it on the command line.

1
aarch64-linux-android-addr2line -f -C -e libil2cpp.so 日志中的地址

Of course, you can also do this through code, because parsing line by line is very laborious, I only use lib2cpp.so and libunity.so here

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import sys
import os

def OutCrash(filename, version, target):

addr2linePath = "/Applications/Unity/Hub/Editor/2020.3.20f1/PlaybackEngines/AndroidPlayer/NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line -f -C -e "
il2cppdebugsoPath = "/Users/bytedance/Desktop/Unity/Android/symbols/" + version + "/" + target + "/libil2cpp.so "
unitydebugsoPath = "/Users/bytedance/Desktop/Unity/Android/symbols/" + version + "/" + target + "/libunity.so "

f = open(filename,'r')
logstr = f.readlines()
il2cppflag = 'libil2cpp'
unityflag = 'libunity'
crashEndFlag = 'libunity.so\n'
for log in logstr:
OutCmd(log,addr2linePath,crashEndFlag,unitydebugsoPath)
OutCmd (log, addr2linePath, il2cppflag, il2cppdebugsoPath)
OutCmd(log,addr2linePath,unityflag,unitydebugsoPath)

def OutCmd(log,addr2linePath,debugFlagStr,debugsoPath):

if log.endswith(debugFlagStr):
startIndex = log.index(' pc ')
endflag = log.index(r' /data/')
addstr = log[startIndex+4:endflag]
print(addstr)
cmdstr = addr2linePath +debugsoPath+addstr
os.system(cmdstr)
else:
unitystart = log.find(debugFlagStr)
if unitystart >= 0:
unitylen = log.index(debugFlagStr)
unitylen = unitylen + len(debugFlagStr) +1
endlen = log.find('(')
if endlen >= 0:
endIndex = log.index('(')
addstr = log[unitylen:endIndex]
addstr = addstr.replace(' ','')

cmdstr = addr2linePath + debugsoPath +addstr
print(addstr)
os.system(cmdstr)

OutCrash(sys.argv[1], sys.argv[2], sys.argv[3])