diff --git a/lib/Projectrans/Config.py b/lib/Projectrans/Config.py index f3d7af0..23225e5 100644 --- a/lib/Projectrans/Config.py +++ b/lib/Projectrans/Config.py @@ -149,6 +149,7 @@ def main() -> None: parser.add_argument('--clear-cache', action='store_true', help='清除 .transpyc_cache 全局缓存(.vpi/.stub.ll/.doc.json)') parser.add_argument('--asan', action='store_true', help='ASan 模式:对全部 .ll 做 opt -passes=asan 插桩,并显式链接 clang64 的 ASan 运行库(mingw64 的 clang 没有 x86_64-w64-windows-gnu 目标的 ASan runtime,不能用 -fsanitize=address)') parser.add_argument('--dwarf', action='store_true', help='函数级 DWARF 调试元数据:每模块一份 DICompileUnit/DIFile,每个 define 一份 DISubprogram 并挂 !dbg,函数入口第一条指令挂 DILocation。使 ASan/llvm-symbolizer 能把崩溃地址反查为 "<文件>.vp:函数起始行"(默认关闭;--asan 时自动开启)。开启时强制重译 App 模块(不复用不含 !llvm.dbg.cu 的旧 .ll/.obj),.ll/.obj 体积与编译时间略增') + parser.add_argument('--ctrlc-stack', action='store_true', help='Ctrl+C 调用栈打印(仅 Windows/PE):链接完成后用 llvm-dwarfdump 把最终 exe 的 .debug_line/.debug_info 导出为 ".stackmap" 地址→源行旁文件(小端定长格式),供 exe 内运行时在 Ctrl+C/Ctrl+Break 时把 CRT 到当前的返回地址逐帧翻译为 "<文件>.vp:行/函数"。会自动开启 DWARF(同 --asan,否则行表为空);找不到 llvm-dwarfdump / 调用失败 / 解析不到任何行记录 / 目标不是 Windows PE 时直接报错终止编译') parser.add_argument('--nowarn-register', action='store_true', help='抑制"外部函数返回类型未登记"类 WARN 输出(透传给自举编译的 TransPyV.exe)') parser.add_argument('--hotspot-sites', nargs='?', const='', default=None, help='调用点热点统计:为每个匹配白名单的直接 call 点插入计数器,运行时 Ctrl+C 各模块按真实次数降序写入 (默认为 hotspot_sites.txt);同时为每个函数定义加计时器,按自身耗时(self)降序写入同目录 hotspot_time.txt,行为 `<函数名> <进入次数>`(单位 tick,x86_64 上为序列化 lfence; rdtsc);self 已扣除被插桩子调用的 inc,但不含未插桩被调方(间接调用、strcmp/memcpy 等无定义外部函数),它们计入调用者的 self') parser.add_argument('--hotspot-callees', default=None, help='--hotspot-sites 的白名单被调函数基名(逗号分隔),默认内存/字符串族 strcmp,strncmp,strchr,strlen,strstr,memcpy,memset,memmove,strcpy,strcat') @@ -162,14 +163,23 @@ def main() -> None: if args.asan: _vlog().info('ASan 模式已启用(opt -passes=asan + clang64 libclang_rt.asan_dynamic*)', 'asan') + # Ctrl+C 调用栈(--ctrlc-stack):链接后用 llvm-dwarfdump 导出 .stackmap。 + # 不在此处做工具/平台校验——真正的 fail-fast 落在链接成功后的旁文件导出步骤 + # (那里才有最终 exe 路径可判 PE)。此处仅提示已启用。 + if args.ctrlc_stack: + _vlog().info('Ctrl+C 调用栈模式已启用(链接后用 llvm-dwarfdump 导出 .stackmap)', 'ctrlc-stack') + # 函数级 DWARF 调试元数据(--dwarf):默认关闭(硬要求——普通构建不承担 # 任何风险,生成的 .ll 与改动前逐字节一致)。--asan 时自动开启:ASan 报告 # 本身就需要行号才能定位,且 ASan 构建已是显式选择的非默认路径。 - dwarf_enabled: bool = bool(args.dwarf or args.asan) + # --ctrlc-stack 同理自动开启:旁文件的"地址→源行"完全来自 DWARF 行表, + # 没有 DI 元数据时 .debug_line 为空、旁文件必为空。 + dwarf_enabled: bool = bool(args.dwarf or args.asan or args.ctrlc_stack) if dwarf_enabled: + _auto_src: str = '--asan' if args.asan else ('--ctrlc-stack' if args.ctrlc_stack else '') _vlog().info( '函数级 DWARF 调试元数据已启用:每个 .vp 模块一份 DICompileUnit + 每函数 DISubprogram/DILocation' - + ('(由 --asan 自动开启)' if not args.dwarf else ''), + + (f'(由 {_auto_src} 自动开启)' if _auto_src and not args.dwarf else ''), 'dwarf') project_file: str | None = args.project @@ -339,7 +349,7 @@ def main() -> None: except: pass _vlog().info(f"includes.binary: 已清空 {precompiled_dir} ({cleared} 个文件)") - trans: Phase2Translator = Phase2Translator(src, temp_dir, output_dir, compile_cmd=cc_cmd, compile_flags=cc_flags, linker_cmd=linker_cmd, linker_flags=linker_flags, linker_output=linker_output, static_libs=static_libs, dynamic_libs=dynamic_libs, include_dirs=project_include_dirs, target_triple=target_triple, target_datalayout=target_datalayout, force_recompile_includes=force_recompile_includes, asan_mode=args.asan, dwarf_enabled=dwarf_enabled) + trans: Phase2Translator = Phase2Translator(src, temp_dir, output_dir, compile_cmd=cc_cmd, compile_flags=cc_flags, linker_cmd=linker_cmd, linker_flags=linker_flags, linker_output=linker_output, static_libs=static_libs, dynamic_libs=dynamic_libs, include_dirs=project_include_dirs, target_triple=target_triple, target_datalayout=target_datalayout, force_recompile_includes=force_recompile_includes, asan_mode=args.asan, dwarf_enabled=dwarf_enabled, ctrlc_stack_enabled=args.ctrlc_stack) trans.sha1_map = Load_sha1_map(temp_dir) trans.slice_level = slice_level # 调用点热点统计与每函数计时(--hotspot-sites):None=未启用(零影响) diff --git a/lib/Projectrans/Phase2Translator.py b/lib/Projectrans/Phase2Translator.py index 3bb9fd5..8e7bd20 100644 --- a/lib/Projectrans/Phase2Translator.py +++ b/lib/Projectrans/Phase2Translator.py @@ -16,7 +16,8 @@ from lib.core.VLogger import get_logger as _vlog from lib.constants.config import mode as _config_mode from lib.core.Handles.HandlesBase import CTypeInfo from lib.includes import t -from lib.core.SymbolUtils import AnnotationContainsName +from lib.core.SymbolUtils import AnnotationContainsName, ExpandSoftKeywords +from lib.core.SymbolUtils import ClassHasFuncLevelCVTable, IsVtableSlotMethod from lib.Projectrans.Utils import compute_sha1, _check_annotation_for_export, find_reachable_files_from_entries, topological_sort_files, parse_python_file from lib.Projectrans.Phase1Generator import _TryGlobalCache, _SaveToGlobalCache, _GLOBAL_CACHE_DIR import TransPyC @@ -30,6 +31,13 @@ from lib.core.LLVMCG.IRMetadataFix import SanitizeIRMetadata _FILE_STAT_CACHE: dict[str, bool] = {} +# --ctrlc-stack(Ctrl+C 调用栈)强制纳入编译+链接的 include 模块源文件名。 +# 该模块对外只有 Install() 入口,且无任何用户代码 import 它 —— 若不强制纳入, +# 产物里既没有 Install 符号、也没有调用它的启动期构造器,功能静默失效。 +# .vp 优先,.py 仅为向后兼容的回退名。 +_CTRLC_STACK_MODULE_FILES: tuple[str, ...] = ('dbgstack.vp', 'dbgstack.py') + + def _CachedIsFile(path: str) -> bool: r: bool | None = _FILE_STAT_CACHE.get(path) if r is None: @@ -72,6 +80,245 @@ def _FindAsanRuntimeDir() -> str | None: return found[-1] +# ---------- --ctrlc-stack:地址→源行 旁文件(.stackmap)---------- +# +# 开启 --ctrlc-stack 时,链接成功后用 llvm-dwarfdump 解析最终 PE 的 +# .debug_line/.debug_info,把已有的"函数级/语句级 DWARF 行标记"导出成 +# .stackmap,供 exe 内运行时处理器在 Ctrl+C/Ctrl+Break 时把 CRT 到当前 +# 的返回地址逐帧翻译回 ".vp:行/函数"。以下函数只在开关开启时被调用;未开启时 +# 不引入任何新依赖、任何新行为。 + +_PE_MAGIC: bytes = b'PE\0\0' + + +def _ReadPEImageLayout(exe_path: str) -> tuple[int, int, dict[str, tuple[int, int]]] | None: + """读取 PE 的 (ImageBase, SizeOfImage, 节表) ;非 PE / 解析失败返回 None。 + + 节表为 {节名: (节 RVA, 节 VirtualSize)}。写端需要这三样才能把 DWARF 地址严格 + 归一到"运行期 RVA"(运行期 dbgstack.vp 用 `帧地址 − GetModuleHandleA(NULL)`): + * 合法行 ⇔ 地址落在映像 VA 区间 [ImageBase, ImageBase + SizeOfImage); + * "运行期可能出现的 RVA 区间"的粗判据 = [.text 节 RVA, SizeOfImage)。 + (原 _ReadPEImageBase 只返回 ImageBase,不足以做上述判定,故扩展为本函数。) + """ + import struct + try: + with open(exe_path, 'rb') as f: + head: bytes = f.read(0x1000) + except Exception: + return None + if len(head) < 0x40 or head[:2] != b'MZ': + return None + e_lfanew: int = struct.unpack_from(' len(head) or head[e_lfanew:e_lfanew + 4] != _PE_MAGIC: + return None + coff_off: int = e_lfanew + 4 + n_sections: int = struct.unpack_from(' len(head): + return None + magic: int = struct.unpack_from(' len(head): + break + name: str = head[base:base + 8].split(b'\x00', 1)[0].decode('ascii', 'replace') + sections[name] = (struct.unpack_from(' list[tuple[int, str, int]]: + """从 llvm-dwarfdump 输出解析出 [(地址, 文件绝对路径, 行号)]。 + + 逐个 CU(debug_line[0x..])重建目录/文件表;行记录过滤掉 line==0 的 + end_sequence 行。文件路径优先用文件表里的绝对 name;name 为相对时按 dir_index + 指向的目录拼接(DWARF v5 取 directory_names,v4 取 include_directories)。 + + ⚠ 地址口径:**原样返回 dwarfdump 打印的地址**(本例为含 ImageBase 的 VA + 0x14000xxxx,但被链接器按 COMDAT 去重丢弃的泛型副本其地址恰为 0)。 + 本函数**不**做"整表 VA/RVA"的判定——那正是旧实现失真的根因(全局最小值一旦为 + 0,整表都不减基址)。归一化(逐条 `地址 − ImageBase`,并丢弃不能归一的条目) + 由调用方 `_WriteStackMap` 依 PE.ImageBase/SizeOfImage 完成。 + """ + rows: list[tuple[int, str, int]] = [] + dirs: dict[int, str] = {} + files: dict[int, tuple[str, int]] = {} + pending_kind: str | None = None + pending_idx: int = -1 + pending_name: str | None = None + pending_dir_index: int = 0 + in_line_section: bool = False + + def _Unquote(text: str) -> str: + text = text.strip() + if len(text) >= 2 and text[0] == '"' and text[-1] == '"': + text = text[1:-1] + return text + + def _Commit() -> None: + nonlocal pending_kind, pending_name, pending_dir_index + if pending_kind == 'file' and pending_name is not None: + files[pending_idx] = (pending_name, pending_dir_index) + pending_kind = None + pending_name = None + pending_dir_index = 0 + + def _Resolve(idx: int) -> str: + entry = files.get(idx) + if entry is None: + return '??' + name, dir_index = entry + if os.path.isabs(name): + return name + base: str = dirs.get(dir_index, '') + if base and not os.path.isabs(base): + root: str = dirs.get(0, '') + if root and os.path.isabs(root): + base = os.path.join(root, base) + return os.path.normpath(os.path.join(base, name)) if base else name + + for line in dump_text.splitlines(): + stripped: str = line.strip() + if not stripped: + continue + if 'debug_line contents' in stripped: + in_line_section = True + continue + if 'contents:' in stripped and 'debug_' in stripped: + in_line_section = False + continue + if not in_line_section: + continue + if stripped.startswith('debug_line['): + _Commit() + dirs = {} + files = {} + continue + m = _DWARF_FILENAMES_RE.match(line) + if m: + _Commit() + pending_kind = 'file' + pending_idx = int(m.group(1)) + continue + m = _DWARF_DIRNAMES_RE.match(line) + if m: + _Commit() + value: str = _Unquote(m.group(2)) + if value: + dirs[int(m.group(1))] = value + else: + pending_kind = 'dir' + pending_idx = int(m.group(1)) + continue + m = _DWARF_NAME_RE.match(line) + if m and pending_kind is not None: + name: str = _Unquote(m.group(1)) + if pending_kind == 'file': + pending_name = name + else: + dirs[pending_idx] = name + pending_kind = None + continue + m = _DWARF_DIRINDEX_RE.match(line) + if m and pending_kind == 'file': + pending_dir_index = int(m.group(1)) + continue + if _DWARF_LINE_HEADER_RE.match(line): + _Commit() + continue + m = _DWARF_LINE_ROW_RE.match(line) + if m: + _Commit() + row_line: int = int(m.group(2)) + if row_line > 0: + rows.append((int(m.group(1), 16), _Resolve(int(m.group(4))), row_line)) + return rows + + +def _ParseDwarfSubprograms(dump_text: str) -> list[tuple[int, int, str]]: + """从 llvm-dwarfdump --debug-info 输出解析 [(low_pc, high_pc, 函数名)]。 + + DW_AT_high_pc 有两种形态且 dwarfdump 都打成 `(0x…)`:`DW_FORM_addr`(绝对 + 结束地址,值 > low_pc)与常量形式(相对 low_pc 的**长度**,值通常 < low_pc)。 + 本函数以 `high_pc <= low_pc` 判定后者并按 `low_pc + 长度` 还原;缺 high_pc 时 + 退化为 [low_pc, low_pc + 1)。 + + ⚠ 地址口径同 `_ParseDwarfLineRows`:**原样返回**,不做整表 VA/RVA 判定;被 + 链接器 COMDAT 去重丢弃的泛型副本其 low_pc 恰为 0(实测 TransPyV.exe 有 756 个), + 由调用方 `_WriteStackMap` 逐条归一化并丢弃。 + """ + result: list[tuple[int, int, str]] = [] + cur_low: int | None = None + cur_high: int | None = None + cur_name: str | None = None + in_prog: bool = False + + def _Flush() -> None: + nonlocal cur_low, cur_high, cur_name, in_prog + if in_prog and cur_low is not None and cur_name: + hi: int = cur_high if cur_high is not None else cur_low + 1 + if hi <= cur_low: + hi = cur_low + cur_high # high_pc 为 offset 形式 + if hi > cur_low: + result.append((cur_low, hi, cur_name)) + cur_low = None + cur_high = None + cur_name = None + in_prog = False + + for line in dump_text.splitlines(): + if _DWARF_TAG_ANY_RE.search(line): + if _DWARF_TAG_SUBPROGRAM_RE.search(line): + _Flush() + in_prog = True + else: + _Flush() + continue + if not in_prog: + continue + m = _DWARF_LOWPC_RE.search(line) + if m: + cur_low = int(m.group(1), 16) + continue + m = _DWARF_HIGHPC_RE.search(line) + if m: + cur_high = int(m.group(1), 16) + continue + m = _DWARF_ATNAME_RE.search(line) + if m and cur_name is None: + cur_name = m.group(1) + _Flush() + return result + + def _PickWorkerCount(n_files: int, per_worker_mb: int = 350) -> int: """按可用物理内存动态限制并行 worker 数,避免多进程编译内存峰值 OOM。 @@ -165,7 +412,9 @@ def _parallel_translate_worker(src_path: str, out_path: str, src_root: str, temp prebuilt_ModuleSha1Map: dict[str, str] | None = None, hotspot_sites_file: str | None = None, hotspot_sites_all: bool = False, - hotspot_callees: list[str] | None = None) -> tuple[str, str, str]: + hotspot_callees: list[str] | None = None, + ctrlc_stack_enabled: bool = False, + dwarf_enabled: bool = False) -> tuple[str, str, str]: # 注意:不可跨任务复用 Phase2Translator/共享符号表!_translate_file 编译过程中 # 会向共享 SymbolTable 写入符号(import insert/类注册等),复用导致前文件的 # 符号污染后文件 → 跨模块 struct 类型前缀错位(如 7cadfe07e983fd90.CompilerFactory @@ -188,6 +437,13 @@ def _parallel_translate_worker(src_path: str, out_path: str, src_root: str, temp trans.hotspot_sites_file = hotspot_sites_file trans.hotspot_sites_all = hotspot_sites_all trans.hotspot_callees = hotspot_callees + # Ctrl+C 调用栈(--ctrlc-stack):worker 子进程无父进程属性,须显式透传 + # (False=未启用,零影响);dbgstack 模块据此注入 Install() 启动期构造器 + trans.ctrlc_stack_enabled = ctrlc_stack_enabled + # 函数级 DWARF(--ctrlc-stack 时 include 模块也必须带行表,否则栈里落到 + # includes/* 的帧取不到源行):worker 子进程无父进程属性,须显式透传 + # (False=未启用,零影响),仅用于 _translate_file 的 dwarf_enabled 形参 + trans.dwarf_enabled = dwarf_enabled if shared_sym_pickle_path and os.path.exists(shared_sym_pickle_path): # 使用 pickle.load(f) 流式加载,避免 pickle.loads(f.read()) 一次性 @@ -215,7 +471,7 @@ def _parallel_translate_worker(src_path: str, out_path: str, src_root: str, temp trans._precollect_inline_symbols() trans._build_shared_symbol_data() - trans._translate_file(src_path, out_path, prebuilt_ModuleSha1Map=prebuilt_ModuleSha1Map) + trans._translate_file(src_path, out_path, prebuilt_ModuleSha1Map=prebuilt_ModuleSha1Map, dwarf_enabled=dwarf_enabled) return ('ok', src_path, '') except Exception as e: # fail-fast(2026-08-18):include 文件编译出错立即终止整个编译进程 @@ -552,6 +808,18 @@ def _count_braces_outside_strings(text: str) -> int: return count +# LLVM IR "无引号标识符"的精确规则(LangRef "Identifiers";实现即 +# LLLexer::ReadVarName):首字符 ∈ [-a-zA-Z$._],后续字符 ∈ [-a-zA-Z$._0-9]。 +# 首字符**不允许是数字**:`$884f3d94ea4d0123.md5_F` 会让 LLLexer::LexDollar +# 走到 ReadVarName() 返回 false ⇒ 产出 error token ⇒ 顶层解析报 +# "expected top-level entity"。故 comdat 名必须以同一规则判定是否加引号 +# (与 LLVM 自身 AsmWriter 的输出形态一致:`$"884f3d94ea4d0123.md5_F"`)。 +# 另:引号名 @"..." 不支持转义,名字含 `"`/`\` 时两种形态都非法 ⇒ 见 +# _AddWeakComdat 内的 fail-fast(不静默替换、不跳过)。 +_LLVM_UNQUOTED_NAME_RE: re.Pattern[str] = re.compile( + r'[-a-zA-Z$._][-a-zA-Z$._0-9]*\Z') + + def _AddWeakComdat(ll_content: str) -> str: """为 weak/linkonce 系列 函数/全局 定义补 COMDAT(COFF/PE 必需)。 @@ -604,7 +872,17 @@ def _AddWeakComdat(ll_content: str) -> str: return ll_content decls: list[str] = [] for name in dict.fromkeys(comdat_names): - if re.match(r'^[0-9A-Za-z_$.-]+$', name): + # fail-fast:LLVM 的引号标识符 @"..." **不支持转义**(LLLexer 读到 `"` 即认为名字 + # 结束),故名字里含 `"` / `\` / 换行时,加不加引号都产不出合法 IR。此处不静默 + # 跳过、也不做字符替换(那会把符号名改得与源码/其它模块不一致,链接期更难查), + # 直接指名报错。名字来自弱符号命名 `.<标识符>`,出现这些字符说明上游符号名 + # 构造有误,应在上游修正。 + if '"' in name or '\\' in name or '\n' in name or '\r' in name: + raise RuntimeError( + f'comdat 名无法表达为 LLVM 标识符(含双引号/反斜杠/换行): {name!r};' + 'LLVM 引号名 @"..." 不支持转义,产出即非法 IR(opt 解析报 ' + '"expected top-level entity")。请在上游符号名构造处修正。') + if _LLVM_UNQUOTED_NAME_RE.match(name): decls.append(f'${name} = comdat any') else: decls.append(f'$"{name}" = comdat any') @@ -636,7 +914,7 @@ class Phase2Translator: _include_sha1_cache: dict[str, str] = {} _include_exist_cache: dict[str, bool] = {} - def __init__(self, src_root: str, temp_dir: str, output_dir: str, compile_cmd: str = 'llc', compile_flags: list[str] | None = None, linker_cmd: str | None = None, linker_flags: list[str] | None = None, linker_output: str | None = None, static_libs: list[str] | None = None, dynamic_libs: list[str] | None = None, include_dirs: list[str] | None = None, entry_files: list[str] | None = None, target_triple: str | None = None, target_datalayout: str | None = None, force_recompile_includes: bool = False, build_pyi_cache: bool = True, asan_mode: bool = False, dwarf_enabled: bool = False) -> None: + def __init__(self, src_root: str, temp_dir: str, output_dir: str, compile_cmd: str = 'llc', compile_flags: list[str] | None = None, linker_cmd: str | None = None, linker_flags: list[str] | None = None, linker_output: str | None = None, static_libs: list[str] | None = None, dynamic_libs: list[str] | None = None, include_dirs: list[str] | None = None, entry_files: list[str] | None = None, target_triple: str | None = None, target_datalayout: str | None = None, force_recompile_includes: bool = False, build_pyi_cache: bool = True, asan_mode: bool = False, dwarf_enabled: bool = False, ctrlc_stack_enabled: bool = False) -> None: self.src_root = os.path.abspath(src_root) self.temp_dir = os.path.abspath(temp_dir) self.output_dir = os.path.abspath(output_dir) @@ -671,7 +949,13 @@ class Phase2Translator: # (零影响,不构造任何 DI 元数据);True=App 源模块的 .ll 携带 # !llvm.dbg.cu + 每函数 DISubprogram + 入口 DILocation,供 ASan / # llvm-symbolizer 反查 ".vp:函数起始行"。 - self.dwarf_enabled: bool = dwarf_enabled + self.dwarf_enabled: bool = dwarf_enabled + # Ctrl+C 调用栈(--ctrlc-stack):False=未启用(零影响,不调用任何外部 + # 工具、不写任何旁文件);True=链接成功后额外用 llvm-dwarfdump 把最终 + # exe 的 DWARF 行表导出为 ".stackmap"(地址→源行旁文件,供 exe 内 + # 运行时在 Ctrl+C/Ctrl+Break 时逐帧翻译返回地址)。要求 --dwarf 已开启 + # (Config 侧联动,否则行表为空),且目标为 Windows PE。 + self.ctrlc_stack_enabled: bool = ctrlc_stack_enabled # 调用点热点统计(--hotspot-sites):None=未启用(零影响);str=调用点计数 # 落盘文件路径(每函数计时落同目录 hotspot_time.txt)。各模块运行时自带 # Ctrl+C 处理器、统一以追加方式写该路径;含 main 的模块在 main 入口清空一次 @@ -889,7 +1173,9 @@ class Phase2Translator: # 行为与改动前完全一致(零影响)。 # --dwarf(函数级 DWARF)同理:缓存 .ll/.obj 不含 !llvm.dbg.cu,必须一并 # 强制重译,否则开启开关后仍复用旧产物、看不到任何 .vp 行号。 - hotspot_on: bool = bool(self.hotspot_sites_file) or bool(self.dwarf_enabled) + # --ctrlc-stack 依赖同一份 .debug_line 行表(Config 已联动开启 --dwarf), + # 这里显式纳入,避免将来联动改动后静默复用无 DI 的旧产物。 + hotspot_on: bool = bool(self.hotspot_sites_file) or bool(self.dwarf_enabled) or bool(self.ctrlc_stack_enabled) for i, src_path in enumerate(py_files, 1): rel = os.path.relpath(src_path, self.src_root) # 复用 parse_python_file 的 AST 缓存(get_file_dependencies 已 parse 过) @@ -1497,7 +1783,10 @@ class Phase2Translator: - list(...) / dict(...) 作为构造函数调用 (ast.Call, func=Name) """ try: - tree = ast.parse(code) + # `.vp` 源码须先过 Viper 软关键字层(`enum X:` / `fn` 等)再解析: + # 否则此处 SyntaxError 会静默 return 原 code(自动注入 import _list/_dict + # 失效),与主翻译路径(Shell/TransPyC 已展开)口径不一致。 + tree = ast.parse(ExpandSoftKeywords(code)) except SyntaxError: return code @@ -1562,8 +1851,11 @@ class Phase2Translator: """翻译单个源文件,嵌入相关 .stub.ll 声明 dwarf_enabled: 函数级 DWARF 调试元数据(--dwarf),默认 False(零影响)。 - 仅 App 源模块传 True;include 文件的 .obj 会落进跨项目共享的 - includes.binary 预编译缓存,注入 DI 会污染该缓存,故一律保持 False。 + App 源模块在 --dwarf/--asan/--ctrlc-stack 时传 True。include 文件默认 + 传 False(其 .obj 会落进跨项目共享的 includes.binary 预编译缓存,注入 DI + 会污染该缓存);仅 --ctrlc-stack 时传 True——此时 include 的 .obj 改落 + 隔离目录(includes.binary_ctrlcstack,见 _get_precompiled_obj_path), + 不会污染共享缓存。 """ _t0: float = time.time() with open(src_path, 'r', encoding='utf-8') as f: @@ -1582,7 +1874,11 @@ class Phase2Translator: f"仍按主流程 sha1 生成前缀以保证链接一致;下次构建将采用新 sha1", "SHA1") # 自动导入: 检测 list 容器使用,自动注入 import _list + # 注入行会让 AST 节点 lineno 整体下移 N(磁盘真实行号 = AST 行号 - N); + # 记下 N 供报错渲染还原磁盘行号(如 _RaiseUnresolvedExpr)。 + _raw_newlines: int = code.count('\n') code = self._inject_auto_imports(code, src_path) + _line_offset: int = code.count('\n') - _raw_newlines if prebuilt_ModuleSha1Map is not None: ModuleSha1Map = prebuilt_ModuleSha1Map @@ -1595,12 +1891,22 @@ class Phase2Translator: # 未启用时不设置 → 与改动前逐字节一致(零影响) if dwarf_enabled: trans.translator.DwarfEnabled = True + # Ctrl+C 调用栈(--ctrlc-stack):仅当本文件就是 dbgstack 模块时置位—— + # 只有该模块需要注入 @llvm.global_ctors 构造器调用自己的 Install() + # (见 BaseGen._RegisterDbgStackInstallViaCtor)。其它模块不设置该属性 + # → Gen.ctrlc_stack_ctor 保持 False,IR 与改动前逐字节一致(零影响)。 + _ctrlc_stack_ctor: bool = bool( + self.ctrlc_stack_enabled + and os.path.basename(src_path) in _CTRLC_STACK_MODULE_FILES) + if _ctrlc_stack_ctor: + trans.translator.CtrlcStackCtor = True trans.SliceLevel = self.slice_level trans.translator.SliceLevel = self.slice_level trans.translator.SliceCount = 0 trans.translator.SliceInfos = [] trans.translator.LlvmGen = None - trans.translator._module_sha1 = sha1 + trans.translator._module_sha1 = sha1 + trans.translator._line_offset = _line_offset trans.translator._ModuleSha1Map = ModuleSha1Map trans.translator._struct_sha1_map = self.struct_sha1_map trans.translator._global_function_default_args = self.function_default_args @@ -1791,6 +2097,14 @@ class Phase2Translator: out_dir = os.path.dirname(out_path) if out_dir: os.makedirs(out_dir, exist_ok=True) + # --ctrlc-stack:核对注入钩子确实生效——本模块要求注入 @llvm.global_ctors + # 构造器(StructGen.finalize 里落成 .ll 文本之前),若产物中没有该全局 + # 说明钩子不可用(如序列化路径被改动绕过),显式报错终止,不静默产出 + # 缺注册的产物(那会让整个功能静默失效)。 + if _ctrlc_stack_ctor and 'llvm.global_ctors' not in result: + raise RuntimeError( + '--ctrlc-stack: dbgstack 模块未注入 @llvm.global_ctors ' + '(模块级收尾钩子未生效,不静默降级)') # 修复 LLVM 22+: external global 不能有初值,改为 linkonce result = re.sub(r'^(@[\w.]+)\s*=\s*external\s+global\s+(\w[\w*]*)\s+(-?\d+|null|zeroinitializer|true|false)$', r'\1 = linkonce global \2 \3', result, flags=re.MULTILINE) # 后处理:添加 llvmlite 不支持的 LLVM 属性(如 willreturn, mustprogress) @@ -1904,6 +2218,13 @@ class Phase2Translator: return opt_r compile_input = asan_ll cmd = [self.compile_cmd] + self.compile_flags + ['-o', obj_path, compile_input] + if self.ctrlc_stack_enabled: + # --ctrlc-stack:让 llc 保留帧指针(-frame-pointer=all)。dbghelp 主路径 + # 不依赖帧指针,但符号表不可用时要靠 RBP 链兜底——llc 默认 + # omit-frame-pointer 会让 RBP 链只走出 1~2 帧(实测"共 2 帧")。 + # 只在开启该开关时加,关闭时命令与改前逐字一致。 + cmd = [self.compile_cmd] + self.compile_flags \ + + ['-frame-pointer=all', '-o', obj_path, compile_input] return subprocess.run(cmd, capture_output=True, text=True, cwd=cwd) def _compile_ll_files(self, active_sha1s: set[str]) -> None: @@ -2379,8 +2700,12 @@ class Phase2Translator: continue py_path: str = os.path.join(root, fname) code, tree = parse_python_file(py_path) - # 字符串预过滤:不含 CVTable 的文件跳过 AST 遍历 - if tree is None or 'CVTable' not in code: + # 字符串预过滤:不含 CVTable 的文件跳过 AST 遍历。 + # `virtual class` / `virtual def` 修饰符在源码里写的是 `virtual` + # (AST 上才变成 @t.CVTable 装饰器),故预过滤必须同时放行 + # `virtual`/`novirtual`,否则含修饰符的 includes 类会被整段跳过—— + # 静默不预收集虚表状态(与本题"不得静默丢弃修饰符"同族问题)。 + if tree is None or ('CVTable' not in code and 'virtual' not in code): continue try: for node in ast.iter_child_nodes(tree): @@ -2394,6 +2719,25 @@ class Phase2Translator: elif isinstance(dec, ast.Name): if dec.id == 'CVTable': has_cvtable = True + # 方法级 virtual 触发类级 vtable(与 + # HandlesClassDef._resolve_class_flags 同口径:类级 + # NoVTable / 无真实父类两种可判定形态)。 + # 记录"类级 CVTable"单独一份:它是 vtable 槽筛选的 + # VtableByClassLevel(忽略方法级标记),两者不可混用。 + class_level_cvtable: bool = has_cvtable + if not has_cvtable: + _has_real_parent: bool = False + for _b in node.bases: + _bn: str = None + if hasattr(_b, 'id'): + _bn = _b.id + elif hasattr(_b, 'attr'): + _bn = _b.attr + if _bn and _bn not in ('Object', 'CVTable', 'NoVTable', 'CStruct', 'CUnion', 'CEnum', 'REnum', 'Exception', 'Enum'): + _has_real_parent = True + break + if ClassHasFuncLevelCVTable(node) and not _has_real_parent: + has_cvtable = True if not has_cvtable: continue class_name: str = node.name @@ -2407,6 +2751,11 @@ class Phase2Translator: # 构造函数不放入 vtable(避免子类与基类 vtable 大小不一致破坏多态分派) if method_name in ('__new__', '__init__', '__before_init__'): continue + # 方法级修饰符:`novirtual def`(⇒ @t.NoVTable)不进槽; + # 判据唯一源 = SymbolUtils.IsVtableSlotMethod,与 + # HandlesClassDef / TPV _should_method_be_virtual 同口径。 + if not IsVtableSlotMethod(item, class_level_cvtable): + continue if method_name == '__init__': func_full_name: str = f"{class_name}.__init__" elif method_name == '__call__': @@ -2437,6 +2786,14 @@ class Phase2Translator: def _scan_include_libraries(self) -> None: """扫描 includes 目录,检测被使用的模块对应的库文件和源文件""" + # --ctrlc-stack:强制纳入 dbgstack 模块 —— 无任何用户代码 import 它, + # 不纳入就不会被翻译 ⇒ 产物没有 Install 符号可注入启动期构造器。 + # 直接借用既有的 used_includes 收集路径(下面按同一套 py_index / + # 传递依赖逻辑把它加入 extra_py_files,再交给 _compile_include_py_files + # 编译并把 .obj 追加进 extra_link_files)。开关关闭时不触碰 used_includes + # → 本函数行为与改动前逐字一致(零影响)。 + if self.ctrlc_stack_enabled: + self.used_includes.add('dbgstack') if not self.used_includes: return @@ -2488,6 +2845,18 @@ class Phase2Translator: _vlog().info(f"\n[includes] 需要链接的库文件: {len(self.extra_link_files)} 个") if self.extra_compile_files: _vlog().info(f"[includes] 需要编译的源文件: {len(self.extra_compile_files)} 个") + # --ctrlc-stack:断言 dbgstack 源文件确实被纳入待编译集合。找不到就必然 + # 得不到 Install 符号、构造器无处注入(功能静默失效)——显式报错终止, + # 不静默跳过(多半是 project.json 的 includes 未包含仓库的 includes 目录)。 + if self.ctrlc_stack_enabled: + _dbgstack_found: bool = any( + os.path.basename(_p) in _CTRLC_STACK_MODULE_FILES + for _p in self.extra_py_files) + if not _dbgstack_found: + raise RuntimeError( + '--ctrlc-stack: 在 includes 目录中找不到 dbgstack.vp/.py,' + '无法编译并链接 Install()(请确认 project.json 的 includes ' + f'包含仓库 includes 目录;已扫描: {self.include_dirs})') def _scan_dir_for_libs(self, directory: str, module_name: str) -> None: """递归扫描目录中的库文件和源文件 @@ -2734,6 +3103,15 @@ class Phase2Translator: 预编译目录位于 include 目录同级,命名为 {basename}.binary/。 文件命名为 {sha1}.{triple_safe}.{rel_model}.obj,区分目标平台和重定位模型。 + + --ctrlc-stack:include 模块也带 DWARF 行表(栈帧要能落到 includes/* 的源行), + 此类 .obj 不得回写进跨项目共享的 includes.binary(会污染其它项目的预编译 + 缓存),改落**兄弟目录** {basename}.binary + 后缀(不新增层级,形态与 + ASan 变体目录 includes.binary_asan 一致)。 + 关闭时后缀为空 → 路径与改动前逐字一致(含 --dwarf/--asan 单独开启的情形)。 + --asan 与 --ctrlc-stack 同开:ASan 插桩也会改变 include obj 内容(见 + _llc_compile),两者产物不可互相复用,故再叠加 _asan 后缀区分 + (优先级:是否隔离由 --ctrlc-stack 决定,--asan 只决定后缀再叠加 _asan)。 """ AbsSrcPath: str = os.path.abspath(src_path) RelModel: str = 'static' @@ -2749,7 +3127,10 @@ class Phase2Translator: continue inc_parent: str = os.path.dirname(abs_inc) inc_base: str = os.path.basename(abs_inc) - precompiled_dir: str = os.path.join(inc_parent, inc_base + '.binary') + _iso_suffix: str = '' + if self.ctrlc_stack_enabled: + _iso_suffix = '_ctrlcstack_asan' if self.asan_mode else '_ctrlcstack' + precompiled_dir: str = os.path.join(inc_parent, inc_base + '.binary' + _iso_suffix) triple_safe: str = (self.triple or 'default').replace(os.sep, '_').replace('/', '_').replace('\\', '_').replace(':', '_') return os.path.join(precompiled_dir, f"{sha1}.{triple_safe}.{RelModel}.obj") except ValueError: @@ -2774,6 +3155,19 @@ class Phase2Translator: # 本函数行为与改动前逐字节一致(零影响)。 hotspot_on: bool = bool(self.hotspot_sites_file) + # --ctrlc-stack:include 模块也必须带 DWARF 行表(否则运行时 Ctrl+C 的栈里 + # 落在 includes/*(memhub/string/viperlib/w32…)的帧取不到源行,只能落到 + # 邻行或 "+0x…(unknown)")。为此翻译时给 include 透传 dwarf_enabled=True + # (见下方 worker/串行四处的 dwarf_enabled=self.ctrlc_stack_enabled),并把 + # 旧目标文件(output 目录 {sha1}.obj 及隔离缓存 includes.binary_ctrlcstack + # 内的旧 .obj)一并失效重译——复用上一次无 DI 的产物会让行表静默缺失。 + # 与 hotspot_on 处理方式一致;同时覆盖 dbgstack.vp(其 .obj 还须含 + # @llvm.global_ctors 启动期构造器)。取舍:开启本开关时 include 目标文件每 + # 次构建都重译(放弃隔离缓存的复用,换取"必然带 DWARF"的正确性保证), + # 仅调试模式可接受。开关关闭时 include_dwarf=False,本函数行为与改动前 + # 逐字节一致(零影响,含 --dwarf/--asan 单独开启的情形)。 + include_dwarf: bool = bool(self.ctrlc_stack_enabled) + # 构建 include 文件的模块 SHA1 映射(预计算所有文件,构建完整映射) include_ModuleSha1Map = self._build_current_ModuleSha1Map(None) need_translate = [] # [(src_path, sha1, module_name, ll_path, obj_path, precompiled_obj)] @@ -2812,18 +3206,20 @@ class Phase2Translator: self._collect_inline_symbols(src_path) continue - # --hotspot-sites:include 的 .py 未变时下面两处会因旧 .obj 存在而直接跳过, - # 复用上一次可能未插桩的目标文件。启用热点时先让这两类旧 .obj 一并失效, - # 使其必然走入 need_translate 重新翻译并嵌入热点计数/计时 IR。 + # --hotspot-sites / --ctrlc-stack:include 的 .py 未变时下面两处会因旧 + # .obj 存在而直接跳过,复用上一次(未插桩 / 不含 DI)的目标文件。 + # 启用时先让这两类旧 .obj 一并失效,使其必然走入 need_translate 重新 + # 翻译并嵌入热点计数/计时 IR 或 DWARF 行表。 # 用 os.path.isfile 而非 _CachedIsFile:避免把"已存在"写入存在性缓存后又被 # 本处删除,使下面两处判定读到过期缓存而漏编(与 App 主循环同一写法)。 - if hotspot_on: + if hotspot_on or include_dwarf: for _stale_obj in (precompiled_obj, obj_path): if _stale_obj and os.path.isfile(_stale_obj): try: os.remove(_stale_obj) except OSError as e: # 不中断构建,但真实错误(如权限不足)必须打印告警,不静默吞掉 + # (报文保持原样:关闭开关时与改动前逐字一致) _vlog().warning( f"Phase2Translator: 删除热点插桩 include 模块的旧目标文件失败: {_stale_obj}: {e}") @@ -2954,7 +3350,9 @@ class Phase2Translator: include_ModuleSha1Map, hotspot_sites_file=self.hotspot_sites_file, hotspot_sites_all=self.hotspot_sites_all, - hotspot_callees=self.hotspot_callees + hotspot_callees=self.hotspot_callees, + ctrlc_stack_enabled=self.ctrlc_stack_enabled, + dwarf_enabled=self.ctrlc_stack_enabled ) in_progress[future] = item @@ -2973,7 +3371,9 @@ class Phase2Translator: include_ModuleSha1Map, hotspot_sites_file=self.hotspot_sites_file, hotspot_sites_all=self.hotspot_sites_all, - hotspot_callees=self.hotspot_callees + hotspot_callees=self.hotspot_callees, + ctrlc_stack_enabled=self.ctrlc_stack_enabled, + dwarf_enabled=self.ctrlc_stack_enabled ) in_progress[future] = item remaining.clear() @@ -3017,12 +3417,12 @@ class Phase2Translator: for item in need_translate: src_path, sha1, module_name, ll_path, obj_path, precompiled_obj = item _vlog().info(f" 翻译: {module_name}.py -> {sha1}.ll") - self._translate_file(src_path, ll_path, prebuilt_ModuleSha1Map=include_ModuleSha1Map) + self._translate_file(src_path, ll_path, prebuilt_ModuleSha1Map=include_ModuleSha1Map, dwarf_enabled=self.ctrlc_stack_enabled) else: item = need_translate[0] src_path, sha1, module_name, ll_path, obj_path, precompiled_obj = item _vlog().info(f" 翻译: {module_name}.py -> {sha1}.ll") - self._translate_file(src_path, ll_path, prebuilt_ModuleSha1Map=include_ModuleSha1Map) + self._translate_file(src_path, ll_path, prebuilt_ModuleSha1Map=include_ModuleSha1Map, dwarf_enabled=self.ctrlc_stack_enabled) # ========== 阶段2:串行后处理(dso_local + stub_decls 注入)========== compile_tasks = [] # [(dso_ll_path, obj_path, src_path, sha1, module_name, ll_path, precompiled_obj, ll_content)] @@ -3035,6 +3435,18 @@ class Phase2Translator: with open(ll_path, 'r', encoding='utf-8') as f: ll_content = f.read() + # --ctrlc-stack 提醒:include 模块若发射了 define 却不带 DI(!llvm.dbg.cu), + # 其 .debug_line 行为空 ⇒ 运行时 Ctrl+C 栈里该 include 帧取不到源行 + # (会退化为 +0x… (unknown))。 + # **只警告、不报错**:像 includes/stdarg.vp 这类模块的 define 是"合成"的 + # (由 extern/sugar 派生、无 .vp 函数体),本就不会有源级 DI —— 按 fail-fast + # 报错会把这类模块全部误杀;而"行号缺失"只降低可读性,不是编译正确性问题。 + if self.ctrlc_stack_enabled and re.search(r'^define ', ll_content, re.MULTILINE) \ + and '!llvm.dbg.cu' not in ll_content: + _vlog().warning( + f"--ctrlc-stack: include 模块 {module_name}.py 发射了 define 但 IR 未携带 " + f"DWARF (!llvm.dbg.cu)——该模块内联帧在 Ctrl+C 栈里将只显示地址: {ll_path}") + # dso_local 正则标注(使用 ^ + MULTILINE 避免匹配字符串常量内容中的 define/declare) ll_content = re.sub(r'^(define|declare)\s+(private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|external)\s+', r'\1 \2 dso_local ', ll_content, flags=re.MULTILINE) ll_content = re.sub(r'^(define|declare)\s+(?!private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|external|dso_local)', r'\1 dso_local ', ll_content, flags=re.MULTILINE) @@ -3190,7 +3602,8 @@ class Phase2Translator: result = compile_results.get(task) if result and result.returncode == 0: self.extra_link_files.append(obj_path) - # 复制到 includes.binary 预编译目录 + # 复制到 includes.binary 预编译目录(--ctrlc-stack 时 precompiled_obj + # 指向隔离目录 includes.binary_ctrlcstack,不会污染共享缓存) if precompiled_obj: precompiled_dir = os.path.dirname(precompiled_obj) try: @@ -3198,6 +3611,13 @@ class Phase2Translator: os.makedirs(precompiled_dir, exist_ok=True) shutil.copy2(obj_path, precompiled_obj) except Exception as _e: + # --ctrlc-stack fail-fast:隔离目录创建 / 写回失败必须显式终止, + # 不静默降级(否则本次构建的 include obj 拿不到、或落到共享缓存 + # 之外的位置,DWARF 行表与后续复用的前提都被破坏)。 + if self.ctrlc_stack_enabled: + raise RuntimeError( + f"--ctrlc-stack: include 预编译缓存写入隔离目录失败: " + f"{precompiled_obj}: {_e}") from _e if _config_mode == "strict": _vlog().warning(f"预编译缓存写入失败: {_e}") # 注册符号 @@ -3220,33 +3640,49 @@ class Phase2Translator: self._extract_include_param_names(sorted_files) def _extract_include_param_names_from_dirs(self) -> None: - """从 include 目录的 Python 源码中提取函数参数名,存入 function_param_names + """从 include 目录的 Python 源码中提取函数参数名与默认值。 - 优化:单次递归遍历 AST,维护当前类上下文,O(N) 复杂度。 - 复用 parse_python_file 的 AST 缓存。 + 顶层 .vp/.py 直接解析;包子目录(如 includes/json)只在该包被 + used_includes 引用时递归解析——避免为每个工程全量递归 includes + (fltk/vqt6/openssl 等上百个文件)带来的解析开销。 + 默认值一并注册(跨模块缺参调用点据此填充,见 HandlesExprCall + ._HandleClosureCallLlvm),此前只注册参数名,导致 include 模块的 + 尾随默认参数在调用点被零值填充。 """ for includes_dir in self.include_dirs: if not os.path.isdir(includes_dir): continue - for filename in os.listdir(includes_dir): - if not (filename.endswith('.vp') or filename.endswith('.py')): - continue - src_path = os.path.join(includes_dir, filename) - try: - source, tree = parse_python_file(src_path) - if tree is None: + for entry in os.listdir(includes_dir): + entry_path = os.path.join(includes_dir, entry) + targets: list[str] = [] + if os.path.isdir(entry_path): + if not self.used_includes or entry not in self.used_includes: continue - sha1 = compute_sha1(source) - self._collect_function_params(tree, sha1) - except Exception as e: - _vlog().warning(f" 提取参数名失败 {src_path}: {e}") + for root, dirs, files in os.walk(entry_path): + dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__'] + targets.extend(os.path.join(root, f) for f in files + if f.endswith('.vp') or f.endswith('.py')) + elif entry.endswith('.vp') or entry.endswith('.py'): + targets.append(entry_path) + for src_path in targets: + try: + source, tree = parse_python_file(src_path) + if tree is None: + continue + sha1 = compute_sha1(source) + self._collect_function_params(tree, sha1) + except Exception as e: + _vlog().warning(f" 提取参数名失败 {src_path}: {e}") def _collect_function_params(self, tree: ast.AST, sha1: str | None) -> None: - """单次递归遍历 AST 收集函数参数名(含类方法),O(N) 复杂度 + """单次递归遍历 AST 收集函数参数名(含类方法)与默认值,O(N) 复杂度 遍历时维护 current_class 上下文:遇到 ClassDef 进入类作用域, 遇到 FunctionDef 记录参数名并标注所属类,函数内部的嵌套定义 不再属于外层类。 + + 默认值只登记非方法(模块级函数)的裸名键,减少与其它模块同名函数的 + 误配;带 sha1 / 类名限定的键始终登记,查找时精确键优先。 """ def visit(node: ast.AST, current_class: str | None) -> None: for child in ast.iter_child_nodes(node): @@ -3263,6 +3699,22 @@ class Phase2Translator: self.function_param_names[f'{current_class}.{func_name}'] = param_names if sha1: self.function_param_names[f'{sha1}.{current_class}.{func_name}'] = param_names + defaults = getattr(child.args, 'defaults', []) or [] + if defaults: + default_vals = [] + for d in defaults: + if isinstance(d, ast.Constant): + default_vals.append(d.value) + else: + default_vals.append(None) + if sha1: + self.function_default_args[f'{sha1}.{func_name}'] = default_vals + if current_class: + self.function_default_args[f'{current_class}.{func_name}'] = default_vals + if sha1: + self.function_default_args[f'{sha1}.{current_class}.{func_name}'] = default_vals + else: + self.function_default_args[func_name] = default_vals # 函数内部的嵌套定义不属于外层类 visit(child, None) else: @@ -3733,6 +4185,197 @@ class Phase2Translator: _vlog().warning(f"剥离可执行文件调试段失败: {_e}", "Exception") break + def _WriteStackMap(self, exe_path: str) -> None: + """--ctrlc-stack:链接成功后导出"地址→源行"旁文件 .stackmap。 + + 用 llvm-dwarfdump 解析最终 PE 的 .debug_line(地址→文件/行)与 + .debug_info(DISubprogram 的 low/high_pc + DW_AT_name → 函数名), + 换算为 RVA 后落盘。旁文件规格(全部小端 little-endian): + + 头部 20 字节 = magic(4)="TPCS" + version(u32=1) + entry_count(u32) + + str_count(u32) + str_bytes(u32) + 记录 entry_count × 20 字节(按 rva 升序): + rva(u64) + file_off(u32) + line(u32) + func_off(u32) + 字符串池 str_bytes 字节(紧接记录区,UTF-8,全部以 '\\0' 结尾) + + 口径(必须与运行期严格一致):运行期 dbgstack.vp 取 + `rva = 帧地址 − GetModuleHandleA(NULL)`,即"链接期 ImageBase 之上的偏移", + 故写端唯一正确口径是 **`RVA = dwarfdump 打印的地址 − PE.ImageBase`**。 + + ⚠ 实测 TransPyV.exe 的 DWARF 里同时存在两个地址域: + * 绝大多数行/子程序地址是**已重定位的 VA**(0x14000xxxx,含 ImageBase); + * 另有 22638 条行记录 / 756 个 subprogram 的地址**恰为 0**——它们是**被 + 链接器按 COMDAT 去重丢弃的泛型实例副本**(实测 36 个 `list[T].*` 名字, + 每个都有 1 份 VA 副本 + 15~21 份 0 地址副本),并非解析错误。 + 旧实现按"整段最小地址是否 >= ImageBase"**整表**判定:只要有一行 0,就整表都不 + 减基址 ⇒ 全部 VA 行留在 0x140000000+ 域、与运行期 RVA 域相差一个 ImageBase, + 运行期下界查找对任意帧都落到同一条记录上(用户实测所有帧都显示同一行)。 + 现改为**逐条**归一化:地址落在映像 VA 区间 [ImageBase, ImageBase+SizeOfImage) + 的记录保留(RVA = 地址 − ImageBase);其余(未重定位的 0、越界值)一律**丢弃并 + 统计**(不静默留垃圾条目)。同一 RVA 只保留行号最大的一条。 + + 写出前自检(任一不满足即 fail-fast 抛异常,遵循 --ctrlc-stack 既有风格): + 记录数 / min~max RVA 与 SizeOfImage 的关系 / 所有 RVA ∈ [0, SizeOfImage) / + RVA 严格升序 / 至少若干条落在运行期可能出现的 RVA 区间 + (粗判据 [.text 节 RVA, SizeOfImage))。 + + 任何一步失败都直接抛异常终止编译(--ctrlc-stack 的硬要求,无兜底)。 + """ + import struct + import bisect + + def _Fail(msg: str) -> None: + full: str = msg + '(这是 --ctrlc-stack 的要求)' + _vlog().error(full) + raise RuntimeError(full) + + layout = _ReadPEImageLayout(exe_path) + if layout is None: + _Fail(f"--ctrlc-stack 只支持 Windows PE 产物,但 {exe_path} 不是有效 PE") + image_base, size_of_image, sections = layout + if size_of_image <= 0: + _Fail(f"--ctrlc-stack 读到的 PE.SizeOfImage 非正({size_of_image}),无法界定 RVA 域") + img_lo: int = image_base + img_hi: int = image_base + size_of_image + # 运行期可能出现的 RVA 区间的**粗判据**下界:.text 节 RVA(缺失时退化为最小节 RVA) + text_rva: int = min((v[0] for v in sections.values()), default=0x1000) + if '.text' in sections: + text_rva = sections['.text'][0] + max_sec_end: int = max((v[0] + v[1] for v in sections.values()), default=size_of_image) + + dwarfdump: str | None = shutil.which('llvm-dwarfdump') or shutil.which('llvm-dwarfdump.exe') + if not dwarfdump: + _Fail("--ctrlc-stack 找不到 llvm-dwarfdump(请确认 PATH 内含 LLVM 工具)") + + try: + proc = subprocess.run( + [dwarfdump, '--debug-line', '--debug-info', exe_path], + capture_output=True) + except Exception as e: + _Fail(f"--ctrlc-stack 调用 llvm-dwarfdump 失败: {e}") + if proc.returncode != 0: + _Fail(f"--ctrlc-stack 的 llvm-dwarfdump 返回 {proc.returncode}: " + + proc.stderr.decode('utf-8', errors='replace').strip()) + dump_text: str = proc.stdout.decode('utf-8', errors='replace') + + rows: list[tuple[int, str, int]] = _ParseDwarfLineRows(dump_text) + if not rows: + _Fail(f"--ctrlc-stack 未能从 {exe_path} 解析出任何 DWARF 行记录(.debug_line 为空?)") + + # ---- 行记录:逐条归一化为"运行期 RVA";不能归一的一律丢弃并统计 ---- + dropped_rows: int = 0 + dropped_row_samples: list[int] = [] + best: dict[int, tuple[str, int]] = {} + for addr, path, line in rows: + if not (img_lo <= addr < img_hi): + dropped_rows += 1 + if len(dropped_row_samples) < 5: + dropped_row_samples.append(addr) + continue + rva: int = addr - image_base + cur = best.get(rva) + if cur is None or line > cur[1]: + best[rva] = (path, line) + if dropped_rows: + _vlog().warning( + f" [--ctrlc-stack] 丢弃 {dropped_rows} 条无法归一化的 DWARF 行记录" + f"(地址不在映像 VA 区间 [0x{img_lo:x}, 0x{img_hi:x}) 内;判定原因:多为被连接器" + f" COMDAT 去重丢弃的泛型副本,其地址恰为 0)示例地址: " + + ", ".join(f"0x{a:x}" for a in dropped_row_samples)) + if not best: + _Fail(f"--ctrlc-stack 从 {exe_path} 得到的行记录全部无法归一到运行期 RVA 域") + + # ---- 子程序(函数名):同样逐条归一化;不能归一的丢弃 ---- + funcs: list[tuple[int, int, str]] = _ParseDwarfSubprograms(dump_text) + func_ranges: list[tuple[int, int, str]] = [] + dropped_funcs: int = 0 + for low, high, name in funcs: + if not (img_lo <= low < img_hi): + dropped_funcs += 1 + continue + lo_rva: int = low - image_base + hi_rva: int = high - image_base + if hi_rva <= lo_rva: + hi_rva = lo_rva + 1 + if hi_rva > size_of_image: + hi_rva = size_of_image + func_ranges.append((lo_rva, hi_rva, name)) + if dropped_funcs: + _vlog().warning( + f" [--ctrlc-stack] 丢弃 {dropped_funcs} 个无法归一化的 DWARF 子程序" + f"(low_pc 不在映像 VA 区间 [0x{img_lo:x}, 0x{img_hi:x}) 内;同上为去重副本)") + func_ranges.sort(key=lambda f: f[0]) + func_lows: list[int] = [f[0] for f in func_ranges] + + def _FuncNameFor(rva: int) -> str | None: + i: int = bisect.bisect_right(func_lows, rva) - 1 + if i >= 0: + low, high, name = func_ranges[i] + if low <= rva < high: + return name + return None + + # 字符串池:偏移 0 固定为一个空串('\0')哨兵,使 func_off=0 可无歧义表示 + # "无函数名";真实串偏移恒 >= 1。 + pool: bytearray = bytearray(b'\x00') + str_offsets: dict[str, int] = {'': 0} + + def _Intern(s: str) -> int: + off: int | None = str_offsets.get(s) + if off is None: + off = len(pool) + pool.extend(s.encode('utf-8')) + pool.append(0) + str_offsets[s] = off + return off + + records: list[tuple[int, int, int, int]] = [] + for rva in sorted(best.keys()): + path, line = best[rva] + name = _FuncNameFor(rva) + records.append((rva, _Intern(path), line, _Intern(name) if name else 0)) + + # ---- 写端自检(fail-fast;与运行期 LoadMapFile 的校验口径一一对应)---- + rvas: list[int] = [r[0] for r in records] + min_rva: int = rvas[0] + max_rva: int = rvas[-1] + for i in range(1, len(rvas)): + if rvas[i] <= rvas[i - 1]: + _Fail(f"--ctrlc-stack 自检失败:记录 RVA 非严格升序" + f"(第 {i} 条 0x{rvas[i]:x} <= 前一条 0x{rvas[i - 1]:x},会破坏运行期二分查找)") + outside: int = sum(1 for r in rvas if not (0 <= r < size_of_image)) + if outside: + _Fail(f"--ctrlc-stack 自检失败:有 {outside} 条记录的 RVA 落在 [0, SizeOfImage=0x{size_of_image:x}) 之外") + # 运行期可能出现的 RVA 区间(粗判据):[.text 节 RVA, SizeOfImage)。一条都没有 ⇒ 口径又错了。 + plausible: int = sum(1 for r in rvas if text_rva <= r < size_of_image) + if plausible <= 0: + _Fail(f"--ctrlc-stack 自检失败:没有任何记录的 RVA 落在运行期可能出现的区间" + f" [0x{text_rva:x}(.text RVA), 0x{size_of_image:x}(SizeOfImage)) 内" + f"(RVA 口径疑似又错了)") + _vlog().info( + f" [--ctrlc-stack] 自检: 记录={len(records)} RVA范围=[0x{min_rva:x}, 0x{max_rva:x}]" + f" PE.SizeOfImage=0x{size_of_image:x}({size_of_image})" + f" 各节最高结束 RVA=0x{max_sec_end:x}" + f" | 全部∈[0,SizeOfImage)=是 严格升序=是" + f" | 落在[.text 0x{text_rva:x},SizeOfImage)内={plausible} 条") + + out: bytearray = bytearray() + out.extend(b'TPCS') + out.extend(struct.pack(' None: """将 active_sha1s 对应的 .obj 文件和 includes 库文件链接为可执行文件""" obj_files = [] @@ -3861,6 +4504,14 @@ SECTIONS { lib_flags.append('-L' + self.asan_rt_dir) lib_flags.append('-l:libclang_rt.asan_dynamic-x86_64.dll.a') lib_flags.append('-l:libclang_rt.asan_dynamic_runtime_thunk-x86_64.a') + # Ctrl+C 栈转储(--ctrlc-stack):includes/dbgstack.vp 的主回溯路径用 + # StackWalk64 / SymInitialize / SymFunctionTableAccess64 / SymGetModuleBase64 + # (均在 dbghelp),故开启该开关时必须显式链接 -ldbghelp,否则链接期报未定义符号。 + # 与 ASan 同理:库要排在目标文件之后 ⇒ 追加到 lib_flags。 + # (若确实无法链 dbghelp:把 includes/dbgstack.vp 的 DBGSTACK_USE_DBGHELP 置 0, + # 退化为纯 RBP 链回溯,此时不该再带本 flag。) + if self.ctrlc_stack_enabled: + lib_flags.append('-ldbghelp') cmd = [self.linker_cmd] + non_lib_flags + ['-o', link_output] + all_link_files + lib_flags _vlog().info(f" 执行: {' '.join(cmd)}") result = subprocess.run( @@ -3912,3 +4563,9 @@ SECTIONS { _vlog().error(f" {e}") _vlog().error(f"\n链接异常,立即终止编译。") sys.exit(1) + + # --ctrlc-stack:链接成功后导出"地址→源行"旁文件(.stackmap)。 + # 刻意放在上面的链接 try/except 之外,使导出阶段的 fail-fast RuntimeError + # 直接向上抛出终止编译,而不被"链接异常兜底"吞掉。 + if self.ctrlc_stack_enabled: + self._WriteStackMap(exe_path) diff --git a/lib/core/LLVMCG/BaseGen.py b/lib/core/LLVMCG/BaseGen.py index cc614f0..266ee14 100644 --- a/lib/core/LLVMCG/BaseGen.py +++ b/lib/core/LLVMCG/BaseGen.py @@ -6,6 +6,7 @@ import llvmlite.ir as ir from lib.includes import t as _t from lib.includes.t import CTypeRegistry from lib.core.Handles.HandlesBase import CTypeInfo as _CTypeInfo, BuiltinTypeMap +from lib.core.SymbolUtils import EnumIntWidth, _ENUM_DEFAULT_INT_WIDTH from lib.constants.config import DEFAULT_HOTSPOT_CALLEES @@ -155,6 +156,33 @@ class BaseGenMixin: self.global_struct_class: dict[str, str] = {} self.var_type_info: dict[str, Any] = {} self.var_const_flags: dict[str, bool] = {} + # ---- const / volatile 登记(与 TransPyV 侧 VarEntry/FieldEntry 逐字段对应, + # 对照表见 Debugger/type-descriptors.md「两侧语义对照表」)---- + # 分两层,因为 TPC 的函数内语句与模块级语句走不同生命周期: + # 函数作用域层(随函数出栈清空,清零点 HandlesFunctions 函数出口): + # var_const_flags 变量(局部/形参)自身 const + # var_volatile_flags 变量自身 volatile + # var_pointee_volatile_flags 变量所指物 volatile(rawptr[t.CVolatile|T]) + # 模块级层(模块存活期持久): + # global_* 模块级同名三类 + # 类字段层(模块存活期持久): + # field_const_flags '类名.字段名' -> True + # field_volatile_flags '类名.字段名' -> True + # field_*_names 裸字段名集合(IR 侧按 GEP 指令名回查, + # GEP 以字段名命名,见 HandlesExprAttr:36 等) + self.var_volatile_flags: dict[str, bool] = {} + self.var_pointee_volatile_flags: dict[str, bool] = {} + self.global_const_flags: dict[str, bool] = {} + self.global_volatile_flags: dict[str, bool] = {} + self.global_pointee_volatile_flags: dict[str, bool] = {} + self.field_const_flags: dict[str, bool] = {} + self.field_const_names: set[str] = set() + self.field_volatile_flags: dict[str, bool] = {} + self.field_volatile_names: set[str] = set() + self.field_pointee_volatile_flags: dict[str, bool] = {} + self.field_pointee_volatile_names: set[str] = set() + # const 变量名 -> 声明处类型文本(仅用于报文的「声明处类型(含 const)」) + self.const_decl_types: dict[str, str] = {} self._current_source_file: str | None = None self._current_source_lines: list[str] = [] self._ns_cache: dict[str, str] = {} @@ -191,6 +219,18 @@ class BaseGenMixin: # _generate_structs 用不完整 class_members 调用 set_body(set_body 只能调用一次) # 用集合而非单值,因为嵌套特化类 _EmitClassLlvm 会清除单值,导致外层类被错误设置 body self._collecting_members_set: set[str] = set() + # 已按父类展平的类成员表登记(HandlesImports._EmitExternalClassDeclLlvm / + # HandlesClassDef._EmitClassLlvm 在展平完成时登记)。两处用途: + # ① _TryLoadClassMembersFromPyi 的 guard 只在"已展平"时短路 —— + # 停在自有字段的冻结表允许后续补齐(展平与加载顺序无关); + # ② FuncGen._member_base 查询前据此判定是否需要重跑展平。 + self._members_flattened: set[str] = set() + # 展平补齐重入保护:避免 _member_base → _TryLoadClassMembersFromPyi → + # _EmitExternalClassDeclLlvm → _member_base 的同类递归。 + self._members_flatten_in_progress: set[str] = set() + # 展平补齐的"依赖状态签名"(类名 → (struct 元素数, 成员表长度)): + # 同一签名下补齐无效时不再重复尝试(HandlesImports.EnsureClassMembersFlattened)。 + self._members_flatten_repair_sig: dict[str, tuple[int, int]] = {} self._last_var_name: str | None = None self._variadic_info: dict | None = None self._vtable_copy_counter: int = 0 @@ -217,6 +257,10 @@ class BaseGenMixin: # True=每个 define 发 DISubprogram + 入口首条指令发 DILocation。 self.dwarf_enabled: bool = False self.DebugInfo: Any = None + # Ctrl+C 调用栈(--ctrlc-stack):仅在翻译 includes/dbgstack.vp 这一个模块时 + # 置 True —— 落成 .ll 文本前为本模块注入 @llvm.global_ctors 构造器调用其 + # Install()(见 _RegisterDbgStackInstallViaCtor)。默认 False(零影响)。 + self.ctrlc_stack_ctor: bool = False pi: dict[str, bool | int] = self._platform_info # 根据目标平台设置宏(声明式,从三元组推导) if pi['is_windows']: @@ -280,6 +324,30 @@ class BaseGenMixin: return prefix return '' + def _FindStructByIrName(self, ns_name: str) -> ir.IdentifiedStructType | None: + """按 **IR 类型名**(`IdentifiedStructType.name`)查已注册结构体。 + + 必要性:`self.structs` 的键有"裸短名 / sha1 前缀全名 / import 别名"多种, + 同一个 IR 名字可被多个键指向。若只按"键"判存在性,对同一 IR 名字会再建 + 第二个 `IdentifiedStructType` 对象 ⇒ 单模块 .ll 里出现**两条同名 type 定义** + (Body 逐字相同)。`llc` 容忍这种重复,但 `llvm-link` 报 + "error: redefinition of type" ⇒ 跨模块 alwaysinline 合并整体回退 + (现场:includes/ast/parser.vp 的 8312ce86a2fd82c0.Keyword —— 裸名 + 'Keyword' 先被 .tokens 的导入占用,astaux 的同名类只剩 'KwNode' 键, + 随后按全名 '8312ce86a2fd82c0.Keyword' 请求时又建了一个同名新对象)。 + + IR 里类型的身份**就是名字**(不是 Python 对象),故存在性必须按名字判定: + 命中即复用同一对象(需要 body 时由既有 set_body 流程补,与"opaque 占位 → + set_body"同语义)。名字不同的同名类冲突由 sha1 前缀命名负责区分,不靠 + "同名两个对象"。 + """ + if not ns_name: + return None + for _st in self.structs.values(): + if isinstance(_st, ir.IdentifiedStructType) and _st.name == ns_name: + return _st + return None + @staticmethod def _CleanStructName(name: str) -> str: """结构体名清洗:去掉指针标记/空格/修饰符(clean_name 既有行为)。 @@ -299,6 +367,23 @@ class BaseGenMixin: .replace('const', 'const_').replace('volatile', 'volatile_')) return head + tail + def _enum_int_type(self, EnumName: str | None, MemberInfo: Any = None) -> ir.IntType: + """枚举运行时常量 / 枚举类型注解的整数位宽类型(两侧 ABI 同点)。 + + 宽度 = "最宽成员类型",在枚举定义处推导并登记 + (HandlesClassDef._RegisterEnumMembers → enum_width),此处读取。 + 优先用调用点手头的成员条目;退回按枚举名查符号表;再退回默认 i32 + (跨模块 stub 序列化不携带 enum_width、旧产物 → 默认 32,与改前逐字一致)。 + """ + Width: int = EnumIntWidth(MemberInfo, 0) + if Width > 0: + return ir.IntType(Width) + if EnumName: + Table: Any = getattr(self, 'SymbolTable', None) + if Table is not None: + return ir.IntType(EnumIntWidth(Table.lookup(EnumName))) + return ir.IntType(_ENUM_DEFAULT_INT_WIDTH) + def _get_or_create_struct(self, name: str, source_sha1: str | None = None, packed: bool = False) -> ir.IdentifiedStructType: clean_name: str = self._CleanStructName(name) # 如果名称包含模块前缀(如 "a9fa0f6200c09e65.zbit_writer"), @@ -319,6 +404,16 @@ class BaseGenMixin: if _existing_sha1 and _existing_sha1 != parts[0]: if clean_name in self.structs: return self.structs[clean_name] + # 按 IR 名去重:该 IR 名字的 struct 可能已由**别的键** + # (裸名/别名)注册 ⇒ 复用,绝不建第二个同名对象 + # (否则单模块 .ll 出现两条同名 type 定义 ⇒ llvm-link + # "redefinition of type" ⇒ 跨模块内联整体回退)。 + _st_same: ir.IdentifiedStructType | None = self._FindStructByIrName(clean_name) + if _st_same is not None: + self.structs[clean_name] = _st_same + if packed and not _st_same.packed: + _st_same.packed = True + return _st_same st_new: ir.IdentifiedStructType = ir.IdentifiedStructType(self.module, name=clean_name, packed=packed) self.structs[clean_name] = st_new return st_new @@ -354,6 +449,14 @@ class BaseGenMixin: st_full.packed = True return st_full ns_name: str = f"{source_sha1}.{clean_name}" + # 按 IR 名去重(见 _FindStructByIrName):同一 IR 名字已有对象 + # ⇒ 复用同一对象,只补注册本键,绝不建第二个同名对象。 + _st_same2: ir.IdentifiedStructType | None = self._FindStructByIrName(ns_name) + if _st_same2 is not None: + self.structs[full_key] = _st_same2 + if packed and not _st_same2.packed: + _st_same2.packed = True + return _st_same2 st_new: ir.IdentifiedStructType = ir.IdentifiedStructType(self.module, name=ns_name, packed=packed) self.structs[full_key] = st_new return st_new @@ -397,7 +500,10 @@ class BaseGenMixin: # 这对自引用 REnum(如 LLVMType 包含 LLVMType*)至关重要: # _parse_llvm_type 先通过此路径创建 opaque 结构体,_TryLoadStructFromStub 再 set_body。 elif Entry and Entry.IsEnum and clean_name not in self.class_methods: - return ir.IntType(32) + # CEnum 存储宽度 = 最宽成员类型(登记在 Entry.enum_width;未登记 + # → 默认 i32)。此前此处恒 i32,与同文件 _enum_int_type 的常量/ + # 注解宽度分裂(宽成员枚举的类型与常量不同宽)。 + return self._enum_int_type(clean_name, Entry) # 治本修复:当 clean_name 是已注册的类(在 class_methods 或 class_members 中)时, # 不应走 IsExceptionClass/IsFunction/IsVariable 分支返回非结构体类型。 # 否则 _get_or_create_struct 返回指针类型(如 _TypedPointerType)而非 IdentifiedStructType, @@ -412,9 +518,9 @@ class BaseGenMixin: return ir.PointerType(ir.IntType(8)) if Entry and Entry.BaseType: if isinstance(Entry.BaseType, type) and issubclass(Entry.BaseType, _t.CEnum): - return ir.IntType(32) + return self._enum_int_type(clean_name, Entry) if Entry.BaseType is _t.CEnum: - return ir.IntType(32) + return self._enum_int_type(clean_name, Entry) ns_name: str if source_sha1: ns_name = f"{source_sha1}.{clean_name}" @@ -422,6 +528,17 @@ class BaseGenMixin: ns_name = f"{self._struct_sha1_map[clean_name]}.{clean_name}" else: ns_name = self._mangle_name(clean_name) + # 按 IR 名去重(见 _FindStructByIrName):该 IR 名字的对象可能已由**别的键** + # (裸名 / sha1 全名 / import 别名)注册 ⇒ 复用同一对象并补注册本键。 + # 否则单模块 .ll 会出现两条同名 type 定义 ⇒ llvm-link + # "error: redefinition of type" ⇒ 跨模块 alwaysinline 合并整体回退 + # (现场:includes/ast/parser.vp 的 8312ce86a2fd82c0.Keyword 定义两次)。 + _st_same3: ir.IdentifiedStructType | None = self._FindStructByIrName(ns_name) + if _st_same3 is not None: + self.structs[clean_name] = _st_same3 + if packed and not _st_same3.packed: + _st_same3.packed = True + return _st_same3 st: ir.IdentifiedStructType = ir.IdentifiedStructType(self.module, name=ns_name, packed=packed) self.structs[clean_name] = st # 尝试解析 typedef 的底层整数位宽 @@ -843,6 +960,223 @@ class BaseGenMixin: info += f"\n → {source_line}" return info + # ========== const / volatile:判定、登记与报文(两侧同口径) ========== + # 语义唯一来源见 Debugger/type-descriptors.md。TPC 与 TransPyV 必须**同判据、 + # 同报文**;任何未消除的差异都列在该文档的「已知差异」一节。 + + @staticmethod + def _IsConstObject(Info: Any) -> bool: + """注解解析结果是否为「变量自身 const」的对象(C 语义下的 const 对象)。 + + 判据(两侧统一):`(DataConst or VarConst)` 且 `PtrCount == 0`。 + `t.CConst` 在本仓库解析为 **DataConst**(t 模块分支:HandlesTypeMerge.py:192-196、 + TypeAnnotationResolver.py:382-389),其字段语义是「指向的数据 const」 + (HandlesBase.py:76-77)。因此: + - 非指针类型(PtrCount==0):该限定退化为「该对象自身 const」⇒ + `a: const int` / `a: t.CConst | int` 的 a 禁止赋值/增量赋值/元素赋值; + - 指针类型(PtrCount>=1):限定只作用于 pointee ⇒ `const T *p` 的 p + 仍可重新赋值(C 语义),**不**登记为 const 变量。 + 后者就是全仓既有两处 `t.CConst | T | t.CPtr` 局部变量(fat32_types.vp:1591 + `buf_ptr`、elf.vp:509 `cmp_name`)在其作用域内被重新赋值却合法的原因。 + """ + if Info is None: + return False + if not (getattr(Info, 'DataConst', False) or getattr(Info, 'VarConst', False)): + return False + return getattr(Info, 'PtrCount', 0) == 0 + + @staticmethod + def _AnnotationDeclText(Annotation: Any) -> str: + """注解节点的源码文本(仅用于报文;取不到时给占位,不伪造)。""" + if Annotation is None: + return '(无注解)' + try: + return ast.unparse(Annotation) + except Exception: + return '<无法还原注解文本>' + + @staticmethod + def _AnnotationHasQualifier(Annotation: Any, AttrName: str) -> bool: + """注解子树里是否出现 `t.`(与 TransPyV TypeGenErr.has_decorator_marker 同口径)。""" + if Annotation is None: + return False + for n in ast.walk(Annotation): + if isinstance(n, ast.Attribute) and n.attr == AttrName: + return True + return False + + @staticmethod + def _AnnotationHasPointeeVolatile(Annotation: Any) -> bool: + """注解是否为 `rawptr[t.CVolatile | T]`(volatile 指针指向物,等价 C 的 volatile T*)。 + + 与 TransPyV TypeGenErr.has_pointee_volatile_marker 同口径:只看 `rawptr[...]` + 的下标实参子树里有没有 CVolatile;多级指针(slice 仍是 Subscript)明确不支持。 + """ + if not isinstance(Annotation, ast.Subscript): + return False + base: Any = Annotation.value + if not isinstance(base, ast.Name) or base.id != 'rawptr': + return False + return BaseGenMixin._AnnotationHasQualifier(Annotation.slice, 'CVolatile') + + def RegisterVarQualifiers(self, Name: str, Info: Any, Annotation: Any, + IsGlobal: bool = False, IsParam: bool = False) -> None: + """登记一个变量(局部/形参/模块级全局)的 const / volatile 限定。 + + 统一的登记入口(TPC 的形参、局部变量、模块级全局三处声明点都调本函数), + 保证「登记点」只有一份判据。 + + IsParam=True 时 **const 判据沿用形参的历史条件** `DataConst or VarConst` + (不看指针层数):这是「形参保持现状 + 不得放宽任何既有 fail-fast」两条 + 硬约束下的唯一选择——把形参也改成指针感知判据虽然更合 C 语义,但会**放宽** + 既有形参检查(`fmt: t.CConst | str` 这类指针形参不再登记),从约束上不允许。 + 该历史条件对 `const T*` 形参过严,但全仓 462 个 .vp 普查(见 + Debugger/type-descriptors.md)表明**零个形参在其函数体内被重新赋值**, + 故这处差异在既有代码上不可观测。非形参的三种声明一律用指针感知判据 + `_IsConstObject`。 + """ + if not Name: + return + if IsParam: + IsConst: bool = bool(Info) and ( + getattr(Info, 'DataConst', False) or getattr(Info, 'VarConst', False)) + else: + IsConst = self._IsConstObject(Info) + if IsConst: + if IsGlobal: + self.global_const_flags[Name] = True + else: + self.var_const_flags[Name] = True + self.const_decl_types[Name] = self._AnnotationDeclText(Annotation) + if self._AnnotationHasQualifier(Annotation, 'CVolatile'): + if IsGlobal: + self.global_volatile_flags[Name] = True + else: + self.var_volatile_flags[Name] = True + if self._AnnotationHasPointeeVolatile(Annotation): + if IsGlobal: + self.global_pointee_volatile_flags[Name] = True + else: + self.var_pointee_volatile_flags[Name] = True + + def RegisterFieldQualifiers(self, ClassName: str | None, FieldName: str, + Info: Any, Annotation: Any) -> None: + """登记一个类字段的 const / volatile 限定(键 `类名.字段名` + 裸字段名)。""" + if not FieldName: + return + if self._IsConstObject(Info): + if ClassName: + self.field_const_flags[f'{ClassName}.{FieldName}'] = True + self.field_const_names.add(FieldName) + self.const_decl_types[FieldName] = self._AnnotationDeclText(Annotation) + if self._AnnotationHasQualifier(Annotation, 'CVolatile'): + if ClassName: + self.field_volatile_flags[f'{ClassName}.{FieldName}'] = True + self.field_volatile_names.add(FieldName) + if self._AnnotationHasPointeeVolatile(Annotation): + if ClassName: + self.field_pointee_volatile_flags[f'{ClassName}.{FieldName}'] = True + self.field_pointee_volatile_names.add(FieldName) + + def _IsVarConst(self, Name: str) -> bool: + """变量(局部/形参/模块级全局)自身是否 const。""" + return bool(Name) and (Name in self.var_const_flags or Name in self.global_const_flags) + + def _IsVarVolatile(self, Name: str) -> bool: + """变量自身是否 volatile。""" + return bool(Name) and (Name in self.var_volatile_flags or Name in self.global_volatile_flags) + + def _IsVarPointeeVolatile(self, Name: str) -> bool: + """变量所指物是否 volatile(等价 C 的 volatile T* 解引用)。""" + return bool(Name) and (Name in self.var_pointee_volatile_flags + or Name in self.global_pointee_volatile_flags) + + def _IsFieldConst(self, ClassName: str | None, FieldName: str) -> bool: + """类字段自身是否 const。 + + 类名可解析时按精确键 `类名.字段名`(**不**回退裸名,避免"另一个同名 + const 字段"造成跨类误伤);类名解析不到时退化为裸名集合(保守报错)。 + """ + if not FieldName: + return False + if ClassName: + return f'{ClassName}.{FieldName}' in self.field_const_flags + return FieldName in self.field_const_names + + def _IsFieldVolatile(self, FieldName: str) -> bool: + return bool(FieldName) and FieldName in self.field_volatile_names + + def _IsFieldPointeeVolatile(self, FieldName: str) -> bool: + return bool(FieldName) and FieldName in self.field_pointee_volatile_names + + @staticmethod + def _StripDedupSuffix(Name: str) -> list[str]: + """IR 名 -> 候选登记名(llvmlite 去重把重复名改成 `名.1` / `名.2`)。""" + Cands: list[str] = [Name] + Head, _, Tail = Name.rpartition('.') + if Head and Tail.isdigit(): + Cands.append(Head) + return Cands + + def _IsPtrVolatile(self, ptr: Any) -> bool: + """按 IR 指针对应的**登记名**判定该次访问是否带 volatile。 + + alloca / GlobalVariable 均以变量名命名,GEP 以字段名命名 + (见 HandlesExprAttr 的 `gep(..., name=AttrName)`),故变量自身 volatile、 + 变量所指物 volatile 与字段 volatile 都在此命中。名字取不到时返回 False + (不做任何猜测/兜底)。 + + 下标 GEP 额外带基址名(`subscript_<基址名>` / `subscript_ptr_<基址名>`, + 见 HandlesExprAttr/HandlesAssign 的下标改写):基址是 pointee-volatile + 变量/字段时,经该下标的 load/store 也要带 volatile(等价 C 的 + `volatile T* p` 的 `p[i]`)。**基址名后缀为空时名字与改造前逐字相同** + ⇒ 无 pointee-volatile 的代码 IR 逐字节不变。 + """ + nm: Any = getattr(ptr, 'name', None) + if not nm or not isinstance(nm, str): + return False + for C in self._StripDedupSuffix(nm): + if self._IsVarVolatile(C) or self._IsVarPointeeVolatile(C): + return True + if C in self.field_volatile_names or C in self.field_pointee_volatile_names: + return True + BaseNames: list[str] = list(self.var_pointee_volatile_flags) + BaseNames += list(self.global_pointee_volatile_flags) + BaseNames += list(self.field_pointee_volatile_names) + for B in BaseNames: + if (nm == f'subscript_{B}' or nm.startswith(f'subscript_{B}.') + or nm == f'subscript_ptr_{B}' or nm.startswith(f'subscript_ptr_{B}.')): + return True + return False + + @staticmethod + def _SubscriptSuffix(BaseNode: Any) -> str: + """下标基址名后缀(供 pointee-volatile 按 IR 名回查,见 `_IsPtrVolatile`)。 + + 基址是 `Name`(变量)→ `_变量名`;是 `obj.字段` → `_字段名`;其余形态返回 + `''`。**返回 `''` 时下标 GEP 名与改造前逐字相同**(既有代码 IR 零变更)。 + """ + if isinstance(BaseNode, ast.Name): + return f'_{BaseNode.id}' if BaseNode.id else '' + if isinstance(BaseNode, ast.Attribute): + return f'_{BaseNode.attr}' if BaseNode.attr else '' + return '' + + def _ConstAssignMessage(self, Kind: str, Name: str) -> str: + """const 变量被赋值时的 fail-fast 报文(两侧同口径)。 + + 含:文件/行/列 + 源码片段(`_get_node_info`)+ 变量名 + 声明处类型(含 const) + + 修法。 + """ + DeclType: str = self.const_decl_types.get(Name, '(声明处类型未登记)') + return ( + f"不能对 const 变量 '{Name}' {Kind}{self._get_node_info()}" + f"(声明处类型(含 const):{DeclType};" + f"修法:删掉该声明里的 `const` 描述符 / `t.CConst`,或改为声明时一次赋值、" + f"之后只用非 const 的副本/中间量;const 对象在其作用域内禁止赋值、" + f"增量赋值与元素赋值,且不会降级为警告)" + ) + # ========== 统一工具方法 ========== def _create_string_global(self, s: str, name: str | None = None) -> ir.Constant: @@ -1766,6 +2100,56 @@ class BaseGenMixin: gv.linkage = 'appending' gv.initializer = ir.Constant(arr_ty, [entry_val]) + def _RegisterDbgStackInstallViaCtor(self) -> None: + """用 @llvm.global_ctors 在进程启动期调用本模块的 Install()(--ctrlc-stack)。 + + 仅在"本翻译单元就是 includes/dbgstack.vp"时由 StructGen.finalize() 调用 + (ctrlc_stack_ctor 由 Phase2Translator._translate_file 按源文件名置位)。 + 生成 internal void @__dbgstack_install_ctor()(entry 块直接 call 本模块的 + Install() 并忽略其 int 返回值),再写入 appending linkage 的 + @llvm.global_ctors —— 每模块一份,链接器按 appending 语义拼进 .init_array, + 进程启动时执行一次。 + + 为什么用构造器而不是"在 main 入口调用":适配 Projectrans 的"每源文件独立 + 翻译单元"模型(不依赖哪个模块含 main),且 CRT 初始化期即执行,能覆盖 + "main 之前就卡死"的场景 —— 这正是本功能的用途。 + + 同模块内取 Install 符号(不走跨模块声明),避免跨模块符号名/mangling 耦合。 + fail-fast:找不到 Install 符号、或模块内已存在 @llvm.global_ctors 时显式 + 报错(不静默降级 —— 否则产物缺注册、功能静默失效)。 + """ + install_fn: ir.Function | None = self._find_function('Install') + if install_fn is None: + raise RuntimeError( + '--ctrlc-stack: 在 dbgstack 模块内找不到 Install() 函数,' + '无法注入启动期构造器(不静默降级)') + i32: ir.IntType = ir.IntType(32) + i8p: ir.PointerType = ir.PointerType(ir.IntType(8)) + void_fnptr: ir.PointerType = ir.PointerType(ir.FunctionType(ir.VoidType(), [])) + ctor: ir.Function = ir.Function( + self.module, ir.FunctionType(ir.VoidType(), []), + name=self._UniqueGlobalName('__dbgstack_install_ctor')) + ctor.linkage = 'internal' + cb: ir.IRBuilder = ir.IRBuilder(ctor.append_basic_block(name='entry')) + cb.call(install_fn, []) # Install() 无参;返回值(int)有意忽略 + cb.ret_void() + if 'llvm.global_ctors' in self.module.globals: + raise RuntimeError( + '--ctrlc-stack: 模块内已存在 @llvm.global_ctors,' + '无法注入 dbgstack 启动期构造器(不静默降级)') + elem_ty: ir.LiteralStructType = ir.LiteralStructType([i32, void_fnptr, i8p]) + arr_ty: ir.ArrayType = ir.ArrayType(elem_ty, 1) + entry_val: ir.Constant = ir.Constant(elem_ty, [ + ir.Constant(i32, 65535), + # 传函数引用字符串(非 Function 对象):与 _RegisterHotspotSitesViaCtor 同法, + # 否则 llvmlite 会以 str(Function) 渲染整个 define 体 + ir.Constant(void_fnptr, ctor.get_reference()), + ir.Constant(i8p, None), + ]) + gv: ir.GlobalVariable = ir.GlobalVariable(self.module, arr_ty, name='llvm.global_ctors') + gv.linkage = 'appending' + gv.initializer = ir.Constant(arr_ty, [entry_val]) + def _InstallHotspotSites(self) -> None: """安装调用点热点统计(--hotspot-sites):种子 CLI 单模块路径。 diff --git a/lib/core/LLVMCG/FuncGen.py b/lib/core/LLVMCG/FuncGen.py index 3dc4867..19fb242 100644 --- a/lib/core/LLVMCG/FuncGen.py +++ b/lib/core/LLVMCG/FuncGen.py @@ -10,6 +10,15 @@ from lib.core.VLogger import get_logger as _vlog from lib.constants.config import mode as _config_mode +class FailFastError(Exception): + """专用 fail-fast 异常:编译器主动抛出的编译错误(不可隐式转换/不可恢复)。 + + 刻意与 llvmlite 自身抛出的 TypeError/ValueError 区分开 —— 后者属于"此处可以尝试 + 简化回退"的类型调整失败,本异常则是必须上抛、禁止被兜底 `except` 吞掉的信号 + (例如 `_adjust_args` 的 L120/L181 两条 fail-fast)。 + """ + + class FuncGenMixin: def setup_from_symbol_table(self, SymbolTable: Any) -> None: @@ -42,7 +51,14 @@ class FuncGenMixin: self._generate_structs() self._create_Vtable_globals() - def register_method(self, ClassName: str, MethodName: str) -> None: + def register_method(self, ClassName: str, MethodName: str, IsVtableSlot: bool = True) -> None: + """登记类方法。 + + IsVtableSlot:该方法是否占用虚表槽(class_methods 即 vtable 槽序唯一真源)。 + 默认 True 与既有语义逐字一致;False 用于方法级 `novirtual def`(⇒ 方法装饰器 + @t.NoVTable)——此时只保证类的方法容器存在,**不**把它排进槽序,使 TPC 与 + TPV(ClassGenBase._should_method_be_virtual 排除该项)的虚表布局逐槽相同。 + """ if ClassName not in self.class_methods: self.class_methods[ClassName] = [] if ClassName not in self.class_members: @@ -54,6 +70,8 @@ class FuncGenMixin: _short_name: str = MethodName.split('.')[-1] if '.' in MethodName else MethodName if _short_name in ('__new__', '__init__', '__before_init__'): return + if not IsVtableSlot: + return if MethodName not in self.class_methods[ClassName]: self.class_methods[ClassName].append(MethodName) @@ -117,9 +135,17 @@ class FuncGenMixin: elif isinstance(arg.type, ir.PointerType) and isinstance(param.type, ir.IntType): # fail-fast:指针→整数隐式转换(跨类型层级,有损)禁止, # 须显式转换(用户隐式转换规则:同层指针可转,指针→整数不允许)。 - raise TypeError( + # 诊断增强(只在即将终止时拼串,零行为影响):把 callee 名、 + # 实参/形参个数与**逐项类型**一并报出——只给单个类型对无法判断 + # 「形参表整体错位一位」(如 self/pool 约定不一致)这类根因。 + _diag: str = "; ".join( + f"[{k}] arg={a.type} param={p.type}" + for k, (a, p) in enumerate(zip(args, func.args))) + raise FailFastError( f"调用参数类型不匹配: 指针类型 {arg.type} 不能隐式转换为" - f"整数参数 {param.type};如需转换请显式 cast") + f"整数参数 {param.type};如需转换请显式 cast" + f"(callee={func.name} 实参{len(args)}个/形参{len(func.args)}个;" + f"逐项: {_diag})") elif isinstance(arg.type, ir.PointerType) and isinstance(param.type, ir.PointerType): if isinstance(arg.type.pointee, ir.PointerType) and isinstance(param.type.pointee, ir.IntType): Loaded: ir.Value = self._load(arg, name=f"Load_arg_{i}") @@ -178,7 +204,7 @@ class FuncGenMixin: elif isinstance(arg.type, (ir.FloatType, ir.DoubleType)) and isinstance(param.type, ir.IntType): # fail-fast:浮点→整数隐式转换(有损)禁止,须显式转换 # (用户隐式转换规则:浮点→整数不允许)。 - raise TypeError( + raise FailFastError( f"调用参数类型不匹配: 浮点类型 {arg.type} 不能隐式转换为" f"整数参数 {param.type};如需转换请显式 cast") elif isinstance(arg.type, ir.IntType) and isinstance(param.type, (ir.FloatType, ir.DoubleType)): @@ -201,7 +227,10 @@ class FuncGenMixin: adjusted.append(i64_val) else: adjusted.append(self.builder.bitcast(arg, param.type, name=f"cast_arg_{i}")) - except Exception: # 参数类型调整失败时使用简化回退逻辑 + # 只收口 llvmlite 自身"可回退"的类型调整失败(builder/IR 构造抛出的 + # TypeError/ValueError);FailFastError 与其余异常一律上抛, + # 避免编译器主动抛出的真因被伪装成 llvmlite 的下游报文。 + except (TypeError, ValueError): # 参数类型调整失败时使用简化回退逻辑 if isinstance(arg.type, ir.PointerType) and isinstance(param.type, (ir.IdentifiedStructType, ir.LiteralStructType, ir.ArrayType)): if arg.type.pointee == param.type or (hasattr(arg.type.pointee, 'name') and hasattr(param.type, 'name') and arg.type.pointee.name == param.type.name): adjusted.append(self._load(arg, name=f"Load_arg_{i}")) @@ -238,39 +267,135 @@ class FuncGenMixin: adjusted.append(arg) return adjusted + def _class_has_vtable(self, ClassName: str | None) -> bool: + """类(或其任一祖先)是否带 vtable 槽。 + + 与 StructGen.py:442-450 的父链传播同源——vtable 沿继承链携带,子类 struct 布局为 + [vtable?] + [基类字段…] + [自身字段…]。只认 class_vtable / + _cross_module_vtable_classes(装饰器集合)会漏掉源码未写 @t.CVTable 的跨模块 + AST 节点类(ast.Try / ast.Match / ast.ExceptHandler…):它们有方法、继承自 + @t.CVTable 的 AST,struct 首元素确实是 vtable 指针,但 StructGen.py:415 在 + "struct body 已从 stub 完整加载"时直接 continue,永不把它们登记进集合 + (实测 TransPyV/temp/_shared_sym.json 的 class_vtable/cross_module_vtable 仅有 + {AST, MemManager, Module})。此时 base 静默取 0 → 该类字段偏移整体前移 1 + (ast.ExceptHandler.children 落到 end_col_offset,i32)。 + """ + if not ClassName: + return False + _Seen: set[str] = set() + _Cur: str | None = ClassName + while _Cur and _Cur not in _Seen: + _Seen.add(_Cur) + if _Cur in self.class_vtable or _Cur in self._cross_module_vtable_classes: + return True + _Cur = self.class_parent.get(_Cur) + return False + + def _NormalizeMemberClass(self, ClassName: str | None) -> str | None: + """成员查询用的类键归一(短名 + 指针包装展开)。 + + - 统一短名解析(治本修复 2026-08-10):struct/class_members/class_vtable/ + class_parent 均以短名注册(跨模块加载 HandlesImports L1604),而调用方 + 可能传入带 sha1 前缀的全名(如 "f47f88ae6545965a.Import")。 + - rawptr[X] / t.CPtr[X] 指针包装归一:rawptr[X] 是内建裸指针类型 + (X*),非真实类——接收者注解/类型名为 'rawptr[AST]' 时字段访问 + (child.parent)应作用于内层 struct X,否则查 class_members 全 miss + → fast-fail "属性 'parent' 在类 'rawptr[AST]' 中找不到(struct=opaque)" + (includes/ast/base.vp _set_parent_list bootstrap 根因)。 + """ + if not ClassName: + return ClassName + _Cls: str = self._extract_short_name(ClassName) + if _Cls.startswith('rawptr[') or _Cls.startswith('t.CPtr['): + _lb: int = _Cls.find('[') + if _Cls.endswith(']') and _lb > 0: + _inner: str = _Cls[_lb + 1:-1].strip() + _short_inner: str = _inner.rsplit('.', 1)[-1] if '.' in _inner else _inner + _hit: str | None = None + if _short_inner in self.structs: + _hit = _short_inner + else: + for _k in self.structs: + if self._extract_short_name(_k) == _short_inner: + _hit = _k + break + if _hit: + _Cls = _hit + return _Cls + + def _member_base(self, ClassName: str | None) -> int: + """类 struct 的成员偏移基址(vtable 占位槽数)——**全仓唯一真源**。 + + `_get_member_offset` 与所有"自算 base 再 GEP 字段"的消费点(构造/初始化、 + 字段默认值、继承方法 this 调整等)都必须走本方法,禁止各自直查 + `class_vtable`/`_cross_module_vtable_classes`——旧实现只认这两个装饰器集合, + 跨模块 AST 节点类(ast.Try/ast.Match/ast.ExceptHandler…)源码未写 + @t.CVTable 且 StructGen 在"struct body 已从 stub 加载"时提前 continue, + 永不把它们登记进集合,于是 base 只能靠"成员表长度 == struct 元素数 - 1" + 这个长度启发式救回;成员表未展平/尚未加载时启发式失效 ⇒ base 静默取 0 + ⇒ 该类所有字段偏移整体前移 1(ast.ExceptHandler.children 落到 + end_col_offset(i32),len() 分派拿到 i32 ⇒ Type of #1 arg mismatch)。 + + 判定规则(与 `StructGen._generate_structs` 生成 struct 的规则同源): + - vtable 归属沿 class_parent 链判定(见 `_class_has_vtable`); + - 再用 struct 实际元素数校准: + delta(=struct 元素数 - 成员表长度) == 0 ⇒ 该 struct 无 vtable 槽 → base=0; + delta == 1 ⇒ 恰多一个槽:首元素为指针 / 无父类 / 继承链归属成立 → base=1; + delta >= 2 ⇒ 长度启发式失效,保留"继承链 vtable 归属"结果(不再静默取 0)。 + + 查询前会调用 `HandlesImports.EnsureClassMembersFlattened`(幂等)把"停在自有 + 字段"的未展平表补齐——未展平表会让下列 delta 校准失真,也会让按表内下标 + 取字段的消费点漏掉继承前缀。 + """ + _Cls: str | None = self._NormalizeMemberClass(ClassName) + if not _Cls: + return 0 + # 查询前确保成员表已按父类展平(幂等、顺序无关)。 + # 未展平的表(停在自有字段)会让本方法的 delta 校准失真,也会让所有 + # "按成员表下标 base+i 取字段"的消费点漏掉继承前缀 —— 见 + # HandlesImports.EnsureClassMembersFlattened(内部有纯内存前置判断与 + # 重入/重复尝试保护,对已展平类零额外开销)。 + _EnsureIH: Any = getattr(self, '_import_handler_ref', None) + if _EnsureIH is not None and hasattr(_EnsureIH, 'EnsureClassMembersFlattened'): + try: + _EnsureIH.EnsureClassMembersFlattened(_Cls, self) + except Exception as _e: + if _config_mode == "strict": + raise + _vlog().warning(f"补齐类成员展平失败: {_e}", "Exception") + _ByInheritVtable: bool = self._class_has_vtable(_Cls) + base: int = 1 if _ByInheritVtable else 0 + _StructBase: ir.IdentifiedStructType | None = self.structs.get(_Cls) + if (isinstance(_StructBase, ir.IdentifiedStructType) + and _StructBase.elements is not None and len(_StructBase.elements) > 0): + _NumElems: int = len(_StructBase.elements) + _NumMembers: int = len(self.class_members.get(_Cls, [])) + _Delta: int = _NumElems - _NumMembers + _FirstElem: ir.Type = _StructBase.elements[0] + _IsVtablePtr: bool = (isinstance(_FirstElem, ir.PointerType) + and isinstance(_FirstElem.pointee, ir.IntType) + and _FirstElem.pointee.width == 8) + if _Delta == 0: + base = 0 + elif _Delta == 1: + _HasParent: bool = self.class_parent.get(_Cls) is not None + base = 1 if (_IsVtablePtr or not _HasParent or _ByInheritVtable) else 0 + return base + def _get_member_offset(self, field_name: str, ClassName: str | None = None) -> int | None: + # base 判定用未归一的原始类名:_member_base 内部自行做同一套归一 + # (_NormalizeMemberClass),二者结果逐字一致(避免二次归一在 + # "rawptr[X] 展开命中 sha1 前缀 struct 键"这一角落改变取值)。 + _MemberBaseKey: str | None = ClassName # 治本修复:同名类 sha1 冲突检测。 # 当 class_sha1_map[ClassName] 的 sha1 与 Gen.structs[ClassName] 的 sha1 不一致时, # class_members[ClassName] 可能存储了错误模块的同名类成员。 # 此处检测冲突,加载正确模块的 class_members 并以 sha1 前缀全名缓存。 _ResolvedClass: str = ClassName if ClassName else '' if ClassName: - # 统一短名解析(治本修复 2026-08-10):struct/class_members/class_vtable/ - # class_parent 均以短名注册(跨模块加载 HandlesImports L1604),而调用方 - # 可能传入带 sha1 前缀的全名(如 "f47f88ae6545965a.Import"),导致下方 - # 查表全部 miss 返回 None → _gep_and_Load_member 静默返回 None → - # AnnAssign InitValue 为 None → 变量 alloca 未初始化 → 运行期读垃圾指针。 - ClassName = self._extract_short_name(ClassName) - # rawptr[X] / t.CPtr[X] 指针包装归一:rawptr[X] 是内建裸指针类型 - # (X*),非真实类——接收者注解/类型名为 'rawptr[AST]' 时字段访问 - # (child.parent)应作用于内层 struct X,否则查 class_members 全 miss - # → fast-fail "属性 'parent' 在类 'rawptr[AST]' 中找不到(struct=opaque)" - # (includes/ast/base.vp _set_parent_list bootstrap 根因)。 - if ClassName.startswith('rawptr[') or ClassName.startswith('t.CPtr['): - _lb_rc: int = ClassName.find('[') - if ClassName.endswith(']') and _lb_rc > 0: - _inner_rc: str = ClassName[_lb_rc + 1:-1].strip() - _short_rc: str = _inner_rc.rsplit('.', 1)[-1] if '.' in _inner_rc else _inner_rc - _hit_rc: str | None = None - if _short_rc in self.structs: - _hit_rc = _short_rc - else: - for _k_rc in self.structs: - if self._extract_short_name(_k_rc) == _short_rc: - _hit_rc = _k_rc - break - if _hit_rc: - ClassName = _hit_rc + # 类键归一(短名 + rawptr[X]/t.CPtr[X] 展开)统一走 _NormalizeMemberClass, + # 与 _member_base 的 base 判定共用同一归一,禁止各写一份。 + ClassName = self._NormalizeMemberClass(ClassName) _ResolvedClass = ClassName _ClassSha1Map: dict[str, str] = getattr(self, 'class_sha1_map', {}) _ExpectedSha1: str | None = _ClassSha1Map.get(ClassName) @@ -303,27 +428,19 @@ class FuncGenMixin: self.class_members[ClassName] = _OldMembers else: self.class_members.pop(ClassName, None) - has_vtable: bool = ClassName and (ClassName in self.class_vtable or ClassName in self._cross_module_vtable_classes) - base: int = 1 if has_vtable else 0 - if ClassName and ClassName in self.structs: - struct_type: ir.IdentifiedStructType = self.structs[ClassName] - if isinstance(struct_type, ir.IdentifiedStructType) and struct_type.elements is not None and len(struct_type.elements) > 0: - if ClassName in self.class_members: - expected_fields: int = len(self.class_members[ClassName]) - actual_elements: int = len(struct_type.elements) - if base > 0 and actual_elements == expected_fields: - base = 0 - elif base == 0 and actual_elements == expected_fields + 1: - # 跨模块类可能未被检测为 CVTable(如 Name 继承 AST@t.CVTable), - # 但实际 struct 首元素是 vtable ptr → 需 base=1。 - # 但若类有继承(class_parent 有值),多出的 1 可能是继承字段而非 vtable, - # 需检查 struct 首元素类型:i8*/i8** 为 vtable,其他为继承字段。 - _first_elem = struct_type.elements[0] if actual_elements > 0 else None - _has_parent = self.class_parent.get(ClassName) is not None - _is_vtable_ptr = (isinstance(_first_elem, ir.PointerType) and - isinstance(_first_elem.pointee, ir.IntType) and _first_elem.pointee.width == 8) - if not _has_parent or _is_vtable_ptr: - base = 1 + # base(vtable 占位槽数):唯一真源 = _member_base(规则与文档见该方法)。 + # 本函数在同一次调用内还会在下方多处重载成员表(懒加载 425-436、模块重载 + # 482-508、字段预注册 558-578、force 重建 587-643),每次都可能改写 + # _member_base 的三个输入(class_parent 父链、structs 布局、class_members + # 长度)。因此 base 与"即将返回的下标 i"必须取自**同一份最新已加载状态**: + # 所有 return 一律用 _BaseNow() 现算,禁止缓存早先算出的 base——缓存会让 + # 偏移整体差 1(ast.ExceptHandler.children 落到 end_col_offset(i32), + # len 分派拿到 i32)。 + def _BaseNow() -> int: + return self._member_base(_MemberBaseKey) + # 预热(与原行为逐字一致):先触发一次 EnsureClassMembersFlattened, + # 再决定下面的懒加载。 + _BaseNow() if ClassName and ClassName not in self.class_members and self._Trans and hasattr(self._Trans, 'ImportHandler'): # 跨模块 class_members 懒加载:struct 已从 stub 加载但 class_members # 可能尚未从 .vpi 填充(vpi/stub 就绪时序不一致),主动按类 sha1 加载。 @@ -336,10 +453,13 @@ class FuncGenMixin: continue if ClassName in self.class_members and len(self.class_members[ClassName]) > 0: break + # 依赖补齐(上面的懒加载)后再确认一次展平(幂等);偏移基址由 _BaseNow() + # 在真正 return 处现算,不在此缓存。 + _BaseNow() if _ResolvedClass and _ResolvedClass in self.class_members: for i, (name, _) in enumerate(self.class_members[_ResolvedClass]): if name == field_name: - return base + i + return _BaseNow() + i # 继承链回退:当字段不在当前类的 class_members 中时(常见于跨模块导入 # 父类未加载,字段被填充为 __inherited_N 占位名),沿 class_parent 链向上查找。 # 子类 struct 布局为 [vtable?] + [基类字段...] + [子类字段...], @@ -348,33 +468,56 @@ class FuncGenMixin: if ClassName and field_name: _parent: str | None = self.class_parent.get(ClassName) _visited: set = {ClassName} if ClassName else set() + _cls_short: str = ClassName.split('.')[-1] if ClassName else '' while _parent and _parent not in _visited: _visited.add(_parent) + # 父类候选键:class_parent 只登记**短名**(含泛型基类时是模板名, + # 如 'GSListNode'),而 class_members 里真实键可能是**实例化名** + # ('GSListNode[GlobalVariable]',见 includes/llvmlite/__module.vp + # `class GlobalVariable(GSListNode[GlobalVariable])`)或模板名。 + # 少这一步 ⇒ 泛型基类字段(Next/Prev 等)沿链永远查不到 → fast-fail + # ("属性 'Next' 在类 'GlobalVariable' 中找不到")。 + _p_tmpl: str = _parent.split('[')[0] + _cand_keys: list = [] + for _k in (_parent, _p_tmpl, f"{_p_tmpl}[{_cls_short}]" if _cls_short else _p_tmpl): + if _k and _k not in _cand_keys: + _cand_keys.append(_k) # 跳过 __inherited_N 占位条目,只在真实字段名中查找 - if _parent in self.class_members: - _members: list = self.class_members[_parent] - _has_placeholder: bool = any(n.startswith('__inherited_') for n, _ in _members) - if not _has_placeholder: - for i, (name, _) in enumerate(_members): - if name == field_name: - return base + i + _hit_idx: int = -1 + for _k in _cand_keys: + if _k in self.class_members: + _members: list = self.class_members[_k] + _has_placeholder: bool = any(n.startswith('__inherited_') for n, _ in _members) + if not _has_placeholder: + for i, (name, _) in enumerate(_members): + if name == field_name: + _hit_idx = i + break + if _hit_idx >= 0: + break + if _hit_idx >= 0: + # 基类字段位于子类 struct 的 base 起始处,索引 i 即基类内偏移 + return _BaseNow() + _hit_idx # 父类 class_members 未加载或含占位条目,尝试从 stub 加载真实字段 if self._Trans and hasattr(self._Trans, 'ImportHandler'): sha1_map: dict[str, str] = getattr(self, 'ModuleSha1Map', {}) - for mod_name, mod_sha1 in sha1_map.items(): - stub_name: str = f"{mod_sha1}.stub.ll" - self._Trans.ImportHandler._TryLoadClassMembersFromPyi(_parent, stub_name, self) - if _parent in self.class_members and not any(n.startswith('__inherited_') for n, _ in self.class_members[_parent]): - for i, (name, _) in enumerate(self.class_members[_parent]): - if name == field_name: - return base + i + for _k in _cand_keys: + for mod_name, mod_sha1 in sha1_map.items(): + stub_name: str = f"{mod_sha1}.stub.ll" + self._Trans.ImportHandler._TryLoadClassMembersFromPyi(_k, stub_name, self) + if _k in self.class_members and not any(n.startswith('__inherited_') for n, _ in self.class_members[_k]): + for i, (name, _) in enumerate(self.class_members[_k]): + if name == field_name: + return _BaseNow() + i + break + if _k in self.class_members and any(n == field_name for n, _ in self.class_members.get(_k, [])): break _parent = self.class_parent.get(_parent) short_name: str | None = ClassName.split('.')[-1] if ClassName and '.' in ClassName else None if short_name and short_name in self.class_members: for i, (name, _) in enumerate(self.class_members[short_name]): if name == field_name: - return base + i + return _BaseNow() + i if ClassName and field_name: if ClassName not in self.class_members or field_name not in [n for n, _ in self.class_members.get(ClassName, [])]: if self._Trans and hasattr(self._Trans, 'ImportHandler'): @@ -385,7 +528,7 @@ class FuncGenMixin: if ClassName in self.class_members and len(self.class_members[ClassName]) > 0: for i, (name, _) in enumerate(self.class_members[ClassName]): if name == field_name: - return base + i + return _BaseNow() + i break if short_name: for mod_name, mod_sha1 in sha1_map.items(): @@ -394,14 +537,14 @@ class FuncGenMixin: if short_name in self.class_members and len(self.class_members[short_name]) > 0: for i, (name, _) in enumerate(self.class_members[short_name]): if name == field_name: - return base + i + return _BaseNow() + i break if ClassName and field_name: for cn_key in self.class_members: if cn_key == ClassName or cn_key.endswith(f'.{ClassName}') or (short_name and (cn_key == short_name or cn_key.endswith(f'.{short_name}'))): for i, (name, _) in enumerate(self.class_members[cn_key]): if name == field_name: - return base + i + return _BaseNow() + i # REnum 变体搜索: ClassName 是 REnum(如 MetaType),成员注册在变体结构 # (MetaType_Var, MetaType_Concrete, MetaType_App)中。变体与 REnum 主结构 # 共享 max_variant_struct 布局,偏移一致(__tag 在偏移 0,字段从偏移 1 开始) @@ -449,7 +592,7 @@ class FuncGenMixin: if short_cn in self.class_members and len(self.class_members[short_cn]) > 0: for i, (name, _) in enumerate(self.class_members[short_cn]): if name == field_name: - return base + i + return _BaseNow() + i break if ClassName and ClassName in self.structs and (ClassName not in self.class_members or len(self.class_members.get(ClassName, [])) == 0): struct_type: ir.IdentifiedStructType = self.structs[ClassName] @@ -466,11 +609,11 @@ class FuncGenMixin: if ClassName in self.class_members: for i, (name, _) in enumerate(self.class_members[ClassName]): if name == field_name: - return base + i + return _BaseNow() + i if short_name and short_name in self.class_members: for i, (name, _) in enumerate(self.class_members[short_name]): if name == field_name: - return base + i + return _BaseNow() + i break # 最后回退:当继承链回退失败但 struct body 已加载时, # 从 struct elements 数和 class_members 数推断继承字段偏移。 @@ -521,8 +664,8 @@ class FuncGenMixin: _num_members_final = len(_members_final) for _Ri, (_Rn, _Rt) in enumerate(_members_final): if _Rn == field_name: - return base + _Ri - _num_inherited_final: int = _num_elements_final - _num_members_final - base + return _BaseNow() + _Ri + _num_inherited_final: int = _num_elements_final - _num_members_final - _BaseNow() if _num_inherited_final == 1: # 推断(缺失槽位 = 位于 base+0 的未展平继承字段)仅在成员表 # 尚未含展平继承字段时成立:若成员表首项本身就是任一已加载 @@ -536,7 +679,7 @@ class FuncGenMixin: pass # 已含展平继承字段 → 落入下方 fast-fail 暴露真实缺失 else: # 单继承字段,偏移 = base(vtable 之后第一个位置) - return base + 0 + return _BaseNow() + 0 # fast-fail(治本修复 2026-08-10):字段未找到不再静默返回 None。 # 静默 None 会让调用方(_gep_and_Load_member L26-27)直接 return None, # AnnAssign InitValue 变 None → 变量 alloca 未初始化 → 运行期读垃圾指针 diff --git a/lib/core/LLVMCG/StructGen.py b/lib/core/LLVMCG/StructGen.py index 56f6a38..100acd9 100644 --- a/lib/core/LLVMCG/StructGen.py +++ b/lib/core/LLVMCG/StructGen.py @@ -140,6 +140,12 @@ class StructGenMixin: _DbgInfo = self._GetDebugInfo() if _DbgInfo is not None: _DbgInfo.Finalize() + # Ctrl+C 调用栈(--ctrlc-stack):同样是"落成 .ll 文本之前"的模块级收尾—— + # 当本模块就是 includes/dbgstack.vp 时,注入 @llvm.global_ctors 构造器调用 + # 其 Install()(见 BaseGen._RegisterDbgStackInstallViaCtor)。未启用/非该模块 + # 时 ctrlc_stack_ctor=False,不做任何事(零影响)。 + if self.ctrlc_stack_ctor: + self._RegisterDbgStackInstallViaCtor() ir_text: str try: ir_text = str(self.module) diff --git a/lib/core/Translator/LlvmGenerator.py b/lib/core/Translator/LlvmGenerator.py index 9eb464f..2595b36 100644 --- a/lib/core/Translator/LlvmGenerator.py +++ b/lib/core/Translator/LlvmGenerator.py @@ -11,7 +11,9 @@ from lib.includes import t from lib.constants.config import mode as _config_mode from lib.core.VLogger import get_logger as _vlog from lib.core.DecoratorPass import run as _run_decorator_pass -from lib.core.SymbolUtils import IsTModule, AnnotationContainsName +from lib.core.SymbolUtils import ( + IsTModule, AnnotationContainsName, RequireDefineConstStorable, +) # 治本修复:导入 ConstEvaluator 用于预扫描 CDefine 常量的复杂表达式求值 from lib.core.ConstEvaluator import ConstEvaluator @@ -87,6 +89,11 @@ class LlvmGeneratorMixin: return val: int | str | float | None = _extract_const_value(Value) if val is not None: + # fail-fast:常量值必须能落进声明类型(define 带类型形态)/ 64 位存储 + # (自动推导形态,本语言无 64 位以上整数类型)——此前超大值会被静默 + # 截断/生成非法 IR 文本。唯一判据见 SymbolUtils.RequireDefineIntStorable。 + RequireDefineConstStorable(Gen, self, NameNode, TypeInfo.BaseType, + Annotation, val, NameNode.id) if not hasattr(Gen, '_define_constants'): Gen._define_constants: dict[str, int | str | float | bool] = {} Gen._define_constants[NameNode.id] = val @@ -162,6 +169,12 @@ class LlvmGeneratorMixin: if isinstance(e, ImportError): raise lineno: int = getattr(Node, 'lineno', 0) + # 注入行偏移还原:AST 行号含 _inject_auto_imports 在文件头注入的 N 行, + # 先按精确偏移减回磁盘真实行号(N 由 Phase2Translator 计算并挂到 translator); + # 报文行号与源码行必须同用还原后的 lineno,二者一致(否则会显示下一行源码)。 + _line_offset: int = int(getattr(self, '_line_offset', 0) or 0) + if _line_offset > 0 and lineno > _line_offset: + lineno -= _line_offset source_line: str | None = Gen._get_source_line(lineno) src_file: str = getattr(Gen, '_current_source_file', '') src_path: str = src_file if src_file else 'unknown' @@ -225,6 +238,12 @@ class LlvmGeneratorMixin: self._HandleModuleLevelExprLlvm(Node, Gen) except Exception as e: lineno: int = getattr(Node, 'lineno', 0) + # 注入行偏移还原:AST 行号含 _inject_auto_imports 在文件头注入的 N 行, + # 先按精确偏移减回磁盘真实行号(N 由 Phase2Translator 计算并挂到 translator); + # 报文行号与源码行必须同用还原后的 lineno,二者一致(否则会显示下一行源码)。 + _line_offset: int = int(getattr(self, '_line_offset', 0) or 0) + if _line_offset > 0 and lineno > _line_offset: + lineno -= _line_offset source_line: str | None = Gen._get_source_line(lineno) src_file: str = getattr(Gen, '_current_source_file', '') src_path: str = src_file if src_file else 'unknown' @@ -252,6 +271,11 @@ class LlvmGeneratorMixin: if not isinstance(Node.value, ast.Call): return call: ast.Call = Node.value + if isinstance(call.func, ast.Name): + # 模块级裸名 __llvmir__(...) 等价 c.LLVMIR(...) + if call.func.id == '__llvmir__': + self._HandleModuleLevelLLVMIR(call, Gen) + return if not isinstance(call.func, ast.Attribute): return if not isinstance(call.func.value, ast.Name) or call.func.value.id != 'c': @@ -676,7 +700,11 @@ class LlvmGeneratorMixin: # 函数级 DWARF 调试元数据(--dwarf / --asan):透传开关给本模块的 # LlvmCodeGenerator;未启用时 dwarf_enabled=False → 不构造 DI、不挂 !dbg, # 生成的 .ll 与改动前逐字节一致(零影响) - self.LlvmGen.dwarf_enabled = bool(getattr(self, 'DwarfEnabled', False)) + self.LlvmGen.dwarf_enabled = bool(getattr(self, 'DwarfEnabled', False)) + # Ctrl+C 调用栈(--ctrlc-stack):透传"本模块需注入 Install() 启动期构造器" + # 标记(仅 includes/dbgstack.vp 的翻译上下文为 True);未启用时为 False + # → finalize() 不做任何事,生成的 .ll 与改动前逐字节一致(零影响) + self.LlvmGen.ctrlc_stack_ctor = bool(getattr(self, 'CtrlcStackCtor', False)) # --- 修复:共享 vtable 状态跨模块 --- # 根因:LlvmGen 每模块重新创建,class_vtable/class_methods 等是实例级属性,