跳转到主要内容
PRODUCT DOCUMENTS

快速找到所需文档,高效完成接入与排障

浏览产品文档与友盟 Skill,展开目录并阅读正文。

符号表

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 符号表示例

image.png

6. HarmonyOS NEXT 符号表

  1. 6.1 获取鸿蒙应用so符号表文件

    1. 找到c++模块所在的buiild-profile.json5文件,确保添加如下图所示的编译参数(参考编译带debug信息的so)

      {
        "apiType": "stageMode",
        "buildOption": {
          "externalNativeOptions": {
            "path": "./src/main/cpp/CMakeLists.txt",
            "arguments": "-DCMAKE_BUILD_TYPE=RelWithDebInfo",
            "cppFlags": "",
          }
        },
        ...
      }
      1. image

      2. 在release模式下重新编译获取带debug信息的so(特别注意要使用libs目录下的so文件)

        1. image

  2. 6.2 如何获取鸿蒙应用nameCache.json文件

    在release模式build应用后在如下图所示目录可获取该文件

    1. image

  3. 6.3 如何获取鸿蒙应用的sourceMap.map文件

    在release模式下可以从如下所示的位置找到sourceMap.map文件

    1. image

7. 符号表上传

7.1 手动上传方式

  1. 按照上述文档说明将符号表文件压缩到一起

  2. 登录平台,找到需要上传的符号表应用,点击顶部的 设置管理 进入应用设置界面

  3. 点击 符号表管理 菜单,点击 上传符号表文件 ,按照表单提示,将第一步压缩好的符号表文件上传即可

当前支持现有版本列表和手动输入两种方式:

  1. 如果是已经有错误上报到U-APM后台的版本,可以直接在上传符号表时的版本下拉框中选择

  2. 如果是即将发布的新版本,支持手动输入版本号,请输入与新版本完全一致的内容,并点击‘添加版本号‘即可手动添加

截屏2021-05-27 下午2

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

image.png

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

image.png

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

截屏2021-06-10 下午3

符号表转译方式

UUID的获取方式从原始日志的“Binary Image”段进行获取

截屏2021-06-10 下午3

U-App V7.1.3及以下

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

9. 常见问题

错误没有成功解析,怎么办?