feat(ctrlc-stack): 新增 --ctrlc-stack 编译期开关(按键打印 .vp 调用栈)
Config/Phase2Translator: --ctrlc-stack 自动开 DWARF;缓存失效;链接后 llvm-dwarfdump 导出 <exe>.stackmap(逐条归一化为 RVA——旧的整体判据会被 lld 去重产生的 0 地址行带偏,导致全表差一个 ImageBase);链接参数追加 -ldbghelp;include 也生成 DWARF 并隔离到 includes.binary_ctrlcstack(_asan);llc 追加 -frame-pointer=all。 BaseGen._RegisterDbgStackInstallViaCtor + StructGen.finalize 钩子: 注入 llvm.global_ctors(priority 65535) 在启动期调用 dbgstack.Install(),写盘前核对 global_ctors 否则 fail-fast。 FuncGen._adjust_args: 指针到整数的 fail-fast 补逐项类型诊断。
This commit is contained in:
1 parent
f7ace9876d
commit
53a345a59a
6 files changed
+1351
-123
No files matched your search
@@ -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 导出为 "<exe>.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 各模块按真实次数降序写入 <path>(默认为 hotspot_sites.txt);同时为每个函数定义加计时器,按自身耗时(self)降序写入同目录 hotspot_time.txt,行为 `<函数名> <self> <inc> <进入次数>`(单位 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 导出 <exe>.stackmap。
|
||||
# 不在此处做工具/平台校验——真正的 fail-fast 落在链接成功后的旁文件导出步骤
|
||||
# (那里才有最终 exe 路径可判 PE)。此处仅提示已启用。
|
||||
if args.ctrlc_stack:
|
||||
_vlog().info('Ctrl+C 调用栈模式已启用(链接后用 llvm-dwarfdump 导出 <exe>.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=未启用(零影响)
|
||||
|
||||
@@ -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:地址→源行 旁文件(<exe>.stackmap)----------
|
||||
#
|
||||
# 开启 --ctrlc-stack 时,链接成功后用 llvm-dwarfdump 解析最终 PE 的
|
||||
# .debug_line/.debug_info,把已有的"函数级/语句级 DWARF 行标记"导出成
|
||||
# <exe>.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('<I', head, 0x3C)[0]
|
||||
if e_lfanew + 24 > 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('<H', head, coff_off + 2)[0]
|
||||
opt_size: int = struct.unpack_from('<H', head, coff_off + 16)[0]
|
||||
opt_off: int = coff_off + 20 # COFF 文件头固定 20 字节
|
||||
if opt_off + 32 > len(head):
|
||||
return None
|
||||
magic: int = struct.unpack_from('<H', head, opt_off)[0]
|
||||
if magic == 0x20B: # PE32+
|
||||
image_base: int = struct.unpack_from('<Q', head, opt_off + 24)[0]
|
||||
elif magic == 0x10B: # PE32
|
||||
image_base = struct.unpack_from('<I', head, opt_off + 28)[0]
|
||||
else:
|
||||
return None
|
||||
# SizeOfImage 在两种可选头里都在 +56(PE32 多出的 BaseOfData 在 ImageBase 之前)
|
||||
size_of_image: int = struct.unpack_from('<I', head, opt_off + 56)[0]
|
||||
sections: dict[str, tuple[int, int]] = {}
|
||||
sec_off: int = opt_off + opt_size
|
||||
for i in range(n_sections):
|
||||
base: int = sec_off + i * 40
|
||||
if base + 40 > len(head):
|
||||
break
|
||||
name: str = head[base:base + 8].split(b'\x00', 1)[0].decode('ascii', 'replace')
|
||||
sections[name] = (struct.unpack_from('<I', head, base + 12)[0], # VirtualAddress = 节 RVA
|
||||
struct.unpack_from('<I', head, base + 8)[0]) # VirtualSize
|
||||
return image_base, size_of_image, sections
|
||||
|
||||
|
||||
# llvm-dwarfdump --debug-line 行表;行记录列形如
|
||||
# Address Line Column File ISA Discriminator Flags
|
||||
# 0x0000000140001000 12 0 1 0 0 is_stmt
|
||||
_DWARF_LINE_HEADER_RE = re.compile(r'^Address\s+Line\s+Column\s+File\b')
|
||||
_DWARF_LINE_ROW_RE = re.compile(r'^(0x[0-9A-Fa-f]+)\s+(\d+)\s+(\d+)\s+(\d+)\b')
|
||||
_DWARF_FILENAMES_RE = re.compile(r'^\s*file_names\[\s*(\d+)\]\s*:')
|
||||
_DWARF_DIRNAMES_RE = re.compile(r'^\s*(?:directory_names|include_directories)\[\s*(\d+)\]\s*[:=]\s*(.*)$')
|
||||
_DWARF_NAME_RE = re.compile(r'^\s*name:\s*(.*)$')
|
||||
_DWARF_DIRINDEX_RE = re.compile(r'^\s*dir_index:\s*(\d+)\s*$')
|
||||
# llvm-dwarfdump --debug-info 里的 DISubprogram(DW_AT_low_pc/high_pc/name)
|
||||
_DWARF_TAG_ANY_RE = re.compile(r'DW_TAG_\w+')
|
||||
_DWARF_TAG_SUBPROGRAM_RE = re.compile(r'DW_TAG_subprogram\b')
|
||||
_DWARF_LOWPC_RE = re.compile(r'DW_AT_low_pc\s*\(\s*(0x[0-9A-Fa-f]+)\s*\)')
|
||||
_DWARF_HIGHPC_RE = re.compile(r'DW_AT_high_pc\s*\(\s*(0x[0-9A-Fa-f]+)\s*\)')
|
||||
_DWARF_ATNAME_RE = re.compile(r'DW_AT_name\s*\("((?:[^"\\]|\\.)*)"\)')
|
||||
|
||||
|
||||
def _ParseDwarfLineRows(dump_text: str) -> 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。此处不静默
|
||||
# 跳过、也不做字符替换(那会把符号名改得与源码/其它模块不一致,链接期更难查),
|
||||
# 直接指名报错。名字来自弱符号命名 `<sha1>.<标识符>`,出现这些字符说明上游符号名
|
||||
# 构造有误,应在上游修正。
|
||||
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 行表导出为 "<exe>.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:链接成功后导出"地址→源行"旁文件 <exe>.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('<IIII', 1, len(records), len(str_offsets), len(pool)))
|
||||
for rva, file_off, line, func_off in records:
|
||||
out.extend(struct.pack('<QIII', rva, file_off, line, func_off))
|
||||
out.extend(pool)
|
||||
|
||||
map_path: str = exe_path + '.stackmap'
|
||||
try:
|
||||
with open(map_path, 'wb') as f:
|
||||
f.write(out)
|
||||
except Exception as e:
|
||||
_Fail(f"--ctrlc-stack 写出旁文件失败 {map_path}: {e}")
|
||||
_vlog().success(
|
||||
f" [--ctrlc-stack] 已写出地址→源行旁文件: {map_path}"
|
||||
f"({len(records)} 条记录,{len(out)} 字节)")
|
||||
|
||||
def _link_obj_files(self, active_sha1s: set[str]) -> 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:链接成功后导出"地址→源行"旁文件(<exe>.stackmap)。
|
||||
# 刻意放在上面的链接 try/except 之外,使导出阶段的 fail-fast RuntimeError
|
||||
# 直接向上抛出终止编译,而不被"链接异常兜底"吞掉。
|
||||
if self.ctrlc_stack_enabled:
|
||||
self._WriteStackMap(exe_path)
|
||||
Reference in new issue
Block a user