符号表
1. 什么是符号表
符号表是内存地址与函数名、文件名、行号的映射表。符号表元素如下所示:<起始地址> <结束地址> <函数> [<文件名:行号>]为了能快速并准确地定位用户APP发生Crash的代码位置,我们使用符号表对APP发生Crash的程序堆栈进行解析和还原。 因此在使用U-APM还原时需要先上传符号表!
2. 为什么要上传符号表
为了能快速并准确地定位用户APP发生Crash的代码位置,使用符号表对APP发生Crash的程序堆栈进行解析和还原。
举一个例子

U-APM 提供了手动上传和API 接口上传 两种方式上传符号表。
3. iOS符号表
3.1 什么是dsym文件
iOS 平台中,dSYM 文件是指具有调试信息的目标文件,存储着文件名、方法名、行号等信息,是和可执行文件的16进制函数地址一一对应的,通过分析崩溃的崩溃文件可以准确知道具体的崩溃信息。文件名通常为:xxx.app.dSYM,其中 xxx 通常表示应用程序的二进制包名,如下图所示:

通常我们可以在 Xcode 打包出来的文件xcarchive里面看到 dSYM 文件以及目录架构:

3.2 如何定位dsym文件
一般情况下,项目编译完dSYM文件跟app文件在同一个目录下,下面以XCode作为IDE详细说明定位dSYM文件。
-> 进入XCode;
-> 打开工程(已编译过);
-> 在左栏找到“Product”项;
-> 鼠标右键点击编译生成的“xxx.app”;
-> 点击“Show in Finder”;
如下图所示:
如果有多个dSYM文件,可以在使用工具时指定输入为dSYM文件所在的目录或者工程目录。
如果有多个dSYM文件,可以在使用工具时指定输入为dSYM文件所在的目录或者工程目录。
3.3 XCode编译后没有生成dSYM文件?
XCode Release编译默认会生成dSYM文件,而Debug编译默认不会生成,对应的Xcode配置如下:
XCode -> Build Settings -> Code Generation -> Generate Debug Symbols -> Yes
XCode -> Build Settings -> Build Option -> Debug Information Format -> DWARF with dSYM File


3.4 开启Bitcode之后需要注意哪些问题?
在点“Upload to App Store”上传到App Store服务器的时候需要声明符号文件(dSYM文件)的生成:

在配置符号表文件之前,需要从App Store中把该版本对应的dSYM文件下载回本地,如下图。
不要使用本地生成的dSYM文件来生成符号表文件。由于本地编译生成的dSYM文件的符号表信息都被隐藏了,如果用本地编译生成的dSYM文件生成符号表文件并配置到U-APM平台之后,还原出来的结果将是类似于“__hiden#XXX”这样的符号。

3.5 如何判断dSYM文件是否与堆栈的UUID匹配?
还原Crash堆栈时,需要根据UUID来匹配符号表文件,因此只有上传的符号表文件的UUID与Crash对应APP的UUID一致时,才能准确地对堆栈进行还原。
需要确定在 U-APM 符号表管理 所选版本的uuid与crash崩溃堆栈中Binary Images的 uuid一致(符号表管理中uuid末尾的0 可忽略)


3.6 如何查看dSYM文件的UUID?
通过命令查看UUID
xcrun dwarfdump --uuid <dSYM文件>
通过符号表文件查看UUID
符号表文件的UUID与dSYM文件的UUID是一致的,因此可以通过符号表工具生成的符号表文件来查看dSYM文件的UUID:
生成符号表文件(.zip) ---> 解压符号表文件(.symbol) ---> 使用文本编辑器打开符号表文件

其中符号表文件的“UUID”信息即Debug SO文件的UUID,亦是符号表文件的UUID,如果文件较大,建议使用“Sublime Text”等文本编辑器来打开符号表文件。
3.7 如何找回已发布到App Store的App对应的dSYM文件?
通过Xcode找回
1、打开 Xcode 顶部菜单栏 -> Window -> Organizer 窗口:

2、找到发布的归档包,右键点击对应归档包,选择Show in Finder操作:

3、右键选择定位到的归档文件,选择显示包内容操作:

4、选择dSYMs目录,目录内即为下载到的 dSYM 文件:

通过iTunes Connect找回
1、登录iTunes Connect;
2、进入“TestFlight”的“构建版本”页面:

3、择对应版本,点“下载dSYM”下载dSYM文件:

通过mdfind工具找回
在U-APM 错误详情页面查询到crash对应的UUID:
然后在Mac的Shell中,用mdfind命令定位dSYM文件:
mdfind"com_apple_xcode_dsym_uuids == <UUID>"
注意,使用mdfind时,UUID需要格式转换(增加“-”): 12345678-1234-1234-1234-xxxxxxxxxxxx
例如,要定位的dSYM的UUID为:E30FC309DF7B3C9F8AC57F0F6047D65F 则定位dSYM文件的命令如下:
mdfind"com_apple_xcode_dsym_uuids == E30FC309-DF7B-3C9F-8AC5-7F0F6047D65F"|12345678-1234-1234-1234-xxxxxxxxxxxx|
建议每次构建或者发布APP版本的时候,备份App对应的dSYM文件!
4. Android 符号表
4.1 Java 符号
多个mapping文件请自行处理追加到一个文件中,最终命名为mapping.txt,并打包成zip文件,,如果需要同时包含so符号,可以把so文件打包在一起。(具体结构可参考下面示例)
4.2 Android so库
保证符号表的so文件与发布的so文件同名,so压缩后最大支持400M;如果一个版本里包含同文件名,不同架构的so,可以压缩到不同的目录中,反符号化时需要通过buildid去关联;建议编译时使用-g参数,带上debug信息,崩溃堆栈可以解析到代码行级,实际发布时,再使用strip命令去掉调试信息;如果不带debug信息,则只能定位到函数名级下列情况下,需要so文件带有buildid
不同CPU架构的so文件名一样,放在不同的路径下
同一个应用版本里,可能有多个版本的so库,使用了动态加载的技术
如何生成带buildid的so,请检查编译参数,确保没有带”—build-id=none”,如果还是没有buildid,可以添加编译参数”ld_flags += -Wl,--build-id=sha1”可通过file命令检查so文件是否带buildid,调试信息
file libmytest.so libmytest.so: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=63f643875228a281430e69123def59c9b6894803, with debug_info, not stripped
4.3 符号文件建议示例
Android
xxx.zip|— mapping.txt —多个mapping需要自行合并
|— libxx1.so —要求与发布的so同名
|— libxx
|— libxx3.so —同文件名,多个架构放不同的目录
|— x86
|— libxx3.so —同文件名,多个架构放不同的目录
5. Flutter Dart 符号表
5.1 生成符号表
要混淆你的应用程序,请在 release 模式下在项目根目录下使用 flutter build 命令,并使用 --obfuscate 和 --split-debug-info 选项。 --split-debug-info 选项指定了 Flutter 输出调试文件的目录。在混淆的情况下,它会输出一个符号表。请参考以下命令:
// Android
flutter build apk --obfuscate --split-debug-info=/<project-name>/<directory>
// iOS
flutter build ipa --obfuscate --split-debug-info=/<project-name>/<directory>一旦你混淆了二进制文件,请务必 保存符号表文件。如果你将来需要解析混淆后的堆栈跟踪,你将需要该文件。
小提示:
--split-debug-info 选项也可以不使用 --obfuscate 来提取 Dart 程序符号
关于这些标志的详细信息,请运行特定构建目标类型的帮助命令,例如:
flutter build apk -h如果输出中没有列出这些标志,请运行 flutter --version 命令,检查你的 Flutter 版本。
5.2 符号表示例

6. HarmonyOS NEXT 符号表
6.1 获取鸿蒙应用so符号表文件
找到c++模块所在的buiild-profile.json5文件,确保添加如下图所示的编译参数(参考编译带debug信息的so)
{ "apiType": "stageMode", "buildOption": { "externalNativeOptions": { "path": "./src/main/cpp/CMakeLists.txt", "arguments": "-DCMAKE_BUILD_TYPE=RelWithDebInfo", "cppFlags": "", } }, ... }
在release模式下重新编译获取带debug信息的so(特别注意要使用libs目录下的so文件)
6.2 如何获取鸿蒙应用nameCache.json文件
在release模式build应用后在如下图所示目录可获取该文件
6.3 如何获取鸿蒙应用的sourceMap.map文件
在release模式下可以从如下所示的位置找到sourceMap.map文件
7. 符号表上传
7.1 手动上传方式
按照上述文档说明将符号表文件压缩到一起
登录平台,找到需要上传的符号表应用,点击顶部的 设置管理 进入应用设置界面
点击 符号表管理 菜单,点击 上传符号表文件 ,按照表单提示,将第一步压缩好的符号表文件上传即可
当前支持现有版本列表和手动输入两种方式:
如果是已经有错误上报到U-APM后台的版本,可以直接在上传符号表时的版本下拉框中选择
如果是即将发布的新版本,支持手动输入版本号,请输入与新版本完全一致的内容,并点击‘添加版本号‘即可手动添加

特别说明:如果是flutter dart符号表,版本号由“flutter应用的版本号+构建号”组合构成,例如 2.0.0+3 。同时,还需要填写或选择flutter应用/模块的名称,这里只有填写正确才能匹配到Dart异常堆栈,进行堆栈还原。

已经上传的符号表可以在符号表管理页面进行查看、删除和下载等操作

7.2 通过API接口上传
分两步接口调用
1 获取文件上传参数
通过友盟OpenApi开放平台提供的“获取符号表文件上传参数”接口,拿到上传所需的必要参数。 具体请参考 接口文档地址,您可以自行构造http请求或者使用SDK。
接口返回示例:
{
"uploadAddress": "https://quickbird.oss-cn-shanghai.aliyuncs.com",
"accessKeyId": "4bQ2yVkZIuFcikCY",
"key": "tmp/20210526/5fb6001a73749c24fd9cb356_e2d49410c3a94ce4811b07eacc0980d2/symbol.so",
"policy": "eyJleHBpcmF0aW9uIjoiMjAyMS0wNS0yNlQxMDoyNjozMS43NjRaIiwiY29uZGl0aW9ucyI6W1siZXEiLCIka2V5IiwidG1wLzIwMjEwNTI2LzVmYjYwMDFhNzM3NDljMjRmZDljYjM1Nl9lMmQ0OTQxM",
"signature": "I6sn0iHXVx3D2x2g9I/ZTY7LsQk=",
"callback": "eyJjYWxsYmFja1VybCI6Imh0dHBzOi8vYXBtLnVtZW5nLmNvbS9oc2Yvc3ltL=="
}返回的内容就是步骤2上传文件所必要的参数
2 通过form表单格式上传文件
通过步骤1获取到上传参数后,就可以上传符号表到文件服务器了。您可以用任意http client工具,按以下格式构造form表单请求上传文件。
请求地址:步骤1返回的 uploadAddress 字段
请求方法:POST
请求Content-Type:multipart/form-data
表单字段 | 取值 |
OSSAccessKeyId | 步骤1返回的 accessKeyId 字段 |
key | 步骤1返回的 key 字段 |
policy | 步骤1返回的 policy 字段 |
signature | 步骤1返回的 signature 字段 |
callback | 步骤1返回的 callback 字段 |
file | 文件流 |
curl请求示例:
curl --location --request POST 'https://quickbird.oss-cn-shanghai.aliyuncs.com' \
--form 'OSSAccessKeyId=4bQ2yVkZIuFcikCY' \
--form 'key="tmp/a469a47579dfcc91017da6069/symbol.so"' \
--form 'policy="eyJleHBpcmF0aW9uIjoiMjAyMS0wNS0yNlQwOTo1MTo1OC44NjBaIiwiY29uZGl0aW9ucyI6W1siZXEiLCIka2V5IiwidG1wLzIwMjEwNTI2LzVmYjYwMDFhNzM3NDljMjRmZDljYjM1Nl83MGNkNDNjYTQ2OWE0NzU3OWRmY2M5MTAxN2RhNjA2OS9zeW1ib2wuc28iXSxbImNvbnRlbnQtbGVuZ3RoLXJ"' \
--form 'signature="bP0f+T5OL66G9DjIKtNClphabfs="' \
--form 'callback="eyJjYWxsYmFja1VybCI6Imh0dHBzOi8vYXBtLnVtZW5nLmNvbS9oc2Yvc3ltL29zcy9ub3RpZnlNc2ciLCJjYWxsYmFja0JvZHki"' \
--form 'file=@"/temp/symbol1.so"'java请求示例:
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import javax.activation.MimetypesFileTypeMap;
public class SymUploadDemo {
public static void main(String[] args) throws Exception {
String uploadAddress = "https://quickbird.oss-cn-shanghai.aliyuncs.com";
Map<String, String> formFields = new HashMap<>();
formFields.put("OSSAccessKeyId","4bQ2yVkZIuFcikCY");
formFields.put("key","tmp/a469a47579dfcc91017da6069/symbol.so");
formFields.put("policy","eyJleHBpcmF0aW9uIjoiMjAyMS0wNS0yNlQwOTo1MTo1OC44NjBaIiwiY29uZGl0aW9ucyI6W1siZXEiLCIka");
formFields.put("signature","I6sn0iHXVx3D2x2g9I/ZTY7LsQk=");
formFields.put("callback","eyJjYWxsYmFja1VybCI6Imh0dHBzOi8vYXBtLnVtZW5nLmNvbS9oc2Yvc3ltL29zcy9ub3RpZnlNc2ciLCJjYWxsYmFja0JvZHki");
String localFile = "/temp/symbol1.so";
String res = formUpload(uploadAddress,formFields,localFile);
System.out.println(res);
}
private static String formUpload(String urlStr, Map<String, String> formFields, String localFile)
throws Exception {
String res = "";
HttpURLConnection conn = null;
String boundary = "9431149156168";
try {
URL url = new URL(urlStr);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(30000);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setRequestMethod("POST");
conn.setRequestProperty("User-Agent",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.6)");
conn.setRequestProperty("Content-Type",
"multipart/form-data; boundary=" + boundary);
OutputStream out = new DataOutputStream(conn.getOutputStream());
// 遍历读取表单Map中的数据,将数据写入到输出流中。
if (formFields != null) {
StringBuffer strBuf = new StringBuffer();
Iterator<Entry<String, String>> iter = formFields.entrySet().iterator();
int i = 0;
while (iter.hasNext()) {
Entry<String, String> entry = iter.next();
String inputName = entry.getKey();
String inputValue = entry.getValue();
if (inputValue == null) {
continue;
}
if (i == 0) {
strBuf.append("--").append(boundary).append("\r\n");
strBuf.append("Content-Disposition: form-data; name=\""
+ inputName + "\"\r\n\r\n");
strBuf.append(inputValue);
} else {
strBuf.append("\r\n").append("--").append(boundary).append("\r\n");
strBuf.append("Content-Disposition: form-data; name=\""
+ inputName + "\"\r\n\r\n");
strBuf.append(inputValue);
}
i++;
}
out.write(strBuf.toString().getBytes());
}
// 读取文件信息,将要上传的文件写入到输出流中。
File file = new File(localFile);
String filename = file.getName();
String contentType = new MimetypesFileTypeMap().getContentType(file);
if (contentType == null || contentType.equals("")) {
contentType = "application/octet-stream";
}
StringBuffer strBuf = new StringBuffer();
strBuf.append("\r\n").append("--").append(boundary)
.append("\r\n");
strBuf.append("Content-Disposition: form-data; name=\"file\"; "
+ "filename=\"" + filename + "\"\r\n");
strBuf.append("Content-Type: " + contentType + "\r\n\r\n");
out.write(strBuf.toString().getBytes());
DataInputStream in = new DataInputStream(new FileInputStream(file));
int bytes = 0;
byte[] bufferOut = new byte[1024];
while ((bytes = in.read(bufferOut)) != -1) {
out.write(bufferOut, 0, bytes);
}
in.close();
byte[] endData = ("\r\n--" + boundary + "--\r\n").getBytes();
out.write(endData);
out.flush();
out.close();
// 读取返回数据。
strBuf = new StringBuffer();
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = null;
while ((line = reader.readLine()) != null) {
strBuf.append(line).append("\n");
}
res = strBuf.toString();
reader.close();
reader = null;
} catch (Exception e) {
System.err.println("Send post request exception: " + e);
throw e;
} finally {
if (conn != null) {
conn.disconnect();
conn = null;
}
}
return res;
}
}
返回http状态码为200表示成功,其他表示异常。上传成功后可以在符号表管理页面看到对应的记录,其中上传方式是“API上传”。
7.3 通过SDK直传
我们提供了多种编程语言实现的SDK,封装了上传符号表的API,您可以通过在代码中集成SDK,实现直接上传符号表文件。具体内容参考: 查看接口文档 下载SDK
以下演示了如何使用 Java SDK 和 Python SDK 上传符号表(其他语言的SDK调用方式类似)
JAVA示例:
引用maven依赖
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>umeng_apm20220214</artifactId>
<version>1.1.0</version>
</dependency>代码示例
public static void main(String[] args_) throws Exception {
// 使用AK&SK初始化账号Client
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
// 必填,您的 AccessKey ID
.setAccessKeyId("ACCESS_KEY_ID")
// 必填,您的 AccessKey Secret
.setAccessKeySecret("ACCESS_KEY_SECRET");
config.endpoint = "apm.openapi.umeng.com";
Client client = new Client(config);
// 一定要设置 _openPlatformEndpoint 参数
client._openPlatformEndpoint = "apm.openapi.umeng.com";
File file = new File("your/file/path");
// 这里要使用 UploadSymbolFileAdvanceRequest, 不要使用 UploadSymbolFileRequest
UploadSymbolFileAdvanceRequest request = new UploadSymbolFileAdvanceRequest();
// 参数说明请参考接口文档
request.setAppVersion("1.0.0+1")
.setDataSourceId("dataSourceId")
.setFileName(file.getName())
.setFileType(4)
.setFlutterName("my-flutter-app")
//设置文件输入流
.setOssUrlObject(new FileInputStream(file));
java.util.Map<String, String> headers = new java.util.HashMap<>();
try {
// 这里要调用 uploadSymbolFileAdvance 方法,不要调用 uploadSymbolFileWithOptions方法
UploadSymbolFileResponse response = client.uploadSymbolFileAdvance(request, headers, new RuntimeOptions());
//打印 API 的返回值
System.out.println(response.toMap());
} catch (TeaException error) {
// 如有需要,请打印 error
com.aliyun.teautil.Common.assertAsString(error.message);
} catch (Exception _error) {
TeaException error = new TeaException(_error.getMessage(), _error);
// 如有需要,请打印 error
com.aliyun.teautil.Common.assertAsString(error.message);
}
}Python示例:
# -*- coding: utf-8 -*-
# This file is auto-generated, don't edit it. Thanks.
import sys
from typing import List
from alibabacloud_umeng_apm20220214.client import Client as umeng_apm20220214Client
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_umeng_apm20220214 import models as umeng_apm_20220214_models
from alibabacloud_tea_util import models as util_models
from alibabacloud_tea_util.client import Client as UtilClient
class Sample:
def __init__(self):
pass
@staticmethod
def create_client(
access_key_id: str,
access_key_secret: str,
) -> umeng_apm20220214Client:
"""
使用AK&SK初始化账号Client
@param access_key_id:
@param access_key_secret:
@return: Client
@throws Exception
"""
config = open_api_models.Config(
# 必填,您的 AccessKey ID,
access_key_id=access_key_id,
# 必填,您的 AccessKey Secret,
access_key_secret=access_key_secret
)
# Endpoint 请参考 https://api.aliyun.com/product/umeng-apm
config.endpoint = f'apm.openapi.umeng.com'
return umeng_apm20220214Client(config)
@staticmethod
def main(
args: List[str],
) -> None:
client = Sample.create_client('access_key_id', 'access_key_secret')
# 一定要设置 _open_platform_endpoint 参数
client._open_platform_endpoint = f'apm.openapi.umeng.com'
with open('your_file_path', 'rb') as file:
# 参数说明请参考接口文档
upload_symbol_file_request = umeng_apm_20220214_models.UploadSymbolFileAdvanceRequest(
app_version='your_app_version',
data_source_id='your_datasource_id',
file_name='your_file_name',
file_type=1,
oss_url_object=file
)
headers = {}
try:
# 复制代码运行请自行打印 API 的返回值
client.upload_symbol_file_advance(upload_symbol_file_request, headers, util_models.RuntimeOptions())
except Exception as error:
# 如有需要,请打印 error
UtilClient.assert_as_string(error.message)
@staticmethod
async def main_async(
args: List[str],
) -> None:
client = Sample.create_client('access_key_id', 'access_key_secret')
# 一定要设置 _open_platform_endpoint 参数
client._open_platform_endpoint = f'apm.openapi.umeng.com'
with open('your_file_path', 'rb') as file:
# 参数说明请参考接口文档
upload_symbol_file_request = umeng_apm_20220214_models.UploadSymbolFileAdvanceRequest(
app_version='your_app_version',
data_source_id='your_datasource_id',
file_name='your_file_name',
file_type=1,
oss_url_object=file
)
headers = {}
try:
# 复制代码运行请自行打印 API 的返回值
await client.upload_symbol_file_advance_async(upload_symbol_file_request, headers, util_models.RuntimeOptions())
except Exception as error:
# 如有需要,请打印 error
UtilClient.assert_as_string(error.message)
if __name__ == '__main__':
Sample.main(sys.argv[1:])8. 符号表查看
iOS符号表查看
在iOS SDK 7.2.0及以上版本中,增强了崩溃采集的能力,从以前的仅支持OC层崩溃,变为支持OC层、Swift层和Cpp层崩溃。因此,在集成SDK7.2.0及以上后,崩溃详细日志发生了改变,具体改变内容如下。
U-APM SDK V1.1.0及以上 或 U-App V7.2.2 ~V7.2.7
日志格式:
崩溃线程Thread0
相关线程(除Thread0)
Binary Image

符号表转译方式
UUID的获取方式从原始日志的“Binary Image”段进行获取

U-App V7.1.3及以下
此类老版本已不适配最新的错误点/错误摘要解析算法,建议您升级SDK体验更好的日志查看功能


