已关闭
appfreeze skill新增符号解析流程 #42
appfreeze skill新增符号解析流程 #42
已关闭
sunyichao创建于 9月2日关闭于 27 天前
共 3 个文件变更+296-13
@@ -50,6 +50,8 @@ HarmonyOS 高级开发工程师 / HarmonyOS 架构师 / 系统 DFX 工程师 /
50 确认以下脚本存在:50 确认以下脚本存在:
51 - `<skill-root>/scripts/freeze/main.py`51 - `<skill-root>/scripts/freeze/main.py`
52 - `<skill-root>/scripts/sample_stack_analyzer.py`52 - `<skill-root>/scripts/sample_stack_analyzer.py`
53+ - `<skill-root>/scripts/llvm_addr2line.py`
54+ - `<skill-root>/scripts/match_so.py`
53 55 
543. **依赖检查**563. **依赖检查**
55 appfreeze Python 脚本仅使用 Python 标准库和本技能内置模块,无需安装第三方 pip 依赖。57 appfreeze Python 脚本仅使用 Python 标准库和本技能内置模块,无需安装第三方 pip 依赖。
@@ -89,7 +91,7 @@ HarmonyOS 高级开发工程师 / HarmonyOS 架构师 / 系统 DFX 工程师 /
89概览中 `resources` 为“有”时执行:91概览中 `resources` 为“有”时执行:
90 92 
91```bash93```bash
92-python "<skill-root>/scripts/freeze/main.py" -p "<file_path>" --section resources94+ python "<skill-root>/scripts/freeze/main.py" -p "<file_path>" --section resources
93```95```
94 96 
95结合概览与资源区段识别:97结合概览与资源区段识别:
@@ -160,12 +162,50 @@ python "<skill-root>/scripts/freeze/main.py" -p "<file_path>" --section event-qu
160 162 
161---163---
162 164 
163-### Step 4 — 故障目标线程 & 关联线程堆栈分析165+### Step 4 — 符号解析 & 关联线程堆栈分析
166+未被整机异常定性时,执行下列流程:
164 167 
165-未被整机异常定性时,执行:168+#### 4a. 符号解析(可追溯源码时执行,无源码追溯时跳过此步骤)
169+**必要依赖**:源码可达性检测
170+- 在当前工作目录(及父目录)中查找以下标志文件:`build-profile.json5`、`oh-package.json5`、`AppScope/app.json5`
171+- 若找到,读取 `AppScope/app.json5` 中的 `bundleName` 字段,与提取 faultlog 中的故障进程包名对比,若一致即源码可达;若不一致即源码不可达,直接跳过符号解析流程。
172+ 
173+**触发条件**(满足其一即需解析):
174+- 栈顶或责任调用链上的栈帧为应用 so,且只有**裸偏移地址 + BuildID**。
175+- 例:`#05 pc 000000000000a128 /data/storage/el1/bundle/libs/arm64/libxxx.so(f3991876654443a608c9cc71f20cf4d8caab449f)`
176+- 栈顶或责任调用链上的关键栈帧的符号信息为「函数名+偏移量」。
177+ 
178+**不解析的场景**:
179+- 系统库帧(`libffrt.so`、`libace_compatible.z.so` 等)—— 日志已带符号名,且不在应用侧责任范围。
180+- 用户未提供带符号的 SO 文件路径,且当前工程没有找到对应 SO 文件。**若 SO 文件不可用**:跳过本步骤,在报告中标注"无带符号so文件,行号定位不可用"
181+ 
182+**so 文件校验**(地址解析前必须执行):
183+从 faultlog 栈帧中提取应用 SO 文件名和 BuildID,校验源码工程目录下同名 .so 是否为同一编译产物(若系统只有 `python3` 可用则替换):
184+```bash
185+ python scripts/match_so.py {so_name} {build_id} -d {search_dir}
186+```
187+- 退出码 0(stdout 输出匹配路径)→ 用该路径执行下方地址解析
188+- 退出码非 0 → 跳过地址解析,在报告中标注「未找到与 faultlog 版本匹配的 SO 文件,行号定位不可用」
189+ 
190+**地址解析**(仅在 so 文件校验通过时执行):
191+通过 Python 脚本自动定位 `llvm-addr2line` 并对 BuildID 匹配的 .so 文件执行地址解析。(若系统只有 `python3` 可用,则使用 `python3` 替代 `python`)
192+```bash
193+ python scripts/llvm_addr2line.py -pCfie {matched_so_file} {offset}
194+```
195+其中 `{matched_so_file}` 为 so 文件校验输出的匹配路径。
196+ 
197+对崩溃栈中关键帧及前后 3 帧应用帧逐一解析,根据解析结果定位源码分析根因。
198+ 
199+- **若脚本执行失败(退出码非 0)**:说明系统中未找到 `llvm-addr2line` 工具。此时:
200+- 提示用户:「未找到 llvm-addr2line 地址解析工具,请安装后重试。可通过以下方式安装:」
201+- 提供安装指引:
202+- **推荐**:确保 DevEco Studio 已安装且 HarmonyOS SDK 已配置,工具位于 `<DEVECO_HOME>/sdk/default/openharmony/native/llvm/bin/`。可设置 `DEVECO_HOME` 环境变量指向 DevEco Studio 安装目录后重试。
203+- 在报告中标注"符号解析工具不可用,堆栈行号定位未执行",跳过此步骤。
204+ 
205+#### 4b. 堆栈分析
166 206 
167```bash207```bash
168-python "<skill-root>/scripts/freeze/main.py" -p "<file_path>" --section fault-stack208+ python "<skill-root>/scripts/freeze/main.py" -p "<file_path>" --section fault-stack
169```209```
170 210 
171> 堆栈调用方向:**栈底(最大编号)→ 栈顶(#00)**,栈顶为最后被调用位置。211> 堆栈调用方向:**栈底(最大编号)→ 栈顶(#00)**,栈顶为最后被调用位置。
@@ -191,7 +231,7 @@ python "<skill-root>/scripts/freeze/main.py" -p "<file_path>" --section fault-st
191仅当故障栈包含 Binder 等待、日志出现 IPC FULL 特征,或此前步骤无法形成证据闭环时执行:231仅当故障栈包含 Binder 等待、日志出现 IPC FULL 特征,或此前步骤无法形成证据闭环时执行:
192 232 
193```bash233```bash
194-python "<skill-root>/scripts/freeze/main.py" -p "<file_path>" --section binder234+ python "<skill-root>/scripts/freeze/main.py" -p "<file_path>" --section binder
195```235```
196 236 
1971. 从 `binder catcher` 信息追踪 IPC 调用链,找到**最终阻塞的对端进程及线程**。2371. 从 `binder catcher` 信息追踪 IPC 调用链,找到**最终阻塞的对端进程及线程**。
@@ -217,7 +257,7 @@ python "<skill-root>/scripts/freeze/main.py" -p "<file_path>" --section binder
217概览中 `attachments` 为“有”且需要采样栈或其他 faultlog 路径时,先执行:257概览中 `attachments` 为“有”且需要采样栈或其他 faultlog 路径时,先执行:
218 258 
219```bash259```bash
220-python "<skill-root>/scripts/freeze/main.py" -p "<file_path>" --section attachments260+ python "<skill-root>/scripts/freeze/main.py" -p "<file_path>" --section attachments
221```261```
222 262 
223每一次Snapshot堆栈是一次的采样(最多采样10次),我们统计函数的次数的时候一次采样堆栈里面就算出现多次,也只统计为一次。请对采样栈通过下面的脚本进行分析:263每一次Snapshot堆栈是一次的采样(最多采样10次),我们统计函数的次数的时候一次采样堆栈里面就算出现多次,也只统计为一次。请对采样栈通过下面的脚本进行分析:
@@ -225,7 +265,7 @@ python "<skill-root>/scripts/freeze/main.py" -p "<file_path>" --section attachme
225**调用独立采样栈分析脚本**:265**调用独立采样栈分析脚本**:
226 266 
227```bash267```bash
228-python "<skill-root>/scripts/sample_stack_analyzer.py" "<sample_stacks.txt路径>"268+ python "<skill-root>/scripts/sample_stack_analyzer.py" "<sample_stacks.txt路径>"
229```269```
230 270 
231这个脚本只会列出采样栈中业务帧的情况,你需要重点关注业务函数分布情况,分析时**必须**给出完整的**函数名称,包含文件位置,行号,列号**。如果脚本结果没有任何信息,不需要列出系统函数。那么你需要直接阅读一下采样栈,271这个脚本只会列出采样栈中业务帧的情况,你需要重点关注业务函数分布情况,分析时**必须**给出完整的**函数名称,包含文件位置,行号,列号**。如果脚本结果没有任何信息,不需要列出系统函数。那么你需要直接阅读一下采样栈,
@@ -254,7 +294,7 @@ python "<skill-root>/scripts/sample_stack_analyzer.py" "<sample_stacks.txt路径
2543. **根本原因**(详细描述触发路径)2943. **根本原因**(详细描述触发路径)
2554. **根因模块**(具体模块名)2954. **根因模块**(具体模块名)
2565. **责任领域**(应用 / 系统 / 混合 / 未定,并给出定界依据)2965. **责任领域**(应用 / 系统 / 混合 / 未定,并给出定界依据)
257-6. **修复建议**(无需输出验证步骤;严格面向已判定的责任领域。系统侧根因只给系统侧代码/架构修改,应用侧根因只给应用侧修改,混合责任分开输出,未定时不强行给修改方案)297+6. **修复建议**(无需输出验证步骤;严格面向已判定的责任领域。系统侧根因只给系统侧代码/架构修改,应用侧根因只给应用侧修改,源码可达时必须给出源码级修复建议,混合责任分开输出,未定时不强行给修改方案)
258 298 
259---299---
260 300 
@@ -289,9 +329,9 @@ python "<skill-root>/scripts/sample_stack_analyzer.py" "<sample_stacks.txt路径
289 原始日志:329 原始日志:
290 <直接从 faultlog 中摘取的原始日志片段>330 <直接从 faultlog 中摘取的原始日志片段>
291 331 
292-2. <关键证据2>332+2. <关键证据2>(源码可达时输出)
293- 原始日志:333+ 符号解析结果及源码定位
294- <直接从 faultlog 中摘取的原始日志片段>334+ <符号解析结果及源码定位根因分析>
295 335 
2963. <…>3363. <…>
297 337 
@@ -300,14 +340,14 @@ python "<skill-root>/scripts/sample_stack_analyzer.py" "<sample_stacks.txt路径
300|----------|----------|--------------|------|340|----------|----------|--------------|------|
301 341 
302【根本原因】342【根本原因】
303-<详细说明导致冻屏的直接原因及其完整触发路径>343+<结合证据链详细说明导致冻屏的直接原因及其完整触发路径>
304 344 
305【根因模块】345【根因模块】
306<模块名称,例如:com.example.app / libxxx.z.so / xxx_service>346<模块名称,例如:com.example.app / libxxx.z.so / xxx_service>
307责任领域 : <应用 / 系统 / 混合 / 未定>347责任领域 : <应用 / 系统 / 混合 / 未定>
308定界依据 : <根因实现、调用契约与代码归属证据;不能只看路径>348定界依据 : <根因实现、调用契约与代码归属证据;不能只看路径>
309 349 
310-【修复建议】350+【修复建议】(源码可达时需给出源码级修复建议)
3111. <针对责任模块直接根因的修改;系统侧根因必须写系统模块的修改,应用侧根因必须写应用代码的修改>3511. <针对责任模块直接根因的修改;系统侧根因必须写系统模块的修改,应用侧根因必须写应用代码的修改>
3122. <针对深层设计、锁/队列/生命周期/接口契约的同责任域改进>3522. <针对深层设计、锁/队列/生命周期/接口契约的同责任域改进>
313================================================================================353================================================================================
@@ -0,0 +1,123 @@
1+#!/usr/bin/env python3
2+# Copyright (c) 2021-2026 Huawei Device Co., Ltd.
3+# Licensed under the Apache License, Version 2.0 (the "License");
4+# you may not use this file except in compliance with the License.
5+# You may obtain a copy of the License at
6+#
7+# http://www.apache.org/licenses/LICENSE-2.0
8+#
9+# Unless required by applicable law or agreed to in writing, software
10+# distributed under the License is distributed on an "AS IS" BASIS,
11+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+# See the License for the specific language governing permissions and
13+# limitations under the License.
14+ 
15+"""
16+llvm_addr2line.py
17+查找 llvm-addr2line 工具并执行地址解析。
18+ 
19+用法:
20+ python llvm_addr2line.py # 只输出工具路径
21+ python llvm_addr2line.py -pCfie <so_file> <offset> # 直接执行地址解析
22+ 
23+ 
24+ 搜索策略:
25+ 1. DEVECO_HOME 环境变量:{DEVECO_HOME}/sdk/default/openharmony/native/llvm/bin
26+ 2. 各平台已知 DevEco Studio 默认安装路径
27+ 3. 系统 PATH 中的 llvm-addr2line
28+ 
29+ 输出:工具路径(无参数)或地址解析结果(有参数)
30+ 退出码:0 = 成功,非 0 = 失败
31+"""
32+ 
33+import os
34+import sys
35+import platform
36+import shutil
37+import subprocess
38+from typing import Optional, Tuple
39+ 
40+ 
41+def check_path(candidate: str) -> bool:
42+ """检查候选路径是否可执行(Windows 下退而检查文件是否存在)。"""
43+ return os.path.isfile(candidate) or os.access(candidate, os.X_OK)
44+ 
45+ 
46+def find_addr2line() -> Tuple[Optional[str], int]:
47+ """按优先级查找 llvm-addr2line 工具,返回 (工具路径, 退出码)。"""
48+ plat = platform.system()
49+ tool_name = "llvm-addr2line.exe" if plat == "Windows" else "llvm-addr2line"
50+ sdk_rel_path = os.path.join("sdk", "default", "openharmony", "native", "llvm", "bin")
51+ deveco_home = os.environ.get("DEVECO_HOME", "")
52+ if deveco_home:
53+ candidate = os.path.join(deveco_home, sdk_rel_path, tool_name)
54+ if check_path(candidate):
55+ return candidate, 0
56+ deveco_paths: list[str] = []
57+ if plat == "Darwin":
58+ deveco_paths = [
59+ "/Applications/DevEco-Studio.app/Contents",
60+ os.path.expanduser("~/Applications/DevEco-Studio.app/Contents"),
61+ ]
62+ elif plat == "Windows":
63+ deveco_paths = [
64+ r"C:\Program Files\DevEco Studio",
65+ r"C:\Program Files (x86)\DevEco Studio",
66+ os.path.expandvars(r"%LOCALAPPDATA%\Programs\DevEco Studio"),
67+ ]
68+ 
69+ for d in deveco_paths:
70+ candidate = os.path.join(d, sdk_rel_path, tool_name)
71+ if check_path(candidate):
72+ return candidate, 0
73+ found = shutil.which(tool_name)
74+ if found:
75+ return found, 0
76+ 
77+ return None, 1
78+ 
79+ 
80+def main() -> None:
81+ """程序入口:无参数时输出工具路径,有参数时定位工具并执行地址解析。"""
82+ args = sys.argv[1:]
83+ 
84+ if args:
85+ path, code = find_addr2line()
86+ if code != 0 or not path:
87+ print("[ERROR] 未找到 llvm-addr2line,无法执行地址解析。", file=sys.stderr)
88+ sys.exit(1)
89+ try:
90+ result = subprocess.run([path] + args, capture_output=True, text=True)
91+ if result.stdout:
92+ print(result.stdout, end="")
93+ if result.stderr:
94+ print(result.stderr, end="", file=sys.stderr)
95+ sys.exit(result.returncode)
96+ except OSError as e:
97+ print(f"[ERROR] 调用 llvm-addr2line 失败: {e}", file=sys.stderr)
98+ sys.exit(1)
99+ 
100+ path, code = find_addr2line()
101+ if path:
102+ print(path)
103+ sys.exit(0)
104+ 
105+ sdk_rel_path = os.path.join("sdk", "default", "openharmony", "native", "llvm", "bin")
106+ plat = platform.system()
107+ tool_name = "llvm-addr2line.exe" if plat == "Windows" else "llvm-addr2line"
108+ 
109+ print("[ERROR] 未找到 llvm-addr2line。", file=sys.stderr)
110+ print("", file=sys.stderr)
111+ print("请确保 DevEco Studio 已安装且 HarmonyOS SDK 已配置。", file=sys.stderr)
112+ print(f"期望路径:<DEVECO_HOME>/{sdk_rel_path}/{tool_name}", file=sys.stderr)
113+ print("", file=sys.stderr)
114+ print("提示:可设置 DEVECO_HOME 环境变量指向 DevEco Studio 安装目录。", file=sys.stderr)
115+ if plat == "Darwin":
116+ print(' macOS 示例: export DEVECO_HOME=/Applications/DevEco-Studio.app/Contents', file=sys.stderr)
117+ elif plat == "Windows":
118+ print(r' Windows 示例: set DEVECO_HOME=C:\Program Files\DevEco Studio', file=sys.stderr)
119+ sys.exit(1)
120+ 
121+ 
122+if __name__ == "__main__":
123+ main()
@@ -0,0 +1,120 @@
1+#!/usr/bin/env python3
2+# Copyright (c) 2021-2026 Huawei Device Co., Ltd.
3+# Licensed under the Apache License, Version 2.0 (the "License");
4+# you may not use this file except in compliance with the License.
5+# You may obtain a copy of the License at
6+#
7+# http://www.apache.org/licenses/LICENSE-2.0
8+#
9+# Unless required by applicable law or agreed to in writing, software
10+# distributed under the License is distributed on an "AS IS" BASIS,
11+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+# See the License for the specific language governing permissions and
13+# limitations under the License.
14+"""
15+match_so.py — 校验本地 .so 文件是否与 faultlog 中的 BuildID 匹配。
16+ 
17+用法:
18+ python match_so.py <so_name> <build_id> [-d <search_dir>]
19+ 
20+示例:
21+ python match_so.py libexample.so f3991876654443a608c9cc71f20cf4d8
22+ python match_so.py libexample.so f3991876654443a608c9cc71f20cf4d8 -d /path/to/symbols
23+ 
24+退出码:
25+ 0 — 找到匹配文件,stdout 输出其绝对路径
26+ 1 — 找到同名文件但 BuildID 不一致
27+ 2 — 未找到同名 .so 文件
28+ 3 — 无法提取 ELF BuildID(工具缺失或文件损坏)
29+"""
30+ 
31+import argparse
32+import os
33+import platform
34+import re
35+import shutil
36+import subprocess
37+import sys
38+ 
39+ 
40+SDK_REL = os.path.join("sdk", "default", "openharmony", "native", "llvm", "bin")
41+ 
42+DEVECO_PATHS = {
43+ "Darwin": [
44+ "/Applications/DevEco-Studio.app/Contents",
45+ os.path.expanduser("~/Applications/DevEco-Studio.app/Contents"),
46+ ],
47+ "Windows": [
48+ r"C:\Program Files\DevEco Studio",
49+ r"C:\Program Files (x86)\DevEco Studio",
50+ ],
51+}
52+ 
53+ 
54+def find_readelf():
55+ """查找 llvm-readelf,优先级:DEVECO_HOME > 已知安装路径 > PATH。"""
56+ plat = platform.system()
57+ name = "llvm-readelf.exe" if plat == "Windows" else "llvm-readelf"
58+ 
59+ for base in [os.environ.get("DEVECO_HOME", "")] + DEVECO_PATHS.get(plat, []):
60+ if not base:
61+ continue
62+ c = os.path.join(base, SDK_REL, name)
63+ if os.path.isfile(c):
64+ return c
65+ 
66+ return shutil.which(name)
67+ 
68+ 
69+def extract_buildid(so_path):
70+ """从 ELF 文件提取 BuildID(小写 hex),失败返回 None。"""
71+ tool = find_readelf()
72+ if not tool:
73+ return None
74+ try:
75+ out = subprocess.run(
76+ [tool, "-n", so_path],
77+ capture_output=True, text=True, timeout=30,
78+ ).stdout
79+ m = re.search(r"Build ID(?:\[sha1\])?\s*[:=]\s*([0-9a-fA-F]+)", out)
80+ return m.group(1).lower() if m else None
81+ except (subprocess.SubprocessError, OSError):
82+ return None
83+ 
84+ 
85+def search_so(so_name, root_dir):
86+ """递归搜索同名 .so,返回路径列表。"""
87+ results = []
88+ for dirpath, _, filenames in os.walk(root_dir):
89+ if so_name in filenames:
90+ results.append(os.path.join(dirpath, so_name))
91+ return results
92+ 
93+ 
94+def main():
95+ """程序入口:解析参数,查找同名 .so 并匹配 BuildID,输出匹配文件路径。"""
96+ parser = argparse.ArgumentParser(description=__doc__.split("\n")[1].strip())
97+ parser.add_argument("so_name", help="栈帧中的 SO 文件名,如 libexample.so")
98+ parser.add_argument("build_id", help="栈帧中记录的 BuildID(hex)")
99+ parser.add_argument("-d", "--dir", default=os.getcwd(), help="搜索根目录(默认当前目录)")
100+ args = parser.parse_args()
101+ 
102+ expected = args.build_id.lower()
103+ found = search_so(args.so_name, args.dir)
104+ 
105+ if not found:
106+ sys.exit(2)
107+ 
108+ for path in found:
109+ actual = extract_buildid(path)
110+ if actual is None:
111+ sys.exit(3)
112+ if actual == expected:
113+ print(path)
114+ sys.exit(0)
115+ 
116+ sys.exit(1)
117+ 
118+ 
119+if __name__ == "__main__":
120+ main()