已合并
feat: 新增 LLVM 工具自动发现脚本与步骤零输入校验门禁 #39
feat: 新增 LLVM 工具自动发现脚本与步骤零输入校验门禁 #39
已合并
fangzy0823创建于 14 天前
7 个文件变更+315-5
@@ -51,6 +51,16 @@ metadata:
514. 检查输入是否存在、是否可读。若脚本未识别日志,直接检查原始文件中的 `Reason:`514. 检查输入是否存在、是否可读。若脚本未识别日志,直接检查原始文件中的 `Reason:`
52 `Fault thread info:``Reason:GWP-ASAN``*** GWP-ASan detected a memory error ***`52 `Fault thread info:``Reason:GWP-ASAN``*** GWP-ASan detected a memory error ***`
53 53 
54+5. 日志类型校验:脚本在收集阶段自动识别 CppCrash FaultLog——文件名以 `cppcrash-` 开头,
55+ 或内容含 `Reason:Signal:` / `Reason:GWP-ASAN` / `*** GWP-ASan detected a memory error ***`
56+ 不符合上述特征的日志不在本技能范围。
57+ 若脚本提示"未在 ... 中找到 cppcrash 日志",停止分析并提示用户:
58+ "该日志不是 CppCrash 故障日志,无法使用本技能分析。请提供 Native 层崩溃(CppCrash)的 FaultLog。"
59+ 
60+6. 堆栈信息校验:脚本在解析后检查日志是否含可分析的调用栈——`Fault thread info:` 区段下
61+ `#NN pc` 格式帧,或 GWP-ASan 报告中的 `#N 0xADDR` 格式帧。两者皆无时停止分析并提示用户:
62+ "该 CppCrash 日志缺少 Native 调用栈信息,无法定位根因。请提供包含完整调用栈的 FaultLog。"
63+ 
54### 步骤一:提取关键日志64### 步骤一:提取关键日志
55 65 
56完整阅读脚本输出,并在以下情况回读原始日志:66完整阅读脚本输出,并在以下情况回读原始日志:
@@ -163,16 +173,18 @@ reference 发生冲突时,以证据等级更高的规则为准。模块栈模
163 173 
164仅在材料齐全时执行,不得将其写成无条件必选步骤:174仅在材料齐全时执行,不得将其写成无条件必选步骤:
165 175 
176+`ohos_llvm_tools.py` 根据环境变量(`DEVECO_HOME` / `DEVECO_SDK_HOME` / `OHOS_SDK`)自动定位 `llvm-addr2line``llvm-objdump`;未找到时按报错提示设置上述任一环境变量或把工具加入系统 `PATH`
177+ 
1661. 有匹配 BuildID 的 `.so` 和符号时运行:1781. 有匹配 BuildID 的 `.so` 和符号时运行:
167 179 
168 ```bash180 ```bash
169- llvm-addr2line -pCfie "<so文件>" "<pc相对偏移>"181+ python "<skill-root>/scripts/ohos_llvm_tools.py" addr2line "<so文件>" "<pc相对偏移>"
170 ```182 ```
171 183 
1722. 行号仍不足以判断多参数调用、虚表、函数指针或访存寄存器时运行:1842. 行号仍不足以判断多参数调用、虚表、函数指针或访存寄存器时运行:
173 185 
174 ```bash186 ```bash
175- llvm-objdump -dS -l -C "<so文件>" > "<so文件>.objdump"187+ python "<skill-root>/scripts/ohos_llvm_tools.py" objdump "<so文件>" [输出目录]
176 ```188 ```
177 189 
1783. 用户提供源码后,结合行号上下文检查空值、边界、所有权、线程和回调时序。1903. 用户提供源码后,结合行号上下文检查空值、边界、所有权、线程和回调时序。
@@ -221,7 +221,7 @@ by thread <tid> here:
221|--------|----------|221|--------|----------|
222| 缺少释放栈 | 标注无法确认释放位置;建议补充完整 GWP-ASan 日志或检查 frame pointer |222| 缺少释放栈 | 标注无法确认释放位置;建议补充完整 GWP-ASan 日志或检查 frame pointer |
223| 缺少申请栈 | 标注无法确认对象创建来源;仍可基于违规访问栈和错误类型定性 |223| 缺少申请栈 | 标注无法确认对象创建来源;仍可基于违规访问栈和错误类型定性 |
224-| 缺少符号 | 输出 `.so+offset`,建议提供符号文件用 `llvm-addr2line` 解析 |224+| 缺少符号 | 输出 `.so+offset`,建议提供符号文件用 `ohos_llvm_tools.py addr2line` 解析 |
225| 只有裸地址 | 结论置信度降级;要求补充 maps / so / BuildID |225| 只有裸地址 | 结论置信度降级;要求补充 maps / so / BuildID |
226| 32 位进程栈不完整 | 标注 GWP-ASan 对 32 位进程分配/释放栈支持可能不足 |226| 32 位进程栈不完整 | 标注 GWP-ASan 对 32 位进程分配/释放栈支持可能不足 |
227 227 
@@ -42,6 +42,15 @@ def build_report(log_path: str, analyze_all: bool = False) -> str:
42 reports = []42 reports = []
43 for index, path in enumerate(selected, start=1):43 for index, path in enumerate(selected, start=1):
44 crash = parse_crash_log(path)44 crash = parse_crash_log(path)
45+ has_gwp_asan_frames = bool(crash.gwp_asan_text) and any(
46+ line.strip().startswith('#') and '0x' in line
47+ for line in crash.gwp_asan_text.splitlines()
48+ )
49+ if not crash.fault_stack.frames and not has_gwp_asan_frames:
50+ raise ValueError(
51+ f'该日志缺少 Native 调用栈'
52+ f'(无 #NN pc 帧且 GWP-ASan 报告无调用栈),无法分析: {path.name}'
53+ )
45 if analyze_all and len(selected) > 1:54 if analyze_all and len(selected) > 1:
46 reports.append(f'【日志 {index}/{len(selected)}{path.name}】')55 reports.append(f'【日志 {index}/{len(selected)}{path.name}】')
47 other_logs = found[1:] if not analyze_all and index == 1 else []56 other_logs = found[1:] if not analyze_all and index == 1 else []
@@ -0,0 +1,279 @@
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+"""OHOS LLVM 工具查找与执行脚本。
16+ 
17+根据环境变量(DEVECO_HOME / DEVECO_CLI_STUDIO_PATH / DEVECO_SDK_HOME / DEVECO_CLI_CLT_PATH / OHOS_SDK)自动搜索
18+llvm-addr2line 和 llvm-objdump,并执行符号解析或反汇编。
19+ 
20+用法::
21+ 
22+ # 地址解析
23+ python ohos_llvm_tools.py addr2line <so_file> <offset>
24+ 
25+ # 反汇编
26+ python ohos_llvm_tools.py objdump <so_file> [output_dir]
27+ 
28+搜索策略:
29+ 1. {DEVECO_HOME}/sdk/default/openharmony/native/llvm/bin
30+ 2. {DEVECO_CLI_STUDIO_PATH}/sdk/default/openharmony/native/llvm/bin
31+ 3. {DEVECO_SDK_HOME}/default/openharmony/native/llvm/bin
32+ 4. {DEVECO_CLI_CLT_PATH}/sdk/default/openharmony/native/llvm/bin
33+ 5. {OHOS_SDK}/{platform}/native/llvm/bin 或 {OHOS_SDK}/native/llvm/bin
34+ 6. 从以上各环境变量的根目录开始递归模糊搜索(优先 llvm/bin
35+ 7. 回退到系统 PATH(shutil.which)
36+"""
37+ 
38+import argparse
39+import os
40+import platform
41+import shutil
42+import subprocess
43+import sys
44+from pathlib import Path
45+from typing import List, Optional, Tuple
46+ 
47+# 用于定位 OHOS LLVM 工具的环境变量(按优先级排列)
48+_ENV_VARS = ('DEVECO_HOME', 'DEVECO_CLI_STUDIO_PATH', 'DEVECO_SDK_HOME', 'DEVECO_CLI_CLT_PATH', 'OHOS_SDK')
49+ 
50+# 递归模糊搜索时跳过的目录名(大小写不敏感),避免遍历无关大目录
51+_SKIP_DIRS = frozenset({
52+ 'node_modules', '.git', '.gradle', '.cxx', 'caches', 'temp',
53+ '.idea', '__pycache__', '.hg', '.svn', 'build-cache',
54+})
55+ 
56+ 
57+def _exe_suffix() -> str:
58+ """Windows 下可执行文件带 .exe 后缀。"""
59+ return '.exe' if sys.platform == 'win32' else ''
60+ 
61+ 
62+def _platform_dir() -> str:
63+ """返回当前系统的 OHOS SDK 平段子目录名。"""
64+ system = platform.system().lower()
65+ return {'windows': 'windows', 'linux': 'linux', 'darwin': 'darwin'}.get(system, system)
66+ 
67+ 
68+def _env_roots() -> List[Path]:
69+ """收集所有已设置且存在环境变量的根目录。"""
70+ roots = []
71+ for name in _ENV_VARS:
72+ value = os.environ.get(name, '').strip()
73+ if value:
74+ root = Path(value)
75+ if root.is_dir():
76+ roots.append(root)
77+ return roots
78+ 
79+ 
80+def _direct_bin_candidates() -> List[Path]:
81+ """根据已知目录结构构造候选 bin 目录列表。
82+ 
83+ 依次对应各 SDK 布局(按优先级):
84+ - DEVECO_HOME: <root>/sdk/default/openharmony/native/llvm/bin
85+ - DEVECO_CLI_STUDIO_PATH: <root>/sdk/default/openharmony/native/llvm/bin
86+ - DEVECO_SDK_HOME: <root>/default/openharmony/native/llvm/bin
87+ - DEVECO_CLI_CLT_PATH: <root>/sdk/default/openharmony/native/llvm/bin
88+ - OHOS_SDK: <root>/<platform>/native/llvm/bin (独立 OHOS SDK)
89+ <root>/native/llvm/bin (command-line-tools)
90+ """
91+ candidates = []
92+ deveco_home = os.environ.get('DEVECO_HOME', '').strip()
93+ if deveco_home:
94+ candidates.append(
95+ Path(deveco_home) / 'sdk' / 'default' / 'openharmony' / 'native' / 'llvm' / 'bin')
96+ deveco_cli_studio = os.environ.get('DEVECO_CLI_STUDIO_PATH', '').strip()
97+ if deveco_cli_studio:
98+ candidates.append(
99+ Path(deveco_cli_studio) / 'sdk' / 'default' / 'openharmony' / 'native' / 'llvm' / 'bin')
100+ deveco_sdk_home = os.environ.get('DEVECO_SDK_HOME', '').strip()
101+ if deveco_sdk_home:
102+ candidates.append(
103+ Path(deveco_sdk_home) / 'default' / 'openharmony' / 'native' / 'llvm' / 'bin')
104+ deveco_cli_clt = os.environ.get('DEVECO_CLI_CLT_PATH', '').strip()
105+ if deveco_cli_clt:
106+ candidates.append(
107+ Path(deveco_cli_clt) / 'sdk' / 'default' / 'openharmony' / 'native' / 'llvm' / 'bin')
108+ ohos_sdk = os.environ.get('OHOS_SDK', '').strip()
109+ if ohos_sdk:
110+ sdk_root = Path(ohos_sdk)
111+ # 独立 OHOS SDK 带平台段;command-line-tools 不带平台段,两者均需尝试
112+ candidates.append(sdk_root / _platform_dir() / 'native' / 'llvm' / 'bin')
113+ candidates.append(sdk_root / 'native' / 'llvm' / 'bin')
114+ return candidates
115+ 
116+ 
117+def _find_in_dir(bin_dir: Path, tool: str) -> Optional[Path]:
118+ """检查指定目录下是否存在目标工具(含平台后缀)。"""
119+ candidate = bin_dir / f'{tool}{_exe_suffix()}'
120+ if candidate.is_file():
121+ return candidate
122+ return None
123+ 
124+ 
125+def _collect_fuzzy_candidates(tool: str) -> List[Path]:
126+ """从各环境变量根目录递归模糊搜索,返回所有命中的工具路径。
127+ 
128+ 遍历根目录下所有子目录(跳过 _SKIP_DIRS 中的无关目录),收集名为
129+ `<tool><suffix>` 的文件。同一文件经 resolve 去重。
130+ """
131+ suffix = _exe_suffix()
132+ target = f'{tool}{suffix}'
133+ matches: List[Path] = []
134+ seen = set()
135+ for root in _env_roots():
136+ for current, dirnames, filenames in os.walk(root):
137+ # 剪枝:原地修改 dirnames 阻止 os.walk 进入无关目录
138+ dirnames[:] = [d for d in dirnames if d.lower() not in _SKIP_DIRS]
139+ if target not in filenames:
140+ continue
141+ path = (Path(current) / target).resolve()
142+ if path in seen:
143+ continue
144+ seen.add(path)
145+ matches.append(path)
146+ return matches
147+ 
148+ 
149+def _rank_key(path: Path) -> Tuple[int, int, str]:
150+ """模糊搜索结果排序键:llvm/bin 路径优先,其次路径更浅,最后按字典序。"""
151+ parts = [p.lower() for p in path.parent.parts]
152+ is_llvm_bin = 'llvm' in parts and 'bin' in parts
153+ return (0 if is_llvm_bin else 1, len(parts), str(path).lower())
154+ 
155+ 
156+def find_llvm_tool(tool: str) -> Optional[Path]:
157+ """查找 OHOS LLVM 工具。
158+ 
159+ 依次按已知路径、递归模糊搜索、系统 PATH 回退,返回首个命中的可执行文件路径。
160+ """
161+ # 1. 已知路径候选
162+ for bin_dir in _direct_bin_candidates():
163+ found = _find_in_dir(bin_dir, tool)
164+ if found:
165+ return found
166+ 
167+ # 2. 递归模糊搜索
168+ fuzzy = _collect_fuzzy_candidates(tool)
169+ if fuzzy:
170+ fuzzy.sort(key=_rank_key)
171+ return fuzzy[0]
172+ 
173+ # 3. 系统 PATH 回退(shutil.which 已处理平台后缀)
174+ which = shutil.which(tool)
175+ return Path(which) if which else None
176+ 
177+ 
178+def run_addr2line(so_file: Path, offset: str) -> int:
179+ """执行 llvm-addr2line 进行地址解析。
180+ 
181+ 命令: llvm-addr2line -pCfie <so_file> <offset>
182+ 解析结果写入标准输出,工具查找信息写入标准错误,互不污染。
183+ """
184+ tool = find_llvm_tool('llvm-addr2line')
185+ if not tool:
186+ _error('未找到 llvm-addr2line。请设置环境变量 DEVECO_HOME / DEVECO_SDK_HOME / '
187+ 'OHOS_SDK,或将 llvm-addr2line 加入系统 PATH。')
188+ return 1
189+ if not so_file.is_file():
190+ _error(f'.so 文件不存在: {so_file}')
191+ return 1
192+ _info(f'使用工具: {tool}')
193+ cmd = [str(tool), '-pCfie', str(so_file), offset]
194+ return _run(cmd)
195+ 
196+ 
197+def run_objdump(so_file: Path, output_dir: Optional[Path] = None) -> int:
198+ """执行 llvm-objdump 进行反汇编。
199+ 
200+ 命令: llvm-objdump -dS -l -C <so_file> > <so_name>.objdump
201+ 反汇编结果写入 <output_dir>/<so_name>.objdump,默认输出到当前目录。
202+ """
203+ tool = find_llvm_tool('llvm-objdump')
204+ if not tool:
205+ _error('未找到 llvm-objdump。请设置环境变量 DEVECO_HOME / DEVECO_SDK_HOME / '
206+ 'OHOS_SDK,或将 llvm-objdump 加入系统 PATH。')
207+ return 1
208+ if not so_file.is_file():
209+ _error(f'.so 文件不存在: {so_file}')
210+ return 1
211+ out_dir = output_dir or Path.cwd()
212+ out_dir.mkdir(parents=True, exist_ok=True)
213+ out_file = out_dir / f'{so_file.name}.objdump'
214+ print(f'使用工具: {tool}')
215+ print(f'输出文件: {out_file}')
216+ cmd = [str(tool), '-dS', '-l', '-C', str(so_file)]
217+ with open(out_file, 'w', encoding='utf-8', errors='replace') as stdout:
218+ return _run(cmd, stdout=stdout)
219+ 
220+ 
221+def _run(cmd: List[str], stdout=None) -> int:
222+ """运行外部命令并返回退出码;标准输出/错误直接继承当前进程。"""
223+ try:
224+ proc = subprocess.run(cmd, stdout=stdout)
225+ except OSError as exc:
226+ _error(f'执行失败: {exc}')
227+ return 1
228+ if proc.returncode != 0:
229+ _error(f'命令返回非零退出码 {proc.returncode}')
230+ return proc.returncode
231+ 
232+ 
233+def _info(msg: str) -> None:
234+ print(msg, file=sys.stderr)
235+ 
236+ 
237+def _error(msg: str) -> None:
238+ print(f'错误: {msg}', file=sys.stderr)
239+ 
240+ 
241+def _configure_output_encoding() -> None:
242+ """统一正常输出、帮助和错误信息的编码。"""
243+ for stream in (sys.stdout, sys.stderr):
244+ if hasattr(stream, 'reconfigure'):
245+ stream.reconfigure(encoding='utf-8', errors='replace')
246+ 
247+ 
248+def main() -> int:
249+ _configure_output_encoding()
250+ parser = argparse.ArgumentParser(
251+ description='OHOS LLVM 工具查找与执行:自动搜索 llvm-addr2line / llvm-objdump 并执行。',
252+ formatter_class=argparse.RawDescriptionHelpFormatter)
253+ sub = parser.add_subparsers(dest='command', metavar='<command>')
254+ 
255+ a2l = sub.add_parser('addr2line', help='运行 llvm-addr2line 解析地址(-pCfie)')
256+ a2l.add_argument('so_file', help='.so 文件路径')
257+ a2l.add_argument('offset', help='pc 相对偏移(十六进制,可带 0x 前缀)')
258+ 
259+ od = sub.add_parser('objdump', help='运行 llvm-objdump 反汇编(-dS -l -C)')
260+ od.add_argument('so_file', help='.so 文件路径')
261+ od.add_argument('output_dir', nargs='?', default=None,
262+ help='输出目录(默认当前目录),生成 <so文件名>.objdump')
263+ 
264+ args = parser.parse_args()
265+ if not args.command:
266+ parser.print_help()
267+ return 2
268+ 
269+ if args.command == 'addr2line':
270+ return run_addr2line(Path(args.so_file), args.offset)
271+ if args.command == 'objdump':
272+ out_dir = Path(args.output_dir) if args.output_dir else None
273+ return run_objdump(Path(args.so_file), output_dir=out_dir)
274+ parser.print_help()
275+ return 2
276+ 
277+ 
278+if __name__ == '__main__':
279+ sys.exit(main())
@@ -119,7 +119,7 @@ def _stack_overflow_line(crash: CppCrashLog) -> List[str]:
119def _render_fault_stack(crash: CppCrashLog) -> str:119def _render_fault_stack(crash: CppCrashLog) -> str:
120 lines = [_title('崩溃线程堆栈'),120 lines = [_title('崩溃线程堆栈'),
121 '说明:调用方向从栈底(最大编号)到栈顶(#00);'121 '说明:调用方向从栈底(最大编号)到栈顶(#00);'
122- '各帧保留 pc 相对偏移,可用 llvm-addr2line -pCfie <so> <偏移> 解析行号']122+ '各帧保留 pc 相对偏移,可用 ohos_llvm_tools.py addr2line <so> <偏移> 解析行号']
123 if not crash.fault_stack.frames:123 if not crash.fault_stack.frames:
124 lines.append('未解析到崩溃线程堆栈')124 lines.append('未解析到崩溃线程堆栈')
125 return '\n'.join(lines)125 return '\n'.join(lines)
@@ -188,7 +188,7 @@ python 01-fault-analysis/cppcrash-analysis/scripts/extract_hilog.py <faultlog文
1884. **按需知识加载**:根据 `.so`、函数和日志证据,仅加载匹配的 ArkUI、ArkData、ArkWeb、JSRuntime、RenderService、JSVM、Text、内存破坏或多线程 reference。1884. **按需知识加载**:根据 `.so`、函数和日志证据,仅加载匹配的 ArkUI、ArkData、ArkWeb、JSRuntime、RenderService、JSVM、Text、内存破坏或多线程 reference。
1895. **调用栈分层**:分别输出崩溃帧、首个非运行时调用方和首个应用侧帧,不把运行时帧直接作为业务根因。1895. **调用栈分层**:分别输出崩溃帧、首个非运行时调用方和首个应用侧帧,不把运行时帧直接作为业务根因。
1906. **证据链分析**:联合信号、故障地址、寄存器、指令、Maps、对象生命周期和线程证据判断第一现场及可信度。1906. **证据链分析**:联合信号、故障地址、寄存器、指令、Maps、对象生命周期和线程证据判断第一现场及可信度。
191-7. **可选深入分析**:仅在符号、二进制或源码可用时执行 `llvm-addr2line``llvm-objdump` 和源码分析;HiLog 只作时序辅助证据。191+7. **可选深入分析**:仅在符号、二进制或源码可用时通过 `ohos_llvm_tools.py`(自动定位 `llvm-addr2line`/`llvm-objdump`)执行符号解析、反汇编和源码分析;HiLog 只作时序辅助证据。
1928. **报告生成**:使用 Markdown 表格输出三级故障分类、代码根因、关键证据、责任归属、可信度、竞争解释和修复建议,不输出内部故障编码。1928. **报告生成**:使用 Markdown 表格输出三级故障分类、代码根因、关键证据、责任归属、可信度、竞争解释和修复建议,不输出内部故障编码。
193 193 
194***194***
@@ -1,5 +1,15 @@
1# 更新日志1# 更新日志
2 2 
3+## [Unreleased] - 2026-08-26
4+ 
5+### 新增
6+- 新增 `scripts/ohos_llvm_tools.py`:按 `DEVECO_HOME` / `DEVECO_CLI_STUDIO_PATH` / `DEVECO_SDK_HOME` / `DEVECO_CLI_CLT_PATH` / `OHOS_SDK` 环境变量优先级链自动定位并执行 `llvm-addr2line``-pCfie`)和 `llvm-objdump``-dS -l -C`),替代直接调用裸命令。
7+- 新增步骤零输入校验:`scripts/main.py` `build_report` 解析后检查 `fault_stack.frames``#NN pc` 帧)或 `gwp_asan_text` 中的真实帧行(`#N 0xADDR`),两者皆空时拒绝分析并提示用户提供完整调用栈。
8+ 
9+### 变更
10+- `SKILL.md` 步骤四、`scripts/report.py` 堆栈提示与 `references/gwp_asan.md` 缺失信息表中的 `llvm-addr2line`/`llvm-objdump` 裸命令改为调用 `ohos_llvm_tools.py`
11+- `SKILL.md` 步骤零新增 items 5-6:描述脚本日志类型识别与堆栈校验逻辑,以及脚本报错后停止分析并提示用户的沟通策略。
12+ 
3## [1.3.0] - 2026-09-0113## [1.3.0] - 2026-09-01
4 14 
5### 变更15### 变更