diff --git a/_list.vp b/_list.vp index 9423171..0c881df 100644 --- a/_list.vp +++ b/_list.vp @@ -40,7 +40,10 @@ class list[T]: self.__elem_size__ = T.__sizeof__() else: self.__elem_size__ = elem_size - self.__data__ = pool.alloc(self.__capacity__ * self.__elem_size__) + # 带申请点 tag:本处是"容量 × 元素大小"形态,尺寸为 0 时 [MEM] 报文直接 + # 指名本行(pool 是基类指针,故走 memhub.MemAllocTagged) + self.__data__ = memhub.MemAllocTagged(pool, self.__capacity__ * self.__elem_size__, + memhub.DETSITE_LIST_43) self.__iter_index__ = 0 def __len__(self) -> sizet: diff --git a/_withcontent.vp b/_withcontent.vp index eee83ec..f8dbd5c 100644 --- a/_withcontent.vp +++ b/_withcontent.vp @@ -12,7 +12,9 @@ define _MAX_PROVIDER_DEPTH = 32 class _ProviderStack: """Provider stack, stores pairs of field names and pointers.""" - names: [32]char + # Field names are stored as string pointers: a `char` element would hold a + # single byte, truncating the name and breaking every provider lookup. + names: [32]str ptrs: [32]rawptr top: uint64 @@ -71,10 +73,7 @@ def _find_provider(field_name: str) -> rawptr: # __mbuddy__ 注入全部失效,_str.upper 对 NULL 取 vtable 崩溃)。显式转换让 # 符号域与递减语义一致,与编译器侧「stop 为负字面量时退回有符号谓词」互为双保险。 for i in range(int(stack.top) - 1, -1, -1): - # 字段名用 strcmp 内容比较:编译器对每次 create_global_string 生成 - # 独立的字符串常量(TPV 不去重),push/find 的指针地址不同, - # 指针 == 永假会导致 provider 匹配失败(__mbuddy__ 注入不上)。 - _cmp_tmp: [2]char = [0, 0] - _cmp_tmp = stack.names[i] - if string.strcmp(_cmp_tmp, field_name) == 0: return stack.ptrs[i] + # Content comparison: every string literal gets its own constant, so the + # push/find pointers differ and pointer equality never matches. + if string.strcmp(stack.names[i], field_name) == 0: return stack.ptrs[i] return None diff --git a/argparse.vp b/argparse.vp index 23cd508..2468e09 100644 --- a/argparse.vp +++ b/argparse.vp @@ -29,18 +29,18 @@ import hashtable # ============================================================ # Type constants -STRING: t.CDefine = 0 -INT: t.CDefine = 1 -BOOL: t.CDefine = 2 +define STRING = 0 +define INT = 1 +define BOOL = 2 # Action constants -STORE: t.CDefine = 0 -STORE_TRUE: t.CDefine = 1 -STORE_FALSE: t.CDefine = 2 -COUNT: t.CDefine = 3 +define STORE = 0 +define STORE_TRUE = 1 +define STORE_FALSE = 2 +define COUNT = 3 # Limits -MAX_ARGS: t.CDefine = 32 +define MAX_ARGS = 32 # Module-level membuddy pointer (assigned inside main by user) _mbuddy: memhub.MemBuddy* diff --git a/ast/parser.vp b/ast/parser.vp index e94e5c0..79c3783 100644 --- a/ast/parser.vp +++ b/ast/parser.vp @@ -1189,6 +1189,14 @@ def _parse_arith(ps: rawptr[Parser]) -> rawptr[AST]: # ============================================================ # C 风格指针后缀('*' / '**')—— 与 TransPyC lib/vast 的 _AtPtrSuffix 同源 # ============================================================ +# Start position (line/col) of the last consumed C-style pointer suffix ('*' / '**'). +# _parse_paren matches it against ps.prev to tell the paren-less cast `(T*)x` from a +# plain group like `(a | b)`. Recorded as ints, not as a token pointer: comparing two +# token pointers whose declared pointer levels differ is rejected by the compare check. +_g_ptr_suffix_line: int = 0 +_g_ptr_suffix_col: int = 0 + + def _ptr_suffix_end_op(op: int) -> int: """后缀末尾 token 是否为“无右操作数”位置(']' ',' ')' ':' '=')。 @@ -1240,21 +1248,45 @@ def _make_ptr_type(ps: rawptr[Parser], base: rawptr[AST]) -> rawptr[AST]: def _consume_ptr_suffix(ps: rawptr[Parser], base: rawptr[AST]) -> rawptr[AST]: """消费连续的 C 风格指针后缀('*' 一层 / '**' 两层),展开为 base | t.CPtr 链。""" + global _g_ptr_suffix_line, _g_ptr_suffix_col while True: _skip_nl(ps) if _match_op(ps, TokOp.StarStar): _advance(ps) + _g_ptr_suffix_line = ps.prev.lineno + _g_ptr_suffix_col = ps.prev.col_offset base = _make_ptr_type(ps, base) base = _make_ptr_type(ps, base) continue if _match_op(ps, TokOp.Star): _advance(ps) + _g_ptr_suffix_line = ps.prev.lineno + _g_ptr_suffix_col = ps.prev.col_offset base = _make_ptr_type(ps, base) continue break return base +def _at_cast_operand(ps: rawptr[Parser]) -> int: + """Whether the current token may start the operand of a paren-less cast `(T*)x`. + + '(' is deliberately absent: `(T*)(x)` is already handled by the trailer loop as a + cast call, and keeping it there leaves that path's AST untouched. Nl is not + skipped either: a line break after ')' starts a new statement, not an operand. + """ + if _cur_type(ps) == TokenType.Name: + return 1 + if _cur_type(ps) == TokenType.Number: + return 1 + if _cur_type(ps) == TokenType.String: + return 1 + op: int = _cur_op(ps) + if op == TokOp.Lsqb or op == TokOp.LBrace or op == TokOp.Star or op == TokOp.Amp or op == TokOp.Minus or op == TokOp.Plus or op == TokOp.Tilde: + return 1 + return 0 + + def _at_ptr_suffix_call(ps: rawptr[Parser], base: rawptr[AST]) -> int: """判定当前 '*'/'**' 串是否为 `T*(x)` 形式的指针后缀**强转调用**。 @@ -1666,11 +1698,25 @@ def _parse_paren(ps: rawptr[Parser]) -> rawptr[AST]: node = Tuple(ps.pool, elts, ASTCtx.Load) _set_pos(node, lineno, col, 0, 0) return node + # Paren-less C cast `(T*)x`: the inner expression ends with a pointer suffix + # (ps.prev is that suffix's '*'/'**' token) and the operand follows ')' directly. + # Same product as `(T*)(x)` / `T*(x)`: a Call whose func is `T | t.CPtr`. + is_ptr_cast: int = 0 + if _g_ptr_suffix_line > 0 and _match_op(ps, TokOp.RPar) and ps.prev.lineno == _g_ptr_suffix_line and ps.prev.col_offset == _g_ptr_suffix_col: + is_ptr_cast = 1 _skip_nl(ps) # (expr\n) 单表达式括号跨行收尾 _expect_op(ps, TokOp.RPar) # Check for generator expression if _cur_kw(ps) == Keyword.For: return _parse_comprehension(ps, first, ASTKind.GeneratorExp, lineno, col) + if is_ptr_cast != 0 and _at_cast_operand(ps) != 0: + operand: rawptr[AST] = _parse_factor(ps) + cast_args: rawptr[list[rawptr[AST]]] = list[rawptr[AST]](ps.pool, 8) + cast_args.append(operand) + cast_kws: rawptr[list[rawptr[AST]]] = list[rawptr[AST]](ps.pool, 8) + cast_node: rawptr[AST] = Call(ps.pool, first, cast_args, cast_kws) + _set_pos(cast_node, first.lineno, first.col_offset, 0, 0) + return cast_node return first @@ -2165,56 +2211,56 @@ def _parse_varargslist(ps: rawptr[Parser], is_lambda: int = 0) -> rawptr[AST]: return Arguments(ps.pool, args, vararg, kwarg, defaults, kw_defaults) -def _parse_array_prefix_declarator(ps: rawptr[Parser]) -> rawptr[AST]: - """C 风格前置数组声明符 `[N]T` / `[]T` → `t.CArray[T, N]` / `t.CArray[T, None]` - - 只在注解位置(_parse_type_ann)消费;当前 token 不是 '[' 时返回 None。 - 脱糖目标与 TPC / StubGen / DeclarationGenerator 的规范数组形态同形 - (`t.CArray[E, N]`;无长度按 `t.CArray[E, None]` = 指针模式),即把 - `coeffs: const [8]double` 就地等价成文档给出的修法 - `coeffs: const t.CArray[double, 8]`(Debugger/type-descriptors.md §3 第 6 例)。 - - 必须在 `_parse_test` 之前调用:否则 `[N]` 会被解析成 list 字面量,随后在元素 - 类型标识符处报"意外的 token"(includes/binascii.vp `HEX_VALS: [256]int` 现象)。 - """ - if _match_op(ps, TokOp.Lsqb) == 0: - return None - lb_lineno: int = _cur_lineno(ps) - lb_col: int = _cur_col(ps) - _advance(ps) # 消费 '[' - count_node: rawptr[AST] = None - if _match_op(ps, TokOp.Rsqb) == 0: - # 长度表达式(常量/常量表达式;非法长度由下游 ArrayDims 判据拒绝) - count_node = _parse_test(ps) - if count_node is None: - _record_parse_err(ps) - return None - if _match_op(ps, TokOp.Rsqb) == 0: - _record_parse_err(ps) # 缺少 ']' - ps.error_count += 1 - return None - _advance(ps) # 消费 ']' - # 元素类型:递归 _parse_type_ann,故 `[N]const int` / `[N]int*` 走同一规则源 - elem: rawptr[AST] = _parse_type_ann(ps) - if elem is None: - return None - if count_node is None: - count_node = _new_const_none(ps.pool, lb_lineno, lb_col) - # 元素大小必须显式传 8(list[T](pool, elem_size) 按字节步长寻址; - # 传 2 会让 8 字节指针按 2 字节步长写入 → 堆越界,ASan 在 _set_parent_list - # 抛 access-violation)。与文件内其它 list[rawptr[AST]] 站点同参。 - elts: rawptr[list[rawptr[AST]]] = list[rawptr[AST]](ps.pool, 8) - elts.append(elem) - elts.append(count_node) - carray: rawptr[AST] = Subscript(ps.pool, - Attribute(ps.pool, Name(ps.pool, "t", ASTCtx.Load), - "CArray", ASTCtx.Load), - Tuple(ps.pool, elts, ASTCtx.Load), - ASTCtx.Load) - _set_pos(carray, lb_lineno, lb_col, 0, 0) - return carray - - +def _parse_array_prefix_declarator(ps: rawptr[Parser]) -> rawptr[AST]: + """C 风格前置数组声明符 `[N]T` / `[]T` → `t.CArray[T, N]` / `t.CArray[T, None]` + + 只在注解位置(_parse_type_ann)消费;当前 token 不是 '[' 时返回 None。 + 脱糖目标与 TPC / StubGen / DeclarationGenerator 的规范数组形态同形 + (`t.CArray[E, N]`;无长度按 `t.CArray[E, None]` = 指针模式),即把 + `coeffs: const [8]double` 就地等价成文档给出的修法 + `coeffs: const t.CArray[double, 8]`(Debugger/type-descriptors.md §3 第 6 例)。 + + 必须在 `_parse_test` 之前调用:否则 `[N]` 会被解析成 list 字面量,随后在元素 + 类型标识符处报"意外的 token"(includes/binascii.vp `HEX_VALS: [256]int` 现象)。 + """ + if _match_op(ps, TokOp.Lsqb) == 0: + return None + lb_lineno: int = _cur_lineno(ps) + lb_col: int = _cur_col(ps) + _advance(ps) # 消费 '[' + count_node: rawptr[AST] = None + if _match_op(ps, TokOp.Rsqb) == 0: + # 长度表达式(常量/常量表达式;非法长度由下游 ArrayDims 判据拒绝) + count_node = _parse_test(ps) + if count_node is None: + _record_parse_err(ps) + return None + if _match_op(ps, TokOp.Rsqb) == 0: + _record_parse_err(ps) # 缺少 ']' + ps.error_count += 1 + return None + _advance(ps) # 消费 ']' + # 元素类型:递归 _parse_type_ann,故 `[N]const int` / `[N]int*` 走同一规则源 + elem: rawptr[AST] = _parse_type_ann(ps) + if elem is None: + return None + if count_node is None: + count_node = _new_const_none(ps.pool, lb_lineno, lb_col) + # 元素大小必须显式传 8(list[T](pool, elem_size) 按字节步长寻址; + # 传 2 会让 8 字节指针按 2 字节步长写入 → 堆越界,ASan 在 _set_parent_list + # 抛 access-violation)。与文件内其它 list[rawptr[AST]] 站点同参。 + elts: rawptr[list[rawptr[AST]]] = list[rawptr[AST]](ps.pool, 8) + elts.append(elem) + elts.append(count_node) + carray: rawptr[AST] = Subscript(ps.pool, + Attribute(ps.pool, Name(ps.pool, "t", ASTCtx.Load), + "CArray", ASTCtx.Load), + Tuple(ps.pool, elts, ASTCtx.Load), + ASTCtx.Load) + _set_pos(carray, lb_lineno, lb_col, 0, 0) + return carray + + def _parse_type_ann(ps: rawptr[Parser]) -> rawptr[AST]: """解析类型注解,支持类型描述符前缀(const / volatile)与 C 风格指针后缀(* / **)。 diff --git a/builtins.vp b/builtins.vp index bd8ad23..6c20e84 100644 --- a/builtins.vp +++ b/builtins.vp @@ -8,53 +8,40 @@ def chr(i: int) -> char: return char(i) def min(a: double, b: double) -> double: return a if a < b else b def max(a: double, b: double) -> double: return a if a > b else b -# rawptr 已内置于编译器(TPC/TPV 双端),作为语法保留字,不可重定义: -# rawptr[T] 固定表示"裸指针,指向原始类型 T"(等价 t.CPtr[T])。 -sizet: t.CTypedef = t.CSizeT -size_t: t.CTypedef = sizet -uzt: t.CTypedef = sizet -int8: t.CTypedef = t.CInt8T -int16: t.CTypedef = t.CInt16T -int32: t.CTypedef = t.CInt32T -int64: t.CTypedef = t.CInt64T -uint8: t.CTypedef = t.CUInt8T -uint16: t.CTypedef = t.CUInt16T -uint32: t.CTypedef = t.CUInt32T -uint64: t.CTypedef = t.CUInt64T +typedef sizet = t.CSizeT +typedef size_t = sizet +typedef int8 = t.CInt8T +typedef int16 = t.CInt16T +typedef int32 = t.CInt32T +typedef int64 = t.CInt64T +typedef uint8 = t.CUInt8T +typedef uint16 = t.CUInt16T +typedef uint32 = t.CUInt32T +typedef uint64 = t.CUInt64T -intfast8: t.CTypedef = t.CIntFast8T -intfast16: t.CTypedef = t.CIntFast16T -intfast32: t.CTypedef = t.CIntFast32T -intfast64: t.CTypedef = t.CIntFast64T -uintfast8: t.CTypedef = t.CUIntFast8T -uintfast16: t.CTypedef = t.CUIntFast16T -uintfast32: t.CTypedef = t.CUIntFast32T -uintfast64: t.CTypedef = t.CUIntFast64T +typedef intfast8 = t.CIntFast8T +typedef intfast16 = t.CIntFast16T +typedef intfast32 = t.CIntFast32T +typedef intfast64 = t.CIntFast64T +typedef uintfast8 = t.CUIntFast8T +typedef uintfast16 = t.CUIntFast16T +typedef uintfast32 = t.CUIntFast32T +typedef uintfast64 = t.CUIntFast64T -intleast8: t.CTypedef = t.CIntLeast8T -intleast16: t.CTypedef = t.CIntLeast16T -intleast32: t.CTypedef = t.CIntLeast32T -intleast64: t.CTypedef = t.CIntLeast64T -uintleast8: t.CTypedef = t.CUIntLeast8T -uintleast16: t.CTypedef = t.CUIntLeast16T -uintleast32: t.CTypedef = t.CUIntLeast32T -uintleast64: t.CTypedef = t.CUIntLeast64T +typedef intleast8 = t.CIntLeast8T +typedef intleast16 = t.CIntLeast16T +typedef intleast32 = t.CIntLeast32T +typedef intleast64 = t.CIntLeast64T +typedef uintleast8 = t.CUIntLeast8T +typedef uintleast16 = t.CUIntLeast16T +typedef uintleast32 = t.CUIntLeast32T +typedef uintleast64 = t.CUIntLeast64T -# C 基础类型名裸名别名(裸名家族,不随 import 注入;编译器最后匹配本文件兜底解析) -# 目标 x86_64-pc-windows-gnu(Windows/mingw-w64,LLP64): -# uchar = 8 位无符号(t.CUnsignedChar,固定 Size=8;C 的 unsigned char) -# short = 16 位有符号(t.CShort,固定 Size=16) -# ushort = 16 位无符号(t.CUnsignedShort,固定 Size=16) -# uint = u32,32 位无符号(等价 uint32/u32,底层 t.CUnsignedInt Size=32) -# long = 32 位有符号(t.CLong,Size 由 t.configure_platform 依 triple 置 32) -# ulong = 32 位无符号(t.CUnsignedLong,Size 由 t.configure_platform 依 triple 置 32) -# longlong = 64 位有符号(t.CLongLong,固定 Size=64) -# ulonglong = 64 位无符号(t.CUnsignedLongLong,固定 Size=64) -uchar: t.CTypedef = t.CUnsignedChar -short: t.CTypedef = t.CShort -ushort: t.CTypedef = t.CUnsignedShort -uint: t.CTypedef = t.CUnsignedInt -long: t.CTypedef = t.CLong -ulong: t.CTypedef = t.CUnsignedLong -longlong: t.CTypedef = t.CLongLong -ulonglong: t.CTypedef = t.CUnsignedLongLong +typedef uchar = t.CUnsignedChar +typedef short = t.CShort +typedef ushort = t.CUnsignedShort +typedef uint = t.CUnsignedInt +typedef long = t.CLong +typedef ulong = t.CUnsignedLong +typedef longlong = t.CLongLong +typedef ulonglong = t.CUnsignedLongLong diff --git a/dbgstack.vp b/dbgstack.vp deleted file mode 100644 index 5b3b238..0000000 --- a/dbgstack.vp +++ /dev/null @@ -1,1742 +0,0 @@ -import t, c -from stdint import * -from w32.win32base import * -import string -import stdio -import stdlib -import w32.win32file as wf -import w32.win32console as wc -import w32.win32process as wp -import w32.win32memory as wm -import w32.win32dbghelp as dbg - -# 转储重入保护(见 DumpMainThread)**不使用 InterlockedCompareExchange**:该名字在 x64 上是 -# winnt.h 的编译器内部函数(映射到 lock cmpxchg 指令),**不是可链接符号**,直接引用会在链接期 -# 报 undefined symbol(实测:ld.lld: undefined symbol: InterlockedCompareExchange)。 -# 本模块改用普通 32 位标志做「检查并占用」:并发回调最多导致一次重复转储(诊断工具可接受), -# 且不引入任何链接期依赖。 - -# ============================================================================= -# dbgstack — 运行时「按键取栈」模块(Ctrl+C / Ctrl+Break 时转储主线程调用栈) -# -# 解决的问题:编译器(或任何长任务)卡死时,用户按一个键就能拿到「现在卡在哪一行」 -# 的完整调用栈——从 CRT 入口(mainCRTStartup / __tmainCRTStartup)到当前帧。 -# -# ----------------------------------------------------------------------------- -# 一、触发语义(SetConsoleCtrlHandler 回调) -# ----------------------------------------------------------------------------- -# Ctrl+C (CTRL_C_EVENT) → 打印调用栈后**返回 FALSE**(交还系统默认处理 ⇒ 进程终止) -# Ctrl+Break (CTRL_BREAK_EVENT)→ 打印调用栈后**返回 TRUE**(已处理 ⇒ 进程继续运行) -# 其它控制事件(关窗/注销/关机,CTRL_CLOSE_EVENT 等)→ 一律返回 FALSE,不转储、不干预。 -# 回调由系统在**新建的线程**上执行,不是主线程——所以「当前执行到哪」必须靠 -# SuspendThread(主线程) + GetThreadContext 取得主线程寄存器,再回溯主线程的栈。 -# 同一时刻**只允许一次转储**:连续按键会由系统在多个新线程上并发回调,而它们共用 -# 同一个 LineBuf 与输出句柄 ⇒ 输出互相撕裂(实测出现过"降级:"行重复且交织)。 -# 故 DumpMainThread 入口用内联汇编 `lock cmpxchg`(AtomicTryEnterDump)对 _DumpLock -# 做原子"检查并占用":抢占失败的调用直接返回(不挂起主线程、不输出任何内容)。 -# 每次转储还会打印自增 Seq 与回调线程 id:若两条转储记录的 Seq 不同 ⇒ 顺序两次; -# 同一 Seq 的行却互相截断 ⇒ 说明仍存在共享缓冲的并发写(据此定位,不再靠猜)。 -# -# ----------------------------------------------------------------------------- -# 二、安装时机与总开关(本模块的开关落地点) -# ----------------------------------------------------------------------------- -# 约定:**旁文件存在** ⟺ 编译时用了 TPC 的 `--ctrlc-stack`。 -# Install() 先求 <当前 exe 完整路径> + ".stackmap";该文件不存在(CreateFileA 报 -# ERROR_FILE_NOT_FOUND)则**立刻返回 0,零副作用**(不分配、不注册 handler); -# 存在才注册 SetConsoleCtrlHandler。故 TPC 侧无需生成任何其它开关代码,只需在 -# 启用该选项时让产物链接 dbghelp(见第五节)并调用一次 Install()(见第六节)。 -# 文件存在但其头部/记录区非法时:打印可定位的警告并把映射标记为不可用——调用栈仍 -# 照常转储(只有地址),不会静默退出(诊断工具取不到信息时给降级信息,不掩盖错误)。 -# -# ----------------------------------------------------------------------------- -# 三、旁文件(.stackmap)格式 —— 小端 -# ----------------------------------------------------------------------------- -# 头部 20 字节:magic="TPCS"(4B) + u32 version=1 + u32 entry_count + u32 str_count -# + u32 str_bytes -# 记录区 entry_count × 20B(**按 rva 升序**):u64 rva; u32 file_off; u32 line; u32 func_off -# 字符串池 str_bytes 字节:\0 结尾 UTF-8 串;file_off/func_off 是池内偏移 -# (func_off = 0 表示无函数名) -# 运行时:GetModuleHandleA(NULL) 取映像基址,帧地址 - 基址 = rva,再二分查找记录区。 -# 命中 → 打印 "<文件>:<行> (<函数名>)";未命中 → 打印 "+0x (unknown)"; -# 地址落在主模块之外(更低,典型为 DLL/系统帧)→ 打印 "0x<地址> (external)"。 -# 装载时做一次全表校验(rva 严格升序、字符串偏移落在池内),不合法即禁用映射—— -# 二分查找依赖升序,破损的表会给出**错行号**,比"只打地址"更坏。 -# 校验失败一律**逐项打印挂在哪一项**(含头部实读的 magic/version/count/str_bytes -# 与文件实际大小),失败原因不外泄为一句"旁文件非法"。 -# ⚠ 读字节必须逐个 & 0xFF:Viper 的 Buf[i] 是**有符号** char,>= 0x80 的字节若直接 -# 转 u32 会符号扩展(例:记录数 1000=0x3E8 的首字节 0xE8 → 0xFFFFFFE8),使头部的 -# entry_count/str_bytes 变成天文数字、`20+count*20+str_bytes > 文件大小` 必然成立 -# ⇒ 映射永远装载失败(这正是"源行映射=无"的真因,详见 ReadU32)。 -# -# ----------------------------------------------------------------------------- -# 四、回溯两条路径与降级条件(**本轮已主副互换,见第十一节 B**) -# ----------------------------------------------------------------------------- -# ⚠ 先读第十一节 B:本轮起**主路径 = RBP 链**(零 dbghelp 依赖),dbghelp 降为 -# "可选加强"。下面这段 dbghelp 说明仍有效,只是它描述的是**加强路径**的历史细节。 -# 加强路径(可选):dbghelp!StackWalk64(MachineType=IMAGE_FILE_MACHINE_AMD64)。 -# SymSetOptions(SYMOPT_DEFERRED_LOADS|SYMOPT_UNDNAME|SYMOPT_NO_PROMPTS) + -# SymInitialize(GetCurrentProcess(), NULL, **TRUE**) —— TRUE 是硬要求:它让 dbghelp -# 枚举已加载模块填进自己的模块表;置 FALSE 时模块表为空,SymFunctionTableAccess64 -# 取不到 RUNTIME_FUNCTION、SymGetModuleBase64 返回 0,StackWalk64 **首次调用即失败** -# (表现为"符号回溯=可用但 0 帧")。配合 DEFERRED_LOADS 不会真的去加载 PDB。 -# 三个回调地址里 FunctionTableAccess/GetModuleBase 仍在 Install 时用 GetProcAddress -# 取(本仓既有的动态解析写法,先例 VET/App/lib/VGuard.vp:852-867):避免依赖 -# 「跨模块导入的函数名作为函数指针实参」这一未经本仓验证的用法,取不到就传 NULL。 -# ReadProcessMemory 回调改用**本模块内定义**的 ReadMemCb(同模块内"函数名作 -# 函数指针实参"已由 CtrlHandler 先例验证):直接传 kernel32!ReadProcessMemory -# 本体时拿不到失败原因,而回调返回 0 会让 StackWalk64 立即终止(恰好是"0 帧")。 -# 主路径(**RBP 链手工回溯**):无条件先走它。它拿不到更多帧(<=1)时**先走第二兜底 -# = 栈扫描(ScanStack,不依赖帧指针)**;再之后才轮到 dbghelp 加强(编译期开关为 1、 -# SymInitialize 成功、两个回调地址与帧缓冲齐备、且**未探测到 ASan 运行时**);任一 -# 前提不满足都会打印明确原因(`[diag] symwalk=disabled reason=…`),**不静默 0 帧**。 -# 本轮新增:每步无条件打 `[diag] rbp:` 原始读数 + 判定、以及 `walk: fallback=stack_scan` -# 扫描推断帧(`~` 前缀)——详见第十二节。 -# 主路径实现:打印当前 RIP,然后沿主线程 RBP 链读 [rbp](调用者 rbp)与 [rbp+8] -# (返回地址)逐帧回溯;要求 rbp 8 字节对齐、严格向高地址增长(`[rbp] > rbp`)、 -# 单步跨度 <= DBGSTACK_RBP_MAX_STEP、返回地址落在主模块基址区间内,否则立即停止 -# (防野指针把回溯带飞);**读内存一律经 ReadProcessMemory,绝不直接解引用野 rbp**。 -# RBP 链只覆盖保持帧指针的函数;帧指针一旦未保留([rbp+8] 不是返回地址)链即在第 1 -# 步断——此时由 ScanStack 兜底,另叠一层 dbghelp 加强(可用时)。 -# --ctrlc-stack 时 TPC 给 llc 加 `-frame-pointer=all`(Phase2Translator.py:2187-2193); -# 该 flag 是否真正生效见第十二节 C 的调查结论与修法落点。 -# 【取证:先取证,再决策】每次转储在 banner 之后**无条件**打印一组前置诊断—— -# Seq / 回调线程 id / 主线程 id / Dumping 进入前值 / MapOk / SymReady / ModInited / -# SymGetModuleBase64(主模块) 的**实际返回值** / 主模块基址 / 帧缓冲与两个回调地址 -# 是否就位;进入 dbghelp 主路径后,**首帧**再无条件打印 StackWalk64 的返回码、传入的 -# machine/RIP/RSP/RBP、返回的 AddrPC,以及 SymFunctionTableAccess64(proc, AddrPC) 的 -# 返回值。这些行**先于任何降级判断**产出,使"0 帧"的根因可直接判定(见第九节)。 -# -# ----------------------------------------------------------------------------- -# 五、链接期依赖(TPC 侧必须知道) -# ----------------------------------------------------------------------------- -# dbghelp(-ldbghelp)+ kernel32(既有)。SymInitialize / StackWalk64 / SymSetOptions / -# SymGetModuleBase64 / SymFunctionTableAccess64 是**静态**引用(后两个用于首帧取证诊断), -# 产物必须能解析 dbghelp 的导入库,否则链接失败。若不便新增 -ldbghelp, -# 把 DBGSTACK_USE_DBGHELP 置 0:该常量比较会在编译期把唯一含 dbghelp 调用的分支 -# 整体去掉(见 DBGSTACK_USE_DBGHELP 的注释),只保留 RBP 链回溯。 -# -# ----------------------------------------------------------------------------- -# 六、TPC 侧调用入口(符号与签名) -# ----------------------------------------------------------------------------- -# import dbgstack -# dbgstack.Install() # 返回 int:1=handler 已注册;0=未安装(无旁文件/失败) -# 调用时机:进程启动期一次即可。两种等价做法—— -# (a) 在入口 main 的第一条语句调用; -# (b) 注入一个 @llvm.global_ctors(.CRT$XCU 语义)构造器调用该符号(本仓先例: -# TransPyC/lib/core/LLVMCG/BaseGen.py:2061-2097 用 llvm.global_ctors 注册 -# Ctrl+C 处理器)。构造器在 CRT 初始化期执行,能覆盖「main 之前就卡住」的场景。 -# 本模块自身**没有**模块级构造器(状态对象由 Install 懒分配),故与 TPC 注入的 -# 构造器不存在初始化顺序依赖,重复调用是幂等的。 -# ⚠ 本模块把「主线程」定义为**调用 Install() 的那个线程**(Install 里 GetCurrentThreadId -# 记录其 id,回调时挂起/回溯的就是它)。故必须在真正的入口线程调用(main 或 CRT -# 构造器所在线程);若在启动期的工作线程里调用,转储的将是那个线程的栈。 -# -# ----------------------------------------------------------------------------- -# 七、为什么直接 WriteFile 而不是 printf -# ----------------------------------------------------------------------------- -# 转储发生在「主线程已被挂起」的时刻:若主线程恰好停在 CRT 的 stdout 锁内,回调线程 -# 再调用 printf 会**永久等待同一把锁**,把"卡死"变成"按了键彻底不动"。 -# 故输出走 kernel32!WriteFile(GetStdHandle(STD_ERROR_HANDLE))(无用户态 CRT 流锁), -# 格式化走 CRT snprintf(只写内存缓冲,不碰 FILE 锁)。stderr 的另一好处:stdout 常被 -# 管道占用(如被父工具读取),stderr 一般直达终端。句柄与全部缓冲都在 Install 时备好, -# 回调线程**零分配**(MemBuddy 非线程安全,回调里绝不触碰池)。 -# -# ----------------------------------------------------------------------------- -# 七之二、转储**同时落文件** + 控制台纯 ASCII(本轮的两次修正) -# ----------------------------------------------------------------------------- -# (a) 落文件(解决"没有 Break 键"与"控制台代码页吞字"):Install 成功时以 -# ` + ".stackdump.txt"` 打开一个**追加写**句柄(OPEN_ALWAYS + -# FILE_APPEND_DATA,不存在则创建),EmitLine 把**同一份内容**既写控制台又追加落盘。 -# 文件是 UTF-8 字节原样,不受控制台代码页影响;每次转储在段前追加一小段分隔头 -# (本地时间戳/pid/触发键/主线程 id/Seq,中文标签,仅文件)。文件写失败**不阻断** -# 控制台,且只提示一次(见 EmitToFile)。Install 成功时打印一行 -# `[dbgstack] stack dump also written to: <路径>` 便于确认去向。 -# (b) 控制台纯 ASCII:原先的运行时文本是中文(UTF-8 字节),而 cmd 的控制台输出代码页 -# 默认为 936(GBK)——WriteFile 到控制台会按 936 解释 UTF-8 字节,一段多字节在多次 -# WriteFile 之间被拆开/重组,表现为**整行消失**(最长的"首帧"行首当其冲)或相邻行错位、 -# 看似"重复且残缺"(含 '�')。故**运行时输出到控制台的文本一律改英文/ASCII** -# (banner / [diag] / Trigger= / exe base= / walk: primary=rbp_chain / frames=N 等), -# 任何代码页下都可读、可 grep;`<文件>.vp:<行>` 帧行保留源路径原文(ASCII 分隔符)。 -# 中文仅保留在:本文件的注释/文档串、以及**只写文件**的分隔头。 -# -# ----------------------------------------------------------------------------- -# 八、依赖的既有声明(未改动)与本模块新增声明 -# ----------------------------------------------------------------------------- -# 既有(`from w32.win32base import *` 取裸名):GetModuleHandleA / GetProcAddress / -# CloseHandle / GetLastError / INVALID_HANDLE_VALUE / ERROR_FILE_NOT_FOUND / -# LARGE_INTEGER / SYSTEMTIME / GetLocalTime / HANDLE / BOOL / ULONG / VOIDPTR; -# wf.CreateFileA / wf.ReadFile / wf.WriteFile / wf.GetFileSizeEx / wf.GetStdHandle / -# wf.GENERIC_READ / wf.FILE_SHARE_READ / wf.FILE_SHARE_WRITE / wf.OPEN_EXISTING / -# wf.OPEN_ALWAYS / wf.FILE_ATTRIBUTE_NORMAL / -# wf.STD_ERROR_HANDLE / wf.STD_OUTPUT_HANDLE; -# wp.OpenThread / wp.SuspendThread / wp.ResumeThread / wp.GetCurrentThreadId / -# wp.GetCurrentProcess / wp.GetCurrentProcessId / wp.THREAD_SUSPEND_RESUME / -# wp.THREAD_GET_CONTEXT; -# string.memset / string.strcpy / stdlib.malloc / free / c.Addr。 -# 本轮新增:wc.SetConsoleCtrlHandler / wc.CTRL_C_EVENT / wc.CTRL_BREAK_EVENT; -# wp.CONTEXT(x64 完整布局)/ wp.CONTEXT_FULL / wp.GetThreadContext; -# wm.ReadProcessMemory(由本模块 ReadMemCb 包装后作 StackWalk64 回调); -# w32/win32dbghelp.vp(STACKFRAME64/ADDRESS64/KDHELP64 + SymSetOptions/ -# SYMOPT_* + SymInitialize/SymFunctionTableAccess64/SymGetModuleBase64/ -# StackWalk64); -# 转储文件:wf.WriteFile 追加写 .stackdump.txt + 自备常量 -# DBGSTACK_FILE_APPEND_DATA(FILE_APPEND_DATA,wf 未定义); -# 转储重入保护用内联汇编的一条 `lock cmpxchg`(AtomicTryEnterDump)对模块级 -# 32 位 _DumpLock 做原子"检查并占用"(不用 InterlockedCompareExchange:x64 上是 -# 编译器内部函数、无导出符号、不可链接)。 -# -# ----------------------------------------------------------------------------- -# 九、"0 帧"的可执行结论(哪种情形 → 看哪个现场字段) -# ----------------------------------------------------------------------------- -# 首帧取证两行(" [diag] StackWalk64 first: ret=… addrpc=… fta=…" 与紧随其后的 -# " [diag] StackWalk64 in: machine=… rip=… rsp=… rbp=…"——原为单条 143 字符长行, -# 拆成两条短行以消除窄终端折行造成的"显示残缺"假象,字段一个不减) -# 与前置诊断行("[diag] Seq=… … modinited=…" / "SymGetModuleBase64(main)=…")合起来直接给出结论: -# (1) modinited == 0 且 SymGetModuleBase64(main) == 0 ⇒ dbghelp 的模块表不含主模块。 -# 根因是 SymInitialize 的 fInvadeProcess 参数(必须 TRUE)或调用时机(要在进程启动期、 -# 模块枚举可用时调用,本模块放在 Install 里)。修法:确认 Install 里 SymInitialize 的 -# 第三个实参为 1,且 Install 在入口线程、模块全部加载后调用;必要时改用 -# SymRefreshModuleList 重新枚举。 -# (2) 首帧 ret == 0,且紧邻有 "[diag] readmem failed addr=… size=… GetLastError=…" -# ⇒ 读栈内存失败。`ReadProcessMemory` 在回调里返回 0 会让 StackWalk64 立刻终止(=0 帧)。 -# 修法:核对 ReadMemCb 的宽度口径(dbghelp 侧是 DWORD nSize + LPDWORD,kernel32 侧是 -# SIZE_T,本模块已用本地 sizet 承接再写回 DWORD)与进程句柄/权限。 -# (3) 首帧 ret != 0 但返回 addrpc == 0,或 fta(SymFunctionTableAccess64(addrpc))== 0 -# ⇒ 上下文/模块基址口径问题(传给 StackWalk64 的 RIP/RSP/RBP 或模块基址不对)。 -# 修法:核对 CONTEXT 的 Rip/Rsp/Rbp 取值与 GetModuleHandleA(NULL) 基址。 -# 补充:首帧行里 `ret=not-called` 表示 SymInitialize/回调地址/帧缓冲前提不全、根本没调用 -# StackWalk64(此时该行仍会产出,保证"转 RBP 链"之前永远有首帧证据)。 -# 三者互斥且可判:只要把上面两组打印字段与判据对照,就能**当场**确定属于哪一类。 -# -# ----------------------------------------------------------------------------- -# 十、本轮新增的两组定位打印(应答"walk 到底跑到没有"与"重复残缺真因") -# ----------------------------------------------------------------------------- -# A. walk 是否被调用、走了哪条出口(全部走 DumpMainThread / WalkViaDbgHelp 的既有输出通道): -# * DumpMainThread 在**调用前无条件**打 " [diag] walk: enter use_dbghelp=… symready=… -# modinited=… frame_buf=… mapok=… asan=…",**调用后无条件**打 " [diag] walk: leave -# frames=… mapok=…"。两行都在 ⇒ walk 一定进了 RBP 主路径(dbghelp 加强可选)。 -# * 若 **连 walk: enter 都不出现** ⇒ DumpMainThread 在本行之前就返回了:只可能是三处 -# fallback(OpenThread / SuspendThread / GetThreadContext 失败,各自都有 -# "fallback: … cannot dump stack" 行)——据该行即可定位被跳过的分支。 -# * WalkViaDbgHelp 的**每一条 return** 都先打 " [diag] walk: return reason=<…> frames=…", -# reason ∈ {null_state, disabled, null_frame_buf, null_context, stackwalk_first_failed, -# stackwalk_failed, addrpc_zero, frame_limit}(含义见函数内注释),故 walk 的走向 -# 必可判定、不再靠猜。本轮起主路径是 RBP 链,DumpMainThread 另打 -# `walk: primary=rbp_chain` / `walk: rbp_chain frames=…` / `walk: dbghelp enhancement engaged`; -# dbghelp 不可用时另打 `[diag] symwalk=disabled reason=…`(见第十一节 B)。 -# B. "重复残缺"真因的判别凭据(唯一凭据 = 行号是否重复): -# * EmitLine 给**每一行**加全局递增前缀 " [dump#] "(k 由 _DumpLineNo 每次 +1)。 -# * 同一段文本出现多份且**序号重复**(同一 k 出现两次)⇒ 该文本被写了两次,即**并发写 -# 同一 LineBuf**;此时才需要"输出互斥"。同一序号不可能出自单线程的一次 EmitLine。 -# * 文本被**截断/换行错位**但**序号严格递增、无重复** ⇒ 内容只写了一次(乱不了号), -# 只可能是**终端长行折行/回显**层的显示效果;修法是缩短长行(本轮已把三条最长行拆短), -# **不动锁**。 -# * 本轮补打:`[dump#skip]` 仅在使用输出互斥时才需要;本模块判定为"显示层", -# 故未加互斥、未打印该标记(判据与结论见交付说明)。 -# -# ----------------------------------------------------------------------------- -# 十一、本轮两根因修复(崩溃 = 属性访问被降级;0 帧 = dbghelp 与环境互斥) -# ----------------------------------------------------------------------------- -# A. **崩溃根因不是分配失败,而是 TPC 把 `_State.<字段>` 静默降级成常量 null。** -# 实测证据(构建产物 IR,模块前缀 = 本文件 sha1 前 16 位 9b4ee486e08837c5): -# * 源码 `string.memset(_State.FrameBuf, 0, …)` 被生成为 -# `call i8* @memset(i8* null, i32 0, i64 248)` —— 参数就是字面 null; -# 同一函数里 `_State.LineBuf`(snprintf)、`_State.FuncTableAddr / ModuleBaseAddr` -# (StackWalk64 的两个回调槽)同样全被折成 null。 -# * `Install` 里的 `calloc(1,248)` 与其"分配失败即 return 0"的判空**都在且正确** -# (IR 里 `@calloc` 返回值被正常 store 到字段 20),故**不是** calloc 返回 NULL。 -# 机理(TPC 侧,只报不改):`TransPyC/lib/core/Handles/HandlesExprAttr.py` -# `_resolve_var_classname`(约 L285-293)对 `rawptr[X]` 形态的模块级全局只看 -# `GVar.type.pointee`(= `X*`,仍是指针)⇒ 认不出 `X`,返回 None;兜底的 -# `_recover_struct_class`(约 L723-740)虽能多解一层指针,但要求 `X` 已进入 -# **函数级**的 `Gen.variables/global_vars`(`HandlesFunctions.py:1848` 每函数清空), -# 而该登记只发生在"裸名 `X` 被求值一次"时(`HandlesExpr.py:479/487`)。 -# ⇒ 凡**一上手就直接读写 `_State.<字段>`、且函数内从未单独求值过裸名 `_State` -# 的函数**,`_handle_attr_name` 在 L507-508 返回 None: -# - 作为调用实参 ⇒ 被静默替换为 null(→ memset(null) 崩溃); -# - 作为 `if X == None:` 的条件 ⇒ 整个 `if` 被 `HandlesIf` L170-171 丢弃 -# (→ 我们原来写的 `if _State.FrameBuf == None: …` **根本没被生成**)。 -# 受影响函数(IR 佐证:这些函数体内 `@_State` 的 load 次数为 0): -# `EntryRva` / `LookupRva` / `EmitFrame` / `WalkViaDbgHelp`。 -# 其余函数(`EmitLine` / `DumpMainThread` / `ReadMemCb` / `Install` …)入口都有 -# `_State == None` 判空 ⇒ 裸名已登记 ⇒ 字段访问正常(这就是为什么 -# `DumpMainThread` 打出的 `frame_buf=1`、`modinited=1` 都是真的)。 -# 本模块的修法(**不去改 TPC**):凡直接使用 `_State.<字段>` 的函数,入口 -# **第一句就写 `if _State == None: …`**(即使是冗余判空)——它既是真正的兜底, -# 也把裸名 `_State` 登记进 `Gen.variables`,使其后的字段访问被正常解析。 -# 崩溃点再加一道局部量兜底:`FBuf: VOIDPTR = _State.FrameBuf` → 判 `FBuf == None` -# → `memset(FBuf, …)`,把"局部变量"这条最可靠的通道用在最危险的一行上。 -# B. **0 帧/符号化互斥**:ASan 运行时会自己初始化 DbgHelp 做符号化,同进程不允许两种 -# 不兼容用法(实测 ASan 侧打印 "Failed to initialize DbgHelp!")。故本模块**不再把 -# dbghelp 当主路径**,改为: -# * 主路径 = `WalkRbpChain`(纯 RBP 链,**零 dbghelp 依赖**); -# * **第二兜底 = `ScanStack` 栈扫描**(零 dbghelp、且**不依赖 RBP / 帧指针**): -# RBP 链只拿到当前 RIP(<=1 帧)时启用(`walk: fallback=stack_scan`),见第十二节; -# * dbghelp 仅作**可选加强**:只有 RBP 链与栈扫描都拿不到更多帧(<=1)且 dbghelp -# 确实可用时才补一次 `WalkViaDbgHelp`。 -# * 降级**必须给原因**(不静默 0 帧):`[diag] symwalk=disabled reason=…`, -# reason ∈ {compile_time_switch, dbghelp_conflict_with_asan, syminitialize_failed, -# callback_missing, null_frame_buf};ASan 探测用 GetModuleHandleA 两个常见 -# ASan 运行时名(clang_rt / libclang_rt 前缀)。 -# `WalkRbpChain` 同步加固(升级为主路径后它必须"绝不把卡死变成崩溃"): -# * 读 [rbp]/[rbp+8] 一律走 `wm.ReadProcessMemory`(**不直接解引用**野 rbp); -# * **每一步无条件打 `[diag] rbp:` 原始读数 + 判定**(先取证后决策,含被拒步;见第十二节); -# * 自洽校验:非 0 / 8 字节对齐 / `[rbp] > rbp` / 单步跨度 <= RBP_MAX_STEP / -# 返回地址落在主模块基址区间 [Base, Base+DBGSTACK_MODULE_MAX) 内才**接受**该帧。 -# -# ----------------------------------------------------------------------------- -# 十二、本轮新增:RBP 链逐步原始读数 + 栈扫描兜底(应答"栈走不长") -# ----------------------------------------------------------------------------- -# 现场(用户实测,`#0` 已正确解析:RIP 在主模块、映射/查表/帧 #0 全对):`#1 = 0x121fa` -# ——远小于主模块基址、也不是栈地址 ⇒ 链在第 1 步即断。⇒ 先把"断在哪一步、为什么断" -# 变成可读数据,再补一条**不依赖 RBP**的路径。 -# A. **逐步原始读数**(`WalkRbpChain` 内,前缀 `[diag] rbp:`,**先于任何 reject 早退**): -# frame= rbp=0x… [rbp]=0x… [rbp+8]=0x… ← 读到的原始 8 字节值 -# frame= rpm_ok=<0/1> rpm_err= accept=<0/1> reject_reason=<…> -# reject_reason ∈ {rbp_zero, not_aligned, rpm_failed, ret_zero, ret_out_of_module, -# not_growing, step_too_large};被接受时 reason=`accept`、accept=1。 -# 仅被接受的步才 EmitFrame(旧实现在校验前就 EmitFrame,会把 `0x121fa` 这类垃圾值当成 -# 一帧 external 打出来,误导读者)。三种断链根因据此一眼可分:读失败 / 值本身不是返回 -# 地址(帧指针未保留)/ 被自洽校验档掉。 -# B. **栈扫描兜底**(`ScanStack`):RBP 链 <=1 帧时启用(`walk: fallback=stack_scan`)。 -# 在 [RSP, RSP+2MiB) 内按 8 字节步长遍历,**4 KiB/块整块读**(失败即停并打地址/err); -# 候选 = 落在主模块区间、且**相邻 8 字节字(A-8 或 A+8)像栈地址**的字;按值去重、 -# 上限 64(诊断打 `scan_candidates=`);输出帧前缀 `~`(` ~#3 0x…`)明确标注 -# **扫描推断**(可含误报,不与权威帧混淆)。全部读取只经 ReadProcessMemory, -# 绝不直接解引用主线程内存(诊断设施不许把"卡死"变成"崩溃")。 -# C. **帧指针是否保留**(调查结论与 TPC 侧修法落点,本模块只报不改): -# * 单一 llc 调用点 = `TransPyC/lib/Projectrans/Phase2Translator.py:2160 _llc_compile`, -# 它在 `--ctrlc-stack` 打开时把 `-frame-pointer=all` 追加到命令(2187-2193); -# App 模块(如 `TransPyV/App/lib/core/Gen/Expr/ExprGenOps.vp`,模块 sha1 -# `8a96a2885bae09ca`)走的是**同一个**调用点(2608 的 _dso.ll / 2319 的内联合并), -# 故 flag 的落点本身没问题。 -# * 缓存失效:App 模块 obj = `output/{sha1}.obj`,`_compile_ll_files` 见其存在即跳过 -# (2621),但 `--ctrlc-stack` 时 `hotspot_on`(1144)为真 ⇒ 1196-1204 会删除旧 obj -# 强制重编;include 模块另有隔离目录 `includes.binary_ctrlcstack`(3097-3099)+ -# 旧 obj 删除(3181-3190)。⇒ App/include 都不会静默复用"无帧指针的旧 obj"。 -# * 交付结论:若 `[diag] rbp:` 显示 `[rbp+8]` 仍不是返回地址(`ret_out_of_module`), -# 则说明 `-frame-pointer=all`(llc 默认值级别的开关)没有生效,最稳的改法是在**IR 里 -# 按函数显式标注** `"frame-pointer"="all"` —— 落点 -# `TransPyV/App/lib/core/Gen/Func/FuncGenBase.vp`(属性拼装 `_append_llvm_attr`,154-181) -# 与 `TransPyC/lib/core/Handles/HandlesFunctions.py:1735`(`func.attributes.add('noredzone')` -# 旁)—— 让帧指针成为**函数属性**而非 llc 命令行默认值,从根上免疫"llc 是否采纳默认值"。 -# ============================================================================= - -# ---- 编译期开关 ------------------------------------------------------------- - -# 1 = 允许在**可选加强**路径上使用 dbghelp!StackWalk64(**产物必须链接 -ldbghelp**); -# 注意:本轮起主回溯路径是 WalkRbpChain(零 dbghelp 依赖),dbghelp 只在 RBP 链 -# 拿不到更多帧且进程环境允许(无 ASan 冲突)时才补一次(见模块头第十一节 B)。 -# 0 = 完全不用 dbghelp。切换机制:常量比较在 HandleIf 处被常量折叠,凡出现在 -# `if DBGSTACK_USE_DBGHELP != 0:` 分支里的语句(Install 的 SymInitialize/GetProcAddress -# 段、WalkViaDbgHelp 的整个函数体、DumpMainThread 的可选加强调用)在置 0 时整体不生成代码 -# ——范式与依据同 includes/memhub.vp 的 MEMBUDDY_THREAD_SAFE 注释("未取分支整体不生成 -# 代码 ⇒ 函数体为空")。故置 0 后本模块不留任何 dbghelp 链接期符号。 -DBGSTACK_USE_DBGHELP: t.CDefine = 1 - -# 触发事件取值见 includes/w32/win32console.vp:CTRL_C_EVENT=0 / CTRL_BREAK_EVENT=1 -# (代码里按 wc.CTRL_C_EVENT / wc.CTRL_BREAK_EVENT 引用,不另立本地别名) - -# ---- 旁文件格式 ------------------------------------------------------------- -# 头部 20 字节;记录 20 字节/条;magic "TPCS" 小端读作 u32 -DBGSTACK_MAP_MAGIC: t.CDefine = 0x53435054 # "TPCS"('T','P','C','S' 小端) -DBGSTACK_MAP_VERSION: t.CDefine = 1 -DBGSTACK_MAP_HEADER: t.CDefine = 20 # 头部字节数 -DBGSTACK_ENTRY_SIZE: t.CDefine = 20 # 每条记录字节数 -DBGSTACK_ENTRY_FILEOFF: t.CDefine = 8 # 记录内 file_off 偏移 -DBGSTACK_ENTRY_LINE: t.CDefine = 12 # 记录内 line 偏移 -DBGSTACK_ENTRY_FUNCOFF: t.CDefine = 16 # 记录内 func_off 偏移 -# 旁文件大小上限(纯防御:避免头部的 entry_count/str_bytes 被写坏后要求巨量分配) -DBGSTACK_MAP_LIMIT: t.CDefine = 67108864 # 64 MiB - -# ---- 运行时缓冲与上限 ------------------------------------------------------- -DBGSTACK_PATH_CAP: t.CDefine = 4096 # exe 路径 + ".stackmap" 缓冲 -DBGSTACK_LINE_CAP: t.CDefine = 1024 # 单行输出缓冲 -DBGSTACK_MAX_FRAMES: t.CDefine = 64 # 最多打印帧数 -DBGSTACK_RBP_MAX_STEP: t.CDefine = 0x100000 # 相邻帧 rbp 跨度上限(1 MiB) -# RBP 链自洽校验用:返回地址落在 [Base, Base+本值) 内才认为"这一帧仍属主模块"并继续回溯。 -# 取 128 MiB —— 远超任何实际 PE 映像,只作"明显越界即停"的粗筛(不要求等于 SizeOfImage)。 -DBGSTACK_MODULE_MAX: t.CDefine = 0x8000000 - -# ---- 栈扫描兜底(ScanStack,见第十二节)------------------------------------- -# 从 RSP 起向**高地址**(栈向高地址方向 = 调用者方向)扫描的窗口上限: -# [RSP, RSP + DBGSTACK_SCAN_MAX_BYTES)。TransPyV 用 -Wl,--stack,0x1000000(16 MiB) -# 保留主线程栈,而 [RSP, StackBase) 只是**已用**部分;取 2 MiB 兼顾"调用链够深"与 -# "不把 TEB/堆区误当栈扫描"两个目标(RBP 链只拿到 1 帧时才启用,属诊断增益路径)。 -DBGSTACK_SCAN_MAX_BYTES: t.CDefine = 0x200000 # 2 MiB -# 分块读取粒度(一次 ReadProcessMemory 读一整块,不逐 8 字节读;读失败即停)。 -DBGSTACK_SCAN_CHUNK_BYTES: t.CDefine = 4096 # 4 KiB/块 -DBGSTACK_SCAN_CHUNK_WORDS: t.CDefine = 512 # = 4096 / 8,块内 u64 字数(缓冲用作局部数组) -DBGSTACK_SCAN_MAX_HITS: t.CDefine = 64 # 候选返回地址数量上限(诊断里打 scan_candidates=) -DBGSTACK_SUSPEND_FAILED: t.CDefine = 0xFFFFFFFF # SuspendThread 失败返回值 -# 旁文件后缀见 DBGSTACK_PATH_CAP 注释(".stackmap",9 字符 + NUL);拼接处直接用 -# 字符串字面量,余量按 16 字节判定,不另设常量。 -# 转储文件后缀 ".stackdump.txt" 共 15 字符 + NUL = 16 字节,恰好是上面"余量按 16 字节判定" -# 的判据上限,故复用同一个余量检查即可,无需再加一道(见 Install 的路径长度校验)。 -# FILE_APPEND_DATA(0x0004) 未在 wf 中定义,本模块自备:以它作 dwDesiredAccess 打开 -# ⇒ 每次 WriteFile 都写到文件末尾(天然"存在则追加");配合 OPEN_ALWAYS ⇒ 不存在则创建。 -DBGSTACK_FILE_APPEND_DATA: t.CDefine = 0x00000004 - - -class DbgStackState(): - """本模块的**单一**全局状态对象(Install 时一次分配,此后只读;回调线程可读)。 - - 缓冲区全部在 Install 阶段分配(回调线程零分配),指针不必再释放——本模块是 - 进程级常驻诊断设施,进程存活期间一直可用(无 uninstall 路径)。 - """ - - Installed: t.CInt # 1 = SetConsoleCtrlHandler 已注册成功 - MapOk: t.CInt # 1 = 旁文件已装载且全表校验通过(可做 地址→源行 映射) - SymReady: t.CInt # 1 = SymInitialize 成功(dbghelp 可用) - ModInited: t.CInt # 1 = dbghelp 模块表已含主模块(SymGetModuleBase64 非 0) - Dumping: t.CInt # 转储重入标志(原子锁 _DumpLock 的可读镜像,供诊断打印) - Seq: t.CInt # 转储序号:每次成功进入转储 +1(判定并发/顺序两次的凭据) - ReadMemFailed: t.CInt # 1 = 本模块读栈内存回调已发生过首次失败(现场值见下三字段) - MissReported: t.CInt # 1 = 源行映射未命中的首次诊断已打印(防刷屏) - ReadMemAddr: t.CUInt64T # 首次读栈内存失败的地址(供首帧诊断紧邻打印) - ReadMemSize: t.CUInt32T # 首次读栈内存失败请求的字节数 - ReadMemErr: t.CUInt32T # 首次读栈内存失败时的 GetLastError - MainThreadId: ULONG # Install 时所在线程(= 主线程)id - OutHandle: HANDLE # 转储输出去向(优先 stderr,退回 stdout) - EntryCount: t.CUInt32T # 记录条数 - StrBytes: t.CUInt32T # 字符串池字节数 - MapBase: bytes # 旁文件全文基址(stdlib.malloc) - StrBase: bytes # 字符串池基址 = MapBase + 20 + EntryCount*20 - FuncTableAddr: VOIDPTR # dbghelp!SymFunctionTableAccess64(StackWalk64 回调 2) - ModuleBaseAddr: VOIDPTR # dbghelp!SymGetModuleBase64(StackWalk64 回调 3) - ContextBuf: VOIDPTR # CONTEXT 缓冲(calloc,16 字节对齐) - FrameBuf: VOIDPTR # STACKFRAME64 缓冲(calloc) - LineBuf: bytes # 输出行缓冲 - - -# 唯一全局状态:None = 尚未安装(= 旁文件不存在时保持的零副作用状态)。 -# 用「指针 + 懒分配」而不是「模块级对象常量」:模块级对象常量会生成初始化构造器, -# 与 TPC 侧注入的 Install() 构造器存在不确定的先后顺序(对象构造会把 Install 写入的 -# 状态清零),指针式全局是零初始化、无构造器,天然免疫该问题。 -_State: rawptr[DbgStackState] = None - - -# ---- 转储原子锁 / 锁定的输出句柄 -------------------------------------------- -# _DumpLock:转储重入锁,0 = 空闲 / 1 = 占用。用内联汇编的一条 `lock cmpxchg` -# (AtomicTryEnterDump)做原子"检查并占用",消除普通标志"读-改-写"的竞态窗口 -# (两个回调线程同时读到 0 后又同时置 1 ⇒ 共用 LineBuf,输出互相截断)。 -# 32 位、模块级、零初始化(无构造器);_State.Dumping 是它的可读镜像。 -# 不用 InterlockedCompareExchange:x64 上是 winnt.h 的编译器内部函数、非可链接符号。 -_DumpLock: t.CUInt32T = 0 - -# _DumpOut:转储期间**锁定**的输出句柄。DumpMainThread 入口一次性确定,本次转储全程只写它, -# 不在中途重取(避免同一段输出分叉到 stdout/stderr 两条流而在控制台交错)。 -# Install 也会写入一次(= 与 _State.OutHandle 同源);EmitLine 优先用它。 -_DumpOut: HANDLE = None - -# _HandlerReg:SetConsoleCtrlHandler 的"只注册一次"守卫。Install 本身对 _State 幂等, -# 这里再加一道显式守卫(防止"同一按键触发多次回调 ⇒ 同一转储被做好几遍")。 -_HandlerReg: t.CInt = 0 - -# _DumpFile:转储文件的追加写句柄(Install 成功后被置为 OpenAlways + FILE_APPEND_DATA 的句柄)。 -# 控制台与文件**双写**同一份内容(见 EmitLine);为 None/无效时只是不写文件,绝不影响控制台。 -_DumpFile: HANDLE = None - -# _DumpFileFailWarned:转储文件写失败只提示一次(防刷屏)。回调线程里只做一次普通写, -# 与 _DumpLock 一样是零初始化的模块级标量,不引入构造器。 -_DumpFileFailWarned: t.CInt = 0 - -# _DumpLineNo:全局递增的**输出行号**(每次 EmitLine +1),由 EmitLine 以 -# " [dump#] " 前缀写进每一行。它是判定"重复残缺"真因的**唯一凭据**: -# * 若同一段文本在屏幕上出现多份、且**行号重复**(同一 k 出现两次)⇒ 该文本被写了两次, -# 即存在**并发写同一 LineBuf**(同一序号不可能来自单线程的一次 EmitLine); -# * 若文本被**截断/换行错位**但**行号严格递增、无重复** ⇒ 内容只写了一次乱不了号, -# 只能是**终端长行折行/回显**层面的显示效果,程序侧无并发。 -# 单线程写(回调次数由 _DumpLock 唯一化)时,序号必然连续无重复。 -_DumpLineNo: t.CInt = 0 - - -# ============================================================ -# 输出(WriteFile,避开 CRT 流锁;见模块头第七节) -# 定义在旁文件读取之前:装载期的校验诊断也要走这条唯一输出路径。 -# 控制台文本一律 ASCII(代码页无关、可 grep);同一份内容同时追加到 .stackdump.txt。 -# ============================================================ - -def WriteConsoleOnly(N: int): - """把 LineBuf 的前 N 字节只写**控制台**(不落文件)。 - - 从 EmitLine 里拆出来的唯一目的:让"转储文件写失败"的提示能在不进 EmitLine(⇒ 不再触发 - 写文件)的前提下直达控制台,避免递归。句柄口径与 EmitLine 完全一致。 - """ - if _State == None or N <= 0: - return - H: HANDLE = _DumpOut - if H == None or H == INVALID_HANDLE_VALUE: - H = _State.OutHandle - if H == None or H == INVALID_HANDLE_VALUE: - return - Wr: ULONG = 0 - wf.WriteFile(H, _State.LineBuf, ULONG(N), t.CPtr(c.Addr(Wr)), None) - - -def EmitToFile(N: int): - """把 LineBuf 的前 N 字节**追加**写到转储文件(UTF-8 字节原样落盘,不受控制台代码页影响)。 - - 文件句柄在 Install 时以 FILE_APPEND_DATA + OPEN_ALWAYS 打开并常驻(回调线程零打开、零分配)。 - 写失败**不影响**控制台:控制台已由 EmitLine 单独先写;这里只提示一次,且提示用局部栈缓冲 - 直写控制台句柄(不再进 EmitLine),彻底避免递归。 - """ - global _DumpFileFailWarned - if _State == None or N <= 0: - return - if _DumpFile == None or _DumpFile == INVALID_HANDLE_VALUE: - return - Wr: ULONG = 0 - if wf.WriteFile(_DumpFile, _State.LineBuf, ULONG(N), t.CPtr(c.Addr(Wr)), None) != 0: - return - if _DumpFileFailWarned != 0: - return - _DumpFileFailWarned = 1 - LB: t.CArray[t.CChar, 160] - M: int = stdio.snprintf( - t.CPtr(c.Addr(LB)), t.CSizeT(160), - "[dbgstack] WARN: stack dump file write failed (further file-write failures not reported)\n") - if M <= 0: - return - H: HANDLE = _DumpOut - if H == None or H == INVALID_HANDLE_VALUE: - H = wf.GetStdHandle(wf.STD_ERROR_HANDLE) - if H == None or H == INVALID_HANDLE_VALUE: - return - W2: ULONG = 0 - wf.WriteFile(H, t.CPtr(c.Addr(LB)), ULONG(M), t.CPtr(c.Addr(W2)), None) - - -def EmitLine(N: int): - """把 LineBuf 的前 N 字节写到**控制台 + 转储文件**(N 由 snprintf 返回,含截断保护)。 - - EmitText / EmitFrame / 一切诊断最终都汇聚到本函数 ⇒ 全模块只有这一处输出编排: - ⓪ **行号前缀**(见 _DumpLineNo 注释):在内容前原地插入 " [dump#] "(k 全局递增)。 - 实现用 string.memmove 把 LineBuf 现有内容整体后移前缀长度,再写前缀——不改调用方、 - 不多占一块缓冲(回调线程零分配),WriteConsoleOnly / EmitToFile 照旧只认 LineBuf。 - 它是"重复残缺"真因的判别凭据:序号重复 ⇒ 并发写;序号连续仅是折行 ⇒ 显示层。 - ① 控制台(WriteFile,避开 CRT 流锁,见模块头第七节):优先转储期间锁定的 _DumpOut, - 装载期回退到 _State.OutHandle(两者同源,Install 时解析一次); - ② 转储文件(EmitToFile):把**同一份内容**追加落盘(UTF-8,不受控制台代码页影响)。 - 文件写失败不阻断控制台(见 EmitToFile)。句柄在转储中途**绝不**重新解析。 - """ - global _DumpLineNo - if _State == None or N <= 0: - return - if N > DBGSTACK_LINE_CAP - 1: - N = DBGSTACK_LINE_CAP - 1 - # ⓪ 行号前缀:原地前插(见 _DumpLineNo 注释与上面的 ⓪) - _DumpLineNo = _DumpLineNo + 1 - Pfx: t.CArray[t.CChar, 32] - Pl: int = stdio.snprintf(t.CPtr(c.Addr(Pfx)), t.CSizeT(32), " [dump#%d] ", _DumpLineNo) - if Pl > 0: - # 前插后总长不得超过缓冲上限(留 1 字节给 '\0') - if N + Pl > DBGSTACK_LINE_CAP - 1: - N = DBGSTACK_LINE_CAP - 1 - Pl - # 连内容末尾的 '\0' 一起后移(N+1),保证仍以 '\0' 结尾 - string.memmove(t.CPtr(_State.LineBuf + t.CInt(Pl)), - t.CPtr(_State.LineBuf), sizet(N + 1)) - string.memmove(t.CPtr(_State.LineBuf), t.CPtr(c.Addr(Pfx)), sizet(Pl)) - N = N + Pl - WriteConsoleOnly(N) - EmitToFile(N) - - -def EmitText(Text: str): - """输出一行**不含格式符**的常量文本(统一走同一缓冲/同一 WriteFile 路径)""" - if _State == None or _State.LineBuf == None: - return - N: int = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), "%s", Text) - EmitLine(N) - - -def EmitEarly(Text: str): - """Install 极早期诊断(_State / LineBuf 尚未就绪)——仍并入**同一条输出通道**。 - - 子代理提到的"极早期失败仍用 stdio.printf / stdout"就是这里:stdio.printf 走 stdout, - 而转储走 stderr,两条流会在控制台交错。现统一为: - * _State 与 LineBuf 就绪 ⇒ 转交 EmitText(同一缓冲、同一句柄); - * 否则 ⇒ 直接用 WriteFile(GetStdHandle(STD_ERROR_HANDLE))(与转储同一去向)。 - Install 阶段不再是回调线程,临时解析一次句柄无碍(转储期才是锁定的)。 - """ - if _State != None and _State.LineBuf != None: - EmitText(Text) - return - H: HANDLE = wf.GetStdHandle(wf.STD_ERROR_HANDLE) - if H == None or H == INVALID_HANDLE_VALUE: - H = wf.GetStdHandle(wf.STD_OUTPUT_HANDLE) - if H == None or H == INVALID_HANDLE_VALUE: - return - Len: sizet = string.strlen(Text) - if Len == 0: - return - Wr: ULONG = 0 - wf.WriteFile(H, Text, ULONG(Len), t.CPtr(c.Addr(Wr)), None) - - -def EmitEarlyCode(Text: str, Code: ULONG): - """EmitEarly 的带 GetLastError 码版本(同上并入唯一输出通道)。 - - 需要一次 snprintf 组行;_State 未就绪时用**局部栈缓冲**(不依赖 _State/LineBuf)。 - """ - if _State != None and _State.LineBuf != None: - N: int = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - "%s(GetLastError=%u)\n", Text, Code) - EmitLine(N) - return - LB: t.CArray[t.CChar, 256] - N2: int = stdio.snprintf(t.CPtr(c.Addr(LB)), t.CSizeT(256), - "%s(GetLastError=%u)\n", Text, Code) - if N2 <= 0: - return - H: HANDLE = wf.GetStdHandle(wf.STD_ERROR_HANDLE) - if H == None or H == INVALID_HANDLE_VALUE: - H = wf.GetStdHandle(wf.STD_OUTPUT_HANDLE) - if H == None or H == INVALID_HANDLE_VALUE: - return - Wr: ULONG = 0 - wf.WriteFile(H, t.CPtr(c.Addr(LB)), ULONG(N2), t.CPtr(c.Addr(Wr)), None) - - -def AtomicTryEnterDump() -> t.CInt: - """原子"检查并占用" _DumpLock(0→1);返回**进入前的旧值**:0 = 本次占用成功。 - - 实现:内联汇编一条 `lock cmpxchg`——eax 为期望旧值 0,ecx 为欲写入的新值 1: - * 若 [锁] == eax(0) ⇒ [锁] = ecx(1)(占用成功,返回 0); - * 否则 eax = [锁](占用失败,返回非 0)。 - 锁为 32 位、天然 4 字节对齐(_DumpLock 是模块级 t.CUInt32T),与 ecx 源操作数宽度一致。 - 不使用 InterlockedCompareExchange(x64 上是编译器内部函数、无导出符号)。 - 回调线程调用:只读/写这一个模块级锁与寄存器,不分配内存。 - """ - Old: t.CUInt32T = 0 - c.Asm(f"""xor eax, eax - mov ecx, 1 - lock cmpxchg [{c.AsmInp(c.Addr(_DumpLock), t.ASM_DESCR.REG_ANY)}], ecx - mov {c.AsmOut(Old, t.ASM_DESCR.OUTPUT_REG)}, eax""", - op=[t.ASM_DESCR.CLOBBER_RAX, t.ASM_DESCR.CLOBBER_RCX, - t.ASM_DESCR.CLOBBER_MEMORY, t.ASM_DESCR.CLOBBER_CC]) - return t.CInt(Old) - - -def AtomicLeaveDump(): - """释放转储锁:_DumpLock → 0(并把可读镜像 _State.Dumping → 0)。 - - 只有持锁者会走到这里;持锁期间其他回调线程看到本锁为 1 时直接返回,故这里直接写 0 - 是安全的(无并发的"释放者/获取者"同时写)。 - """ - global _DumpLock - _DumpLock = 0 - if _State != None: - _State.Dumping = 0 - - -# ============================================================ -# 旁文件读取(小端,按字节取,规避未对齐访问) -# ============================================================ - -def ReadU32(Buf: bytes, Off: int) -> t.CUInt32T: - """小端读 u32(Buf[Off..Off+3];Buf 为无符号字节基址) - - ⚠ 每个字节都必须 & 0xFF:Viper 的 `Buf[i]` 类型是**有符号** char,字节 >= 0x80 - 时 `t.CUInt32T(Buf[i])` 会符号扩展(0xE8 → 0xFFFFFFE8),高位被垃圾污染。 - 写端落盘的 entry_count/str_bytes 里只要有一个字节 >= 0x80(记录数 128~255、 - 32768~65535… 或字符串池 > 127 字节,几乎必然发生)就会把 `20+count*20+str_bytes` - 算成数十亿 ⇒ 越界校验必然失败 ⇒ 映射永远装载不上。这是"源行映射=无"的真因。 - 先转 u32 再与 0xFF 相与,则无论该转换是符号扩展还是零扩展,结果都恒为 0..255。 - """ - Val: t.CUInt32T = t.CUInt32T(Buf[Off]) & t.CUInt32T(0xFF) - Val = Val | ((t.CUInt32T(Buf[Off + 1]) & t.CUInt32T(0xFF)) << 8) - Val = Val | ((t.CUInt32T(Buf[Off + 2]) & t.CUInt32T(0xFF)) << 16) - Val = Val | ((t.CUInt32T(Buf[Off + 3]) & t.CUInt32T(0xFF)) << 24) - return Val - - -def ReadU64(Buf: bytes, Off: int) -> t.CUInt64T: - """小端读 u64(由两次 u32 合成,避免 4 字节对齐偏移上的 8 字节未对齐读)""" - Lo: t.CUInt64T = t.CUInt64T(ReadU32(Buf, Off)) - Hi: t.CUInt64T = t.CUInt64T(ReadU32(Buf, Off + 4)) - return Lo | (Hi << 32) - - -# ---- 校验失败的定位诊断(一律由下面这一族函数打印后返回 0) ------------------- - -def MapFailPlain(Size: t.CInt64T, Reason: str) -> int: - """校验失败但尚未读到头部(尺寸/分配/读取阶段):打印原因 + 文件实际大小。""" - if _State == None or _State.LineBuf == None: - return 0 - N: int = stdio.snprintf( - _State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - "[dbgstack] WARN: .stackmap invalid -- %s (file %lld bytes), addresses only this run\n", - Reason, Size) - EmitLine(N) - return 0 - - -def MapFailHdr(Size: t.CInt64T, Reason: str, Head: bytes) -> int: - """校验失败但已读到头部:额外打印**实读**的头部字段(一眼区分口径不符 / 文件写坏)。""" - if _State == None or _State.LineBuf == None: - return 0 - N: int = stdio.snprintf( - _State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - "[dbgstack] WARN: .stackmap invalid -- %s" - " (read magic=0x%x version=%u entry_count=%u str_count=%u str_bytes=%u; file %lld bytes)," - " addresses only this run\n", - Reason, ReadU32(Head, 0), ReadU32(Head, 4), ReadU32(Head, 8), ReadU32(Head, 12), - ReadU32(Head, 16), Size) - EmitLine(N) - return 0 - - -def MapFailEntry(Size: t.CInt64T, Index: int, Reason: str, Got: t.CUInt64T) -> int: - """记录区逐条校验失败:附上出错记录的下标与值(rva 或字符串池偏移)。""" - if _State == None or _State.LineBuf == None: - return 0 - N: int = stdio.snprintf( - _State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - "[dbgstack] WARN: .stackmap invalid -- %s (entry %d, value=0x%llx; file %lld bytes)," - " addresses only this run\n", - Reason, Index, Got, Size) - EmitLine(N) - return 0 - - -def EntryRva(Index: int) -> t.CUInt64T: - """第 Index 条记录的 rva(记录区起点 = MapBase + 20) - - ⚠ 入口第一句的裸名 `_State` 判空**不能删**:TPC 的属性访问层必须先在函数级 - `Gen.variables` 里登记 `_State`,才能解析这个模块级 `rawptr[DbgStackState]` 全局的 - 结构体类型;否则本函数内 `_State.MapBase` 会被**静默降级为常量 null**(详见模块头 - 第十一节 A,附构建产物 IR 证据)。判空本身也是真的兜底(_State 未安装时给 0)。 - """ - if _State == None: - return 0 - Off: int = DBGSTACK_MAP_HEADER + Index * DBGSTACK_ENTRY_SIZE - return ReadU64(_State.MapBase, Off) - - -def LookupRva(Rva: t.CUInt64T) -> int: - """查 rva(表按 rva 升序):返回**最大的 rva <= 目标**那条的下标;无则 -1。 - - 粒度约定(与生成侧 TPC `_WriteStackMap` 对齐):表里写的是 **DWARF 行表的行起点 - 地址**,而栈帧里的返回地址几乎不会精确落在某个行起点上 ⇒ 必须用下界(floor) - 查找,否则每一帧都会退化成 +0x…(unknown)、整个功能等于白做。 - 语义:命中 = 存在 rva <= 目标(取其中最大者,即"目标落在哪一行起点之后"); - 未命中 = 目标小于表内最小 rva。 - - ⚠ 本轮**只**在入口新增一句裸名 `_State` 判空(**搜索算法逐字未动**):原因同 - EntryRva —— 不求值裸名 `_State` 时,`_State.EntryCount` 会被 TPC 静默降级为 - null(⇒ Hi = -1 ⇒ 二分一次都不进 ⇒ 所有帧都落 "unknown")。若不需要这个兜底, - 回退本 3 行即可,但"地址→源行"会整片失效。 - """ - if _State == None: - return -1 - Lo: t.CInt = 0 - Hi: t.CInt = t.CInt(_State.EntryCount) - 1 - Best: t.CInt = -1 - while Lo <= Hi: - Mid: t.CInt = Lo + (Hi - Lo) / 2 - E: t.CUInt64T = EntryRva(t.CInt(Mid)) - if E <= Rva: - Best = Mid - Lo = Mid + 1 - else: - Hi = Mid - 1 - return Best - - -def LoadMapFile(hFile: HANDLE, Size: t.CInt64T) -> int: - """读入旁文件并校验(头部 / 记录区升序 / 字符串偏移范围)。返回 1=MapOk 置位。 - - 校验失败一律禁用映射(MapOk=0)并返回 0:破损的表会给出**错行号**,比"只打地址"更坏, - 故不降级使用。失败时**由本函数自己打印挂在哪一项**(MapFailPlain/MapFailHdr/ - MapFailEntry,含头部实读值与文件大小),不再让调用方打一句笼统的"文件非法"—— - "文件存在 ⇒ Install 装了 handler ⇒ 映射却不可用"这类问题必须能一眼定性。 - - 与写端(TPC `_WriteStackMap`)的校验口径对照(逐字段): - magic 小端 u32 == 0x53435054("TPCS") ← 写端 b'TPCS' - version u32 == 1 ← 写端常量 1 - entry_count 参与 `20+count*20+str_bytes <= 文件大` ← 写端 len(records) - str_bytes 同上 ← 写端 len(pool) - str_count **只用于诊断打印,不参与校验**(写端 len(str_offsets) 含偏移 0 的 - 空串哨兵;读端只依赖"偏移落池 + 池末必有 \\0"的边界性质) - 记录 rva 严格升序 ← 写端 sorted(best.keys()) - file_off < str_bytes 且 >= 1 ← 写端 _Intern(path) 恒 >= 1 - func_off == 0 表示无函数名;否则 < str_bytes ← 写端 `_Intern(name) if name else 0` - """ - if Size < t.CInt64T(DBGSTACK_MAP_HEADER): - return MapFailPlain(Size, "file smaller than the 20-byte header") - if Size > t.CInt64T(DBGSTACK_MAP_LIMIT): - return MapFailPlain(Size, "file exceeds the 64 MiB limit (header likely corrupt)") - Buf: bytes = stdlib.malloc(sizet(Size)) - if Buf == None: - return MapFailPlain(Size, "load buffer allocation failed (file too large or out of memory)") - Got: ULONG = 0 - if wf.ReadFile(hFile, Buf, ULONG(Size), t.CPtr(c.Addr(Got)), None) == 0: - Err: ULONG = GetLastError() - stdlib.free(t.CPtr(Buf)) - if _State != None and _State.LineBuf != None: - RN: int = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - "[dbgstack] WARN: read .stackmap failed (GetLastError=%u)," - " addresses only this run\n", - Err) - EmitLine(RN) - return 0 - if t.CSizeT(Got) != t.CSizeT(Size): - stdlib.free(t.CPtr(Buf)) - return MapFailPlain(Size, "read fewer bytes than the file size") - # 头部:magic / version / entry_count / str_count / str_bytes - if ReadU32(Buf, 0) != t.CUInt32T(DBGSTACK_MAP_MAGIC): - stdlib.free(t.CPtr(Buf)) - return MapFailHdr(Size, "magic is not \"TPCS\" (writer/reader endianness or offset mismatch)", Buf) - if ReadU32(Buf, 4) != t.CUInt32T(DBGSTACK_MAP_VERSION): - stdlib.free(t.CPtr(Buf)) - return MapFailHdr(Size, "version is not 1 (stackmap written by another TPC version)", Buf) - Cnt: t.CUInt32T = ReadU32(Buf, 8) - StrCnt: t.CUInt32T = ReadU32(Buf, 12) - SBytes: t.CUInt32T = ReadU32(Buf, 16) - # 记录区 + 字符串池必须完整落在文件内(u64 运算,杜绝 u32 回绕) - Need: t.CUInt64T = t.CUInt64T(DBGSTACK_MAP_HEADER) \ - + t.CUInt64T(Cnt) * t.CUInt64T(DBGSTACK_ENTRY_SIZE) \ - + t.CUInt64T(SBytes) - if Need > t.CUInt64T(Size): - stdlib.free(t.CPtr(Buf)) - return MapFailHdr(Size, "20+entry_count*20+str_bytes exceeds end of file " - "(header corrupt or file truncated)", Buf) - if SBytes == 0: - stdlib.free(t.CPtr(Buf)) - return MapFailHdr(Size, "str_bytes is 0 (string pool missing)", Buf) - # 字符串池的最后一个字节必须是 '\0':写端每条串都以 \0 结尾,故池末恒为 \0。 - # 有了这条,配合下面的"偏移落池内"校验,任何 %s 打印都不会越出池尾。 - PoolEnd: int = DBGSTACK_MAP_HEADER + t.CInt(Cnt) * DBGSTACK_ENTRY_SIZE + t.CInt(SBytes) - 1 - if Buf[PoolEnd] != 0: - stdlib.free(t.CPtr(Buf)) - return MapFailHdr(Size, "string pool does not end with '\\0' (pool corrupt)", Buf) - # 全表校验:rva 严格升序(二分查找的前提)+ 字符串偏移落在池内 - Prev: t.CUInt64T = 0 - i: t.CInt = 0 - while i < t.CInt(Cnt): - Off: int = DBGSTACK_MAP_HEADER + t.CInt(i) * DBGSTACK_ENTRY_SIZE - Cur: t.CUInt64T = ReadU64(Buf, Off) - if i > 0 and Cur <= Prev: - stdlib.free(t.CPtr(Buf)) - return MapFailEntry(Size, t.CInt(i), - "record rva not strictly ascending (breaks binary search)", Cur) - Prev = Cur - FO: t.CUInt32T = ReadU32(Buf, Off + DBGSTACK_ENTRY_FILEOFF) - FN: t.CUInt32T = ReadU32(Buf, Off + DBGSTACK_ENTRY_FUNCOFF) - if t.CUInt64T(FO) >= t.CUInt64T(SBytes): - stdlib.free(t.CPtr(Buf)) - return MapFailEntry(Size, t.CInt(i), "file_off outside the string pool", t.CUInt64T(FO)) - if FN != 0 and t.CUInt64T(FN) >= t.CUInt64T(SBytes): - stdlib.free(t.CPtr(Buf)) - return MapFailEntry(Size, t.CInt(i), - "func_off is neither 0 (no name) nor inside the string pool", - t.CUInt64T(FN)) - i += 1 - _State.MapBase = Buf - _State.EntryCount = Cnt - _State.StrBytes = SBytes - _State.StrBase = Buf + (DBGSTACK_MAP_HEADER + t.CInt(Cnt) * DBGSTACK_ENTRY_SIZE) - _State.MapOk = 1 - return 1 - - -# ============================================================ -# 单帧打印:地址 → rva → 源行 -# ============================================================ - -def EmitFrame(Index: int, Addr: t.CUInt64T, Base: t.CUInt64T, Scanned: int): - """按「命中 / 未命中 / 越出主模块」三分支打印第 Index 帧 - - Scanned:0 = 权威帧(当前 RIP / RBP 链逐帧读出的返回地址);非 0 = **扫描推断**帧 - (ScanStack 在栈上扫出的候选返回地址)。推断帧的行前缀用 `~#`(如 ` ~#3 0x…`) - 与权威帧的 ` #3` 明确区分——它们只是"栈上看起来像返回地址的值",可含误报, - 不得与权威帧混为一谈(见模块头第十二节)。 - - ⚠ 入口第一句的裸名 `_State` 判空**不能删**:本函数一上手就用 `_State.LineBuf` - (snprintf 目标)与 `_State.MapOk`;不求值裸名 `_State` 时这两处都会被 TPC 静默 - 降级为 null(snprintf(null) ⇒ 写零页崩溃 / `MapOk` 分支整段消失)。详见模块头 - 第十一节 A。本函数是 RBP 主路径的每帧出口,必须绝对安全。 - """ - if _State == None: - return - Tag: str = " #" - if Scanned != 0: - Tag = " ~#" - N: int = 0 - if Base == 0 or Addr < Base: - # 不在主模块映像内(DLL / 系统模块 / 已损坏的帧):只打地址,不做 rva 归属 - N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - "%s%d 0x%llx (external)\n", Tag, Index, Addr) - EmitLine(N) - return - Rva: t.CUInt64T = Addr - Base - if _State.MapOk == 0: - # 无映射(旁文件缺失/破损,或编译期未生成):打印可定位的降级信息(RVA) - N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - "%s%d 0x%llx +0x%llx (unknown)\n", Tag, Index, Addr, Rva) - EmitLine(N) - return - Hit: int = LookupRva(Rva) - if Hit < 0: - # 表已装载却查不到:必须说清"为什么查不到"(表空 / 目标 rva 小于表内最小 rva), - # 否则又退化成一句无信息的 "(unknown)"。只打一次,避免逐帧刷屏。 - if _State.MissReported == 0: - _State.MissReported = 1 - if _State.EntryCount == 0: - EmitText(" srcline map: rva not hit -- map is empty (0 entries)\n") - else: - First: t.CUInt64T = EntryRva(0) - Last: t.CUInt64T = EntryRva(t.CInt(_State.EntryCount) - 1) - M: int = stdio.snprintf( - _State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " srcline map: rva 0x%llx not hit (%u entries: first rva=0x%llx last rva=0x%llx;" - " target < first => writer RVA base mismatch)\n", - Rva, _State.EntryCount, First, Last) - EmitLine(M) - N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - "%s%d 0x%llx +0x%llx (unknown)\n", Tag, Index, Addr, Rva) - EmitLine(N) - return - Off: int = DBGSTACK_MAP_HEADER + Hit * DBGSTACK_ENTRY_SIZE - FileOff: t.CUInt32T = ReadU32(_State.MapBase, Off + DBGSTACK_ENTRY_FILEOFF) - Line: t.CUInt32T = ReadU32(_State.MapBase, Off + DBGSTACK_ENTRY_LINE) - FuncOff: t.CUInt32T = ReadU32(_State.MapBase, Off + DBGSTACK_ENTRY_FUNCOFF) - FileName: str = str(_State.StrBase + t.CInt(FileOff)) - FuncName: str = "unknown" - if FuncOff != 0: - FuncName = str(_State.StrBase + t.CInt(FuncOff)) - N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - "%s%d 0x%llx %s:%u (%s)\n", Tag, Index, Addr, FileName, Line, FuncName) - EmitLine(N) - - -# ============================================================ -# 两条回溯路径 -# ============================================================ - -def ReadMemCb(hProcess: HANDLE, Addr: ULONGLONG, Buf: VOIDPTR, Size: DWORD, - pRead: t.CPtr) -> BOOL: - """StackWalk64 的回调 1(PREAD_PROCESS_MEMORY_ROUTINE64)的**本模块替身**。 - - 为什么不用 kernel32!ReadProcessMemory 本体: - (1) 它只返回 BOOL,回调返回 0 会让 StackWalk64 **就地终止**(实测现象正是"0 帧"), - 而失败原因(GetLastError)会被静默丢掉 ⇒ 这里把首次失败的现场(地址/大小/错误码) - 记录下来,由首帧诊断紧邻打印(只记一次); - (2) dbghelp 的原型是 `DWORD nSize` + `LPDWORD lpNumberOfBytesRead`,而 kernel32 的 - ReadProcessMemory 是 `SIZE_T nSize` + `SIZE_T*`(x64 都是 8 字节)。把 dbghelp - 传进来的 4 字节 LPDWORD 直接当 SIZE_T* 交给 kernel32,成功时会被写穿 4 字节。 - 故这里用本地 `sizet` 承接读出字节数,再按 DWORD 写回 dbghelp 的缓冲。 - 传参方式:本模块内"函数名直接作 t.CPtr 实参"(同 SetConsoleCtrlHandler(CtrlHandler) - 的写法),不涉及未验证的跨模块函数名→函数指针转换。 - 线程安全:只读全局状态、写已预留缓冲;与调用方同线程,期间 LineBuf 空闲。 - """ - Read: sizet = 0 - R: BOOL = wm.ReadProcessMemory(hProcess, t.CPtr(Addr), Buf, sizet(Size), - t.CPtr(c.Addr(Read))) - if pRead != None: - Rd: rawptr[t.CUInt32T] = (rawptr[t.CUInt32T])(pRead) - Rd[0] = t.CUInt32T(Read) - if R == 0 and _State != None and _State.ReadMemFailed == 0: - # 只**记录**首次失败现场(地址/大小/错误码),不在回调里直接打印: - # 打印挪到首帧诊断里紧邻 "StackWalk64 first: ret=..." 之后(见 WalkViaDbgHelp), - # 让"回调返回 0 导致 0 帧"这条根因与首帧返回码同屏可判(见模块头第九节)。 - _State.ReadMemFailed = 1 - _State.ReadMemAddr = t.CUInt64T(Addr) - _State.ReadMemSize = t.CUInt32T(Size) - _State.ReadMemErr = GetLastError() - return R - - -def WalkViaDbgHelp(hProcess: HANDLE, hThread: HANDLE, CtxPtr: VOIDPTR, - Base: t.CUInt64T) -> int: - """dbghelp!StackWalk64 逐帧回溯;返回打印的帧数(0 = 未走出任何帧 ⇒ 调用方降级)。 - - 【本轮起定位为**可选加强**路径】主回溯路径是 WalkRbpChain(零 dbghelp 依赖)。 - 本函数只在"RBP 链拿不到更多帧"且调用方判定 dbghelp 当前可用(无 ASan 冲突)时被调, - 见模块头第十一节 B;若 dbghelp 与 ASan 冲突,调用方会打印 - `[diag] symwalk=disabled reason=dbghelp_conflict_with_asan` 并根本不进来。 - - STACKFRAME64 的字段访问一律经**显式子结构指针**(rawptr[dbg.ADDRESS64])完成, - 不做 fr.AddrPC.Offset 这种嵌套成员访问:三个 ADDRESS64 在 STACKFRAME64 内的偏移 - 是 0 / 16 / 32 / 48(= AddrPC / AddrReturn / AddrFrame / AddrStack,见 - includes/w32/win32dbghelp.vp 的布局注释)。 - 初始化:整块清零后预填 AddrPC/AddrFrame/AddrStack 的 Offset = 上下文的 Rip/Rbp/Rsp, - 三者 Mode = AddrModeFlat(3);MachineType 每次调用都传 IMAGE_FILE_MACHINE_AMD64。 - STACKFRAME64 没有"结构体尺寸/字段数"成员(那是 StackWalkEx 的 STACKFRAME_EX 才有的), - 调用方无需也无需能填——只需保证本仓的 sizeof 与 C 侧一致(0xF8)。 - 【首帧无条件取证】i == 0 时**无论成功失败都打印两行**(先 "[diag] StackWalk64 first: - ret=… addrpc=… fta=…",再 "[diag] StackWalk64 in: machine=… rip=… rsp=… rbp=…"——返回码、 - 传入的 machine/RIP/RSP/RBP、返回的 AddrPC 以及 SymFunctionTableAccess64(proc, AddrPC) 的 - 返回值)——这是判定"0 帧"根因的唯一现场证据(见模块头第四节取证段与第九节)。拆成两行 - 只为避免单条 143 字符长行在窄终端折行(字段一个不减)。i>0 时返回 0 是正常走到栈顶。 - 【return 全打印】每条 return 前都打 "[diag] walk: return reason=… frames=…"(见函数内注释)。 - 函数体整体包在 `if DBGSTACK_USE_DBGHELP != 0` 里:开关为 0 时该分支(唯一一处 - dbg.StackWalk64 调用所在)整体不被生成 ⇒ **不产生 dbghelp 链接期符号**。 - """ - # 【return 路径全覆盖】每条 return 前都打一行短诊断(见模块头第十节),使"walk 到底有没有 - # 被调用、走了哪条出口"在下一次按键后必然可见(配合 DumpMainThread 的 walk: enter/leave)。 - # reason 取值一览: - # null_state 全局状态 _State 缺失(安装未完成/已损毁) - # disabled 编译期开关 DBGSTACK_USE_DBGHELP == 0(产物未链接 dbghelp) - # null_frame_buf 帧缓冲缺失(Install 阶段 calloc 失败) - # null_context CONTEXT 缓冲指针为空(调用方传参异常) - # stackwalk_first_failed 首帧 StackWalk64 返回 0(根因由首帧取证行给出) - # stackwalk_failed 后续帧 StackWalk64 返回 0(正常走到栈顶) - # addrpc_zero StackWalk64 成功但返回 AddrPC == 0 - # frame_limit 达到 DBGSTACK_MAX_FRAMES 上限而结束 - # - # ⚠ 本条裸名 `_State` 判空**必须是函数第一句**:TPC 的属性访问层要先在函数级 - # `Gen.variables` 里登记 `_State`,才能解析它的结构体类型;否则本函数内**所有** - # `_State.<字段>` 都会被静默降级为常量 null(实测:memset 收到 i8* null 写零页崩溃、 - # StackWalk64 收到两个 null 回调)。详见模块头第十一节 A(附构建产物 IR 证据)。 - if _State == None: - EmitText(" [diag] walk: return reason=null_state frames=0\n") - return 0 - if DBGSTACK_USE_DBGHELP == 0: - EmitText(" [diag] walk: return reason=disabled (DBGSTACK_USE_DBGHELP=0) frames=0\n") - return 0 - # 帧缓冲经**局部量**兜底:局部变量这条通道最可靠(不受上面的属性访问问题影响), - # 把最危险的一行(memset)彻底钉死在"已判空"之上。 - FBuf: VOIDPTR = _State.FrameBuf - if FBuf == None: - EmitText(" [diag] walk: return reason=null_frame_buf frames=0\n") - return 0 - if CtxPtr == None: - EmitText(" [diag] walk: return reason=null_context frames=0\n") - return 0 - Frames: int = 0 - Reason: str = "frame_limit" - if DBGSTACK_USE_DBGHELP != 0: - string.memset(FBuf, 0, dbg.STACKFRAME64.__sizeof__()) - ctx: rawptr[wp.CONTEXT] = (rawptr[wp.CONTEXT])(CtxPtr) - Pc: rawptr[dbg.ADDRESS64] = (rawptr[dbg.ADDRESS64])(FBuf) - Fm: rawptr[dbg.ADDRESS64] = (rawptr[dbg.ADDRESS64])( - t.CPtr(t.CUInt64T(FBuf) + 32)) - St: rawptr[dbg.ADDRESS64] = (rawptr[dbg.ADDRESS64])( - t.CPtr(t.CUInt64T(FBuf) + 48)) - Pc.Offset = ctx.Rip - Pc.Mode = t.CUInt32T(dbg.ADDR_MODE_FLAT) - Fm.Offset = ctx.Rbp - Fm.Mode = t.CUInt32T(dbg.ADDR_MODE_FLAT) - St.Offset = ctx.Rsp - St.Mode = t.CUInt32T(dbg.ADDR_MODE_FLAT) - i: int = 0 - while i < DBGSTACK_MAX_FRAMES: - # 传入值必须在调用**之前**取(StackWalk64 会就地改写 STACKFRAME64) - InRip: t.CUInt64T = t.CUInt64T(Pc.Offset) - InRsp: t.CUInt64T = t.CUInt64T(St.Offset) - InRbp: t.CUInt64T = t.CUInt64T(Fm.Offset) - Ok: BOOL = dbg.StackWalk64(t.CUInt32T(dbg.IMAGE_FILE_MACHINE_AMD64), hProcess, - hThread, t.CPtr(FBuf), CtxPtr, - ReadMemCb, _State.FuncTableAddr, - _State.ModuleBaseAddr, None) - Cur: t.CUInt64T = t.CUInt64T(Pc.Offset) - if i == 0: - # 首帧取证(无条件,先于任何失败判断):返回码 + 返回 AddrPC - # + SymFunctionTableAccess64(AddrPC)。三者对照即可判定"0 帧"属于哪一类 - # (ModInited/读内存失败/上下文口径,见模块头第九节)。 - # ⚠ 原为**单条**长行(~143 字符),在窄终端会折行 ⇒ 拆成两条 <100 的短文, - # 字段不减(拆行只为避免折行造成的"显示残缺"假象)。 - Fta: t.CUInt64T = 0 - if Cur != 0: - Fta = t.CUInt64T(dbg.SymFunctionTableAccess64(hProcess, Cur)) - D0: int = stdio.snprintf( - _State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] StackWalk64 first: ret=%d addrpc=0x%llx fta=0x%llx\n", - t.CInt(Ok), Cur, Fta) - EmitLine(D0) - D1: int = stdio.snprintf( - _State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] StackWalk64 in: machine=0x%x rip=0x%llx rsp=0x%llx rbp=0x%llx\n", - t.CUInt32T(dbg.IMAGE_FILE_MACHINE_AMD64), InRip, InRsp, InRbp) - EmitLine(D1) - if Ok == 0: - if i == 0: - Reason = "stackwalk_first_failed" - else: - Reason = "stackwalk_failed" - D2: int = stdio.snprintf( - _State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] StackWalk64 returned 0 at frame %d (addrpc=0x%llx), stop\n", - i, Cur) - EmitLine(D2) - if _State.ReadMemFailed != 0: - # 紧邻首帧返回码打印读栈内存失败现场("0 帧"根因之一): - # 回调返回 0 ⇒ StackWalk64 就地终止,见模块头第九节 (2)。 - DR: int = stdio.snprintf( - _State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] readmem failed addr=0x%llx size=%u GetLastError=%u\n", - _State.ReadMemAddr, _State.ReadMemSize, _State.ReadMemErr) - EmitLine(DR) - break - if Cur == 0: - Reason = "addrpc_zero" - D3: int = stdio.snprintf( - _State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] StackWalk64 frame %d addrpc=0, stop\n", i) - EmitLine(D3) - break - EmitFrame(i, Cur, Base, 0) - i += 1 - Frames = i - # 统一的"离开 walk"诊断(覆盖上面所有 break/循环正常结束):reason 已在各出口置好 - N: int = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] walk: return reason=%s frames=%d\n", Reason, Frames) - EmitLine(N) - return Frames - - -def WalkRbpChain(Rip: t.CUInt64T, Rbp: t.CUInt64T, Base: t.CUInt64T) -> int: - """**主回溯路径**:打印 RIP,再沿 RBP 链读 [rbp](调用者 rbp)与 - [rbp+8](返回地址)逐帧回溯。 - - 【本轮改动:每一步无条件打**原始读数**(先取证,后决策)】 - 每一次循环迭代(**含被拒的那一步**,即所有 reject 分支与 return 之前)都先产出两条 - ASCII 短诊断(前缀 `[diag] rbp:`): - 行 1: `frame= rbp=0x… [rbp]=0x… [rbp+8]=0x…`(**读到的原始 8 字节值**) - 行 2: `frame= rpm_ok=<0/1> rpm_err= accept=<0/1> reject_reason=<…>` - 为什么要拆成两行:合并后单行 >140 字符(再加 EmitLine 的 `[dump#k]` 前缀)在窄终端 - 会折行,反而看不清——与既有首帧取证拆两行同一取舍,字段一个不减。 - 有了它们,"链为何在第 1 步就断"下一次按键即可直接判定,三种可能一眼可分: - (a) `rpm_ok=0`(读失败,err 给原因)⇒ 地址不可读; - (b) `rpm_ok=1` 但 `[rbp+8]` 本身就不是返回地址(远小于基址/越出映像) - ⇒ 该处 RBP 不是帧指针(帧指针未被保留,见模块头第十二节); - (c) 值像返回地址却被自洽校验档掉 ⇒ `reject_reason=not_growing/step_too_large`。 - `reject_reason` 取值(与代码里的赋值一一对应): - rbp_zero / not_aligned / rpm_failed / ret_zero / ret_out_of_module / - not_growing / step_too_large;本步被接受时为 `accept`(打印 accept=1)。 - 仅当本步**被接受**时才 EmitFrame(权威帧,`#` 前缀)——被拒的值一律只进诊断行, - 不再混进栈里(旧实现在校验之前就 EmitFrame,会把 `0x121fa` 这类垃圾值当成 - 一帧 external 打出来,误导读者)。 - - * **零 dbghelp 依赖** ⇒ 不受「ASan 已占用 DbgHelp」影响。 - * **读内存一律经 ReadProcessMemory**,绝不直接解引用野 rbp:诊断设施不允许把"卡死" - 变成"崩溃"。 - * 自洽校验(任一不满足即停,宁可少打也不把回溯带飞): - rbp 为 0 / 未 8 字节对齐 / 读内存失败 / 返回地址为 0 / - 返回地址不在主模块基址区间 [Base, Base+DBGSTACK_MODULE_MAX) 内 / - 调用者 rbp 未严格向高地址增长(`[rbp] > rbp`)/ - 单步跨度 > DBGSTACK_RBP_MAX_STEP / 达帧数上限。 - (Base == 0 时无法判定区间,跳过该条,不作误杀。) - - ⚠ 入口第一句的裸名 `_State` 判空**不能删**:本函数本轮起用 `_State.LineBuf` 组诊断行, - 不求值裸名 `_State` 时该字段会被 TPC 静默降级为常量 null(snprintf(null) 写零页崩溃)。 - 详见模块头第十一节 A。 - """ - if _State == None: - return 0 - EmitFrame(0, Rip, Base, 0) - Cur: t.CUInt64T = Rbp - i: int = 1 - HProc: HANDLE = wp.GetCurrentProcess() - Two: t.CArray[t.CUInt64T, 2] # [0]=[rbp](调用者 rbp) [1]=[rbp+8](返回地址) - while i < DBGSTACK_MAX_FRAMES: - # ---- 本步原始读数 + 判定(**先于任何 reject 早退**产出,见函数头)---- - Rd: sizet = 0 - RpmOk: t.CInt = 0 - RpmErr: ULONG = 0 - V0: t.CUInt64T = 0 # [rbp] 调用者 rbp - V1: t.CUInt64T = 0 # [rbp+8] 返回地址 - Reason: str = "accept" - Reject: t.CInt = 0 # 0 = 接受本步;1 = 拒绝(原因见 Reason) - if Cur == 0: - Reason = "rbp_zero" - Reject = 1 - elif (Cur & t.CUInt64T(7)) != 0: - Reason = "not_aligned" - Reject = 1 - else: - # 用 ReadProcessMemory 读 16 字节(地址无效/不可读时返回 0,**不触发访问冲突**); - # 直接解引用(如原实现的联合类型强转)在野 rbp 上会崩,主路径不能这么干。 - OkRead: BOOL = wm.ReadProcessMemory(HProc, t.CPtr(Cur), t.CPtr(c.Addr(Two)), - sizet(16), t.CPtr(c.Addr(Rd))) - if OkRead == 0 or t.CSizeT(Rd) < t.CSizeT(16): - # GetLastError 必须**紧邻** ReadProcessMemory 取,中途不得插入其它 API - RpmErr = GetLastError() - Reason = "rpm_failed" - Reject = 1 - else: - RpmOk = 1 - V0 = Two[0] - V1 = Two[1] - if V1 == 0: - Reason = "ret_zero" - Reject = 1 - elif Base != 0: - if V1 < Base or V1 >= Base + t.CUInt64T(DBGSTACK_MODULE_MAX): - # 值明显不是返回地址(远小于主模块基址 / 越出映像) - # ⇒ 该 RBP 不是帧指针,链在此断(见模块头第十二节) - Reason = "ret_out_of_module" - Reject = 1 - if Reject == 0: - if V0 <= Cur: - Reason = "not_growing" - Reject = 1 - elif V0 - Cur > t.CUInt64T(DBGSTACK_RBP_MAX_STEP): - Reason = "step_too_large" - Reject = 1 - D0: int = stdio.snprintf( - _State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] rbp: frame=%d rbp=0x%llx [rbp]=0x%llx [rbp+8]=0x%llx\n", - i, Cur, V0, V1) - EmitLine(D0) - Acc: t.CInt = 1 - if Reject != 0: - Acc = 0 - D1: int = stdio.snprintf( - _State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] rbp: frame=%d rpm_ok=%d rpm_err=%u accept=%d reject_reason=%s\n", - i, RpmOk, RpmErr, Acc, Reason) - EmitLine(D1) - if Reject != 0: - break - # 本步被接受 ⇒ 输出**权威帧**(`#` 前缀),再继续沿链上行 - EmitFrame(i, V1, Base, 0) - i += 1 - Cur = V0 - return i - - -def ScanStack(StartIndex: int, Rsp: t.CUInt64T, Base: t.CUInt64T) -> int: - """**第二兜底:栈扫描**(RBP 链之后的路径;**不依赖 RBP / 帧指针**)。 - - 为什么要它:若编译产物根本没保留帧指针(见模块头第十二节的调查结论), - `[rbp+8]` 读到的就不是返回地址,RBP 链在第 1 步即断(实测 `frames=2`)。 - 此时唯一能继续的办法是**直接扫已用的栈区**。 - - 算法(全部只读,绝不直接解引用主线程内存): - 1. 在 [Rsp, Rsp+DBGSTACK_SCAN_MAX_BYTES) 区间内按 8 字节步长遍历(**向高地址 = 调用者 - 方向**,故发现顺序天然按栈地址升序); - 2. 一次 ReadProcessMemory 读**一整块**(DBGSTACK_SCAN_CHUNK_BYTES=4 KiB,缓冲是 - 局部数组,回调线程零分配),而不是逐 8 字节读(把上千次系统调用压到几十次); - 任一块读失败即**停止并打印原因**(地址/GetLastError); - 3. 候选:字 W 落在主模块区间 [Base, Base+DBGSTACK_MODULE_MAX) 内,且**相邻 8 字节字** - 像栈地址——`[A-8]`(帧指针被保留时这里正是被压入的保存 rbp)或 `[A+8]` - (返回地址之后的调用者栈帧内容)任一落在 [Rsp, 区间上界) 即接受。取"任一"以同时 - 覆盖两种帧布局;放宽召回,误报由下面的 `~` 标注与数量上限控制。 - (块内首字的 `[A-8]` 与末字的 `[A+8]` 不可知,按 0 处理 ⇒ 仅少一次佐证。) - 4. 按**候选返回地址值**去重(同一个地址在栈上出现多次只留第一个),上限 - DBGSTACK_SCAN_MAX_HITS=64;`scan_candidates=` 打进诊断; - 5. 逐个用 EmitFrame(..., Scanned=1) 输出,帧前缀 `~` ⇒ 明确标注**扫描推断** - (可含误报,不与权威帧混淆)。 - 返回实际输出的推断帧数(0 = 没扫到)。 - - ⚠ 入口第一句的裸名 `_State` 判空**不能删**(同 WalkRbpChain:本函数用 _State.LineBuf)。 - """ - if _State == None: - return 0 - if Rsp == 0 or Base == 0: - # 无 RSP(未取到上下文)或无主模块基址(区间无从判定)⇒ 不扫,避免整片误报。 - # 与其它降级路径一致:**不静默**,给可 grep 的跳过原因。 - if Rsp == 0: - EmitText(" [diag] scan: skipped reason=no_rsp\n") - else: - EmitText(" [diag] scan: skipped reason=no_base\n") - return 0 - HProc: HANDLE = wp.GetCurrentProcess() - Upper: t.CUInt64T = Rsp + t.CUInt64T(DBGSTACK_SCAN_MAX_BYTES) - ModEnd: t.CUInt64T = Base + t.CUInt64T(DBGSTACK_MODULE_MAX) - Words: t.CArray[t.CUInt64T, DBGSTACK_SCAN_CHUNK_WORDS] # 4 KiB 块缓冲(局部,零分配) - Found: t.CArray[t.CUInt64T, DBGSTACK_SCAN_MAX_HITS] # 去重后的候选返回地址 - Hits: int = 0 - Prev: t.CUInt64T = 0 # [A-8]:块内首字的前一条未知,置 0(只少一次佐证) - Addr: t.CUInt64T = Rsp - Stop: str = "reached_scan_limit" - D0: int = stdio.snprintf( - _State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] scan: rsp=0x%llx upper=0x%llx chunk=%d hits_max=%d\n", - Rsp, Upper, DBGSTACK_SCAN_CHUNK_BYTES, DBGSTACK_SCAN_MAX_HITS) - EmitLine(D0) - while Addr < Upper: - Rd: sizet = 0 - OkRead: BOOL = wm.ReadProcessMemory(HProc, t.CPtr(Addr), t.CPtr(c.Addr(Words)), - sizet(DBGSTACK_SCAN_CHUNK_BYTES), - t.CPtr(c.Addr(Rd))) - N: int = t.CInt(t.CSizeT(Rd) / t.CSizeT(8)) # 本块实到的 u64 字数 - if OkRead == 0 or N <= 0: - Er: ULONG = GetLastError() - D1: int = stdio.snprintf( - _State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] scan: block read failed addr=0x%llx err=%u, stop\n", Addr, Er) - EmitLine(D1) - Stop = "block_read_failed" - break - k: int = 0 - while k < N: - W: t.CUInt64T = Words[t.CInt(k)] - if W >= Base and W < ModEnd: - # 相邻 8 字节字(A-8 或 A+8)任一像栈地址 ⇒ 接受 W 为候选返回地址 - NearStack: t.CInt = 0 - if Prev >= Rsp and Prev < Upper: - NearStack = 1 - if k + 1 < N: - Q: t.CUInt64T = Words[t.CInt(k + 1)] - if Q >= Rsp and Q < Upper: - NearStack = 1 - if NearStack != 0 and Hits < DBGSTACK_SCAN_MAX_HITS: - # 去重(按候选地址值;已按栈地址升序发现 ⇒ 保持原序) - Dup: t.CInt = 0 - t_i: int = 0 - while t_i < Hits: - if Found[t_i] == W: - Dup = 1 - break - t_i += 1 - if Dup == 0: - Found[Hits] = W - Hits += 1 - Prev = W - k += 1 - Addr = Addr + t.CUInt64T(DBGSTACK_SCAN_CHUNK_BYTES) - D2: int = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] scan: scan_candidates=%d stop=%s\n", Hits, Stop) - EmitLine(D2) - # 输出推断帧:总数不得越过全局帧上限(权威帧已占 StartIndex 个) - Lim: int = DBGSTACK_MAX_FRAMES - StartIndex - j: int = 0 - while j < Hits: - if j >= Lim: - break - EmitFrame(StartIndex + j, Found[j], Base, 1) - j += 1 - return j - - -# ============================================================ -# 转储主线程 -# ============================================================ - -def WriteDumpHeaderFile(IsBreak: int): - """向转储文件追加一段**分隔头**(只写文件,不写控制台)。 - - 目的:同一文件是**多次**追加的(每次按键一段),故每段前放一个可 grep 的小分隔头: - 本地时间戳 / pid / 触发键 / 主线程 id / Seq。此处用中文标签——文件是 UTF-8,不受控制台 - 代码页影响(这正是"文件版保留中文、控制台版全 ASCII"两种受众的分工点); - 控制台文本见 EmitLine,一律 ASCII。 - """ - if _State == None or _DumpFile == None or _DumpFile == INVALID_HANDLE_VALUE: - return - St: SYSTEMTIME = SYSTEMTIME() - GetLocalTime(t.CPtr(c.Addr(St))) - Trig: str = "Ctrl+C" - if IsBreak != 0: - Trig = "Ctrl+Break" - N: int = stdio.snprintf( - _State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - "\n===== 栈转储 %04u-%02u-%02u %02u:%02u:%02u.%03u pid=%u 触发=%s 主线程id=%u Seq=%d =====\n", - t.CUInt32T(St.wYear), t.CUInt32T(St.wMonth), t.CUInt32T(St.wDay), - t.CUInt32T(St.wHour), t.CUInt32T(St.wMinute), t.CUInt32T(St.wSecond), - t.CUInt32T(St.wMilliseconds), wp.GetCurrentProcessId(), - Trig, _State.MainThreadId, _State.Seq) - if N <= 0: - return - if N > DBGSTACK_LINE_CAP - 1: - N = DBGSTACK_LINE_CAP - 1 - Wr: ULONG = 0 - wf.WriteFile(_DumpFile, _State.LineBuf, ULONG(N), t.CPtr(c.Addr(Wr)), None) - - -def DumpMainThread(IsBreak: int) -> int: - """挂起主线程 → 取 CONTEXT → 回溯 → 打印 → 恢复主线程。返回打印帧数(0=未取到)。 - - 关键不变量:**SuspendThread 成功之后,任何返回路径都必须 ResumeThread + CloseHandle**, - 否则主线程永久冻结(比卡死更糟)。 - 重入不变量:入口用 AtomicTryEnterDump(内联汇编 `lock cmpxchg`)原子"检查并占用" - _DumpLock;所有返回路径都要 AtomicLeaveDump 释放;抢占失败的调用立刻返回、 - **不输出任何内容**(不挂起主线程、不碰 LineBuf)。 - 取证不变量:banner 之后**无条件**打印一组前置诊断(见下),先于任何失败/降级分支。 - walk 取证:进主路径 gate 前打 "[diag] walk: enter …"、gate 后打 "[diag] walk: leave frames=…" - (两条都**无条件**);两行都在 ⇒ 必已进入 WalkViaDbgHelp,否则"walk: enter 缺失"即指明 - 在本函数更早的 fallback 处(OpenThread/SuspendThread/GetThreadContext)就已 return(见第十节)。 - 输出形状(可 grep):每行由 EmitLine 统一加 " [dump#] " 前缀(k 全局递增,判别并发/折行)。 - """ - global _DumpOut - if _State == None or _State.LineBuf == None: - return 0 - # 重入保护(见模块头第一节):SetConsoleCtrlHandler 的回调由系统在**新线程**上执行, - # 连续按键会并发进入本函数,而它们共用同一个 LineBuf / 输出句柄 ⇒ 输出互相撕裂 - # (实测出现过"降级:"行重复且交织)。原子"检查并占用"消掉普通标志的读-改-写竞态窗口。 - EnterOld: t.CInt = AtomicTryEnterDump() - if EnterOld != 0: - return 0 - _State.Dumping = 1 - _State.Seq = _State.Seq + 1 - # 输出句柄在转储**开始**时一次性确定并锁定:此后全程只写 _DumpOut,不在中途重取。 - _DumpOut = _State.OutHandle - # 单次转储范围内重置"防刷屏"标志:让每一次转储都自带完整诊断,不因上一次已打印而沉默。 - _State.ReadMemFailed = 0 - _State.MissReported = 0 - EmitText("=== TPC stack dump (Ctrl+Break: keep running / Ctrl+C: exit) ===\n") - # 文件侧分隔头(仅文件:时间戳/pid/触发键/主线程 id;见 WriteDumpHeaderFile) - WriteDumpHeaderFile(t.CInt(IsBreak)) - if _DumpOut == None or _DumpOut == INVALID_HANDLE_VALUE: - EmitText(" WARN: no usable console handle (stderr/stdout invalid), console output discarded\n") - # ---- 前置诊断(无条件;先取证,再决策):不挂起主线程即可取到的现场状态 ---- - # 这些行必须出现在任何失败/降级分支之前,才能回答"0 帧到底是哪一种"(见模块头第九节)。 - # 标签与字段全 ASCII:任何代码页下都可读、可 grep(详见 WalkViaDbgHelp 首帧处的说明)。 - MBase: t.CUInt64T = t.CUInt64T(GetModuleHandleA(None)) - ModBaseRet: t.CUInt64T = 0 - if DBGSTACK_USE_DBGHELP != 0: - if _State.SymReady != 0: - ModBaseRet = t.CUInt64T(dbg.SymGetModuleBase64(wp.GetCurrentProcess(), MBase)) - FB: t.CInt = 1 if _State.FrameBuf != None else 0 - FT: t.CInt = 1 if _State.FuncTableAddr != None else 0 - MB: t.CInt = 1 if _State.ModuleBaseAddr != None else 0 - N: int = stdio.snprintf( - _State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] Seq=%d cbthread=%u mainthread=%u dump_enter_prev=%d " - "mapok=%d symready=%d modinited=%d\n", - _State.Seq, wp.GetCurrentThreadId(), _State.MainThreadId, EnterOld, - _State.MapOk, _State.SymReady, _State.ModInited) - EmitLine(N) - N = stdio.snprintf( - _State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] modbase=0x%llx SymGetModuleBase64(main)=0x%llx\n", - MBase, ModBaseRet) - EmitLine(N) - N = stdio.snprintf( - _State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] frame_buf=%d cb_fta=%d cb_mba=%d\n", - FB, FT, MB) - EmitLine(N) - Trigger: str = "Ctrl+C" - AfterTag: str = "exit" - if IsBreak != 0: - Trigger = "Ctrl+Break" - AfterTag = "continue" - N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " Trigger=%s after_dump=%s mainthread=%u cbthread=%u\n", - Trigger, AfterTag, _State.MainThreadId, wp.GetCurrentThreadId()) - EmitLine(N) - hThread: HANDLE = wp.OpenThread( - ULONG(wp.THREAD_SUSPEND_RESUME | wp.THREAD_GET_CONTEXT), 0, _State.MainThreadId) - if hThread == None or hThread == INVALID_HANDLE_VALUE: - N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " fallback: OpenThread(main) failed (GetLastError=%u), cannot dump stack\n", - GetLastError()) - EmitLine(N) - AtomicLeaveDump() - return 0 - if wp.SuspendThread(hThread) == ULONG(DBGSTACK_SUSPEND_FAILED): - N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " fallback: SuspendThread(main) failed (GetLastError=%u), cannot dump stack\n", - GetLastError()) - EmitLine(N) - CloseHandle(hThread) - AtomicLeaveDump() - return 0 - # CONTEXT:先清零再写 ContextFlags(x64 必须含 CONTEXT_AMD64 位) - string.memset(_State.ContextBuf, 0, wp.CONTEXT.__sizeof__()) - ctx: rawptr[wp.CONTEXT] = (rawptr[wp.CONTEXT])(_State.ContextBuf) - ctx.ContextFlags = t.CUInt32T(wp.CONTEXT_FULL) - if wp.GetThreadContext(hThread, t.CPtr(_State.ContextBuf)) == 0: - N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " fallback: GetThreadContext failed (GetLastError=%u), cannot dump stack\n", - GetLastError()) - EmitLine(N) - wp.ResumeThread(hThread) - CloseHandle(hThread) - AtomicLeaveDump() - return 0 - Base: t.CUInt64T = t.CUInt64T(GetModuleHandleA(None)) - Rip: t.CUInt64T = t.CUInt64T(ctx.Rip) - Rsp: t.CUInt64T = t.CUInt64T(ctx.Rsp) - Rbp: t.CUInt64T = t.CUInt64T(ctx.Rbp) - # ---- 问题 B:ASan 与 dbghelp 互斥探测 + dbghelp 可用性判定 ---- - # ASan 运行时会自己初始化 DbgHelp 做符号化,同一进程不允许两种不兼容用法(实测 ASan - # 侧打印 "Failed to initialize DbgHelp!")。故只要探测到 ASan 运行时,就**不用 dbghelp** - # 作主路径,并把原因打出来(硬要求:不得静默 0 帧)。两个命名都探:本仓 --asan 借用的是 - # clang64 的 `-l:libclang_rt.asan_dynamic-x86_64.dll.a`(Phase2Translator.py:4402), - # 官方 LLVM 发行版的运行时 DLL 名则是 `clang_rt.asan_dynamic-x86_64.dll`。 - AsanPresent: t.CInt = 0 - if GetModuleHandleA("clang_rt.asan_dynamic-x86_64.dll") != None: - AsanPresent = 1 - if GetModuleHandleA("libclang_rt.asan_dynamic-x86_64.dll") != None: - AsanPresent = 1 - DbgUsable: t.CInt = 0 - if DBGSTACK_USE_DBGHELP != 0 and _State.SymReady != 0 \ - and _State.FuncTableAddr != None and _State.ModuleBaseAddr != None \ - and _State.FrameBuf != None: - DbgUsable = 1 - if AsanPresent != 0: - # ASan 在场 ⇒ 即便 SymInitialize 报成功,符号化行为也不可靠(且会把 ASan 的符号化 - # 搞坏)。统一按"冲突"处理:dbghelp 降级为不可用,主路径走 RBP 链。 - DbgUsable = 0 - N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] symwalk: use_dbghelp=%d symready=%d asan=%d dbg_usable=%d\n", - DBGSTACK_USE_DBGHELP, _State.SymReady, AsanPresent, DbgUsable) - EmitLine(N) - if DbgUsable == 0: - # 每种降级都给**明确原因**(不静默 0 帧);顺序按"最具体优先"。 - if DBGSTACK_USE_DBGHELP == 0: - EmitText(" [diag] symwalk=disabled reason=compile_time_switch" - " (DBGSTACK_USE_DBGHELP=0)\n") - elif AsanPresent != 0: - EmitText(" [diag] symwalk=disabled reason=dbghelp_conflict_with_asan\n") - elif _State.SymReady == 0: - EmitText(" [diag] symwalk=disabled reason=syminitialize_failed\n") - elif _State.FuncTableAddr == None or _State.ModuleBaseAddr == None: - EmitText(" [diag] symwalk=disabled reason=callback_missing\n") - else: - EmitText(" [diag] symwalk=disabled reason=null_frame_buf\n") - MapTag: str = "none (see [dbgstack] warnings above; addresses only)" - if _State.MapOk != 0: - MapTag = "loaded" - # 本轮起主路径 = RBP 链(零 dbghelp 依赖);dbghelp 仅在 RBP 链拿不到更多帧时作可选加强。 - SymTag: str = "rbp-chain primary (dbghelp not required)" - if DbgUsable != 0: - SymTag = "rbp-chain primary, dbghelp optional" - N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " exe base=0x%llx RIP=0x%llx RSP=0x%llx RBP=0x%llx\n", - Base, Rip, Rsp, Rbp) - EmitLine(N) - N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " srcline_map=%s symwalk=%s\n", MapTag, SymTag) - EmitLine(N) - Frames: int = 0 - # 【A:无条件「进入 walk」取证】本行先于所有门控/分支产出。若下一次按键后连本行都没有 ⇒ - # 说明 DumpMainThread 在本行之前就返回了(OpenThread/SuspendThread/GetThreadContext 三处 - # fallback,各自已有 "fallback: ... cannot dump stack" 行),据此定位跳过的分支。 - N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] walk: enter use_dbghelp=%d symready=%d modinited=%d " - "frame_buf=%d mapok=%d asan=%d\n", - DBGSTACK_USE_DBGHELP, _State.SymReady, _State.ModInited, FB, _State.MapOk, - AsanPresent) - EmitLine(N) - if DbgUsable == 0: - # 保留既有约定:转 RBP 链之前**永远**有一条 [diag] StackWalk64 first: 可判(此时是 - # not-called,附前提字段;若本次真的会调 StackWalk64,则由 WalkViaDbgHelp 内部产出)。 - DNC: int = stdio.snprintf( - _State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] StackWalk64 first: ret=not-called symready=%d cb_fta=%d cb_mba=%d frame_buf=%d\n", - _State.SymReady, FT, MB, FB) - EmitLine(DNC) - # ---- 主路径:RBP 链(**不依赖 dbghelp**;--ctrlc-stack 已给 llc 加 -frame-pointer=all)---- - EmitText(" [diag] walk: primary=rbp_chain\n") - Frames = WalkRbpChain(Rip, Rbp, Base) - N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] walk: rbp_chain frames=%d\n", Frames) - EmitLine(N) - # ---- 第二兜底:栈扫描(**不依赖 RBP / 帧指针**;见 ScanStack)---- - # 仅当 RBP 链只拿到当前 RIP(<=1 帧,即链在第 1 步就断)时才启用——被测函数的 RBP - # 不是帧指针时,`[rbp+8]` 不是返回地址,只有直接扫栈才能继续。扫描帧以 `~` 前缀 - # 标注为「扫描推断」,不与权威帧混淆(见模块头第十二节)。 - if Frames <= 1: - EmitText(" [diag] walk: fallback=stack_scan\n") - Sc: int = ScanStack(Frames, Rsp, Base) - Frames = Frames + Sc - N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] walk: scan frames=%d total=%d\n", Sc, Frames) - EmitLine(N) - # ---- 可选加强:RBP 链只拿到当前 RIP(<=1 帧)且 dbghelp 当前可用时,才补一次 dbghelp ---- - # 外层常量条件单独成 if(便于编译期折叠):置 0 时本段整体不生成 ⇒ 连对 WalkViaDbgHelp - # 的调用都不出现,本模块不留任何 dbghelp 链接期符号(与 DBGSTACK_USE_DBGHELP 注释一致)。 - if DBGSTACK_USE_DBGHELP != 0: - if Frames <= 1 and DbgUsable != 0: - EmitText(" [diag] walk: dbghelp enhancement engaged\n") - F2: int = WalkViaDbgHelp(wp.GetCurrentProcess(), hThread, _State.ContextBuf, Base) - if F2 > Frames: - Frames = F2 - # 【A:无条件「离开 walk」取证】与 walk: enter 成对;两行都在 ⇒ walk 一定进了 RBP 主路径 - # (若 dbghelp 被启用,其内部还会再产出一行 walk: return reason=…)。 - N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " [diag] walk: leave frames=%d mapok=%d\n", Frames, _State.MapOk) - EmitLine(N) - if Frames == 0: - # 走到这里说明 RBP 链、栈扫描与(可能的)dbghelp 加强都没拿到帧:给一句可 grep 的结论。 - EmitText(" [diag] walk: no frames from any path (rbp_chain/stack_scan/dbghelp)\n") - # 拆成两次 EmitLine:使 frames= 与结束行各自带上 [dump#k] 前缀(每行都可独立编号/定位) - N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - " frames=%d\n", Frames) - EmitLine(N) - EmitText("=== stack dump end ===\n") - wp.ResumeThread(hThread) - CloseHandle(hThread) - AtomicLeaveDump() - return Frames - - -# ============================================================ -# 控制事件处理例程 / 安装 -# ============================================================ - -def CtrlHandler(CtrlType: ULONG) -> BOOL: - """SetConsoleCtrlHandler 回调(系统在**新线程**上调用)。 - - Ctrl+C → 转储后返回 FALSE(交还默认处理 ⇒ 进程终止); - Ctrl+Break → 转储后返回 TRUE(已处理 ⇒ 进程继续); - 其它控制事件 → 不转储、返回 FALSE(不干预系统默认行为)。 - 线程安全:只读全局状态、写已预留缓冲,不分配内存、不碰 MemBuddy 池。 - """ - if _State == None or _State.Installed == 0: - return 0 - IsBreak: t.CInt = 0 - if CtrlType == ULONG(wc.CTRL_BREAK_EVENT): - IsBreak = 1 - elif CtrlType != ULONG(wc.CTRL_C_EVENT): - return 0 - DumpMainThread(t.CInt(IsBreak)) - if IsBreak != 0: - return 1 - return 0 - - -def NoteError(Text: str, Code: ULONG): - """安装期/降级诊断:'(GetLastError=%u)' 一行写到转储同一去向(stderr 优先)""" - if _State == None or _State.LineBuf == None: - return - N: int = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - "%s(GetLastError=%u)\n", Text, Code) - EmitLine(N) - - -def Install() -> int: - """注册控制事件处理器。返回 1=已注册(含降级可用),0=未安装。 - - 0 有两种情形,都有明确诊断(旁文件不存在是**预期**的关闭状态,静默返回): - * .stackmap 不存在(未启用 --ctrlc-stack)→ 静默返回 0,零副作用; - * 旁文件存在但打开/读取/解析失败,或关键缓冲分配失败 → 打印警告后返回 0。 - 幂等:一旦尝试过(无论成败)就不再重复分配/注册,直接返回上次结果——避免重试 - 泄漏状态对象与缓冲。成功路径的提示写 stderr,不污染调用方的 stdout 约定。 - """ - global _State, _DumpOut, _HandlerReg, _DumpFile - if _State != None: - return t.CInt(_State.Installed) - # ① + ".stackmap"(此时 _State 尚未建立,失败提示走 EmitEarly: - # 与转储同一去向 stderr,不再用 stdio.printf ⇒ stdout/stderr 不会双流交错) - Pb: bytes = stdlib.malloc(DBGSTACK_PATH_CAP) - if Pb == None: - EmitEarly("[dbgstack] WARN: path buffer alloc failed, ctrl handler not installed\n") - return 0 - Plen: ULONG = wf.GetModuleFileNameA(None, Pb, ULONG(DBGSTACK_PATH_CAP)) - if Plen == 0 or t.CSizeT(Plen) >= t.CSizeT(DBGSTACK_PATH_CAP): - stdlib.free(t.CPtr(Pb)) - EmitEarly("[dbgstack] WARN: GetModuleFileNameA failed, ctrl handler not installed\n") - return 0 - if t.CSizeT(Plen) + 16 > t.CSizeT(DBGSTACK_PATH_CAP): - # 余量 16 字节同时覆盖 ".stackmap"(10) 与 ".stackdump.txt"(16),见 DBGSTACK_FILE_APPEND_DATA 注释。 - stdlib.free(t.CPtr(Pb)) - EmitEarly("[dbgstack] WARN: exe path too long to append suffix safely, ctrl handler not installed\n") - return 0 - string.strcpy(str(Pb + t.CInt(Plen)), ".stackmap") - # ② 旁文件存在性 = 总开关(不存在 ⇒ 立刻返回,零副作用) - hFile: HANDLE = wf.CreateFileA(Pb, wf.GENERIC_READ, wf.FILE_SHARE_READ, t.CPtr(0), - wf.OPEN_EXISTING, wf.FILE_ATTRIBUTE_NORMAL, t.CPtr(0)) - if hFile == INVALID_HANDLE_VALUE: - Err0: ULONG = GetLastError() - stdlib.free(t.CPtr(Pb)) - if Err0 != ULONG(ERROR_FILE_NOT_FOUND): - EmitEarlyCode("[dbgstack] WARN: cannot open .stackmap, ctrl handler not installed ", Err0) - return 0 - # ③ 状态对象 + 全部缓冲(回调线程零分配) - _State = stdlib.malloc(DbgStackState.__sizeof__()) - if _State == None: - stdlib.free(t.CPtr(Pb)) - CloseHandle(hFile) - EmitEarly("[dbgstack] WARN: state alloc failed, ctrl handler not installed\n") - return 0 - string.memset(_State, 0, DbgStackState.__sizeof__()) - _State.LineBuf = stdlib.malloc(DBGSTACK_LINE_CAP) - _State.ContextBuf = t.CPtr(stdlib.calloc(1, wp.CONTEXT.__sizeof__())) - _State.FrameBuf = t.CPtr(stdlib.calloc(1, dbg.STACKFRAME64.__sizeof__())) - _State.OutHandle = wf.GetStdHandle(wf.STD_ERROR_HANDLE) - if _State.OutHandle == None or _State.OutHandle == INVALID_HANDLE_VALUE: - _State.OutHandle = wf.GetStdHandle(wf.STD_OUTPUT_HANDLE) - # 输出句柄在此一次性确定并锁定到 _DumpOut:转储期只认它,不再重取。 - _DumpOut = _State.OutHandle - _State.MainThreadId = wp.GetCurrentThreadId() - if _State.LineBuf == None or _State.ContextBuf == None or _State.FrameBuf == None: - # 安装期就把"哪个缓冲没分配上"变成显式诊断(而不是等按了键、在回调线程里才发现): - # 三个缓冲逐个点名;任何一项失败都让 Install 返回 0(= 未安装,走零副作用降级), - # 因为缺任一缓冲都无法安全转储。此处 _State 已建立,EmitEarly 会自动转发到 - # EmitText(同一输出通道 + 行号前缀);_State.LineBuf 为 None 时 EmitEarly 自行 - # 退化为直接 WriteFile,不会因缺缓冲而丢提示。 - stdlib.free(t.CPtr(Pb)) - CloseHandle(hFile) - if _State.LineBuf == None: - EmitEarly("[dbgstack] WARN: LineBuf alloc failed, ctrl handler not installed\n") - elif _State.ContextBuf == None: - EmitEarly("[dbgstack] WARN: ContextBuf alloc failed, ctrl handler not installed\n") - elif _State.FrameBuf == None: - EmitEarly("[dbgstack] WARN: FrameBuf alloc failed, ctrl handler not installed\n") - else: - EmitEarly("[dbgstack] WARN: runtime buffer alloc failed, ctrl handler not installed\n") - return 0 - # ③b 打开转储文件 .stackdump.txt(追加写:OPEN_ALWAYS 不存在则创建 + FILE_APPEND_DATA - # 使每次 WriteFile 都追加到末尾)。放在旁文件装载之前 ⇒ 装载期诊断也一并落盘。 - # 写的是 UTF-8 字节原样,不受控制台代码页影响。(文件本身仅供查看,无需 CloseHandle - # 之外的清理:进程存活期间常驻,供每次转储持续追加。) - string.strcpy(str(Pb + t.CInt(Plen)), ".stackdump.txt") - _DumpFile = wf.CreateFileA(Pb, DBGSTACK_FILE_APPEND_DATA, - wf.FILE_SHARE_READ | wf.FILE_SHARE_WRITE, t.CPtr(0), - wf.OPEN_ALWAYS, wf.FILE_ATTRIBUTE_NORMAL, t.CPtr(0)) - if _DumpFile == INVALID_HANDLE_VALUE: - _DumpFile = None - EmitText("[dbgstack] WARN: cannot open stack dump file, console-only output\n") - else: - DN: int = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), - "[dbgstack] stack dump also written to: %s\n", str(Pb)) - EmitLine(DN) - # ④ 旁文件装载(失败只禁用映射,仍安装 handler 以便至少拿到地址栈) - # 校验失败的具体原因由 LoadMapFile 自己打印(MapFailPlain/Hdr/Entry),此处不再兜一句笼统警告。 - Sz: LARGE_INTEGER = LARGE_INTEGER() - if wf.GetFileSizeEx(hFile, t.CPtr(c.Addr(Sz))) == 0: - NoteError("[dbgstack] WARN: GetFileSizeEx(.stackmap) failed, addresses only ", GetLastError()) - else: - LoadMapFile(hFile, t.CInt64T(Sz.QuadPart)) - CloseHandle(hFile) - stdlib.free(t.CPtr(Pb)) - # ⑤ dbghelp 初始化(**必须在此完成**:Install 在进程启动期的主线程上跑,而 Ctrl+C/Break - # 回调在新线程上方可执行——dbghelp 的模块枚举在回调线程里首次调用可能不可用) - # + 两个跨模块回调地址(动态解析;不依赖跨模块函数名 → 函数指针的隐式转换) - if DBGSTACK_USE_DBGHELP != 0: - # SymSetOptions 必须在 SymInitialize 之前:DEFERRED_LOADS 让下面的模块枚举 - # 不会真去加载符号/PDB,NO_PROMPTS 避免弹交互对话框卡住进程启动。 - dbg.SymSetOptions(t.CUInt32T(dbg.SYMOPT_DEFERRED_LOADS | dbg.SYMOPT_UNDNAME - | dbg.SYMOPT_NO_PROMPTS)) - # fInvadeProcess **必须为 1(TRUE)**:它让 dbghelp 枚举已加载模块填进自己的模块表。 - # 置 0 时模块表为空 ⇒ SymFunctionTableAccess64 取不到 RUNTIME_FUNCTION(返回 NULL)、 - # SymGetModuleBase64 返回 0 ⇒ StackWalk64 **首次调用即失败、一帧都走不出** - # (实测现象正是"符号回溯=可用 / RIP-RSP-RBP 正常,却 0 帧")。这是缺陷 2 的真因。 - if dbg.SymInitialize(wp.GetCurrentProcess(), None, 1) != 0: - _State.SymReady = 1 - else: - NoteError("[dbgstack] WARN: SymInitialize failed (ASan also uses DbgHelp" - " and conflicts with us; RBP chain is the primary path anyway) ", GetLastError()) - # 问题 B:ASan 运行时会自己初始化 DbgHelp 做符号化,与本模块的 SymInitialize 互斥。 - # 安装期就探测一次并显式告警(转储时 DumpMainThread 会据此打印 - # symwalk=disabled reason=dbghelp_conflict_with_asan,并改以 RBP 链为主路径)。 - HAsan: HANDLE = GetModuleHandleA("clang_rt.asan_dynamic-x86_64.dll") - if HAsan == None: - HAsan = GetModuleHandleA("libclang_rt.asan_dynamic-x86_64.dll") - if HAsan != None: - EmitText("[dbgstack] WARN: ASan runtime detected, it also initializes DbgHelp;" - " dbghelp symwalk disabled, RBP chain will be the primary path\n") - Hdbg: HANDLE = GetModuleHandleA("dbghelp.dll") - if Hdbg != None and Hdbg != INVALID_HANDLE_VALUE: - _State.FuncTableAddr = GetProcAddress(Hdbg, "SymFunctionTableAccess64") - _State.ModuleBaseAddr = GetProcAddress(Hdbg, "SymGetModuleBase64") - if _State.FuncTableAddr == None or _State.ModuleBaseAddr == None: - EmitText("[dbgstack] WARN: StackWalk64 callback addresses incomplete," - " dbghelp enhancement unavailable (RBP chain is the primary path)\n") - # 自检:dbghelp 的模块表里是否真的能找到主模块(SymGetModuleBase64 返回非 0)。 - # 置 0 时 StackWalk64 必然 0 帧;结果同时进 DumpMainThread 的头部诊断行 - # (`[diag] StackWalk64 first:` / `symwalk=`)与 dbg_usable 判定,不必等按了键才发现。 - if _State.SymReady != 0: - if dbg.SymGetModuleBase64(wp.GetCurrentProcess(), - t.CUInt64T(GetModuleHandleA(None))) != 0: - _State.ModInited = 1 - else: - EmitText("[dbgstack] WARN: dbghelp module table lacks main module (SymGetModuleBase64=0)," - " StackWalk64 will produce 0 frames\n") - # ⑥ 注册(必须最后一步:此前的一切状态都已就绪)。用 _HandlerReg 守卫"只注册一次": - # 同一按键被触发多次回调会重复执行同一转储(输出重复),这里彻底杜绝重复注册。 - if _HandlerReg == 0: - if wc.SetConsoleCtrlHandler(CtrlHandler, 1) == 0: - NoteError("[dbgstack] WARN: SetConsoleCtrlHandler failed ", GetLastError()) - return 0 - _HandlerReg = 1 - _State.Installed = 1 - EmitText("[dbgstack] ready: Ctrl+C prints stack then exits, Ctrl+Break prints then continues\n") - return 1 diff --git a/gzip.vp b/gzip.vp index ea76b16..554c34a 100644 --- a/gzip.vp +++ b/gzip.vp @@ -67,7 +67,7 @@ class GzipFile: self.err = -5 # 读取失败或被截断 return # gzip 魔数校验 - ub: uchar*= buf + ub: uchar* = buf if ub[0] != 0x1F or ub[1] != 0x8B or ub[2] != 8: stdlib.free(buf) self.err = -6 diff --git a/hashlib/__md5.vp b/hashlib/__md5.vp index cdf1216..4cef426 100644 --- a/hashlib/__md5.vp +++ b/hashlib/__md5.vp @@ -17,7 +17,7 @@ define md5_I(x: uint32, y: uint32, z: uint32) -> uint32: return y ^ (x | ~z) define md5_rotl(x: uint32, n: int) -> uint32: return (x << n) | (x >> (32 - n)) # MD5 constant table -md5_T: t.CArray[uint32, 64] = [ +md5_T: const [64]uint32 = [ 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, @@ -28,7 +28,7 @@ md5_T: t.CArray[uint32, 64] = [ 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391 ] -md5_S: t.CArray[int, 64] = [ +md5_S: const [64]int = [ 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, @@ -38,12 +38,12 @@ md5_S: t.CArray[int, 64] = [ # MD5 Context class md5: - state: t.CArray[uint32, 4] = [ + state: [4]uint32 = [ 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476 ] # 4 32-bit state registers count: uint64 = 0 # processed bits - buf: t.CArray[uint8, MD5_BLOCK_LEN] # Group buffer + buf: [MD5_BLOCK_LEN]uint8 # Initialize MD5 context def __init__(self): string.memset(self.buf, 0, MD5_BLOCK_LEN) @@ -53,7 +53,7 @@ class md5: b: uint32 = self.state[1] c: uint32 = self.state[2] d: uint32 = self.state[3] - w: t.CArray[uint32, 16] + w: [16]uint32 # Byte order 32-bit words for i in range(16): w[i] = ((uint32(block[i * 4 + 0]) << 0) | @@ -85,7 +85,7 @@ class md5: self.state[3] += d # Batch input data def update(self, s: str): - data: rawptr[uint8] = uint8(s, t.CPtr) + data: uint8* = uint8*(s) length: sizet = len(s) idx: sizet = self.count % MD5_BLOCK_LEN self.count += length @@ -104,7 +104,7 @@ class md5: if length > 0: string.memcpy(self.buf + idx, data, length) # End calculation, output 16-byte digest - def final(self, out: t.CArray[uint8, MD5_DIGEST_LEN]): + def final(self, out: [MD5_DIGEST_LEN]uint8): idx: sizet = self.count % MD5_BLOCK_LEN # Pad 0x80 end self.buf[idx] = 0x80 diff --git a/hashlib/__sha1.vp b/hashlib/__sha1.vp index 7b60a02..5a5ee37 100644 --- a/hashlib/__sha1.vp +++ b/hashlib/__sha1.vp @@ -17,18 +17,18 @@ define sha1_f3(b: uint32, c: uint32, d: uint32) -> uint32: return (b & c) | (b & # SHA1 Context class sha1: - state: t.CArray[uint32, 5] = [ + state: [5]uint32 = [ 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0 ] # 5 32-bit state registers count: uint64 = 0 # processed bits - buf: t.CArray[uint8, SHA1_BLOCK_LEN] + buf: [SHA1_BLOCK_LEN]uint8 def __init__(self): string.memset(self.buf, 0, SHA1_BLOCK_LEN) # Single-group compression transform def transform(self, block: rawptr[uint8]): - w: t.CArray[uint32, 80] + w: [80]uint32 data: rawptr[uint8] = block # First 16 words for i in range(16): @@ -77,7 +77,7 @@ class sha1: self.state[3] += d self.state[4] += e def update(self, s: str): - data: rawptr[uint8] = uint8(s, t.CPtr) + data: uint8* = uint8*(s) length: sizet = len(s) idx: sizet = self.count % SHA1_BLOCK_LEN self.count += length @@ -95,7 +95,7 @@ class sha1: length -= SHA1_BLOCK_LEN if length > 0: string.memcpy(self.buf + idx, data, length) - def final(self, out: t.CArray[uint8, SHA1_DIGEST_LEN]): + def final(self, out: [SHA1_DIGEST_LEN]uint8): idx: sizet = self.count % SHA1_BLOCK_LEN self.buf[idx] = 0x80 idx += 1 diff --git a/hashlib/__sha256.vp b/hashlib/__sha256.vp index 1666e79..8ca3d3d 100644 --- a/hashlib/__sha256.vp +++ b/hashlib/__sha256.vp @@ -9,7 +9,7 @@ define SHA256_BLOCK_LEN = 64 define SHA256_DIGEST_LEN = 32 # SHA256 Constants K -sha256_K: t.CArray[uint32, 64] = [ +sha256_K: const [64]uint32 = [ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, @@ -30,20 +30,20 @@ define Maj(x: uint32, y: uint32, z: uint32) -> uint32: return (x & y) ^ (x & z) # SHA256 Context class sha256: - state: array[uint32, 8] = [ + state: [8]uint32 = [ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 ] # 8 32-bit state registers count: uint64 = 0 # processed bits - buf: t.CArray[uint8, SHA256_BLOCK_LEN] + buf: [SHA256_BLOCK_LEN]uint8 def __init__(self): string.memset(self, 0, sha256.__sizeof__()) # string.memset(self.buf, 0, SHA256_BLOCK_LEN) # SHA256 single-group compression transform def transform(self, block: rawptr[uint8]): - w: t.CArray[uint32, 64] + w: [64]uint32 for i in range(16): w[i] = ((uint32(block[i * 4 + 0]) << 24) | (uint32(block[i * 4 + 1]) << 16) | @@ -80,7 +80,7 @@ class sha256: self.state[6] += g self.state[7] += h def update(self, s: str): - data: rawptr[uint8] = uint8(s, t.CPtr) + data: uint8* = uint8*(s) length: sizet = len(s) idx: sizet = self.count % SHA256_BLOCK_LEN self.count += length @@ -98,7 +98,7 @@ class sha256: length -= SHA256_BLOCK_LEN if length > 0: string.memcpy(self.buf + idx, data, length) - def final(self, out: t.CArray[uint8, SHA256_DIGEST_LEN]): + def final(self, out: [SHA256_DIGEST_LEN]uint8): idx: sizet = self.count % SHA256_BLOCK_LEN self.buf[idx] = 0x80 idx += 1 diff --git a/hashlib/__sha512.vp b/hashlib/__sha512.vp index 880a357..314d350 100644 --- a/hashlib/__sha512.vp +++ b/hashlib/__sha512.vp @@ -17,7 +17,7 @@ define sha512_BigSigma1(x: uint64) -> uint64: return sha512_ror(x, 14) ^ sha512_ define sha512_Sigma0(x: uint64) -> uint64: return sha512_ror(x, 1) ^ sha512_ror(x, 8) ^ sha512_shr(x, 7) define sha512_Sigma1(x: uint64) -> uint64: return sha512_ror(x, 19) ^ sha512_ror(x, 61) ^ sha512_shr(x, 6) -sha512_K: t.CArray[uint64, 80] = [ +sha512_K: const [80]uint64 = [ 0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, 0x3956c25bf348b538, 0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, 0xd807aa98a3030242, 0x12835b0145706fbe, 0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, @@ -42,18 +42,18 @@ sha512_K: t.CArray[uint64, 80] = [ # SHA512 Context class sha512: - state: t.CArray[uint64, 8] = [ + state: [8]uint64 = [ 0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, 0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179 ] - count: t.CArray[uint64, 2] = [0, 0] - buf: t.CArray[uint8, SHA512_BLOCK_LEN] + count: [2]uint64 = [0, 0] + buf: [SHA512_BLOCK_LEN]uint8 def __init__(self): string.memset(self, 0, sha512.__sizeof__()) - def transform(self, block: t.CArray[uint8, SHA512_BLOCK_LEN]): - w: t.CArray[uint64, 80] + def transform(self, block: [SHA512_BLOCK_LEN]uint8): + w: [80]uint64 a: uint64 b: uint64 c: uint64 @@ -99,7 +99,7 @@ class sha512: self.state[6] += g self.state[7] += h def update(self, s: str): - data: rawptr[uint8] = uint8(s, t.CPtr) + data: uint8* = uint8*(s) length: sizet = len(s) idx: uint32 = uint32(self.count[0] % SHA512_BLOCK_LEN) self.count[0] += length @@ -119,7 +119,7 @@ class sha512: length -= SHA512_BLOCK_LEN if length > 0: string.memcpy(self.buf + idx, data, length) - def final(self, out: t.CArray[uint8, SHA512_DIGEST_LEN]): + def final(self, out: [SHA512_DIGEST_LEN]uint8): bits_lo: uint64 = self.count[0] << 3 bits_hi: uint64 = (self.count[1] << 3) | (self.count[0] >> 61) idx: uint32 = uint32(self.count[0] % SHA512_BLOCK_LEN) diff --git a/llvmlite/__builder.vp b/llvmlite/__builder.vp index 5100386..776a2d7 100644 --- a/llvmlite/__builder.vp +++ b/llvmlite/__builder.vp @@ -25,7 +25,7 @@ from .__function import Function, BasicBlock, block_append_text, block_set_termi # r = builder.build_load(i32_ty, a) # builder.build_ret(r) # -# 注意: 加 @t.NoVTable 以与硬编码 BUILDER_SIZE=32 保持一致 +# 注意: 加 @t.NoVTable 以与硬编码 BUILDER_SIZE=40 保持一致 # (方法以实例方法实现,无虚表;文件末尾的同名顶层函数为一行委托)。 # ============================================================ novirtual class IRBuilder: @@ -33,6 +33,7 @@ novirtual class IRBuilder: Func: rawptr[Function] # 所属函数 CurBlock: rawptr[BasicBlock] # 当前插入点 Counter: int # SSA 名计数器 + DbgLoc: str # current DILocation ref (e.g. "!5"; None = no !dbg on emitted instructions) # ============================================================ # 内部辅助 @@ -75,9 +76,31 @@ novirtual class IRBuilder: def _emit(self, text: str): - """将指令文本追加到当前块""" + """Append instruction text to the current block. + + When DbgLoc is set, append a ", !dbg !N" suffix. This is the single + write funnel, so all build_* paths get it. Multi-line text (switch) + keeps the suffix after the final ']', never on an inner line; a + trailing newline is handled too (see _append_dbg_suffix). + """ if self is None or self.CurBlock is None: return - block_append_text(self.Pool, self.CurBlock, text) + out: str = text + if self.DbgLoc is not None: + out = _append_dbg_suffix(self.Pool, text, self.DbgLoc) + block_append_text(self.Pool, self.CurBlock, out) + + def set_debug_location(self, ref: str): + """Set the current DILocation ref (e.g. "!5"); later instructions get ", !dbg !5". + + ref=None clears it (default: byte-identical to output without debug info). + """ + if self is None: return + self.DbgLoc = ref + + def clear_debug_location(self): + """Clear the current DILocation ref (later instructions carry no !dbg)""" + if self is None: return + self.DbgLoc = None def _mark_terminated(self): @@ -1164,8 +1187,8 @@ novirtual class IRBuilder: # ============================================================ # 工厂函数 # ============================================================ -# IRBuilder 硬编码大小: Pool(8) + Func(8) + CurBlock(8) + Counter(4) = 32 字节 -BUILDER_SIZE: t.CDefine = 32 +# IRBuilder 硬编码大小: Pool(8) + Func(8) + CurBlock(8) + Counter(4) + pad(4) + DbgLoc(8) = 40 字节 +BUILDER_SIZE: t.CDefine = 40 def new_builder(pool: memhub.MemBuddy*, func: rawptr[Function]) -> rawptr[IRBuilder]: """创建 IRBuilder,关联到函数 func""" @@ -1176,6 +1199,7 @@ def new_builder(pool: memhub.MemBuddy*, func: rawptr[Function]) -> rawptr[IRBuil ptr.Func = func ptr.CurBlock = None ptr.Counter = 0 + ptr.DbgLoc = None return ptr @@ -1213,6 +1237,46 @@ def _emit(builder: rawptr[IRBuilder], text: str): builder._emit(text) +# ============================================================ +# _append_dbg_suffix - append a ", !dbg !N" location suffix to instruction text +# +# Single-line: appended at the end. Multi-line (switch): appended after the +# whole text (after the final ']'), never on an inner line; a trailing +# newline has the suffix inserted before it. +# ============================================================ +def _append_dbg_suffix(pool: memhub.MemBuddy*, text: str, loc: str) -> str: + """Return text + " , !dbg !N" (suffix after the last line for multi-line text)""" + if text is None or loc is None: + return text + tlen: sizet = string.strlen(text) + slen: sizet = string.strlen(loc) + # room: ins(<=tlen) + ", !dbg "(7) + loc(slen) + optional '\n' + NUL + buf: str = pool.alloc(tlen + slen + 16) + if buf is None: + return text + # insert before a trailing newline (multi-line instruction case) + ins: sizet = tlen + if tlen > 0 and text[tlen - 1] == '\n': + ins = tlen - 1 + string.strncpy(buf, text, ins) + buf[ins] = '\0' + string.strcat(buf, ", !dbg ") + string.strcat(buf, loc) + if ins < tlen: + string.strcat(buf, "\n") + return buf + + +def builder_set_debug_location(builder: rawptr[IRBuilder], ref: str): + """Set the builder's current DILocation ref (e.g. "!5"; later instructions get ", !dbg !5")""" + builder.set_debug_location(ref) + + +def builder_clear_debug_location(builder: rawptr[IRBuilder]): + """Clear the builder's current DILocation ref""" + builder.clear_debug_location() + + def _mark_terminated(builder: rawptr[IRBuilder]): """标记当前块已终止""" builder._mark_terminated() diff --git a/llvmlite/__function.vp b/llvmlite/__function.vp index 7902a7d..c557750 100644 --- a/llvmlite/__function.vp +++ b/llvmlite/__function.vp @@ -85,6 +85,7 @@ novirtual class Function(GSListNode[Function]): AttrGroup: str # 字符串属性组内容(如 '"section"=".text.startup"',None=无) AttrGroupId: int # 属性组编号(>0 时函数行输出 #N,模块尾输出 attributes #N) NameLen: sizet # 函数名长度(创建时一次算好,恒等于 strlen(Name);追加在末尾,既有字段偏移不变) + DbgRef: str # this function's DISubprogram ref (e.g. "!3"; None = no debug, emits " !dbg !3") # ============================================================ @@ -145,8 +146,8 @@ def new_param(pool: memhub.MemBuddy*, ty: rawptr[LLVMType], return ptr -# Function 硬编码大小: Next(8) + Name(8) + RetTy(8) + Params(8) + Blocks(8) + IsDeclared(4) + IsVarArg(4) + Attrs(8) + AttrGroup(8) + AttrGroupId(8) + NameLen(8) = 80 字节 -FUNC_SIZE: t.CDefine = 80 +# Function 硬编码大小: Next(8) + Name(8) + RetTy(8) + Params(8) + Blocks(8) + IsDeclared(4) + IsVarArg(4) + Attrs(8) + AttrGroup(8) + AttrGroupId(8) + NameLen(8) + DbgRef(8) = 88 字节 +FUNC_SIZE: t.CDefine = 88 def new_function(pool: memhub.MemBuddy*, name: str, ret_ty: rawptr[LLVMType]) -> rawptr[Function]: @@ -162,6 +163,7 @@ def new_function(pool: memhub.MemBuddy*, name: str, ptr.Attrs = None ptr.AttrGroup = None ptr.AttrGroupId = 0 + ptr.DbgRef = None # NameLen 创建时一次算好:Name 终生不变(本文件仅此处赋值 ptr.Name), # 故 NameLen 恒等于 strlen(Name),可替代对函数名的反复 strlen 热点。 # name=None 时置 0,与 new_line 的 "text is not None" 判空约定一致 @@ -340,6 +342,16 @@ def param_set_attrs(param: rawptr[Param], attrs: str): param.Attrs = attrs +def function_set_dbg(func: rawptr[Function], ref: str): + """Set the function-level debug ref (DISubprogram node ref, e.g. "!3"). + + Define only: FunctionPrint emits " !dbg " after the signature line. + ref=None clears it (default: byte-identical to output without debug info). + """ + if func is None: return + func.DbgRef = ref + + # ============================================================ # _ll_name_needs_quote - 检查 LLVM 标识符是否需要加引号 # @@ -1218,6 +1230,11 @@ def FunctionPrint(buf: str, size: sizet, _append_attr_group_def(buf, size, pool, func) return + # function-level debug location (define only; declare has no body, no !dbg) + if func.DbgRef is not None: + _append_cstr(buf, size, " !dbg ") + _append_cstr(buf, size, func.DbgRef) + # 函数体 _append_cstr(buf, size, " {\n") diff --git a/llvmlite/__init__.vp b/llvmlite/__init__.vp index dc1567d..c8c145d 100644 --- a/llvmlite/__init__.vp +++ b/llvmlite/__init__.vp @@ -26,16 +26,19 @@ from .__function import ( block_append_line, block_append_text, function_set_attrs, param_set_attrs, function_set_attr_group, + function_set_dbg, function_get_name, function_get_name_len, function_get_ret_ty, function_get_params, function_get_param_head, function_get_next, function_is_declared, param_get_name, param_get_ty, param_get_next, FunctionPrint, G_PeepEnable, ) from .__module import ( - LLVMModule, GlobalVariable, NamedTypeNode, new_module, new_global_variable, + LLVMModule, GlobalVariable, NamedTypeNode, DINode, DINamedMeta, + new_module, new_global_variable, module_add_function, module_add_global, module_add_named_type, module_set_named_type_packed, module_set_target, module_set_datalayout, + module_add_di_node, module_add_named_metadata, global_set_constant, global_set_linkage, global_set_unnamed_addr, global_set_section, LLVMModulePrint, OUTPUT_FULL, OUTPUT_STUB, OUTPUT_TEXT, module_ensure_opaque_for_type, @@ -43,6 +46,7 @@ from .__module import ( from .__builder import ( IRBuilder, new_builder, position_at_end, builder_cur_block_is_terminated, + builder_set_debug_location, builder_clear_debug_location, build_alloca, build_alloca_aligned, build_alloca_at_entry, build_load, build_store, build_add, build_sub, build_mul, build_sdiv, build_udiv, build_srem, build_urem, build_and, build_or, build_xor, build_shl, build_lshr, build_ashr, diff --git a/llvmlite/__module.vp b/llvmlite/__module.vp index 46153a1..d0863af 100644 --- a/llvmlite/__module.vp +++ b/llvmlite/__module.vp @@ -40,6 +40,27 @@ class NamedTypeNode(GSListNode[NamedTypeNode]): IsPacked: int # 1=packed 结构体(输出 <{...}>,无字段填充) +# ============================================================ +# DINode: DI metadata node, emitted as "!N = ". +# +# Text is the node body (may contain !M references to other nodes). +# ============================================================ +@t.NoVTable +class DINode(GSListNode[DINode]): + Text: str # node body, e.g. "!DIFile(filename: \"a.vp\", directory: \"/x\")" + + +# ============================================================ +# DINamedMeta: named metadata, emitted as "! = !{ }". +# +# Refs is the brace body (e.g. "!1" / "!1, !2"). +# ============================================================ +@t.NoVTable +class DINamedMeta(GSListNode[DINamedMeta]): + Name: str # e.g. "llvm.dbg.cu" / "llvm.module.flags" + Refs: str # reference list text, e.g. "!1" + + # ============================================================ # LLVMModule: LLVM IR 模块容器 # @@ -62,7 +83,13 @@ class LLVMModule: NamedTypeHead: rawptr[NamedTypeNode] # 命名结构体类型链表头 NamedTypeTail: rawptr[NamedTypeNode] # 命名结构体类型链表尾 NamedTypeCount: sizet # 命名结构体类型计数 - # 总计: 12 字段 * 8 = 96 字节(无 vtable) + DIMetaHead: rawptr[DINode] # DI metadata node list head + DIMetaTail: rawptr[DINode] # DI metadata node list tail + DIMetaCount: sizet # DI node count (= next !N id) + DINamedHead: rawptr[DINamedMeta] # named metadata list head + DINamedTail: rawptr[DINamedMeta] # named metadata list tail + DINamedCount: sizet # named metadata count + # 总计: 18 字段 * 8 = 144 字节(无 vtable) # ============================================================ # 实例方法(对象化重构:原 module_* 顶层函数改写为方法, @@ -103,6 +130,84 @@ class LLVMModule: if self is None: return self.DataLayout = layout + # ---- DI debug metadata ---- + def add_di_node(self, pool: memhub.MemBuddy*, text: str) -> int: + """Append a DI metadata node; return its !N id (0-based, unique), -1 on failure. + + text is the node body incl. !M references built from earlier ids. + """ + if self is None or pool is None or text is None: + return -1 + node: rawptr[DINode] = pool.alloc(DI_NODE_SIZE) + if node is None: + return -1 + string.memset(node, 0, DI_NODE_SIZE) + # copy body (caller buffer may be reused) + tlen: sizet = string.strlen(text) + tcopy: str = pool.alloc(tlen + 1) + if tcopy is None: + return -1 + string.strcpy(tcopy, text) + node.Text = tcopy + if self.DIMetaHead is None: + self.DIMetaHead = node + else: + self.DIMetaTail.Next = node + self.DIMetaTail = node + node_id: int = int(self.DIMetaCount) + self.DIMetaCount += 1 + return node_id + + def add_named_metadata(self, pool: memhub.MemBuddy*, name: str, refs: str): + """Register named metadata "! = !{ }"; merge into an existing + entry with the same name (refs is the brace body, e.g. "!3").""" + if self is None or pool is None or name is None: + return + cur: rawptr[DINamedMeta] = self.DINamedHead + cur_steps: sizet = 0 + while cur is not None: + cur_steps += 1 + if cur_steps > IR_DUMP_CHAIN_LIMIT: + stdio.printf("[IR-DUMP] 链表成环: module di named(DINamedCount=%zu,dump 终止)\n", + self.DINamedCount) + stdlib.exit(1) + if cur.Name is not None and string.strcmp(cur.Name, name) == 0: + if refs is not None: + old_len: sizet = 0 + if cur.Refs is not None: + old_len = string.strlen(cur.Refs) + merged: str = pool.alloc(old_len + string.strlen(refs) + 3) + if merged is not None: + merged[0] = '\0' + if cur.Refs is not None: + string.strcat(merged, cur.Refs) + string.strcat(merged, ", ") + string.strcat(merged, refs) + cur.Refs = merged + return + cur = cur.Next + entry: rawptr[DINamedMeta] = pool.alloc(DI_NAMED_SIZE) + if entry is None: + return + string.memset(entry, 0, DI_NAMED_SIZE) + ncopy: str = pool.alloc(string.strlen(name) + 1) + if ncopy is None: + return + string.strcpy(ncopy, name) + entry.Name = ncopy + if refs is not None: + rcopy: str = pool.alloc(string.strlen(refs) + 1) + if rcopy is None: + return + string.strcpy(rcopy, refs) + entry.Refs = rcopy + if self.DINamedHead is None: + self.DINamedHead = entry + else: + self.DINamedTail.Next = entry + self.DINamedTail = entry + self.DINamedCount += 1 + # ---- 命名结构体类型管理 ---- def add_named_type(self, pool: memhub.MemBuddy*, ty: rawptr[LLVMType]): """将命名结构体类型添加到模块(用于输出 %"name" = type { ... } 定义行) @@ -361,16 +466,60 @@ class LLVMModule: func = func.Next func_idx += 1 + # DI metadata, text/full only: never in stub, since StubMerger concatenates + # stub + text into one .ll and stub metadata would duplicate !N / llvm.dbg.cu. + # Node defs precede named metadata: ids ascend from 0, so every !M ref is + # backward (no forward reference needed). + if (mode == OUTPUT_TEXT or mode == OUTPUT_FULL) and self.DIMetaHead is not None: + node: rawptr[DINode] = self.DIMetaHead + nid: int = 0 + id_buf: str = pool.alloc(32) + node_steps: sizet = 0 + while node is not None: + node_steps += 1 + if node_steps > IR_DUMP_CHAIN_LIMIT: + stdio.printf("[IR-DUMP] 链表成环: module di meta(DIMetaCount=%zu,dump 终止)\n", + self.DIMetaCount) + stdlib.exit(1) + if node.Text is not None and id_buf is not None: + viperlib.snprintf(id_buf, 32, "!%d = ", nid) + _append_cstr(buf, size, id_buf) + _append_cstr(buf, size, node.Text) + _append_cstr(buf, size, "\n") + node = node.Next + nid += 1 + + # named metadata "! = !{ }" (llvm.dbg.cu / llvm.module.flags ...) + if (mode == OUTPUT_TEXT or mode == OUTPUT_FULL) and self.DINamedHead is not None: + if self.DIMetaHead is not None: + _append_cstr(buf, size, "\n") + nm: rawptr[DINamedMeta] = self.DINamedHead + nm_steps: sizet = 0 + while nm is not None: + nm_steps += 1 + if nm_steps > IR_DUMP_CHAIN_LIMIT: + stdio.printf("[IR-DUMP] 链表成环: module di named(DINamedCount=%zu,dump 终止)\n", + self.DINamedCount) + stdlib.exit(1) + _append_cstr(buf, size, "!") + if nm.Name is not None: + _append_cstr(buf, size, nm.Name) + _append_cstr(buf, size, " = !{ ") + if nm.Refs is not None: + _append_cstr(buf, size, nm.Refs) + _append_cstr(buf, size, " }\n") + nm = nm.Next + # ============================================================ # 工厂函数 # ============================================================ def new_module(pool: memhub.MemBuddy*, name: str) -> rawptr[LLVMModule]: """创建一个模块""" - # 硬编码 96 字节:12 字段 * 8 字节(无 vtable,@t.NoVTable) - ptr: rawptr[LLVMModule] = pool.alloc(96) + # 硬编码 144 字节:18 字段 * 8 字节(无 vtable,@t.NoVTable) + ptr: rawptr[LLVMModule] = pool.alloc(144) if ptr is None: return None - string.memset(ptr, 0, 96) + string.memset(ptr, 0, 144) ptr.Name = name ptr.FuncHead = None ptr.FuncTail = None @@ -383,6 +532,12 @@ def new_module(pool: memhub.MemBuddy*, name: str) -> rawptr[LLVMModule]: ptr.NamedTypeHead = None ptr.NamedTypeTail = None ptr.NamedTypeCount = 0 + ptr.DIMetaHead = None + ptr.DIMetaTail = None + ptr.DIMetaCount = 0 + ptr.DINamedHead = None + ptr.DINamedTail = None + ptr.DINamedCount = 0 return ptr @@ -392,6 +547,12 @@ GV_SIZE: t.CDefine = 72 # NamedTypeNode 硬编码大小: Next(8) + Ty(8) + IsPacked(8) = 24 字节 NAMED_TYPE_NODE_SIZE: t.CDefine = 24 +# DINode hardcoded size: Next(8) + Text(8) = 16 bytes +DI_NODE_SIZE: t.CDefine = 16 + +# DINamedMeta hardcoded size: Next(8) + Name(8) + Refs(8) = 24 bytes +DI_NAMED_SIZE: t.CDefine = 24 + def new_global_variable(pool: memhub.MemBuddy*, name: str, ty: rawptr[LLVMType]) -> rawptr[GlobalVariable]: """创建一个全局变量""" @@ -435,6 +596,21 @@ def module_set_datalayout(mod: rawptr[LLVMModule], layout: str): mod.set_datalayout(layout) +# ============================================================ +# DI debug metadata +# ============================================================ +def module_add_di_node(mod: rawptr[LLVMModule], pool: memhub.MemBuddy*, + text: str) -> int: + """Append a DI metadata node; return its !N id (-1 on failure)""" + return mod.add_di_node(pool, text) + + +def module_add_named_metadata(mod: rawptr[LLVMModule], pool: memhub.MemBuddy*, + name: str, refs: str): + """Register named metadata "! = !{ }" """ + mod.add_named_metadata(pool, name, refs) + + # ============================================================ # 命名结构体类型管理 # ============================================================ diff --git a/llvmlite/__types.vp b/llvmlite/__types.vp index f43f04c..f22e75c 100644 --- a/llvmlite/__types.vp +++ b/llvmlite/__types.vp @@ -13,10 +13,9 @@ from linkedlist import GSListNode # 继承 GSListNode[ParamNode] 获取强类型 Next: ParamNode|CPtr # Ty 用 rawptr[t\.CVoid] 前向引用,使用时转型为 (rawptr[LLVMType]) # -# 注意: 必须加 @t.NoVTable(理由见 __function.py 的 Line 注释)。 +# 注意: 必须加 novirtual(理由见 __function.py 的 Line 注释)。 # ============================================================ -@t.NoVTable -class ParamNode(GSListNode[ParamNode]): +novirtual class ParamNode(GSListNode[ParamNode]): Ty: rawptr # LLVMType*(前向引用,使用时转型) @@ -56,7 +55,7 @@ class LLVMType(t.REnum): # 浮点类型: half/float/double/fp128 class Float: - Bits: int # 16=half, 32=float, 64=double, 128=fp128 + Bits: int # 16=half, 32=float, 64=double, 128=fp128 # 无值类型 class Void: @@ -74,11 +73,25 @@ class LLVMType(t.REnum): # ============================================================ # 内部分配辅助 # ============================================================ +def _TypeSizeGuard() -> sizet: + """LLVMType 的字节尺寸;为 0 时响亮失败。 + + __sizeof__() 在布局未就绪(结构体未注册 / opaque / 字段链未填充)时被 + 算成 0,此时 alloc(0) 只会静默返回 NULL,类型对象随之丢失且无从追查。 + """ + sz: sizet = LLVMType.__sizeof__() + if sz == 0: + stdio.printf("FATAL: llvmlite.LLVMType.__sizeof__() == 0" + "(布局未注册或 opaque),拒绝 alloc(0)\n") + stdlib.exit(1) + return sz + + def _new_type(pool: memhub.MemBuddy*) -> rawptr[LLVMType]: """从 mpool 分配一个零初始化的 LLVMType""" - ptr: rawptr[LLVMType] = pool.alloc(LLVMType.__sizeof__()) - if ptr: - string.memset(ptr, 0, LLVMType.__sizeof__()) + sz_nt: sizet = _TypeSizeGuard() + ptr: rawptr[LLVMType] = pool.alloc(sz_nt) + if ptr: string.memset(ptr, 0, sz_nt) return ptr @@ -684,10 +697,11 @@ def _DeepCopyDepth(pool: memhub.MemBuddy*, """ if ty is None: return None - new_ty: rawptr[LLVMType] = (rawptr[LLVMType])(pool.alloc(LLVMType.__sizeof__())) + sz_dc: sizet = _TypeSizeGuard() + new_ty: rawptr[LLVMType] = (rawptr[LLVMType])(pool.alloc(sz_dc)) if new_ty is None: return None - string.memset(new_ty, 0, LLVMType.__sizeof__()) + string.memset(new_ty, 0, sz_dc) if depth >= DEEPCOPY_MAX_DEPTH: # 深度超限:浅拷贝 tag 后停止递归(复制当前变体但不深拷子类型) match ty: diff --git a/math.vp b/math.vp index d543ccd..1e9d9fd 100644 --- a/math.vp +++ b/math.vp @@ -7,8 +7,9 @@ # * Scope and feature set: # * - Fundamental mathematical constants: π, e, π/2, π/4, reciprocal π, 2/π, ln(2), ln(10), 2/sqrt(π), DBL_MAX. # * - Angle conversion: radians ↔ degrees helpers. -# * - Trigonometric functions: sin, cos, tan, sinf, cosf, tanf. Argument reduction is performed using subtraction of -# * 2π until the value falls into [-π, π]; polynomial approximations are truncated series tuned for the target interval. +# * - Trigonometric functions: sin, cos, tan, sinf, cosf, tanf. Argument reduction is O(1): a three-constant +# * Cody-Waite step for |x| < 2^20, and a Payne-Hanek step (20-word table of 2/π) above. Both yield r ∈ [-π/4, π/4] +# * as a double-double fed to the truncated Taylor series. # * - Inverse trigonometric functions: asin, acos, atan, atan2. asin uses a minimax polynomial suitable for [-1, 1]; # * atan relies on a Taylor core limited to |x| ≤ 1 combined with range folding via reciprocal and quadrant logic. # * - Hyperbolic trigonometry: sinh, cosh, tanh, built atop exp and exp(-x). @@ -69,30 +70,33 @@ define U_M_2_SQRT_PI: double = 1.77245385090551602730 define DBL_MAX: double = 1.7976931348623157e308 define NEG_1E308: double = -1e308 +# Argument reduction constants for sin/cos/tan: pi/2 split into HI+MID+LO and 2/pi (all exact doubles), +# plus the 2^20 limit below which Cody-Waite applies (so each fn*HK product stays exact in 53 bits). +define U_M_PIO2_HI: double = 1.57079632673412561417e+00 +define U_M_PIO2_MID: double = 6.07710050650619224932e-11 +define U_M_PIO2_LO: double = 3.52155986518320018165e-27 +define U_M_INV_PIO2: double = 6.36619772367581382433e-01 +define U_M_REDUCE_LIMIT: double = 1048576.0 + def radians(degrees: double) -> double: return degrees * (U_M_PI / 180.0) def degrees(radians: double) -> double: return radians * (180.0 / U_M_PI) -def sin(x: double) -> double: - while x > U_M_PI: - x -= 2.0 * U_M_PI - while x < -U_M_PI: - x += 2.0 * U_M_PI +def _sin_core(x: double) -> double: + """sin(r) Taylor series, 8 terms, valid on |r| <= pi/4.""" x2: double = x * x result: double = x term: double = x - coeffs: const [8]double = [6.0, 20.0, 42.0, 72.0, 110.0, 156.0, 210.0, 272.0] sign: double = -1.0 + coeffs: const [8]double = [6.0, 20.0, 42.0, 72.0, 110.0, 156.0, 210.0, 272.0] for c in coeffs: term = term * x2 / c result += sign * term sign = -sign return result -def cos(x: double) -> double: - while x > U_M_PI: - x -= 2.0 * U_M_PI - while x < -U_M_PI: - x += 2.0 * U_M_PI + +def _cos_core(x: double) -> double: + """cos(r) Taylor series, 8 terms, valid on |r| <= pi/4.""" x2: double = x * x result: double = 1.0 term: double = 1.0 @@ -104,6 +108,154 @@ def cos(x: double) -> double: sign = -sign return result + +def _mul_add(a: uint64, b: uint64, c: uint64, lo: rawptr[uint64], hi: rawptr[uint64]): + """Write (hi:lo) = a*b + c using 32-bit limbs for the full 128-bit product.""" + a0: uint64 = a & 0xFFFFFFFF + a1: uint64 = a >> 32 + b0: uint64 = b & 0xFFFFFFFF + b1: uint64 = b >> 32 + w0: uint64 = a0 * b0 + t: uint64 = a1 * b0 + (w0 >> 32) + w1: uint64 = t & 0xFFFFFFFF + w2: uint64 = t >> 32 + w1 = a0 * b1 + w1 + h: uint64 = a1 * b1 + w2 + (w1 >> 32) + l: uint64 = (w1 << 32) | (w0 & 0xFFFFFFFF) + l2: uint64 = l + c + if l2 < l: h = h + 1 + *lo = l2 + *hi = h + + +def _two_prod(a: double, b: double, ph: rawptr[double], pl: rawptr[double]): + """Write a*b as the exact double-double (ph + pl) via Dekker splitting (no FMA required).""" + split: double = 134217729.0 + p: double = a * b + ca: double = split * a + ahi: double = ca - (ca - a) + alo: double = a - ahi + cb: double = split * b + bhi: double = cb - (cb - b) + blo: double = b - bhi + *ph = p + *pl = ((ahi * bhi - p) + ahi * blo + alo * bhi) + alo * blo + + +def _rem_pio2(x: double, rh: rawptr[double], rl: rawptr[double]) -> int: + """O(1) reduction of finite x >= 0: x = n*(pi/2) + r with |r| <= pi/4; returns n mod 4, writes r = rh + rl.""" + if x < U_M_REDUCE_LIMIT: + # Cody-Waite three-step with pi/2 = HI + MID + LO; each fn*HK product is exact for fn < 2^20 + q: double = x * U_M_INV_PIO2 + k: int = int(q) + if q - double(k) > 0.5: k = k + 1 + fn: double = double(k) + r: double = x - fn * U_M_PIO2_HI + w1: double = fn * U_M_PIO2_MID + y0: double = r - w1 + w2: double = fn * U_M_PIO2_LO + rhv: double = y0 - w2 + *rh = rhv + *rl = ((r - y0) - w1) + ((y0 - rhv) - w2) + return k & 3 + # Payne-Hanek: a 20-word (1280-bit) 2/pi table; multiply the 53-bit mantissa and read off the fraction + twopi: const [20]uint64 = [ + 0xA2F9836E4E441529, 0xFC2757D1F534DDC0, 0xDB6295993C439041, 0xFE5163ABDEBBC561, + 0xB7246E3A424DD2E0, 0x06492EEA09D1921C, 0xFE1DEB1CB129A73E, 0xE88235F52EBB4484, + 0xE99C7026B45F7E41, 0x3991D639835339F4, 0x9C845F8BBDF9283B, 0x1FF897FFDE05980F, + 0xEF2F118B5A0A6D1F, 0x6D367ECF27CB09B7, 0x4F463F669E5FEA2D, 0x7527BAC7EBE5F17B, + 0x3D0739F78A5292EA, 0x6BFB5FB11F8D5D08, 0x56033046FC7B6BAB, 0xF0CFBC209AF4361D + ] + xv: double = x + xb: uint64 = 0 + *&xb = *&xv + ef: uint64 = (xb >> 52) & 0x7FF + m: uint64 = (xb & 0xFFFFFFFFFFFFF) | 0x10000000000000 + e: int = int(ef) - 1075 + shift: int = 1280 - e + qw: int = shift >> 6 + s: int = shift & 63 + b: [21]uint64 + carry: uint64 = 0 + for j in range(20): + lo: uint64 = 0 + hi: uint64 = 0 + _mul_add(m, twopi[19 - j], carry, &lo, &hi) + b[j] = lo + carry = hi + b[20] = carry + lo2: uint64 = b[qw] >> s + if s == 63: lo2 = (b[qw] >> 63) | (b[qw + 1] << 1) + k: int = int(lo2 & 3) + a1: uint64 = b[qw] & ((uint64(1) << s) - 1) + a2: uint64 = b[qw - 1] + a3: uint64 = b[qw - 2] + fhi: uint64 = a2 + flo: uint64 = a3 + if s != 0: + sh: int = 64 - s + fhi = (a1 << sh) | (a2 >> s) + flo = (a2 << sh) | (a3 >> s) + # fractional part f = fhi*2^-64 + flo*2^-128; split off the top 53 bits so df is exact + f53: uint64 = fhi >> 11 + df: double = double(f53) + de: double = double(fhi - (f53 << 11)) + fh: double = df * 1.1102230246251565e-16 + fl: double = de * 5.421010862427522e-20 + double(flo) * 2.9387358770557188e-39 + r0h: double = fh + r0l: double = fl + n: int = k + if fh > 0.5 or (fh == 0.5 and fl > 0.0): + n = (k + 1) & 3 + r0h = fh - 1.0 + ph: double = 0.0 + pl: double = 0.0 + _two_prod(r0h, U_M_PIO2_HI, &ph, &pl) + *rh = ph + *rl = pl + r0h * U_M_PIO2_MID + r0h * U_M_PIO2_LO + r0l * U_M_PIO2_HI + return n + + +def sin(x: double) -> double: + u: uint64 = 0 + xv: double = x + *&u = *&xv + ef: uint64 = (u >> 52) & 0x7FF + if ef == 0x7FF: return x - x # inf/nan -> NaN + neg: int = int(u >> 63) + ax: double = x if neg == 0 else -x + rh: double = 0.0 + rl: double = 0.0 + n: int = _rem_pio2(ax, &rh, &rl) + sr: double = _sin_core(rh) + _cos_core(rh) * rl + cr: double = _cos_core(rh) - _sin_core(rh) * rl + res: double = 0.0 + if n == 0: res = sr + elif n == 1: res = cr + elif n == 2: res = -sr + else: res = -cr + return -res if neg != 0 else res + + +def cos(x: double) -> double: + u: uint64 = 0 + xv: double = x + *&u = *&xv + ef: uint64 = (u >> 52) & 0x7FF + if ef == 0x7FF: return x - x # inf/nan -> NaN + neg: int = int(u >> 63) + ax: double = x if neg == 0 else -x + rh: double = 0.0 + rl: double = 0.0 + n: int = _rem_pio2(ax, &rh, &rl) + sr: double = _sin_core(rh) + _cos_core(rh) * rl + cr: double = _cos_core(rh) - _sin_core(rh) * rl + if n == 0: return cr + elif n == 1: return -sr + elif n == 2: return -cr + else: return sr + + def tan(x: double) -> double: return sin(x) / cos(x) @@ -364,38 +516,8 @@ def sqrtf(x: float) -> float: guess = (guess + x / guess) / 2.0 return guess -def sinf(x: float) -> float: - while x > 3.14159265: - x -= 6.28318530 - while x < -3.14159265: - x += 6.28318530 - x2: float = x * x - result: float = x - term: float = x - sign: float = -1.0 - coeffs: const [8]float = [6.0, 20.0, 42.0, 72.0, 110.0, 156.0, 210.0, 272.0] - for c in coeffs: - term = term * x2 / c - result += sign * term - sign = -sign - return result - - -def cosf(x: float) -> float: - while x > 3.14159265: - x -= 6.28318530 - while x < -3.14159265: - x += 6.28318530 - x2: float = x * x - result: float = 1.0 - term: float = 1.0 - sign: float = -1.0 - coeffs: const [8]float = [2.0, 12.0, 30.0, 56.0, 90.0, 132.0, 182.0, 240.0] - for c in coeffs: - term = term * x2 / c - result += sign * term - sign = -sign - return result +def sinf(x: float) -> float: return float(sin(double(x))) +def cosf(x: float) -> float: return float(cos(double(x))) def tanf(x: float) -> float: c: float = cosf(x) diff --git a/memhub.vp b/memhub.vp index 3a5bf9b..6dd47ba 100644 --- a/memhub.vp +++ b/memhub.vp @@ -15,160 +15,129 @@ import stdlib # All subclasses override alloc/free/reset via vtable, invokable polymorphically through MemManager pointer # ============================================================ -MEMHUB_ALIGN: t.CDefine = 8 +define MEMHUB_ALIGN = 8 -# MemSlab bitmap constants -MEMSLAB_MIN_BLOCK: t.CDefine = 16 -MEMSLAB_BITMAP_BYTES: t.CDefine = 256 +# MemSlab bitmap constants. +define MEMSLAB_MIN_BLOCK = 16 +define MEMSLAB_BITMAP_BYTES = 256 -# MemBuddy buddy constants -MEMBUDDY_MIN_BLOCK: t.CDefine = 32 -MEMBUDDY_MAX_ORDERS: t.CDefine = 32 -MEMBUDDY_HEADER_SIZE: t.CDefine = 8 -# 块尾 canary:alloc 时在块尾(用户数据区之外)写 8 字节哨兵, -# check_alive_blocks 校验——越界写穿数据区会破坏 canary,把 -# "崩溃点漂移/事发地遥远"变成"确定性定位到越界写发生的文件" -MEMBUDDY_CANARY: t.CDefine = 8 +# MemBuddy buddy constants. +define MEMBUDDY_MIN_BLOCK = 32 +define MEMBUDDY_MAX_ORDERS = 32 +define MEMBUDDY_HEADER_SIZE = 8 +# Block-tail canary: alloc writes 8 sentinel bytes past the user area and check_alive_blocks +# verifies them; an overflow breaks the canary and pinpoints the writing file deterministically. +define MEMBUDDY_CANARY = 8 -# 分配守卫(诊断,SlotsBugTest 二代编 list 头覆盖排查): -# alloc 时把用户数据区填充 0xCD 哨兵。若对象初始化(list.__init__ 等) -# 未执行,其字段保持 0xCDCD...,CTORDBG/VDBG 打印字段时一眼可辨 -# "未初始化(0xCDCD)" 与 "被覆盖(其他值)"。0=关闭(性能),1=开启。 -# 热点优化(实测):默认开启时每次 alloc 都 memset 用户数据区(约 2278 万次, -# 占全程序 memset 的 68%),纯诊断用途。默认改为 0(关闭);排查未初始化读取 -# 时置 1 即可——开启后填充代码逐字节不变(沿用 MEMBUDDY_THREAD_SAFE 的编译期 -# 开关范式)。关闭不破坏 self_check/canary 不变量:块尾哨兵取自 -# MEMBUDDY_CANARY/MEMBUDDY_CANARY_PATTERN,free 后填充由 detection 分支 -# (detection!=0)控制,二者与 FILL_GUARD 解耦。 -MEMBUDDY_FILL_GUARD: t.CDefine = 0 -MEMBUDDY_FILL_PATTERN: t.CDefine = 0xCD +# Diagnostic fill guard: alloc can fill the user area with 0xCD so fields a constructor never wrote +# stay 0xCDCD... and stand out in field dumps. 0 = off (default), 1 = on. Off does not affect the +# self_check/canary invariants: the tail sentinel comes from MEMBUDDY_CANARY/CANARY_PATTERN and the +# post-free fill is controlled by detection, both independent of this switch. +define MEMBUDDY_FILL_GUARD = 0 +define MEMBUDDY_FILL_PATTERN = 0xCD -# [DET] 分配检测(alloc 前后 2 字节 0xCDCD 标记 + 记录表探测越界写): -# 检测模式(MemBuddy(..., detection=1))下每次 alloc 布局为 -# [0xCDCD][请求大小内存][0xCDCD](实际比请求多 4 字节) -# 返回指针 = 实际用户区 + 2(前标记之后);回收时向前偏移 2 字节。 -# 每次 alloc 后探测记录表全部后标记(ptr+size 处)——被破坏 = 越界写, -# 立即汇报 ptr/size 并终止(定位"存活块被部分覆盖"的写入者, -# 如 SlotsBugTest KwEntry cargs 只写 Data/IterIndex 的块归属)。 -MEMBUDDY_DET_MARKER: t.CDefine = 0xCDCD -# 检测记录区默认大小(32MB,每条记录 {ptr u64, size u64} = 16B) -MEMBUDDY_DET_RECORD_SIZE: t.CDefine = 33554432 -# 采样周期(2 的幂):每 N 次 alloc 做一次全表扫描(分配重叠 + 探测)。 -# 记录追加本身 O(1) 不受影响;损坏是持久性的,不依赖即时扫描,采样 -# 不损失检测能力——2M 次 alloc 若每次全表 O(n) 扫描 = O(n²) 太慢 -# (用户反馈 detection 模式过慢),采样把扫描次数降为 1/N。 -# 运行期覆盖:MemBuddy 实例字段 deep_detect != 0(命令行 --detection-deep, -# 见 main.vp 的 strstr 取值与 MemBuddy(...) 传参)时忽略本周期、等价于 1 -# (= 每次 alloc 都探测),判定点见本文件 alloc 内 L856(_det_probe)与 -# L800-801(周期全表扫描)两处。默认 32:未开该开关时掩码 x & 31 与历史 -# 实现逐字节一致(deep_detect 恒 0,见 L801 / L856 两处判定写法)。 -MEMBUDDY_DET_PROBE_PERIOD: t.CDefine = 32 +# [DET] Allocation detection (2-byte 0xCDCD markers around the user area + record-table probing): +# in detection mode (MemBuddy(..., detection=1)) each alloc is laid out as +# [0xCDCD][requested bytes][0xCDCD] (4 bytes more than requested) +# with the returned pointer = user area + 2; free steps back 2 bytes. Every surviving tail marker +# (at ptr+size) is checked: a broken one = out-of-bounds write, reported with ptr/size and terminated. +define MEMBUDDY_DET_MARKER = 0xCDCD +# Default record area size (32 MiB; each record {ptr u64, size u64} = 16 B). +define MEMBUDDY_DET_RECORD_SIZE = 33554432 +# Sampling period (power of two): every N allocs run one full-table scan (overlap + probe). The +# record append stays O(1); corruption is persistent, so a delayed scan still finds it, while a full +# O(n) scan per alloc would be O(n^2). Runtime override: a MemBuddy with deep_detect != 0 +# (--detection-deep) ignores this period and behaves as 1. +define MEMBUDDY_DET_PROBE_PERIOD = 32 -# [热点] 周期全表扫描总开关(编译期,默认 0=关闭;范式同 MEMBUDDY_THREAD_SAFE/ -# MEMBUDDY_FILL_GUARD——常量比较不依赖运行期取值,默认 0 时该分支不执行)。 -# alloc 每 MEMBUDDY_DET_PROBE_PERIOD 次分配会执行两项 O(n) 全扫描,其耗时全部 -# 落在 MemBuddy.alloc 的 self 内(无外部调用可分摊): -# 1. 记录表重叠检测:while 遍历 _det_records[0 .. _det_count),_det_count ≈ 未回收 -# 的分配数。量级实测:reset_to 每次回收约 3.15e4 块(_coalesce 22.547e6 次 / -# 716 次 reset_to)、check_alive_blocks 每次校验约 3.94e4 活块 -# (_block_size_at_order 6.27e6 次 / 159 次),即 _det_count ~3e4; -# 2. [MEMDBG] 空闲链残留扫描:遍历本 order free list(受 _memdbg_enable 控制)。 -# 实测 MemBuddy.alloc self=681.85e9 tick / 25,419,348 次 ≈ 26,824 tick/次,而该 -# 采样块每 32 次才进入一次 ⇒ 单次采样块约 8.6e5 tick,alloc 函数体其余部分 -# (O(1) 块头/canary 写入 + chk_stack/记录追加)占比可忽略;按 32 采样只把次数 -# 降为 1/32,并不改变 O(n²/PERIOD) 的量级(n=_det_count)。 -# ⇒ 默认 0:alloc 回到纯 O(1) 分配路径(该分支不执行)。排查重复分配 / free list -# 残留 / 越界写时置 1(同时需 _memdbg_enable 才有第 2 项检查)。 -# 置 1 时该分支与历史实现逐字节一致;该分支除诊断 printf+fflush+exit 外无任何 -# 状态写入,故关闭只少诊断、不改变分配器语义。 -# 运行期覆盖:MemBuddy 实例字段 deep_detect != 0(命令行 --detection-deep)可在 -# 不改本编译期常量的前提下打开该扫描,判定点见本文件 alloc 内 L800 的 -# "MEMBUDDY_PERIOD_SCAN != 0 or self.deep_detect != 0"(配合 L801 掩码短路 -# → 每次 alloc 都跑该全扫描)。默认 0 = 与历史实现一致:该诊断分支不执行。 -MEMBUDDY_PERIOD_SCAN: t.CDefine = 0 +# [hot] Full-table periodic-scan master switch (compile-time, default 0 = off; same pattern as +# MEMBUDDY_THREAD_SAFE/MEMBUDDY_FILL_GUARD). Every MEMBUDDY_DET_PROBE_PERIOD allocs run two O(n) +# scans inside MemBuddy.alloc (record overlap + [MEMDBG] residue), the alloc O(n) hotspot, so the +# whole block is gated here. Default 0 keeps alloc O(1) (branch not taken); set 1 (with +# _memdbg_enable for the second check) to hunt duplicate allocation / free-list residue / overflow. +# Runtime override: deep_detect != 0 forces the scan without changing this constant. The branch only +# prints and exits (no state writes), so off only drops diagnostics. +define MEMBUDDY_PERIOD_SCAN = 0 -# [MEMDBG] 临时插桩(2026-09-04,定位 free list 重复 put / 悬空 free / alloc -# 重复出块;定位后整体移除,勿长期保留)。改动点: -# 1. alloc:采样分支内校验"新出块仍残留于同 order free list"; -# 2. free :归还前校验"同 order free list 已含本块"(double-put 实锤); -# 3. free :无效/悬空指针(池内但 header 偶数=已释放)不再静默忽略; -# 4. 既有 [DET]/[MEM] 终止路径补 stdio.fflush(None)(stdout 重定向丢输出)。 -MEMBUDDY_DBG: t.CDefine = 1 -# [MEMDBG] 运行时开关:默认开(随 MEMBUDDY_DBG 编译默认值)。 -# 需要让负向用例(如 BuddyTest 故意双重 free,期望"静默拒绝")不终止时, -# 可在运行期关闭(沿用 ast._mbuddy/sys._mbuddy 式跨模块全局写法): +# [MEMDBG] Temporary instrumentation (locate duplicate free-list put / dangling free / duplicate +# block handout; remove once located). Points: (1) alloc - in the sampling branch, check the fresh +# block is not still on its order's free list; (2) free - before returning, check the block is not +# already on that list; (3) free - no longer silently ignore invalid/dangling pointers (in-pool but +# even header); (4) existing [DET]/[MEM] exit paths add stdio.fflush(None). +define MEMBUDDY_DBG = 1 +# [MEMDBG] Runtime switch, default on (follows MEMBUDDY_DBG). A negative test (e.g. BuddyTest doing a +# deliberate double free, expecting a silent reject) can disable it at runtime: # import memhub # memhub._memdbg_enable = 0 _memdbg_enable: int = MEMBUDDY_DBG -# [锁] 线程安全编译期开关:默认 0 = 单线程(关闭自旋锁,_lock/_unlock 为空体)。 -# 自举编译等单线程场景下每次 alloc 省去一次原子 xchg + 一次原子 store -# (实测 _lock/_unlock/__atomic_test_and_set/__atomic_clear 各约 2026 万次, -# 且在单线程下为纯开销)。置 1 = 多线程(保留原有原子自旋加解锁)。 -# 选编译期(t.CDefine 常量)而非运行期开关的理由:常量比较在 HandleIf 处被 -# 常量折叠,未取分支整体不生成代码 ⇒ _lock/_unlock 退化为空函数,真正消除 -# 原子开销;运行期开关仍需每次分配做一次全局读 + 分支。既有 _memdbg_enable -# 用运行期是因为它要支持"同一次运行内切换"(负向用例),而线程模型在编译期 -# 即已固定。多线程使用 MemBuddy 必须置 1(语义与历史实现逐字节一致)。 -MEMBUDDY_THREAD_SAFE: t.CDefine = 0 +# [lock] Thread-safety compile-time switch: default 0 = single-threaded (the spinlock is a no-op +# body). Bootstrapping is single-threaded and each alloc otherwise pays one atomic xchg + one atomic +# store. Set 1 = multi-threaded (keeps the atomic spinlock). Compile-time is chosen because a +# constant comparison folds the branch away entirely, truly removing the atomic cost, whereas a +# runtime switch still needs a global read + branch per alloc. Multi-threaded MemBuddy use must +# set this to 1. +define MEMBUDDY_THREAD_SAFE = 0 -# 块尾 canary 常量(0xA5A5A5A5A5A5A5A5,64 位)——热点优化(实测):原 -# _canary_pattern() 函数被调用约 2658 万次(每次 alloc 写块尾 + check_alive_blocks -# 校验),改为 t.CDefine 后调用点直接内联为立即数,消除全部函数调用。 -# 值构造:lo = 0xA5A5A5A5(低 32 位,其高 32 位为 0);(lo << 32) 恰好填满高 32 位, -# 再 | lo 填低 32 位 ⇒ 0xA5A5A5A5A5A5A5A5,与原函数 (lo << 32) | lo 逐位相同。 -# 注意:不写十进制大字面量(11936128518282651045)——本仓库踩过"大十进制字面量 -# 被解析截断"的坑;此处沿用原函数的移位组合,由 ConstEvaluator 求值(值 -# >0x7FFFFFFF 时 _make_define_constant 升 i64,与原函数返回的 t.CUInt64T 同位宽)。 -MEMBUDDY_CANARY_PATTERN: t.CDefine = (0xA5A5A5A5 << 32) | 0xA5A5A5A5 +# Block-tail canary value (0xA5A5A5A5A5A5A5A5, 64-bit). A t.CDefine makes every use an immediate +# (the old _canary_pattern() call ran ~26.58M times). Built by shifting (lo << 32) | lo to avoid +# writing the large decimal literal this repo has seen truncated. +define MEMBUDDY_CANARY_PATTERN = (0xA5A5A5A5 << 32) | 0xA5A5A5A5 -# [FREEFRAME] 空闲块元数据印记(取证用;仅在 detection != 0 时写入/校验)。 +# [FREEFRAME] Free-block metadata stamps (forensics; written/checked only when detection != 0). # -# 存在理由:`_fl_pop` 的取值守卫只能把"空闲块首字被写穿"变成确定性报文,报文里的 -# 两个数(block / next)**不足以指名写入者**——next 常是非指针数据,说明有越界写/ -# 野指针写落进了空闲块首字。本机制把"哪个空闲块的哪几个字被改成什么、跨度多长" -# 记在空闲块自身,并在**释放点**把越界写归因到具体的分配块(= 写入者的所有者)。 +# Rationale: _fl_pop's value guard turns "a free block's first word was written through" into a +# deterministic report, but its two numbers (block / next) cannot name the writer (next is often +# non-pointer data). Stamps record "which words of which free block became what, over what span" in +# the block itself, and the release point attributes the overflow to a concrete allocation. # -# 空闲块布局(仅 detection != 0 时占用;空闲期字节 8..31 无其他用途——memhub 对空闲 -# 块只读写 word0): -# word0 = next 指针(既有语义,_fl_push/_fl_pop/_fl_find_and_remove 的原字段) -# word1 = MEMBUDDY_FREEFRAME_TAG ^ (order << 32) —— 阶印记 -# word2 = MEMBUDDY_FREEFRAME_TAG2 —— 第二校验字 -# word3 = word0 的镜像(next 副本) -# 判定:四字全为期望值 ⇒ 元数据完好;否则按"哪几个字不符"推断写穿跨度 -# (只 word0 不符 ⇒ 写入者写了 8..15 字节且起点在左邻块 canary 之后)。 -# 两常量均 < 2^63,避开本仓"大十进制字面量被解析截断"的坑(同 MEMBUDDY_CANARY_PATTERN)。 -MEMBUDDY_FREEFRAME_TAG: t.CDefine = 0x4652454500000000 -MEMBUDDY_FREEFRAME_TAG2: t.CDefine = 0x4652454600000000 +# Free-block layout (occupied only when detection != 0; bytes 8..31 are otherwise unused since +# memhub only reads/writes word0 of a free block): +# word0 = next pointer (existing semantics) +# word1 = MEMBUDDY_FREEFRAME_TAG ^ (order << 32) -- order stamp +# word2 = MEMBUDDY_FREEFRAME_TAG2 -- second check word +# word3 = mirror of word0 (next copy) +# All four equal to their expected values => intact; otherwise the number of mismatched words bounds +# the write span. Both constants are < 2^63, avoiding the large-literal truncation issue. +define MEMBUDDY_FREEFRAME_TAG = 0x4652454500000000 +define MEMBUDDY_FREEFRAME_TAG2 = 0x4652454600000000 -# [DETSITE] 调用点 tag 常量(稳定、可读):标识"某次 alloc 的申请点(文件:行)"。 -# 存在理由:记录表每条只有 {ptr, size},[FREEFRAME] 报文里"写入者 sz=32"只能落到 -# 尺寸档,无法收敛到具体代码点。alloc_tagged 在同一记录下标处登记一个 tag, -# 报文据此打印 site=文件名:行,直接指名写入者的申请点。 -# 取值稳定:固定常量,非随机/非时间;0 保留为"无 tag"(报文打 site=?,不伪造)。 -DETSITE_NONE: t.CDefine = 0 # 无 tag(alloc / 未登记) -DETSITE_STRUCTGENMIXIN_570: t.CDefine = 1 # StructGenMixin.vp:570 add_field 字段名拷贝 -DETSITE_STRUCTGENMIXIN_698: t.CDefine = 2 # StructGenMixin.vp:698 _deep_copy_list_ast elts 数据 -DETSITE_STRUCTGENMIXIN_1804: t.CDefine = 3 # StructGenMixin.vp:1804 跨模块 SHA1 缓冲 32B -DETSITE_CALLGENCTOR_1175: t.CDefine = 4 # CallGenCtor.vp:1175 泛型构造 extra_args -DETSITE_CALLGENCTOR_1186: t.CDefine = 5 # CallGenCtor.vp:1186 泛型构造 arg_filled -DETSITE_STRUCTGENMIXIN_641: t.CDefine = 6 # StructGenMixin.vp:641 _deep_copy_default_ast Constant 节点 -DETSITE_STRUCTGENMIXIN_659: t.CDefine = 7 # StructGenMixin.vp:659 _deep_copy_default_ast 字符串副本 -DETSITE_STRUCTGENMIXIN_693: t.CDefine = 8 # StructGenMixin.vp:693 _deep_copy_list_ast elts 容器 +# [DETSITE] Call-site tag constants (stable, readable): identify the site (file:line) of an alloc. +# A record holds only {ptr, size}, so a writer's size alone cannot pin the code point; alloc_tagged +# stores a tag at the same record index and the report prints site=file:line. Values are fixed (not +# random/time based); 0 means "no tag" (report prints site=?, never fabricated). +define DETSITE_NONE = 0 # no tag (plain alloc / not registered) +define DETSITE_STRUCTGENMIXIN_570 = 1 # StructGenMixin.vp:570 add_field field-name copy +define DETSITE_STRUCTGENMIXIN_698 = 2 # StructGenMixin.vp:698 _deep_copy_list_ast elts data +define DETSITE_STRUCTGENMIXIN_1804 = 3 # StructGenMixin.vp:1804 cross-module SHA1 buffer 32B +define DETSITE_CALLGENCTOR_1175 = 4 # CallGenCtor.vp:1175 generic ctor extra_args +define DETSITE_CALLGENCTOR_1186 = 5 # CallGenCtor.vp:1186 generic ctor arg_filled +define DETSITE_STRUCTGENMIXIN_641 = 6 # StructGenMixin.vp:641 _deep_copy_default_ast Constant node +define DETSITE_STRUCTGENMIXIN_659 = 7 # StructGenMixin.vp:659 _deep_copy_default_ast string copy +define DETSITE_STRUCTGENMIXIN_693 = 8 # StructGenMixin.vp:693 _deep_copy_list_ast elts container +define DETSITE_LIST_43 = 9 # _list.vp:43 list.__init__ element array capacity*elem_size +define DETSITE_STRUCTGENMIXIN_372 = 10 # StructGenMixin.vp:372 register_struct field array (permanent pool) +define DETSITE_STRUCTGENMIXIN_374 = 11 # StructGenMixin.vp:374 register_struct field array (main pool) +define DETSITE_LEAFKIT_1013 = 12 # CallGenLeafKit.vp:1013 global function table max_count*FuncEntry.__sizeof__() +define DETSITE_VARGEN_27 = 13 # VarGenMixin.vp:27 variable table MAX_VARS*VarEntry.__sizeof__() +define DETSITE_VPIREADER_2626 = 14 # VpiReader.vp:2626 vpi class field-name table 8*GenTypes.FIELD_MAX (per class) +define DETSITE_VPIREADER_2627 = 15 # VpiReader.vp:2627 vpi class field-type table 8*GenTypes.FIELD_MAX (per class) +define DETSITE_CLASSGENDEF_153 = 16 # ClassGenDef.vp:153 parent-name table 8*GenTypes.PARENT_MAX (per class) +define DETSITE_CLASSGENVTABLE_91 = 17 # ClassGenVTable.vp:91 vtable method-name table 8*VTBL_METHOD_MAX (per class, permanent pool) -# [DETSITE] 记录表 tag 的模块级平行表(不占 MemBuddy 字段、不改其字段布局): -# 第 i 条 = {ptr, tag}(2×u64),与 MemBuddy._det_records 同索引、同生命周期。 -# ptr 仅用于索引校验(索引错位或跨实例共用时不匹配 ⇒ 返回 0 ⇒ 报打 site=?)。 -# detection==0 时从不写入、从不读取(alloc_tagged 直接转发)。 -_det_site_arena: t.CUInt64T | t.CPtr = None +# [DETSITE] Module-level side table for record tags (no MemBuddy field, no layout change): +# entry i = {ptr, tag} (2×u64), same index and lifetime as MemBuddy._det_records. ptr is used only +# for index validation (a shifted index or cross-instance sharing mismatches -> 0 -> site=?). +# Never written/read when detection == 0 (alloc_tagged forwards directly). +_det_site_arena: uint64* = None _det_site_cap: sizet = 0 -def _det_site_put(idx: sizet, ptr: t.CUInt64T, tag: t.CUInt64T): - """登记第 idx 条记录的调用点 tag(懒扩容,翻倍)。 +def _det_site_put(idx: sizet, ptr: uint64, tag: uint64): + """Record the call-site tag for record idx (lazily doubling the table). - stdlib.malloc/realloc 失败时不登记(返回),报告端据此打 site=?—— - 不静默降级、不伪造 site。仅 detection != 0 时被调用。 + If stdlib.malloc/realloc fails nothing is recorded (return), so the report prints site=? -- + never a silent downgrade or a fabricated site. Called only when detection != 0. """ global _det_site_arena, _det_site_cap if idx >= _det_site_cap: @@ -184,45 +153,137 @@ def _det_site_put(idx: sizet, ptr: t.CUInt64T, tag: t.CUInt64T): new_ptr = t.CPtr(stdlib.realloc(t.CPtr(_det_site_arena), new_cap * 16)) if new_ptr is None: return - _det_site_arena = (t.CUInt64T | t.CPtr)(t.CPtr(new_ptr)) + _det_site_arena = (uint64*)(new_ptr) _det_site_cap = new_cap _det_site_arena[idx * 2] = ptr _det_site_arena[idx * 2 + 1] = tag -def _det_site_get(idx: sizet, ptr: t.CUInt64T) -> t.CUInt64T: - """取第 idx 条记录的 tag;越界或 ptr 不符(错位/跨实例)返回 0(= 无 tag)。""" +def _det_site_get(idx: sizet, ptr: uint64) -> uint64: + """Tag of record idx; out of range or ptr mismatch (shifted / cross-instance) returns 0.""" if _det_site_arena is None or idx >= _det_site_cap: return 0 - if t.CUInt64T(_det_site_arena[idx * 2]) != ptr: + if uint64(_det_site_arena[idx * 2]) != ptr: return 0 - return t.CUInt64T(_det_site_arena[idx * 2 + 1]) + return uint64(_det_site_arena[idx * 2 + 1]) -def _det_site_name(tag: t.CUInt64T) -> str: - """tag → "文件名:行" 注册表;未登记(含 0)返回 "?"(不伪造 site)。""" - if tag == t.CUInt64T(DETSITE_STRUCTGENMIXIN_570): +def _det_site_name(tag: uint64) -> str: + """tag -> "file:line" registry; unregistered tags (including 0) return "?".""" + if tag == uint64(DETSITE_STRUCTGENMIXIN_570): return "StructGenMixin.vp:570" - if tag == t.CUInt64T(DETSITE_STRUCTGENMIXIN_698): + if tag == uint64(DETSITE_STRUCTGENMIXIN_698): return "StructGenMixin.vp:698" - if tag == t.CUInt64T(DETSITE_STRUCTGENMIXIN_1804): + if tag == uint64(DETSITE_STRUCTGENMIXIN_1804): return "StructGenMixin.vp:1804" - if tag == t.CUInt64T(DETSITE_CALLGENCTOR_1175): + if tag == uint64(DETSITE_CALLGENCTOR_1175): return "CallGenCtor.vp:1175" - if tag == t.CUInt64T(DETSITE_CALLGENCTOR_1186): + if tag == uint64(DETSITE_CALLGENCTOR_1186): return "CallGenCtor.vp:1186" - if tag == t.CUInt64T(DETSITE_STRUCTGENMIXIN_641): + if tag == uint64(DETSITE_STRUCTGENMIXIN_641): return "StructGenMixin.vp:641" - if tag == t.CUInt64T(DETSITE_STRUCTGENMIXIN_659): + if tag == uint64(DETSITE_STRUCTGENMIXIN_659): return "StructGenMixin.vp:659" - if tag == t.CUInt64T(DETSITE_STRUCTGENMIXIN_693): + if tag == uint64(DETSITE_STRUCTGENMIXIN_693): return "StructGenMixin.vp:693" + if tag == uint64(DETSITE_LIST_43): + return "_list.vp:43" + if tag == uint64(DETSITE_STRUCTGENMIXIN_372): + return "StructGenMixin.vp:372" + if tag == uint64(DETSITE_STRUCTGENMIXIN_374): + return "StructGenMixin.vp:374" + if tag == uint64(DETSITE_LEAFKIT_1013): + return "CallGenLeafKit.vp:1013" + if tag == uint64(DETSITE_VARGEN_27): + return "VarGenMixin.vp:27" + if tag == uint64(DETSITE_VPIREADER_2626): + return "VpiReader.vp:2626" + if tag == uint64(DETSITE_VPIREADER_2627): + return "VpiReader.vp:2627" + if tag == uint64(DETSITE_CLASSGENDEF_153): + return "ClassGenDef.vp:153" + if tag == uint64(DETSITE_CLASSGENVTABLE_91): + return "ClassGenVTable.vp:91" return "?" -def _canary_pattern() -> t.CUInt64T: - """块尾 canary 值(0xA5A5A5A5A5A5A5A5,64 位)——统一引用编译期常量""" - return t.CUInt64T(MEMBUDDY_CANARY_PATTERN) +# [DETSITE] Tag of the alloc_tagged call site currently executing (0 = not an alloc_tagged path). +# alloc itself cannot see the call site but alloc_tagged knows its tag; this global lets both the +# zero-size and the allocation-failure guards print site=file:line directly. Registration is +# independent of detection (location is not detection data). +_alloc_pending_site: uint64 = 0 + +# Process-wide zero-size request counter: incremented only in the failure branch (never on the +# allocation hot path), to distinguish "first ever" from "repeatedly 0 from data". +_zero_alloc_hits: sizet = 0 + + +def _alloc_site_enter(tag: uint64) -> uint64: + """Enter an alloc_tagged call site: store tag and return the overridden previous tag. + + The caller restores the returned value after alloc returns, so nested alloc_tagged calls on the + same pool (or alternating across pools) do not overwrite each other's location. + """ + global _alloc_pending_site + prev: uint64 = _alloc_pending_site + _alloc_pending_site = tag + return prev + + +def _alloc_site_leave(prev: uint64): + """Leave an alloc_tagged call site: restore the tag from before entering.""" + global _alloc_pending_site + _alloc_pending_site = prev + + +def _alloc_site_current() -> uint64: + """Tag of the current alloc_tagged call site; 0 = unknown (report prints site=?).""" + return _alloc_pending_site + + +# free 被拒(_is_valid_ptr 判无效)只报一次:这类"静默 no-op"会让块永不回收, +# 历史上正是它把 1 GiB 主池漏满且毫无线索,故首次出现必须留下现场。 +_rej_free_reported: int = 0 + + +def _zero_alloc_note() -> sizet: + """Count one zero-size request and return the running total.""" + global _zero_alloc_hits + _zero_alloc_hits += 1 + return _zero_alloc_hits + + +# [CTX] Current context (stage name / current file or module name): set by TPV via SetAllocCtx at +# key stages, used only to attribute zero-size / allocation-failure reports. The two fields are kept +# separate (no concatenation, no allocation). None = not set => report prints ? (never fabricated). +_alloc_ctx_stage: bytes = None +_alloc_ctx_detail: bytes = None + + +def SetAllocCtx(stage: bytes, detail: bytes = None): + """Set the current context (TPV side); the caller keeps both strings alive for the stage.""" + global _alloc_ctx_stage, _alloc_ctx_detail + _alloc_ctx_stage = stage + _alloc_ctx_detail = detail + + +def _alloc_ctx_stage_str() -> bytes: + """Current stage name; "?" when unset.""" + if _alloc_ctx_stage is None: + return "?" + return _alloc_ctx_stage + + +def _alloc_ctx_detail_str() -> bytes: + """Current file/module name; "?" when unset.""" + if _alloc_ctx_detail is None: + return "?" + return _alloc_ctx_detail + + +def _canary_pattern() -> uint64: + """Block-tail canary value (64-bit) -- single reference to the compile-time constant.""" + return uint64(MEMBUDDY_CANARY_PATTERN) def _align_up(val: sizet, align: sizet) -> sizet: if align == 0: return val @@ -230,8 +291,8 @@ def _align_up(val: sizet, align: sizet) -> sizet: def _largest_pow2_le(val: sizet) -> sizet: - # 判据 `p <= val / 2` 与 `p * 2 <= val` 等价(整数除法),但不做乘法: - # 原写法在 val > 2^63 时 p * 2 回绕为 0 ⇒ `0 <= val` 恒真 ⇒ 死循环。 + # `p <= val / 2` equals `p * 2 <= val` (integer division) without multiplying: the original + # `p * 2` wraps to 0 for val > 2^63, making `0 <= val` always true (infinite loop). if val == 0: return 0 p: sizet = 1 while p <= (val >> 1): @@ -239,14 +300,13 @@ def _largest_pow2_le(val: sizet) -> sizet: return p -def _block_size_at_order(order: t.CInt) -> sizet: - """第 order 阶的块大小(等价于 MEMBUDDY_MIN_BLOCK << order) +def _block_size_at_order(order: int) -> sizet: + """Block size at `order` (equivalent to MEMBUDDY_MIN_BLOCK << order). - 热点优化(实测):本函数被调用约 1.008 亿次(占全程序调用 4.67%,第 7 名), - 原实现 `for i in range(order): bs = bs << 1` 每次做 order 次移位;语义上等价于 - 一次左移,故直接合成,把 O(order) 降为 O(1)。 - 边界:order <= 0 时返回最小块——与原 range() 空循环的行为完全一致, - 同时避免「负数移位」的未定义行为。 + Hot-path optimisation: called ~100.8M times; the original `for i in range(order): bs <<= 1` did + `order` shifts, equivalent to one left shift, so it is composed directly (O(order) -> O(1)). + Edge: order <= 0 returns the minimum block (identical to the old empty range loop) and avoids + the undefined negative shift. """ if order <= 0: return MEMBUDDY_MIN_BLOCK @@ -318,7 +378,7 @@ class MemManager: if data_ptr is None: return None buf.__before_init__() - buf.__init__(char(t.CUInt64T(data_ptr), t.CPtr), capacity) + buf.__init__(data_ptr, capacity) return buf @@ -336,12 +396,12 @@ class MemPool(MemManager): self.offset = 0 self.high_water = 0 - # 容量预检(纯判定:不移动 offset、不触发 exit(1))。 - # 返回 1 = 本次可分配;0 = 会耗尽(与 alloc 的判定逐条一致:size==0、 - # 同一 _align_up、同一 offset+aligned>size)。 - # 用途:调用方在"一次给足"的场景(如条目级 reset 复用的工作池)先自检, - # 把"池耗尽 ⇒ printf + exit(1)"转成可控失败。 - def AllocOk(self, size: sizet) -> t.CInt: + # Capacity pre-check (pure decision: does not move offset, does not exit(1)). + # Returns 1 = this alloc fits; 0 = it would exhaust (same criteria as alloc: size == 0, the + # same _align_up, the same offset+aligned > size). A caller that gives one big chunk at once + # (e.g. a per-entry reuse pool) can self-check and turn "exhausted => printf + exit(1)" into a + # controllable failure. + def AllocOk(self, size: sizet) -> int: if size == 0: return 0 aligned: sizet = _align_up(size, MEMHUB_ALIGN) @@ -349,15 +409,14 @@ class MemPool(MemManager): return 0 return 1 - # 池耗尽仍 fail-fast(printf 诊断 + exit(1)),本轮**不**改成"返回 None": - # * MemPool 是无释放的 bump 分配器; - # * 它的实例会经 `rawptr[memhub.MemManager]` 形参流入大量"不判 None"的调用方 - # —— includes/ast/**(AST 各节点 __new__,如 exprs.vp:23/astaux.vp:23)、 - # includes/llvmlite/**、includes/numpy/** 等(全仓 `.alloc(` 调用点 200+ 处); - # * 这些点若拿到 None 会从"清晰的 [OOM] 诊断 + 立即终止"退化为空指针解引用 - # (更难定位),且本轮无法编译/运行逐点补齐 None 判断。 - # 需要可控失败的新调用点请先用 AllocOk() 预检(对齐 MemBuddy 的 None 契约由 - # MemBuddy 提供;MemPool 的契约仍为"绝不返回 None,耗尽即终止")。 + # Exhaustion stays fail-fast (printf + exit(1)) rather than returning None: + # * MemPool is a free-less bump allocator; + # * its instances flow through `rawptr[memhub.MemManager]` parameters to many callers that do + # not check None (includes/ast/**, includes/llvmlite/**, includes/numpy/**, 200+ `.alloc(` + # sites), where None would regress a clear [OOM] diagnostic into a null dereference; + # * this round cannot compile/run to add per-site None checks. + # New callers needing a controllable failure should pre-check with AllocOk(). The None contract + # is provided by MemBuddy; MemPool's contract stays "never returns None, terminates on exhaustion". def alloc(self, size: sizet) -> t.CPtr: if size == 0: return None aligned: sizet = _align_up(size, MEMHUB_ALIGN) @@ -369,17 +428,17 @@ class MemPool(MemManager): stdio.printf("[OOM] 说明: 内存池已耗尽。可能原因: (1) 池大小不足,需增大 POOL_SIZE; (2) 存在内存泄漏。\n") stdio.printf("[OOM] 提示: 需要可控失败而非终止的调用方,请先用 MemPool.AllocOk(size) 预检。\n") stdlib.exit(1) - ptr: t.CPtr = t.CPtr(t.CUInt64T(self.base) + self.offset) + ptr: t.CPtr = self.base + self.offset self.offset += aligned if self.offset > self.high_water: self.high_water = self.offset return ptr - def free(self, ptr: t.CPtr) -> t.CInt: + def free(self, ptr: t.CPtr) -> int: # Bump allocator does not support individual free operations return 1 - def reset(self) -> t.CInt: + def reset(self) -> int: self.offset = 0 self.high_water = 0 return 1 @@ -388,30 +447,27 @@ class MemPool(MemManager): # ============================================================ # Heap-construction factories # -# 类构造表达式(`memhub.MemPool(arena, cap)`)的存储落在**调用者栈帧**上: -# 生成序列为 alloca → __before_init__ → __init__,见已落盘 IR -# VET/.tpv_build/temp/d4/d46198bb42ab7aec.text.ll:60/76/81 与 -# VET/output/d46198bb42ab7aec_dso.ll:2963/3005/3008。把该指针存进模块级 -# 变量或交给别的帧,持有者拿到的就是即将失效的栈地址。 +# A class-construction expression (`memhub.MemPool(arena, cap)`) places its storage on the CALLER's +# stack frame (alloca -> __before_init__ -> __init__); storing that pointer in a module-level +# variable or handing it to another frame leaves the holder a soon-dead stack address. # -# 需要跨帧存活的管理器用下面的工厂:先在 stdlib.malloc 的堆块上取存储, -# 再就地构造。两步各自的作用: -# * __before_init__ 写 vtable 指针(如 MemPool_Vtable,见 -# VET/output/c1ba4395169f2768.text.ll:345-350)并把字段清零/置默认值; -# * __init__ 填充 base/size/offset/high_water 等构造参数。 -# 构造完成后,经 MemManager 基类指针的虚调用(alloc/free/reset/realloc/ -# calloc/AllocOk)按 vtable 命中子类实现。 +# Managers that must outlive a frame are built by the factories below: take storage from a +# stdlib.malloc heap block, then construct in place. The two steps: +# * __before_init__ writes the vtable pointer and zeroes/defaults the fields; +# * __init__ fills base/size/offset/high_water. +# Afterwards, virtual calls through a MemManager base pointer (alloc/free/reset/realloc/calloc/ +# AllocOk) hit the subclass implementation via the vtable. # -# 用法(MemBuddy 先例:TransPyV/Test/App/nested_match_test.vp:33-38): +# Usage (MemBuddy precedent): # p: rawptr[memhub.MemPool] = memhub.NewMemPool(arena, cap) # if p is None: ... # -# 生命周期:管理器对象由 stdlib.malloc 分配,与 arena 同属进程生命周期; -# 常驻用途(进程内复用的工作池)不 free。 +# Lifetime: the manager object is stdlib.malloc'd and shares the arena's process lifetime; +# long-lived (per-process reuse) pools are not freed. # ============================================================ -# 在堆上构造 MemPool(arena bump 分配器)。base 为 arena 首地址,size 为其字节数。 -# 返回 None 表示 malloc 失败。 +# Construct a MemPool (arena bump allocator) on the heap. base = arena start, size = its byte count. +# Returns None when malloc fails. def NewMemPool(base: t.CPtr, size: sizet) -> rawptr[MemPool]: obj: rawptr[MemPool] = stdlib.malloc(MemPool.__sizeof__()) if obj is None: @@ -421,6 +477,20 @@ def NewMemPool(base: t.CPtr, size: sizet) -> rawptr[MemPool]: return obj +# [DETSITE] Tagged allocation for any MemManager: lets a caller that only holds a base pointer +# (MemManager*) register its site. It only sets _alloc_pending_site (location, independent of +# detection) and then calls the existing virtual alloc => no new virtual function, no vtable change, +# no contract change; for a MemBuddy pool its alloc_tagged is also available (that one additionally +# records a record-table tag for [FREEFRAME] reports). +def MemAllocTagged(pool: rawptr[MemManager], size: sizet, tag: sizet) -> t.CPtr: + if pool is None: + return None + prev: uint64 = _alloc_site_enter(uint64(tag)) + r: t.CPtr = pool.alloc(size) + _alloc_site_leave(prev) + return r + + # ============================================================ # MemSlab - Fixed-size block pool # Bitmap stored at the start of arena; subsequent area split into block_size units managed by free list @@ -430,7 +500,7 @@ class MemSlab(MemManager): block_count: sizet # Total block count used_count: sizet # Used block count free_list: t.CPtr # Free list head - alloc_map: t.CUInt8T | t.CPtr # Allocation bitmap (at start of arena) + alloc_map: uint8* # Allocation bitmap (at start of arena) alloc_map_size: sizet # Bitmap byte count usable: t.CPtr # Usable block area start (after bitmap) usable_size: sizet # Usable block area size @@ -457,7 +527,7 @@ class MemSlab(MemManager): self.alloc_map = base self.alloc_map_size = map_bytes - self.usable = t.CPtr(t.CUInt64T(base) + map_bytes) + self.usable = base + map_bytes self.usable_size = size - map_bytes self.block_size = bs @@ -474,8 +544,8 @@ class MemSlab(MemManager): self.free_list = None i: sizet = 0 while i < self.block_count: - block: t.CPtr = t.CPtr(t.CUInt64T(self.usable) + i * bs) - c.DerefAs(block, self.free_list) + block: t.CPtr = self.usable + i * bs + *((uint64*)(block)) = uint64(self.free_list) self.free_list = block i += 1 @@ -489,41 +559,41 @@ class MemSlab(MemManager): stdio.printf("[OOM] 说明: 内存池已耗尽。可能原因: (1) 池大小不足,需增大 POOL_SIZE; (2) 存在内存泄漏。\n") stdlib.exit(1) if size > self.block_size: - # 超块大小请求 = 调用方错误:优雅失败返回 NULL(类 malloc 语义,调用方自行判断)。 - # 与 free_list 耗尽(真正的 OOM,打印后 exit(1))区分开。 + # Request larger than a block = caller error: fail gracefully with NULL (malloc-like, + # caller decides), distinct from free-list exhaustion (real OOM, prints then exit(1)). stdio.printf("[WARN] MemSlab: 请求 %zu 字节超过 block_size=%zu,分配失败返回 NULL;应改用 MemBuddy/MemPool。\n", size, self.block_size) return None block: t.CPtr = self.free_list - self.free_list = t.CPtr(c.Deref((t.CUInt64T | t.CPtr)(block))) + self.free_list = t.CPtr(*((uint64*)(block))) self.used_count += 1 - idx: sizet = (t.CUInt64T(block) - t.CUInt64T(self.usable)) / self.block_size - self.alloc_map[idx / 8] = self.alloc_map[idx / 8] | t.CUInt8T(1 << (idx % 8)) + idx: sizet = (uint64(block) - uint64(self.usable)) / self.block_size + self.alloc_map[idx / 8] = self.alloc_map[idx / 8] | uint8(1 << (idx % 8)) return block - def free(self, ptr: t.CPtr) -> t.CInt: + def free(self, ptr: t.CPtr) -> int: if ptr is None: return 0 - p: t.CUInt64T = t.CUInt64T(ptr) - if p < t.CUInt64T(self.usable): return 0 - if p >= t.CUInt64T(self.usable) + self.block_count * self.block_size: return 0 - if (p - t.CUInt64T(self.usable)) % self.block_size != 0: return 0 - idx: sizet = (p - t.CUInt64T(self.usable)) / self.block_size + p: uint64 = uint64(ptr) + if p < uint64(self.usable): return 0 + if p >= uint64(self.usable) + self.block_count * self.block_size: return 0 + if (p - uint64(self.usable)) % self.block_size != 0: return 0 + idx: sizet = (p - uint64(self.usable)) / self.block_size byte_idx: sizet = idx / 8 - bit_idx: t.CInt = t.CInt(idx % 8) + bit_idx: int = int(idx % 8) if (self.alloc_map[byte_idx] >> bit_idx) & 1 == 0: return 0 - self.alloc_map[byte_idx] = self.alloc_map[byte_idx] & t.CUInt8T(~(1 << bit_idx)) - c.DerefAs(ptr, self.free_list) + self.alloc_map[byte_idx] = self.alloc_map[byte_idx] & uint8(~(1 << bit_idx)) + *((uint64*)(ptr)) = uint64(self.free_list) self.free_list = ptr self.used_count -= 1 return 1 - def reset(self) -> t.CInt: + def reset(self) -> int: self.used_count = 0 self.free_list = None i: sizet = 0 while i < self.block_count: - block: t.CPtr = t.CPtr(t.CUInt64T(self.usable) + i * self.block_size) - c.DerefAs(block, self.free_list) + block: t.CPtr = self.usable + i * self.block_size + *((uint64*)(block)) = uint64(self.free_list) self.free_list = block if i / 8 < self.alloc_map_size: self.alloc_map[i / 8] = 0 @@ -533,55 +603,53 @@ class MemSlab(MemManager): # ============================================================ # MemBuddy - Buddy system allocator -# Free list head array stored at arena start; subsequent memory managed in power-of-two sized blocks -# Thread-safety is compile-time gated by MEMBUDDY_THREAD_SAFE:0=单线程默认(关闭自旋锁, -# 消除每次分配一次的原子加解锁开销);多线程使用必须置 1(保留原子自旋)。 -# supports block coalescing +# The free-list head array sits at the arena start; the rest is managed in power-of-two blocks +# with coalescing. Thread-safety is compile-time gated by MEMBUDDY_THREAD_SAFE (0 = single-threaded +# default with the spinlock compiled out; multi-threaded use must set 1). # -# Checkpoint 支持(供编译器等长任务回收阶段性分配): -# mark() → 返回当前分配水位(不分配内存) -# reset_to(m) → 释放水位 m 之后分配的所有块(回收到 free list) -# 记录栈从 usable 顶部预留 _CHK_RESERVE 字节,不参与 buddy 分配。 +# Checkpoint support (reclaim staged allocations in long tasks such as the compiler): +# mark() -> returns the current allocation watermark (allocates nothing) +# reset_to(m) -> frees every block allocated after watermark m (back onto the free list) +# The record stack is allocated from stdlib.malloc, separate from the buddy area. # ============================================================ class MemBuddy(MemManager): # Override parent __provides__: MemBuddy provides both __mbuddy__ and __memmgr__. - # Within `with MemBuddy(...)` context, classes (e.g. _str) with __requires__=['__mbuddy__'] - # can be automatically injected via _find_provider + # Within a `with MemBuddy(...)` context, classes (e.g. _str) with __requires__=['__mbuddy__'] + # can be injected automatically via _find_provider. __provides__: list[str] = ['__mbuddy__', '__memmgr__'] - max_order: t.CInt # Maximum order - free_lists: t.CUInt64T | t.CPtr # Free list head array (located at arena start) - lock_val: t.CVolatile | t.CInt # Spinlock - usable: t.CPtr # Start of usable memory region (skips free_lists) - usable_size: sizet # Size of usable memory (power of two) - chk_stack: t.CUInt64T | t.CPtr # Checkpoint 分配记录栈(存 block 地址) - chk_cap: sizet # 记录栈容量(条目数) - chk_depth: sizet # 当前记录深度 - chk_base: t.CPtr # 记录栈起始地址(由 stdlib.malloc 分配,独立于 buddy 区) - big_alloc_count: sizet # ≥8KB 大分配计数(OOM 诊断:定位大缓冲反复分配) - # [DET] 分配检测(默认关,排查越界写/存活块覆盖时开启): - # detection=1 → alloc 前后 2 字节 0xCDCD 标记 + 记录表 + 每次 alloc 后探测 - detection: t.CInt = 0 - # 运行期重诊断开关(命令行 --detection-deep → main.vp strstr → 构造传参): - # deep_detect=1 ⇒ 忽略 MEMBUDDY_DET_PROBE_PERIOD(等价 1,每次 alloc 都探测) - # + 打开 MEMBUDDY_PERIOD_SCAN 的周期全表扫描。仅诊断用,极慢。 - # 默认 0 = 与历史实现完全一致(范式同上方 detection 字段)。 - deep_detect: t.CInt = 0 + max_order: int # maximum order + free_lists: uint64* # free-list head array (at the arena start) + lock_val: t.CVolatile | int # spinlock + usable: t.CPtr # start of usable memory (skips free_lists) + usable_size: sizet # usable memory size (sum of decomposed blocks) + chk_stack: uint64* # checkpoint record stack (stores block addresses) + chk_cap: sizet # record-stack capacity (entries) + chk_depth: sizet # current record depth + chk_base: t.CPtr # record-stack base (stdlib.malloc, separate from the buddy area) + big_alloc_count: sizet # count of allocations >= 8KB (OOM diagnostics) + # [DET] Allocation detection (off by default; enable to hunt overflows / live-block overwrites): + # detection=1 -> 2-byte 0xCDCD markers + record table + a probe after each alloc + detection: int = 0 + # Deep-diagnostic switch (--detection-deep -> constructor argument): deep_detect=1 ignores + # MEMBUDDY_DET_PROBE_PERIOD (behaves as 1, probing every alloc) and enables the periodic + # full-table scan. Diagnostics only, very slow. Default 0 = identical to the historical build. + deep_detect: int = 0 detection_size: sizet = MEMBUDDY_DET_RECORD_SIZE - _det_arena: t.CPtr = None # 记录区基址(stdlib.malloc,供 realloc 扩容) - _det_records: t.CUInt64T | t.CPtr = None # 记录表(每 2×u64 = 1 条:{ptr, size}) - _det_count: sizet = 0 # 已记录条数 - _det_cap: sizet = 0 # 记录容量(条) + _det_arena: t.CPtr = None # record-area base (stdlib.malloc, for realloc growth) + _det_records: uint64* = None # record table (each 2×u64 = {ptr, size}) + _det_count: sizet = 0 # number of records + _det_cap: sizet = 0 # record capacity (entries) def __init__(self, base: t.CPtr, size: sizet, - detection: t.CInt = 0, deep_detect: t.CInt = 0): + detection: int = 0, deep_detect: int = 0): self.base = base self.size = size self.detection = detection - # 默认形参(范式同上 detection):既有全部构造点(2 参 / 3 参)无需改动 + # Default argument (same pattern as detection): existing 2-arg / 3-arg construction sites + # need no change. self.deep_detect = deep_detect - # 显式赋值(不能依赖字段默认值——TPC 类字段默认值对 CDefine 常量 - # 可能不生效,垃圾 detection_size → malloc(垃圾) 1-byte 记录区 → - # 扩容 realloc double-free,ASAN 已捕获) + # Explicit assignment: do not rely on field defaults for a CDefine constant (a garbage + # detection_size would malloc a 1-byte record area and then realloc double-free). self.detection_size = MEMBUDDY_DET_RECORD_SIZE self._det_arena = None self._det_records = None @@ -603,7 +671,7 @@ class MemBuddy(MemManager): self.free_lists = base # Initialize all free list heads to be 0 - i: t.CInt + i: int for i in range(MEMBUDDY_MAX_ORDERS + 1): self.free_lists[i] = 0 @@ -615,15 +683,9 @@ class MemBuddy(MemManager): remaining: sizet = size - fl_bytes - # 注意:不能在 __init__ 里预留 checkpoint 记录栈! - # 记录栈改为 mark() 时从 buddy 池懒分配(见 mark())。 - # - # 非 2 的幂 arena 支持:主池 1.2GB(非 2 幂)不再被 - # _largest_pow2_le 砍成 1GB 浪费 0.2GB。整个区域按二进制 - # 分解拆成若干个 2 的幂空闲块压入对应 free list;buddy 合并 - # 按 offset^bs 匹配,跨分解边界的块因 order 不匹配自然停止 - # 合并,不会破坏 buddy 一致性。remaining 恰为 2 的幂时分解 - # 结果与原实现(单个 max_order 块)完全等价。 + # The checkpoint stack is not reserved here; mark() lazily allocates it from the buddy pool. + # Non-power-of-two arenas are split by binary decomposition into power-of-two free blocks; + # buddy coalescing stops at decomposition boundaries because the orders no longer match. max_blk: sizet = _largest_pow2_le(remaining) if max_blk < MEMBUDDY_MIN_BLOCK: self.usable = None @@ -631,57 +693,56 @@ class MemBuddy(MemManager): self.max_order = 0 return - self.usable = t.CPtr(t.CUInt64T(base) + fl_bytes) + self.usable = base + fl_bytes - # 计算 max_order: log2(max_blk / MIN_BLOCK) + # max_order = log2(max_blk / MIN_BLOCK) self.max_order = 0 bs: sizet = MEMBUDDY_MIN_BLOCK while bs < max_blk: bs = bs << 1 self.max_order += 1 - # 二进制分解:把 [usable, usable+remaining) 拆成 2 的幂空闲块。 - # 尾部碎片(< MIN_BLOCK)不入 free list;usable_size 记实际 - # 可分配字节(used/free 统计不含碎片)。 + # Binary decomposition: split [usable, usable+remaining) into power-of-two free blocks. + # The tail fragment (< MIN_BLOCK) is not pushed; usable_size counts only real blocks. pos: sizet = 0 rem: sizet = remaining used_area: sizet = 0 while rem >= MEMBUDDY_MIN_BLOCK: blk: sizet = _largest_pow2_le(rem) - o: t.CInt = 0 + o: int = 0 ob: sizet = MEMBUDDY_MIN_BLOCK while ob < blk: ob = ob << 1 o += 1 - self._fl_push(o, t.CPtr(t.CUInt64T(self.usable) + pos)) + self._fl_push(o, self.usable + pos) pos += blk used_area += blk rem -= blk self.usable_size = used_area - # 分配记录区:stdlib.malloc 独立分配(不占用 buddy 区, - # 与 chk_stack 同策略)。容量 = detection_size // 16(每条 {ptr,size})。 - # 2026-09-04:记录+重叠检测从 detection 解耦——非 detection 也分配 - # 记录区并启用同地址重复分配检测(不做前后标记、不改 alloc 布局), - # 定位 free-list/重复分配类损坏(memhub 翻译早期容器 Count 槽被写坏)。 + # Allocate the record area separately with stdlib.malloc (not from the buddy area, same + # policy as chk_stack). Capacity = detection_size // 16 (each entry {ptr, size}). + # Records + overlap detection are decoupled from detection: even without it the record area + # is allocated and same-address duplicate-allocation detection is on (no markers, no layout + # change), which locates free-list / duplicate-allocation corruption. det_arena: t.CPtr = t.CPtr(stdlib.malloc(self.detection_size)) if det_arena is not None: self._det_arena = det_arena - self._det_records = (t.CUInt64T | t.CPtr)(det_arena) + self._det_records = (uint64*)(det_arena) self._det_cap = self.detection_size // 16 self._det_count = 0 # === Checkpoint support === def mark(self) -> sizet: - """返回当前分配水位(checkpoint) + """Return the current allocation watermark (checkpoint). - 记录栈用 stdlib.malloc 分配(独立于 buddy 区,避免占用 buddy 池、 - 避免 reset_to 误释放栈自身,也避免栈内存与 buddy 块重叠)。 - 初始容量 2M 条目(16MB):单文件翻译(parse AST + 生成 IR)可产生 - 几十万次 alloc,64K 初始容量会在首个大文件就触发 realloc 扩容, - 扩容失败时块不再记录 → reset_to 永久 no-op → 所有分配泄漏 → - 1GB 主池 OOM(NewAstTest Phase2 根因)。 + The record stack is stdlib.malloc'd (independent of the buddy area, so it neither consumes + the pool nor risks reset_to freeing it or overlapping a buddy block). Initial capacity is + 2M entries (16 MB): translating a single file can produce hundreds of thousands of allocs, + so a 64K initial capacity would grow on the first big file, and a failed growth would stop + recording blocks -> reset_to becomes a permanent no-op -> all allocations leak -> 1 GB main + pool OOM. """ if self.chk_stack is None: cap: sizet = 2 * 1024 * 1024 @@ -689,22 +750,23 @@ class MemBuddy(MemManager): if base is None: return 0 self.chk_base = base - self.chk_stack = (t.CUInt64T | t.CPtr)(base) + self.chk_stack = (uint64*)(base) self.chk_cap = cap self.chk_depth = 0 return self.chk_depth def reset_to(self, mark: sizet): - """释放水位 mark 之后分配的所有块,回收到 free list + """Free every block allocated after watermark `mark`, returning them to the free list. - 栈中每个 block:若 header 仍标记为已分配(奇数),_coalesce 归还; - 若已被单独 free(header==0),跳过避免重复归还。 + Each recorded block: if its header is still marked allocated (odd), _coalesce returns it; + if it was already freed individually (header == 0), skip to avoid a duplicate return. """ if self.chk_stack is None: return if mark > self.chk_depth: - # mark 大于当前深度:调用方 mark 值异常(可能读错字段/跨实例), - # reset 将 no-op → 之后分配全泄漏 → 1GB 主池 OOM。fail-fast 暴露。 + # mark beyond the current depth: the caller's mark is bogus (wrong field / cross- + # instance), which makes reset a no-op and leaks every later allocation (1 GB OOM). + # fail-fast. stdio.printf( "[MEM] reset_to mark=%zu > chk_depth=%zu(mark 异常,reset 失效,终止)\n", mark, self.chk_depth) @@ -717,144 +779,132 @@ class MemBuddy(MemManager): block: t.CPtr = t.CPtr(self.chk_stack[self.chk_depth]) if block is None: continue - stored: t.CPtr = t.CPtr(c.Deref(t.CUInt64T(block, t.CPtr))) - stored_val: t.CUInt64T = t.CUInt64T(stored) + stored: t.CPtr = t.CPtr(*((uint64*)(block))) + stored_val: uint64 = uint64(stored) if (stored_val & 1) == 0: - continue # 已被单独 free,跳过 - order: t.CInt = t.CInt(stored_val >> 1) + continue # already freed individually: skip + order: int = int(stored_val >> 1) if order < 0 or order > self.max_order: continue - # [FREEFRAME] 归还前所有者检查(仅 detection):回收块是"所有者已知"的 - # 释放点,若其块尾 canary 已被写穿 ⇒ 它的所有者就是写穿右邻空闲块首字的 - # 写入者。detection == 0 时该调用立即返回(无额外内存访问)。 + # [FREEFRAME] Pre-free owner check (detection only): a reclaimed block is an owner-known + # release point, so a broken tail canary means its owner is the writer that ran into the + # right free block's first word. Returns immediately when detection == 0. self._ff_owner_check(block, order) self._coalesce(block, order) - # [DET] reset-aware:记录表与 chk 栈等长(每次 alloc 各追加一条, - # free 只置标志不删),截断 chk_depth 必须同步截断 _det_count, - # 否则 reset 后旧记录仍标 active → 下一轮 alloc 复用同一块被 - # 误报为"分配重叠/越界写"(SlotsBugTest 7B buf + 72B Token 误报根因)。 - # 2026-09-04 记录表已与 detection 解耦(detection=0 也启用记录+重叠 - # 检测),截断条件不能挂 detection——否则检测模式下 reset_to 后 - # 旧记录残留 active,复用块被误报重叠(记录[647] sz=72 误报例)。 + # [DET] reset-aware: the record table and chk stack have equal length (each alloc appends + # one, free only sets a flag), so truncating chk_depth must truncate _det_count too; + # otherwise old records stay "active" and a reused block is misreported as an overlap. + # The record table is decoupled from detection, so this condition must not be gated on it. if self._det_records is not None: self._det_count = mark self._unlock() def reset_all(self): - """回收全部已分配块(等价 reset_to(0))""" + """Free every allocated block (equivalent to reset_to(0)).""" if self.chk_stack is None: return self.reset_to(0) # === Free list operations === - def _fl_push(self, order: t.CInt, block: t.CPtr): - old_head: t.CUInt64T = self.free_lists[order] - c.DerefAs(block, t.CPtr(old_head)) - self.free_lists[order] = t.CUInt64T(block) - # [FREEFRAME] detection 模式下补写空闲块印记(word1..3),供 _fl_pop 出块前 - # 校验。detection == 0 时只多一次可预测未命中的分支(零内存写),正常路径 - # 逐字节等价。本函数是**唯一**的"块进空闲链"写点,故印记覆盖全部入链路径。 + def _fl_push(self, order: int, block: t.CPtr): + old_head: uint64 = self.free_lists[order] + *((uint64*)(block)) = old_head + self.free_lists[order] = uint64(block) + # [FREEFRAME] In detection mode also stamp the free block (words 1..3), checked by + # _fl_pop before popping. With detection == 0 this is one predictable branch and no + # memory write. This is the ONLY "block enters the free list" write site, so the stamps + # cover every path onto the list. if self.detection != 0: self._ff_push_mark(order, block, old_head) - def _fl_block_ok(self, block: t.CPtr) -> t.CInt: - """free list 块地址取值校验:必须落在 [usable, usable+usable_size) 且按 - MEMBUDDY_MIN_BLOCK 对齐(与 self_check 的"范围 + 对齐"同口径)。 + def _fl_block_ok(self, block: t.CPtr) -> int: + """Validate a free-list block address: it must lie in [usable, usable+usable_size) and + be aligned to MEMBUDDY_MIN_BLOCK (same range+alignment criteria as self_check). - 与 _is_valid_ptr 的区别:本函数校验的是**空闲块**,其块头为 0(alloc 时 - 才写 (order<<1)|1),故不能沿用 _is_valid_ptr 的"header 必须为奇数"判据。 - 供 _fl_pop 在解引用前判定链表头 / next 指针是否已被写穿成垃圾值。 + Unlike _is_valid_ptr, this checks a FREE block whose header is 0 (allocated blocks write + (order<<1)|1), so the "header must be odd" test does not apply. Used by _fl_pop to reject + a list head / next pointer overwritten with garbage before dereferencing it. """ if block is None: return 0 if self.usable is None: return 0 - if t.CUInt64T(block) < t.CUInt64T(self.usable): + if uint64(block) < uint64(self.usable): return 0 - if t.CUInt64T(block) >= t.CUInt64T(self.usable) + self.usable_size: + if uint64(block) >= uint64(self.usable) + self.usable_size: return 0 - if (t.CUInt64T(block) - t.CUInt64T(self.usable)) % MEMBUDDY_MIN_BLOCK != 0: + if (uint64(block) - uint64(self.usable)) % MEMBUDDY_MIN_BLOCK != 0: return 0 return 1 - # === [FREEFRAME] 空闲块印记:取证(只读/只写诊断字,仅 detection 模式) === + # === [FREEFRAME] free-block stamps: forensics (read/write diagnostic words; detection only) === - def _ff_word(self, block: t.CPtr, idx: t.CInt) -> t.CUInt64T: - """读空闲块第 idx 个 8 字节字(0=next 指针,1=阶印记,2=校验字,3=next 镜像)""" - p: t.CUInt64T | t.CPtr = (t.CUInt64T | t.CPtr)( - t.CPtr(t.CUInt64T(block) + t.CUInt64T(idx) * 8)) - return p[0] + def _ff_word(self, block: t.CPtr, idx: int) -> uint64: + """Read word idx of a free block (0=next, 1=order stamp, 2=check word, 3=next mirror).""" + return (uint64*)(block)[idx] - def _ff_word_set(self, block: t.CPtr, idx: t.CInt, val: t.CUInt64T): - """写空闲块第 idx 个 8 字节字(仅诊断字;不触碰 word0 的既有语义)""" - p: t.CUInt64T | t.CPtr = (t.CUInt64T | t.CPtr)( - t.CPtr(t.CUInt64T(block) + t.CUInt64T(idx) * 8)) - p[0] = val + def _ff_word_set(self, block: t.CPtr, idx: int, val: uint64): + """Write word idx of a free block (diagnostic words only; word0 is never touched).""" + p: uint64* = (uint64*)(block) + p[idx] = val - def _ff_stamp(self, order: t.CInt) -> t.CUInt64T: - """第 order 阶空闲块的阶印记 = TAG ^ (order << 32)(order 可从印记反解)""" - return t.CUInt64T(MEMBUDDY_FREEFRAME_TAG) ^ (t.CUInt64T(order) << 32) + def _ff_stamp(self, order: int) -> uint64: + """Order stamp of a free block of `order` = TAG ^ (order << 32) (order is recoverable).""" + return uint64(MEMBUDDY_FREEFRAME_TAG) ^ (uint64(order) << 32) - def _ff_push_mark(self, order: t.CInt, block: t.CPtr, next_val: t.CUInt64T): - """入链后写印记(word1..3)。调用方已判 detection != 0,且已写 word0=next_val。 + def _ff_push_mark(self, order: int, block: t.CPtr, next_val: uint64): + """Stamp words 1..3 after linking. The caller has already checked detection != 0 and + written word0 = next_val. - 只被 _fl_push(唯一的"块进空闲链"写点)与 _fl_find_and_remove 的中间摘除 - (改 prev 的 next 后同步镜像)调用,故"空闲块在链上"期间 word1..3 恒等于 - 期望值——任何偏离都只可能来自链外写入(越界写/野指针写)。 + Called only by _fl_push (the sole link-in site) and by _fl_find_and_remove's middle + unlink (mirror sync after rewriting prev's next), so while a block is on the list words + 1..3 always equal the expected values; any deviation can only come from an off-list + write (overflow / wild pointer). """ self._ff_word_set(block, 1, self._ff_stamp(order)) - self._ff_word_set(block, 2, t.CUInt64T(MEMBUDDY_FREEFRAME_TAG2)) + self._ff_word_set(block, 2, uint64(MEMBUDDY_FREEFRAME_TAG2)) self._ff_word_set(block, 3, next_val) - def _ff_report(self, order: t.CInt, block: t.CPtr): - """[FREEFRAME] 取证转储(调用方随后 fflush + exit(1))。 + def _ff_report(self, order: int, block: t.CPtr): + """[FREEFRAME] forensic dump (the caller then fflush + exit(1)). - 三块内容,构成"指名写入者"的证据: - 1. 空闲块四字的实测值 + 期望值 ⇒ 被写穿的字节区间(跨度); - 2. 左邻探测:对每个阶 o 取 L = A - (MIN_BLOCK<>1) == o,则 L 就是"块尾正好落在 A"的分配块(**连续越界**的唯一 - 可能起点),再校验 L 的块尾 canary:损坏者**可能**是越界写来源——几何 + - canary 本身**不足以**断言归属,故补 `_ff_left_classify` 的证据化判据; - 3. 记录表近邻(|ptr-A| <= 256):该区域分配过哪些对象、各多大, - 据此反查写入者的分配尺寸与调用点。 + Three parts that together name the writer: + 1. the free block's four words (actual vs expected) => the overwritten byte span; + 2. left-neighbour probe: for each order o take L = A - (MIN_BLOCK<>1) == o then L is the allocation whose tail lands exactly on A (the + only possible contiguous-overflow origin); its tail canary is checked, but geometry + plus canary alone cannot attribute, hence the evidence-based _ff_left_classify; + 3. nearby records (|ptr-A| <= 256): what was allocated around this area and how big, to + trace back the writer's size and call site. """ - w0: t.CUInt64T = self._ff_word(block, 0) - w1: t.CUInt64T = self._ff_word(block, 1) - w2: t.CUInt64T = self._ff_word(block, 2) - w3: t.CUInt64T = self._ff_word(block, 3) - exp1: t.CUInt64T = self._ff_stamp(order) - exp2: t.CUInt64T = t.CUInt64T(MEMBUDDY_FREEFRAME_TAG2) + w0: uint64 = self._ff_word(block, 0) + w1: uint64 = self._ff_word(block, 1) + w2: uint64 = self._ff_word(block, 2) + w3: uint64 = self._ff_word(block, 3) + exp1: uint64 = self._ff_stamp(order) + exp2: uint64 = uint64(MEMBUDDY_FREEFRAME_TAG2) stdio.printf("[FREEFRAME] 空闲块四字: block=%llu order=%d w0(next)=%llu w1=%llu(期望=0x%llx) w2=%llu(期望=0x%llx) w3(next镜像)=%llu\n", - t.CUInt64T(block), order, w0, w1, exp1, w2, exp2, w3) - dev_n: t.CInt = 0 + uint64(block), order, w0, w1, exp1, w2, exp2, w3) + dev_n: int = 0 if w3 != w0: dev_n += 1 if w1 != exp1: dev_n += 1 if w2 != exp2: dev_n += 1 - # 跨度推断(通用式,替换原"只覆盖 n=1 / n=3"的两点枚举——实测 n=2 落空档): - # dev_n 只约束"本空闲块内至少被连续写穿的 n 个 8 字节字",不能反推唯一长度, - # 只能给区间。frame-only(仅本块内被改的字): - # 写入跨度 ∈ [8*n, 8*(n+1)-1] 字节 - # n=1 ⇒ 8..15(起点落本块之前的 8 字节内);n=2 ⇒ 16..23;n=3 ⇒ >=24。 - # 若左邻块的块尾 canary 亦被写穿(见下方"左邻探测"行),说明同一次连续写从 - # 本块之前 8 字节起步 ⇒ 跨度再前移一个字(起点 = canary 处): - # 含 canary 跨度 ∈ [8*(n+1), 8*(n+2)-1] 字节(n=2 ⇒ 24..31)。 - # 起点判定(几何):free-frame 的 4 个字在偏移 0/8/16/24,一律以本块基址为界; - # 写入者只要覆盖偏移 0 就落在本块首字(word0),故"起点落在本块首字之前"由 - # 左邻 canary 是否被改写给出(canary 在本块之前 8 字节)。 + # Span inference: dev_n only bounds "how many 8-byte words inside THIS block were + # overwritten", so only a range is derivable: block-only span = [8*n, 8*(n+1)-1] bytes; + # if the left neighbour's tail canary is also broken the same run started 8 bytes earlier, + # shifting the span to [8*(n+1), 8*(n+2)-1]. The four stamped words sit at offsets + # 0/8/16/24, so a run reaching offset 0 covers word0. lo_f: sizet = sizet(dev_n) * 8 hi_f: sizet = (sizet(dev_n) + 1) * 8 - 1 stdio.printf("[FREEFRAME] 跨度推断: 不符字数=%d/3 ⇒ 本空闲块内写入跨度 ∈ [%zu, %zu] 字节(通用式 [8n, 8n+7],n=不符字数;n=1⇒8..15, n=2⇒16..23, n=3⇒>=24);含左邻 canary 则 ∈ [%zu, %zu]\n", dev_n, lo_f, hi_f, lo_f + 8, hi_f + 8) - # w1 / w3 成因判定(代码证据,见 _fl_push 注释:本文件的"块进空闲链"写点唯一): - # * 入链唯一入口 _fl_push 在 detection 模式下无条件写 w1=阶印记(非 0, - # `_ff_push_mark` 一次写 w1/w2/w3)⇒ "进过链"的块 w1 恒非 0。 - # * w2 == TAG2 ⇒ 本块确实被 _ff_push_mark 打过印记 ⇒ w1=0 只能由链外写入者 - # 清零,**不是**"入链未打印记"(代码里不存在绕开 _fl_push 的入链路径)。 - # * w2 != TAG2 ⇒ 印记整体不可信(入链即未打 / 三字俱被覆盖),w1=0 不单独归因。 + # w1 / w3 cause: _fl_push is the only link-in site and always stamps w1 (non-zero), so + # w2 == TAG2 proves the stamp was applied and w1 == 0 must be an off-list write; if + # w2 != TAG2 the whole stamp is untrustworthy and w1 == 0 is not attributed on its own. if w2 == exp2: stdio.printf("[FREEFRAME] w1 成因: w2==TAG2 ⇒ 本块确经 _fl_push 打过印记(_ff_push_mark 三字同写)⇒ w1=0 是链外写入者清零,非入链漏打印记\n") else: @@ -863,51 +913,47 @@ class MemBuddy(MemManager): stdio.printf("[FREEFRAME] w3 成因: 若写入范围未达偏移 24 ⇒ w3 保持入链原值 = 入链时的 old_head ⇒ =0 表示入链时 free_lists[order]==0(本块是链尾,next=NULL)\n") else: stdio.printf("[FREEFRAME] w3 成因: w3!=0 但 !=w0 ⇒ 或 w0 被改(镜像失配)或 w3 本身被改,需结合跨度区间判读\n") - # 左邻探测:max_order 若被写穿则夹到容量,避免转储路径自旋 - mo: t.CInt = self.max_order + # Left-neighbour probe: clamp max_order if it was written through, to avoid spinning. + mo: int = self.max_order if mo < 0 or mo > MEMBUDDY_MAX_ORDERS: mo = MEMBUDDY_MAX_ORDERS stdio.printf("[FREEFRAME] 左邻探测(块尾正好落在 A 的分配块 = 越界写来源): max_order=%d usable=%llu\n", - mo, t.CUInt64T(self.usable)) - o: t.CInt = 0 - found_l: t.CInt = 0 + mo, uint64(self.usable)) + o: int = 0 + found_l: int = 0 while o <= mo: bs_o: sizet = MEMBUDDY_MIN_BLOCK << sizet(o) - if t.CUInt64T(block) >= t.CUInt64T(self.usable) + bs_o: - l_addr: t.CUInt64T = t.CUInt64T(block) - bs_o - lp: t.CUInt64T | t.CPtr = (t.CUInt64T | t.CPtr)(t.CPtr(l_addr)) - lhdr: t.CUInt64T = lp[0] - if (lhdr & 1) != 0 and t.CInt(lhdr >> 1) == o: - can_p: t.CUInt64T | t.CPtr = (t.CUInt64T | t.CPtr)( - t.CPtr(t.CUInt64T(block) - MEMBUDDY_CANARY)) - lcan: t.CUInt64T = can_p[0] + if uint64(block) >= uint64(self.usable) + bs_o: + l_addr: uint64 = uint64(block) - bs_o + lp: uint64* = (uint64*)(l_addr) + lhdr: uint64 = lp[0] + if (lhdr & 1) != 0 and int(lhdr >> 1) == o: + lcan: uint64 = *((uint64*)(block - MEMBUDDY_CANARY)) found_l += 1 - if lcan != t.CUInt64T(MEMBUDDY_CANARY_PATTERN): + if lcan != uint64(MEMBUDDY_CANARY_PATTERN): stdio.printf("[FREEFRAME] 阶 o=%d L=%llu hdr=0x%llX 用户ptr=%llu canary=0x%llx ⇒ canary 损坏(本块可能是写入者,也可能是被同一写者波及的受害者)——归属见下方「左邻归属判据/判定」两行\n", o, l_addr, lhdr, l_addr + MEMBUDDY_HEADER_SIZE, lcan) - # 反查"写入者这次分配":记录表键 = 该分配块返回的用户指针 - # (detection 模式下 alloc 返回 block+10 且记录同值;否则 block+8)。 - # 命中的 sz 即写入者 alloc 的**请求尺寸**,据此落"哪一个分配点"。 + # Look up the writer's allocation: the record key is the block's returned + # user pointer (detection: block+10, else block+8). The hit's sz is the + # writer's REQUESTED size, which pins the allocation site. if self._det_records is not None: - l_usr: t.CUInt64T = l_addr + MEMBUDDY_HEADER_SIZE + l_usr: uint64 = l_addr + MEMBUDDY_HEADER_SIZE if self.detection != 0: l_usr = l_usr + 2 lr_i: sizet = 0 - lr_hit: t.CInt = 0 + lr_hit: int = 0 while lr_i < self._det_count: - if t.CUInt64T(self._det_records[lr_i * 2]) == l_usr: - # 单次加载 + 显式位测(打印与判定同源):size 槽的原始 - # 8 字节只读一次,sz/freemark 皆由该字派生——不用 - # `x >> 63`(对 64 位整数只可能得 0/1,任何大数都说明 - # 取值/位宽有误),freemark 用 (1<<63) 掩码比较得到。 - w_raw_r: t.CUInt64T = self._det_records[lr_i * 2 + 1] - w_sz_r: t.CUInt64T = w_raw_r & ((t.CUInt64T(1) << 63) - 1) - w_fr_r: t.CInt = 0 - if (w_raw_r & (t.CUInt64T(1) << 63)) != 0: + if uint64(self._det_records[lr_i * 2]) == l_usr: + # Single load + explicit bit test (print and decision share the + # source word): the raw 8 bytes of the size slot are read once. + w_raw_r: uint64 = self._det_records[lr_i * 2 + 1] + w_sz_r: uint64 = w_raw_r & ((uint64(1) << 63) - 1) + w_fr_r: int = 0 + if (w_raw_r & (uint64(1) << 63)) != 0: w_fr_r = 1 - # 调用点 tag(与本条记录同索引):命中则以稳定常量 - # 映射为 文件名:行,无 tag / 未登记一律 "?",不伪造。 + # Call-site tag (same index as the record): a hit maps to + # file:line via a stable constant; no tag -> "?", never faked. w_site_r: str = _det_site_name( _det_site_get(lr_i, l_usr)) stdio.printf("[FREEFRAME] 写入者分配记录: [%zu] ptr=%llu sz=%llu freemark=%d 原始字=%llu site=%s(sz 即写入者 alloc 请求尺寸;freemark=1 表该 ptr 已 free、0 表存活;site=该记录的调用点 tag 映射,无 tag 打 ?)\n", @@ -916,11 +962,12 @@ class MemBuddy(MemManager): lr_i += 1 if lr_hit == 0: stdio.printf("[FREEFRAME] 写入者分配记录: 无(该用户指针无记录 ⇒ 记录表曾扩容/截断,或该块非经 alloc 得)\n") - # [FREEFRAME] 写入者 payload 头/尾转储:区分"对象越界写"与 - # "野指针落点恰为本块尾"(见 _ff_owner_dump 注释)。 + # [FREEFRAME] Dump the writer's payload head/tail to separate an object + # overflow from a wild pointer landing on this block's tail (see + # _ff_owner_dump). self._ff_owner_dump(t.CPtr(l_addr), o) - # [FREEFRAME] 左邻归属的**证据化**判定(几何 + canary 不足以 - # 断言归属;判据见 _ff_left_classify)。 + # [FREEFRAME] Evidence-based left-neighbour attribution (geometry plus + # canary alone cannot attribute; criteria in _ff_left_classify). self._ff_left_classify(l_addr, bs_o, lcan, w0) else: stdio.printf("[FREEFRAME] 阶 o=%d L=%llu hdr=0x%llX 用户ptr=%llu canary=0x%llx(完好:不是它的连续越界写)\n", @@ -929,19 +976,19 @@ class MemBuddy(MemManager): o += 1 if found_l == 0: stdio.printf("[FREEFRAME] 无候选:左邻不存在「块头奇数且阶与几何一致」的分配块 ⇒ **不能**用左邻几何归因(写入者可能是野指针落点,或来自更远处,需另证)\n") - # 记录表近邻:给出该区域分配过哪些对象(detection 模式下记录 ptr = 用户指针 + 2) + # Nearby records: which objects were allocated in this area (detection: ptr = user + 2). if self._det_records is not None: stdio.printf("[FREEFRAME] 记录表近邻(|ptr-A| <= 256): ") - printed: t.CInt = 0 + printed: int = 0 ri2: sizet = 0 while ri2 < self._det_count and printed < 8: - rp2: t.CUInt64T = self._det_records[ri2 * 2] - rs2: t.CUInt64T = self._det_records[ri2 * 2 + 1] - if rp2 >= t.CUInt64T(block) - 256 and rp2 <= t.CUInt64T(block) + 256: - # 单次加载 + 显式位测(与"写入者分配记录"行同源;不用 `>> 63`) - n_sz: t.CUInt64T = rs2 & ((t.CUInt64T(1) << 63) - 1) - n_fr: t.CInt = 0 - if (rs2 & (t.CUInt64T(1) << 63)) != 0: + rp2: uint64 = self._det_records[ri2 * 2] + rs2: uint64 = self._det_records[ri2 * 2 + 1] + if rp2 >= uint64(block) - 256 and rp2 <= uint64(block) + 256: + # Single load + explicit bit test (same source as the writer-record line). + n_sz: uint64 = rs2 & ((uint64(1) << 63) - 1) + n_fr: int = 0 + if (rs2 & (uint64(1) << 63)) != 0: n_fr = 1 stdio.printf("[%zu]ptr=%llu sz=%llu freemark=%d ", ri2, rp2, n_sz, n_fr) @@ -950,18 +997,21 @@ class MemBuddy(MemManager): stdio.printf("\n") stdio.fflush(None) - def _ff_owner_dump(self, block: t.CPtr, order: t.CInt): - """[FREEFRAME] 写入者(分配块)payload 头/尾各 16 字节 + 其请求尺寸转储。 + def _ff_owner_dump(self, block: t.CPtr, order: int): + """[FREEFRAME] Dump the writer (allocation) block's payload head/tail (16 bytes each) and + its requested size. - 用途(把 §32.5 的两种等价读法在报文层分开): - * payload 头呈"已初始化对象"形态(指针/字符串/连续非零字段)而该块容量 - 远小于写入跨度 ⇒ "某对象 > 块容量却放进本阶块"(对象越界写穿右邻); - * payload 头/尾仍是 alloc 填充值(0xCD...)/零 ⇒ 更可能是"野指针落点恰为 - 本块尾"(写者基址不是本块)。 - 并反查记录表给出该块的 alloc 请求尺寸(= 所有者被允许写的字节数), - 与 `_ff_report` 的"写入者分配记录"行同源(同一 slot 单次加载 + 显式位测)。 + Purpose (separate the two readings of an overflow at the report level): + * payload head looks like an initialised object (pointers/strings/non-zero fields) while + the block capacity is far below the write span => an object larger than its block ran + over into the right neighbour; + * payload head/tail still hold the alloc fill (0xCD...) or zeros => more likely a wild + pointer landing exactly on this block's tail. + Also looks up this block's alloc requested size (= bytes the owner may write), same source + as _ff_report's writer-record line. - 边界:池外/超池块一律不读(转储路径不得新增越界读)。 + Boundary: a block outside / larger than the pool is never read (the dump path must not add + out-of-bounds reads). """ if self.detection == 0: return @@ -970,39 +1020,39 @@ class MemBuddy(MemManager): if order < 0 or order > MEMBUDDY_MAX_ORDERS: return big: sizet = MEMBUDDY_MIN_BLOCK << sizet(order) - if t.CUInt64T(block) < t.CUInt64T(self.usable): + if uint64(block) < uint64(self.usable): return - if t.CUInt64T(block) + big > t.CUInt64T(self.usable) + self.usable_size: + if uint64(block) + big > uint64(self.usable) + self.usable_size: return - # payload 头 16B(起点 = block+8;detection 模式下前 2 字节为 alloc 前标记) - head_addr: t.CUInt64T = t.CUInt64T(block) + MEMBUDDY_HEADER_SIZE - # payload 尾 16B(canary 之前 16 字节) - tail_addr: t.CUInt64T = t.CUInt64T(block) + big - MEMBUDDY_CANARY - 16 + # Payload head 16B (start = block+8; in detection the first 2 bytes are the front marker). + head_addr: uint64 = uint64(block) + MEMBUDDY_HEADER_SIZE + # Payload tail 16B (16 bytes before the canary). + tail_addr: uint64 = uint64(block) + big - MEMBUDDY_CANARY - 16 stdio.printf("[FREEFRAME] 写入者 payload 头16B(=%llu): ", head_addr) - bi_h: t.CInt = 0 + bi_h: int = 0 while bi_h < 16: - bv_h: t.CUInt8T = (t.CUInt8T | t.CPtr)(t.CPtr(head_addr + t.CUInt64T(bi_h)))[0] - stdio.printf("%02X ", t.CUInt32T(bv_h)) + bv_h: uint8 = *((uint8*)(head_addr + uint64(bi_h))) + stdio.printf("%02X ", uint32(bv_h)) bi_h += 1 stdio.printf("| 尾16B(=%llu): ", tail_addr) - bi_t: t.CInt = 0 + bi_t: int = 0 while bi_t < 16: - bv_t: t.CUInt8T = (t.CUInt8T | t.CPtr)(t.CPtr(tail_addr + t.CUInt64T(bi_t)))[0] - stdio.printf("%02X ", t.CUInt32T(bv_t)) + bv_t: uint8 = *((uint8*)(tail_addr + uint64(bi_t))) + stdio.printf("%02X ", uint32(bv_t)) bi_t += 1 stdio.printf("\n") if self._det_records is not None: - usr_d: t.CUInt64T = t.CUInt64T(block) + MEMBUDDY_HEADER_SIZE + usr_d: uint64 = uint64(block) + MEMBUDDY_HEADER_SIZE if self.detection != 0: usr_d = usr_d + 2 ri_d: sizet = 0 - hit_d: t.CInt = 0 + hit_d: int = 0 while ri_d < self._det_count: - if t.CUInt64T(self._det_records[ri_d * 2]) == usr_d: - w_d: t.CUInt64T = self._det_records[ri_d * 2 + 1] - d_sz: t.CUInt64T = w_d & ((t.CUInt64T(1) << 63) - 1) - d_fr: t.CInt = 0 - if (w_d & (t.CUInt64T(1) << 63)) != 0: + if uint64(self._det_records[ri_d * 2]) == usr_d: + w_d: uint64 = self._det_records[ri_d * 2 + 1] + d_sz: uint64 = w_d & ((uint64(1) << 63) - 1) + d_fr: int = 0 + if (w_d & (uint64(1) << 63)) != 0: d_fr = 1 stdio.printf("[FREEFRAME] 写入者请求尺寸: [%zu] sz=%llu freemark=%d 原始字=%llu;本块可写=%zu 字节(bs=%zu − 头%zu − canary%zu)\n", ri_d, d_sz, d_fr, w_d, @@ -1015,47 +1065,42 @@ class MemBuddy(MemManager): stdio.printf("[FREEFRAME] 写入者请求尺寸: 无记录(记录表曾扩容/截断,或该块非经 alloc 得)\n") stdio.fflush(None) - def _ff_addr_in_pool(self, a: t.CUInt64T) -> t.CInt: - """a 是否落在本池区间 [usable, usable + usable_size)。判据用,不解引用 a。""" + def _ff_addr_in_pool(self, a: uint64) -> int: + """Whether a lies in this pool's range [usable, usable + usable_size). Predicate only.""" if self.usable is None: return 0 - if a < t.CUInt64T(self.usable): + if a < uint64(self.usable): return 0 - if a >= t.CUInt64T(self.usable) + self.usable_size: + if a >= uint64(self.usable) + self.usable_size: return 0 return 1 - def _ff_left_classify(self, l_addr: t.CUInt64T, l_bs: sizet, - lcan: t.CUInt64T, w0: t.CUInt64T): - """[FREEFRAME] 左邻归属的**证据化**判定(几何 + canary 损坏本身不足以断言归属)。 + def _ff_left_classify(self, l_addr: uint64, l_bs: sizet, + lcan: uint64, w0: uint64): + """[FREEFRAME] Evidence-based attribution of the left neighbour (geometry plus a damaged + canary alone cannot attribute). - 实测四组判据(只读 L 的块内字节;L 的 payload 第 i 字 = L + 8 + 8i): - ① `canary == A.word0`:同一 8 字节值出现在相邻两字 ⇒ 该次写按 8 字节字推进; - ② L payload 尾 16B 是否全零:非全零 ⇒ 写者从 payload 内部起步(可能是连续 - 越界);全零 ⇒ 写者**未**回退到偏移 32..47——连续溢出必然先覆盖尾 16B - 才到 canary,故"起点 ≤ 偏移 48 且跳过 32..47"排除「从 payload 尾部连续溢出」; - ③ 写入值(= canary 值)在 L payload 内出现的字数:0 ⇒ 写入值与 payload 内容 - 无关(在 canary 处起步,而非沿 payload 递增写出去); - ④ L payload 内「池内地址 / 池外非零字」计数:本池 alloc 只会把**池内地址** - 或 0 写进本池缓冲;出现池外非零字 ⇒ 该 payload 不是「按 8 字节槽写元素 - 指针」的结果 ⇒ **写入宽度 ≠ 申请宽度**(按更宽的结构体宽度跨字写),或 - payload 被第三方覆盖。记录项只证明「本块由该 alloc 点分配」,其 `sz` 是 - **申请**尺寸、**不是**写入范围。 - - 输出只给实测值 + 由值唯一推出的条件式结论(结论 A/B/C),不做无条件归属断言。 - 预期输出样例(三次复现的现场:L 的 canary 与 A.word0 同为 0x00140000002B0000 - = 元素节点源码位置 lineno/end_lineno=43、col_offset/end_col_offset=20 落在 - L+56..L+79 的字节切片;L payload 首字 = 被深拷贝节点的 vptr 0x00007FF6C414FEB0, - 尾 16B 全零;记录 sz=32): - [FREEFRAME] 左邻归属判据(实测): canary==A.word0=1;L payload 尾16B全零=1;写入值(=canary)在 L payload 出现=0 字;L payload 非零字 池内=0 池外=4(payload 共 6 字,起点 L+8;4 个字 = 同一 vptr,模型见 _ff_left_classify 注释) - [FREEFRAME] ⇒ 结论A:… - [FREEFRAME] ⇒ 结论B:… + Four measured criteria (read only L's bytes; L's payload word i = L + 8 + 8i): + 1. canary == A.word0: the same 8-byte value in adjacent words => the write advanced in + 8-byte units; + 2. whether L's payload tail 16B is all zero: non-zero => the writer started inside the + payload; all zero => the writer never reached offsets 32..47, excluding a run growing + out of the payload tail; + 3. how many payload words equal the written value (= canary): 0 => the written value is + unrelated to the payload (the write started at the canary); + 4. count of in-pool addresses / out-of-pool non-zero words in L's payload: this pool's + alloc only writes in-pool addresses or 0, so an out-of-pool non-zero word means the + payload was not produced by "store a pointer per 8-byte slot" (write width != request + width). + A record only proves the block was allocated at that site; its sz is the REQUESTED size, + not the written range. Output states measured values and the value-implied conclusion + (A/B/C) only, never an unconditional attribution. """ nw: sizet = (l_bs - MEMBUDDY_HEADER_SIZE - MEMBUDDY_CANARY) / 8 - tail_zero: t.CInt = 1 + tail_zero: int = 1 if nw >= 2: - w_a: t.CUInt64T = self._ff_word(t.CPtr(l_addr), t.CInt(nw) - 1) - w_b: t.CUInt64T = self._ff_word(t.CPtr(l_addr), t.CInt(nw)) + w_a: uint64 = self._ff_word(t.CPtr(l_addr), int(nw) - 1) + w_b: uint64 = self._ff_word(t.CPtr(l_addr), int(nw)) if w_a != 0 or w_b != 0: tail_zero = 0 hit_v: sizet = 0 @@ -1063,7 +1108,7 @@ class MemBuddy(MemManager): out_pool: sizet = 0 i: sizet = 0 while i < nw: - wv: t.CUInt64T = self._ff_word(t.CPtr(l_addr), t.CInt(i) + 1) + wv: uint64 = self._ff_word(t.CPtr(l_addr), int(i) + 1) if wv == lcan: hit_v += 1 if wv != 0: @@ -1072,13 +1117,13 @@ class MemBuddy(MemManager): else: out_pool += 1 i += 1 - can_eq_w0: t.CInt = 0 + can_eq_w0: int = 0 if lcan == w0: can_eq_w0 = 1 stdio.printf("[FREEFRAME] 左邻归属判据(实测): canary==A.word0=%d;L payload 尾16B全零=%d;写入值(=canary)在 L payload 出现=%zu 字;L payload 非零字 池内=%zu 池外=%zu(payload 共 %zu 字,起点 L+%zu)\n", can_eq_w0, tail_zero, hit_v, in_pool, out_pool, nw, sizet(MEMBUDDY_HEADER_SIZE)) - case_n: t.CInt = 0 + case_n: int = 0 if tail_zero != 0 and can_eq_w0 != 0 and hit_v == 0: case_n = 1 if out_pool != 0: @@ -1091,107 +1136,109 @@ class MemBuddy(MemManager): if case_n == 0: stdio.printf("[FREEFRAME] ⇒ 结论C:可区分特征均未命中 ⇒ 不据「几何 + canary 损坏」断言归属(需结合记录表现场与 payload 全量转储另证)\n") - def _ff_verify(self, order: t.CInt, block: t.CPtr): - """[FREEFRAME] 出块前校验空闲块印记(仅 detection 模式,调用方已判)。 + def _ff_verify(self, order: int, block: t.CPtr): + """[FREEFRAME] Verify the free-block stamps before popping (detection only, caller-checked). - 印记/镜像不符 ⇒ 该块入链后其字被链外写入 ⇒ 立即转储现场并终止 - (比"next 是否为合法块地址"更早、更精确:写入者若恰好写了 8 字节的合法 - 块地址,旧守卫会放过,本校验不会)。 + A stamp/mirror mismatch means the block's words were written off-list after linking, so + dump the scene and terminate at once -- earlier and more precise than "is next a valid + block address" (a writer that happened to write a valid 8-byte block address would pass + the old guard, not this one). """ - # 先过池内/对齐判据:_fl_find_and_remove 的链头值没有范围校验,若槽位本身被 - # 写穿成池外值,读 word1..3 会成池外读(word0 的读取是既有行为,扩展读不应 - # 增加新的越界面)⇒ 非池内对齐地址一律不校验(交给既有的取值守卫处理)。 + # Range/alignment first: _fl_find_and_remove's head value has no range check, so if the + # slot itself was overwritten with an out-of-pool value, reading words 1..3 would read out + # of the pool (reading word0 is existing behaviour; the extra reads must not widen the + # boundary) => never verify a non-in-pool, unaligned address. if self._fl_block_ok(block) == 0: return - w0: t.CUInt64T = self._ff_word(block, 0) - w1: t.CUInt64T = self._ff_word(block, 1) - w2: t.CUInt64T = self._ff_word(block, 2) - w3: t.CUInt64T = self._ff_word(block, 3) - if w1 == self._ff_stamp(order) and w2 == t.CUInt64T(MEMBUDDY_FREEFRAME_TAG2) and w3 == w0: + w0: uint64 = self._ff_word(block, 0) + w1: uint64 = self._ff_word(block, 1) + w2: uint64 = self._ff_word(block, 2) + w3: uint64 = self._ff_word(block, 3) + if w1 == self._ff_stamp(order) and w2 == uint64(MEMBUDDY_FREEFRAME_TAG2) and w3 == w0: return stdio.printf("[FREEFRAME] 空闲块元数据被写穿: order=%d block=%llu w0(next)=%llu w1=%llu(期望=0x%llx) w2=%llu(期望=0x%llx) w3(next镜像)=%llu(入链后被链外写入,立即转储)\n", - order, t.CUInt64T(block), w0, w1, self._ff_stamp(order), w2, - t.CUInt64T(MEMBUDDY_FREEFRAME_TAG2), w3) + order, uint64(block), w0, w1, self._ff_stamp(order), w2, + uint64(MEMBUDDY_FREEFRAME_TAG2), w3) self._ff_report(order, block) stdio.fflush(None) stdlib.exit(1) - def _ff_owner_check(self, block: t.CPtr, order: t.CInt): - """[FREEFRAME] 释放前所有者检查(仅 detection 模式):校验块尾 canary。 + def _ff_owner_check(self, block: t.CPtr, order: int): + """[FREEFRAME] Pre-free owner check (detection only): verify the block-tail canary. - 连续越界写者必然先写穿自身块尾 canary,再进入右邻块首字(canary 后 8 字节 - 即右邻块头)⇒ canary 损坏即"本块所有者就是写穿邻块首字的写入者"。报文给出 - order / 用户指针 / 合法写长,并逐字节转储 canary 起 24 字节(写穿跨度与写入 - 值)。调用点:free 的活块分支、reset_to 的每个回收块(都是"所有者已知"的 - 释放点)。 + A contiguous overflow writer always overwrites its own block-tail canary before entering + the right neighbour's first word (the neighbour header is 8 bytes past the canary), so a + damaged canary means this block's owner is that writer. The report gives order / user + pointer / legal write length and dumps 24 bytes from the canary. Called from free's + live-block branch and reset_to's reclaimed blocks (both owner-known release points). """ if self.detection == 0: return bs: sizet = MEMBUDDY_MIN_BLOCK << sizet(order) - # 范围前置校验:reset_to 的 block 直接取自 chk_stack(未做池内校验), - # 越界值会令下面的 canary 读落在池外;池外一律不报(无归属可言)。 + # Range pre-check: reset_to passes blocks straight from chk_stack (no pool-range check), so + # an out-of-range value would make the canary read land outside the pool; such cases are not + # reported (no attribution). if self.usable is None: return - if t.CUInt64T(block) < t.CUInt64T(self.usable): + if uint64(block) < uint64(self.usable): return - if t.CUInt64T(block) + bs > t.CUInt64T(self.usable) + self.usable_size: + if uint64(block) + bs > uint64(self.usable) + self.usable_size: return - can_addr: t.CUInt64T = t.CUInt64T(block) + bs - MEMBUDDY_CANARY - cp: t.CUInt64T | t.CPtr = (t.CUInt64T | t.CPtr)(t.CPtr(can_addr)) - can_val: t.CUInt64T = cp[0] - if can_val == t.CUInt64T(MEMBUDDY_CANARY_PATTERN): + can_addr: uint64 = uint64(block) + bs - MEMBUDDY_CANARY + cp: uint64* = (uint64*)(can_addr) + can_val: uint64 = cp[0] + if can_val == uint64(MEMBUDDY_CANARY_PATTERN): return stdio.printf("[FREEFRAME] 释放前 canary 损坏(本块所有者**越过了本块可用区**;写入宽度见下方 payload 转储,`sz` 是申请尺寸、不是写入范围): block=%llu order=%d 用户ptr=%llu 合法写长=%zu canary=0x%llx 期望=0x%llx\n", - t.CUInt64T(block), order, t.CUInt64T(block) + MEMBUDDY_HEADER_SIZE, + uint64(block), order, uint64(block) + MEMBUDDY_HEADER_SIZE, bs - MEMBUDDY_HEADER_SIZE - MEMBUDDY_CANARY, can_val, - t.CUInt64T(MEMBUDDY_CANARY_PATTERN)) - # canary 起 24 字节(8=只写坏 canary、16=连带右邻块首字、24=再连带其第二字); - # 池尾块只转储池内部分(避免转储路径越界读)。 - dump_n: t.CInt = 8 - if t.CUInt64T(block) + bs + 16 <= t.CUInt64T(self.usable) + self.usable_size: + uint64(MEMBUDDY_CANARY_PATTERN)) + # 24 bytes from the canary (8 = canary only, 16 = plus the neighbour header, 24 = plus its + # second word); a pool-tail block dumps only the in-pool part (no out-of-pool read). + dump_n: int = 8 + if uint64(block) + bs + 16 <= uint64(self.usable) + self.usable_size: dump_n = 24 stdio.printf("[FREEFRAME] canary 起 %d 字节: ", dump_n) - bi: t.CInt = 0 + bi: int = 0 while bi < dump_n: - bv: t.CUInt8T = (t.CUInt8T | t.CPtr)(t.CPtr(can_addr + t.CUInt64T(bi)))[0] - stdio.printf("%02X ", t.CUInt32T(bv)) + bv: uint8 = *((uint8*)(can_addr + uint64(bi))) + stdio.printf("%02X ", uint32(bv)) bi += 1 stdio.printf("\n") - # [FREEFRAME] 写入者 payload 头/尾 + 请求尺寸(与 canary 转储同一现场: - # canary 被写穿 ⇒ 本块所有者即写穿右邻空闲块首字的写入者)。 + # [FREEFRAME] Writer payload head/tail + requested size (same scene as the canary dump: + # a broken canary means this block's owner overflowed into the right free block's first word). self._ff_owner_dump(block, order) stdio.fflush(None) stdlib.exit(1) - def _fl_pop(self, order: t.CInt) -> t.CPtr: - """弹出第 order 阶空闲链头(O(1):本函数体内无任何循环/递归)。 + def _fl_pop(self, order: int) -> t.CPtr: + """Pop the head of order `order`'s free list (O(1): no loop/recursion in this body). - 取值校验(fail-fast):本函数只有两次内存访问——读 free_lists[order]、 - 再把该值当块地址解引用 block[0]。原实现无任何前置校验,故 free_lists[order] - 一旦被写穿(如 0xFFFF…FF = -1),第二次访问即"解引用任意地址" - (ASAN:access-violation on unknown address 0xffffffffffffffff;崩溃点 - 远离真正的写入者)。此处先判 order 在 free_lists 容量内,再判头指针与 - next 指针落在池内且对齐;不满足就地打印现场并终止——内存管理器崩溃必须 - 响亮。不做静默跳过/降级:跳过会把"堆已损坏"伪装成"分配失败",失败点 - 沿调用链漂移到别处,与本文件既有 fail-fast 口径([MEMDBG]/[DET] 均 - printf+fflush+exit)不一致。 + Value validation (fail-fast): this function has only two memory accesses -- read + free_lists[order], then dereference that value as block[0]. Without validation a written- + through free_lists[order] (e.g. 0xFFFF...FF) makes the second access "dereference an + arbitrary address" (ASAN: access-violation on unknown address 0xffffffffffffffff), far from + the real writer. So validate that order is within capacity, then that the head and next + pointers are in-pool and aligned; otherwise print the scene and terminate -- allocator + corruption must be loud. No silent skip/downgrade: that would disguise "heap corrupted" as + "alloc failed" and shift the failure point along the call chain. """ if order < 0 or order > MEMBUDDY_MAX_ORDERS: stdio.printf("[MEM] _fl_pop 调用者=_split_to_order order 越界: order=%d 容量=%d MAX_ORDERS=%d max_order=%d\n", order, MEMBUDDY_MAX_ORDERS + 1, MEMBUDDY_MAX_ORDERS, self.max_order) stdio.fflush(None) stdlib.exit(1) - # 结构不变量(__init__ 唯一写点,此后不再改):free_lists 数组基址 == usable - 264 - # (264 = (MEMBUDDY_MAX_ORDERS+1)*8)。它被破坏意味着 MemBuddy **对象**本身被写穿 - # (而不是池内数据),此时 `self.free_lists[order]` 的地址本身就是野地址 - # (如 free_lists=-1、order=0 ⇒ 首访地址恰为 0xffffffffffffffff)。必须在 - # 任何 free_lists[…] 取值之前判掉,否则守卫也来不及。 - if self.usable is not None and t.CUInt64T(self.free_lists) != t.CUInt64T(self.usable) - (MEMBUDDY_MAX_ORDERS + 1) * 8: + # Structural invariant (written once in __init__, never after): the free_lists array base + # == usable - 264 (264 = (MEMBUDDY_MAX_ORDERS+1)*8). Breaking it means the MemBuddy OBJECT + # itself was written through (not pool data), so the address of self.free_lists[order] is + # itself wild (e.g. free_lists=-1, order=0 => the first access is 0xffffffffffffffff). Must + # be rejected before ANY free_lists[...] access, or even the guard is too late. + if self.usable is not None and uint64(self.free_lists) != uint64(self.usable) - (MEMBUDDY_MAX_ORDERS + 1) * 8: stdio.printf("[MEM] _fl_pop 调用者=_split_to_order free_lists 基址非法(MemBuddy 对象被写穿): free_lists=%llu usable=%llu max_order=%d\n", - t.CUInt64T(self.free_lists), t.CUInt64T(self.usable), self.max_order) + uint64(self.free_lists), uint64(self.usable), self.max_order) stdio.fflush(None) stdlib.exit(1) - head_val: t.CUInt64T = self.free_lists[order] + head_val: uint64 = self.free_lists[order] if head_val == 0: return None if self._fl_block_ok(t.CPtr(head_val)) == 0: @@ -1200,112 +1247,111 @@ class MemBuddy(MemManager): stdio.fflush(None) stdlib.exit(1) block: t.CPtr = t.CPtr(head_val) - # [FREEFRAME] detection 模式:出块前校验空闲块印记/镜像(比"next 是否为合法 - # 块地址"更早、更灵敏——写入者若恰好写了 8 字节合法块地址,下面的旧守卫会 - # 放过,本校验不会)。detection == 0 时只多一次可预测未命中的分支,无额外 - # 内存访问。 + # [FREEFRAME] detection mode: verify the free-block stamps/mirror before popping (earlier + # and more sensitive than the "is next a valid block address" guard below, which a writer + # that happened to write a valid 8-byte block address would pass). With detection == 0 it + # is one predictable non-taken branch, no extra memory access. if self.detection != 0: self._ff_verify(order, block) - next_ptr: t.CPtr = t.CPtr(c.Deref((t.CUInt64T | t.CPtr)(block))) - next_val: t.CUInt64T = t.CUInt64T(next_ptr) + next_ptr: t.CPtr = t.CPtr(*((uint64*)(block))) + next_val: uint64 = uint64(next_ptr) if next_val != 0 and self._fl_block_ok(t.CPtr(next_val)) == 0: stdio.printf("[MEM] _fl_pop 调用者=_split_to_order next 指针非法: order=%d block=%llu next=%llu max_order=%d usable_size=%zu(空闲块首字被写穿:next 非池内对齐块地址)\n", order, head_val, next_val, self.max_order, self.usable_size) stdio.fflush(None) - # [FREEFRAME] detection 模式下追加取证转储(左邻探测 + 记录表近邻), - # 用来把"哪个块被写穿"进一步落到"哪个分配块写了它"。 + # [FREEFRAME] In detection mode add a forensic dump (left-neighbour probe + nearby + # records) to pin down which allocation wrote over the block. if self.detection != 0: self._ff_report(order, block) stdlib.exit(1) self.free_lists[order] = next_val return block - def _fl_find_and_remove(self, order: t.CInt, target: t.CPtr) -> t.CInt: - head_val: t.CUInt64T = self.free_lists[order] + def _fl_find_and_remove(self, order: int, target: t.CPtr) -> int: + head_val: uint64 = self.free_lists[order] if head_val == 0: return 0 head: t.CPtr = t.CPtr(head_val) - # [FREEFRAME] detection 模式:本函数会**沿链走**(遍历 next),沿途节点若被写穿 - # 会令遍历走进野链(成环/超上限报文只说明"堆坏了",不指名)。故对头节点与被 - # 访问节点逐一校验印记——命中即在现场转储并终止,把"沿链漂移"变成"指名到块"。 + # [FREEFRAME] detection mode: this function walks the chain (following next), so a + # written-through node would send the walk into a wild chain (a cycle/over-limit report + # only says "heap is broken", naming nothing). Verify the head and every visited node, so + # a hit dumps the scene and terminates, turning a drifting walk into a named block. if self.detection != 0: self._ff_verify(order, head) - if t.CUInt64T(head) == t.CUInt64T(target): - next_ptr: t.CPtr = t.CPtr(c.Deref((t.CUInt64T | t.CPtr)(head))) - self.free_lists[order] = t.CUInt64T(next_ptr) + if uint64(head) == uint64(target): + next_ptr: t.CPtr = t.CPtr(*((uint64*)(head))) + self.free_lists[order] = uint64(next_ptr) return 1 prev: t.CPtr = head - cur: t.CPtr = t.CPtr(c.Deref((t.CUInt64T | t.CPtr)(head))) - # 环检测(与 self_check 同界):free list 的 next 指针被越界写破坏后会成环, - # 无步数上限时本循环永不终止(free/coalesce 卡死且崩溃点漂移)。上限取 - # "全部最小块数 + 2",合法自由块数不可能超过它;超限即判定堆已损坏, - # 打印现场并停止遍历(返回 0 = 未找到),不再自旋。 + cur: t.CPtr = t.CPtr(*((uint64*)(head))) + # Cycle guard (same bound as self_check): a corrupted next pointer can make the free list + # cyclic, and without a step bound this loop would never end (free/coalesce hang with a + # drifting crash). The bound is "all minimum blocks + 2", which no valid free count can + # exceed; over the bound the heap is damaged: print the scene and stop (return 0 = not + # found) instead of spinning. max_steps: sizet = self.usable_size / MEMBUDDY_MIN_BLOCK + 2 steps: sizet = 0 - while t.CUInt64T(cur) != 0: + while uint64(cur) != 0: steps += 1 if steps > max_steps: stdio.printf("[MEM] free_lists[%d] 链成环或节点数超上限(next 指针被破坏): head=%llu target=%llu;停止遍历避免自旋,堆已损坏\n", - order, head_val, t.CUInt64T(target)) + order, head_val, uint64(target)) return 0 - # [FREEFRAME] 被访问节点的印记校验(detection):下一跳取自 cur 的 word0, - # 若 cur 已被写穿,此处当场指名 cur 并终止(否则会沿野链漂移)。 + # [FREEFRAME] Stamp check of the visited node (detection): the next hop comes from + # cur's word0, so if cur was written through, name it here and terminate (otherwise + # the walk drifts along a wild chain). if self.detection != 0: self._ff_verify(order, cur) - if t.CUInt64T(cur) == t.CUInt64T(target): - next_ptr: t.CPtr = t.CPtr(c.Deref((t.CUInt64T | t.CPtr)(cur))) - c.DerefAs(prev, next_ptr) - # [FREEFRAME] 中间摘除改写了 prev 的 word0(next),必须同步其镜像 - # word3——否则 _ff_verify 会把 memhub 自己的合法摘除误报为"被写穿"。 - # prev 的阶 = 本链表阶 order(_fl_push 写入的印记阶)。 + if uint64(cur) == uint64(target): + next_ptr: t.CPtr = t.CPtr(*((uint64*)(cur))) + *((uint64*)(prev)) = uint64(next_ptr) + # [FREEFRAME] Middle unlink rewrote prev's word0 (next), so its mirror word3 must + # be synced too; otherwise _ff_verify would misreport memhub's own valid unlink as + # "written through". prev's order is this list's order (the stamp written by + # _fl_push). if self.detection != 0: - self._ff_word_set(prev, 3, t.CUInt64T(next_ptr)) + self._ff_word_set(prev, 3, uint64(next_ptr)) return 1 prev = cur - cur = t.CPtr(c.Deref((t.CUInt64T | t.CPtr)(cur))) + cur = t.CPtr(*((uint64*)(cur))) return 0 # === Buddy system core === - def _buddy_of(self, block: t.CPtr, order: t.CInt) -> t.CPtr: - offset: sizet = t.CUInt64T(block) - t.CUInt64T(self.usable) - # order 恒 ≥ 0(调用链:_coalesce 的 order 源自合法 header (stored>>1) 且只递增, - # 起点为已释放块的阶)。内联左移等价于 _block_size_at_order(order),省去每次 - # 调用的 call/ret(_buddy_of 实测 3517 万次,为 _block_size_at_order 主要来源)。 + def _buddy_of(self, block: t.CPtr, order: int) -> t.CPtr: + # Buddy = block with the order's size bit flipped in its pool byte offset. + offset: sizet = uint64(block) - uint64(self.usable) bs: sizet = MEMBUDDY_MIN_BLOCK << sizet(order) buddy_offset: sizet = offset ^ bs - return t.CPtr(t.CUInt64T(self.usable) + buddy_offset) + return self.usable + buddy_offset - def _order_for_size(self, size: sizet) -> t.CInt: - """由字节数求阶:最小的 order 使 MEMBUDDY_MIN_BLOCK << order >= size。 + def _order_for_size(self, size: sizet) -> int: + """Smallest order with MEMBUDDY_MIN_BLOCK << order >= size. - 上界守卫(终止性):原实现 `while bs < size` 在 size 巨大(含被写穿/溢出 - 后的近 2^64 值)时,bs << 1 会回绕为 0 ⇒ `0 < size` 恒真 ⇒ 死循环;且 - order(t.CInt=i32) 会先溢出为负(负 order 再参与 `MIN_BLOCK << order` - 是未定义行为)。语义不变的界:order 只有 ≤ max_order 才有意义,更大一律 - 返回 max_order + 1——调用方 alloc(`order <= self.max_order`)与 realloc - (`new_order <= old_order`)都据此判定"超池"而走各自的既有失败分支, - 与改前的最终结果一致(改前返回的任何 > max_order 的值同样被拒)。 + Upper bound (termination): for a huge size (near 2^64 after corruption/overflow) `bs << 1` + would wrap to 0 and `0 < size` would stay true forever; order (i32) would also go negative + (undefined in `MIN_BLOCK << order`). The bound is semantics-preserving: only order <= + max_order is meaningful, so anything larger returns max_order + 1, which both callers + (alloc's `order <= self.max_order`, realloc's `new_order <= old_order`) reject as "too big" + exactly as before. """ - order: t.CInt = 0 + order: int = 0 bs: sizet = MEMBUDDY_MIN_BLOCK while bs < size and order <= self.max_order: bs = bs << 1 order += 1 return order - def _split_to_order(self, to_order: t.CInt) -> t.CPtr: - # max_order 字段取值校验(fail-fast):下面的两个 while 都以 `self.max_order` - # 为界,而 `self.free_lists[found_order]` 直接用 found_order 作下标。若 - # max_order 被写穿(负数或 > 容量),循环界随之失效 ⇒ 读 free_lists 越界, - # 读到的垃圾值再经 _fl_pop 解引用(ASAN 现场)。此处先在入口判其合法。 + def _split_to_order(self, to_order: int) -> t.CPtr: + # max_order must be in range before it indexes free_lists (a corrupt field would + # otherwise read out of bounds and feed garbage to _fl_pop). fail-fast. if self.max_order < 0 or self.max_order > MEMBUDDY_MAX_ORDERS: stdio.printf("[MEM] _split_to_order max_order 非法(字段被写穿): to_order=%d max_order=%d 容量=%d MAX_ORDERS=%d usable_size=%zu\n", to_order, self.max_order, MEMBUDDY_MAX_ORDERS + 1, MEMBUDDY_MAX_ORDERS, self.usable_size) stdio.fflush(None) stdlib.exit(1) - found_order: t.CInt = to_order + found_order: int = to_order while found_order <= self.max_order: if self.free_lists[found_order] != 0: break @@ -1314,70 +1360,67 @@ class MemBuddy(MemManager): return None block: t.CPtr = self._fl_pop(found_order) while found_order > to_order: + # Split the block in half, pushing the upper half back onto the free list. found_order -= 1 - # 循环条件保证 found_order > to_order ≥ 0,故递减后 found_order ≥ 0; - # 内联左移等价于 _block_size_at_order(found_order),省去每次拆分的 call/ret。 bs: sizet = MEMBUDDY_MIN_BLOCK << sizet(found_order) - buddy: t.CPtr = t.CPtr(t.CUInt64T(block) + bs) + buddy: t.CPtr = block + bs self._fl_push(found_order, buddy) return block - def _coalesce(self, block: t.CPtr, order: t.CInt): - # 取值校验(fail-fast,与 _fl_pop/_split_to_order 同口径):本函数的 - # `self.free_lists[order]` 直接用 order 作下标(首轮循环),而 order 来自块头 - # `stored >> 1`(free/reset_to 路径)——块头被写穿时会给出任意下标 ⇒ - # free_lists 越界读/写。合法调用点(free 经 _is_valid_ptr、reset_to 显式判界) - # 都已保证 order ∈ [0, max_order] ⇒ 只在"已被写穿"时改变行为。 + def _coalesce(self, block: t.CPtr, order: int): + # Value validation (fail-fast, same rule as _fl_pop/_split_to_order): the first loop + # indexes self.free_lists[order] directly, and order comes from the header `stored >> 1` + # (free/reset_to paths) -- a written-through header yields an arbitrary index => out-of- + # bounds free_lists access. Valid callers (free via _is_valid_ptr, reset_to with an + # explicit bound) already guarantee order in [0, max_order], so behaviour changes only on + # corruption. if order < 0 or order > MEMBUDDY_MAX_ORDERS: stdio.printf("[MEM] _coalesce order 非法(块头被写穿): order=%d 容量=%d MAX_ORDERS=%d max_order=%d block=%llu\n", order, MEMBUDDY_MAX_ORDERS + 1, MEMBUDDY_MAX_ORDERS, - self.max_order, t.CUInt64T(block)) + self.max_order, uint64(block)) stdio.fflush(None) stdlib.exit(1) while order < self.max_order: - # 提前退出(不改变语义,仅省去"注定找不到"的两次调用):本阶空闲链表 - # 为空 ⇒ _fl_find_and_remove 入口即判 head==0 返回 0 ⇒ 本阶不可能合并。 - # 这里先判空,可省去本轮 _buddy_of(内部含 _block_size_at_order)与 - # _fl_find_and_remove 两次调用;判据与 _fl_find_and_remove 的入口条件 - # 逐字相同,且两函数均无副作用,故对空闲链表语义与顺序完全等价。 + # Early exit (no semantic change, just skips two doomed calls): an empty free list at + # this order makes _fl_find_and_remove return 0 immediately, so coalescing is + # impossible; checking first saves this round's _buddy_of and _fl_find_and_remove. + # The test is identical to _fl_find_and_remove's entry condition and neither function + # has side effects, so the free-list semantics and order are fully equivalent. if self.free_lists[order] == 0: break buddy: t.CPtr = self._buddy_of(block, order) - found: t.CInt = self._fl_find_and_remove(order, buddy) + found: int = self._fl_find_and_remove(order, buddy) if found == 0: break - if t.CUInt64T(buddy) < t.CUInt64T(block): + if uint64(buddy) < uint64(block): block = buddy order += 1 self._fl_push(order, block) - def _is_valid_ptr(self, ptr: t.CPtr) -> t.CInt: + def _is_valid_ptr(self, ptr: t.CPtr) -> int: if ptr is None: return 0 if self.usable is None: return 0 - block: t.CPtr = t.CPtr(t.CUInt64T(ptr) - MEMBUDDY_HEADER_SIZE) - if t.CUInt64T(block) < t.CUInt64T(self.usable): return 0 - if t.CUInt64T(block) >= t.CUInt64T(self.usable) + self.usable_size: return 0 - offset: sizet = t.CUInt64T(block) - t.CUInt64T(self.usable) + block: t.CPtr = ptr - MEMBUDDY_HEADER_SIZE + if uint64(block) < uint64(self.usable): return 0 + if uint64(block) >= uint64(self.usable) + self.usable_size: return 0 + offset: sizet = uint64(block) - uint64(self.usable) if offset % MEMBUDDY_MIN_BLOCK != 0: return 0 - stored: t.CPtr = t.CPtr(c.Deref((t.CUInt64T | t.CPtr)(block))) - stored_val: t.CUInt64T = t.CUInt64T(stored) + stored: t.CPtr = t.CPtr(*((uint64*)(block))) + stored_val: uint64 = uint64(stored) if (stored_val & 1) == 0: return 0 - order: t.CInt = t.CInt(stored_val >> 1) + order: int = int(stored_val >> 1) if order < 0: return 0 if order > self.max_order: return 0 - # 记录栈一致性校验:分配时(alloc)每个 block 地址都会压入 chk_stack。 - # reset_to(mark) 会把 [mark, chk_depth) 的块弹栈并归还 free list, - # 之后这些地址会被重新分配。若一个 header 恰好是合法奇数(假阳性), - # 但地址不在当前 chk_stack[0..chk_depth) 存活区间内,说明已被回收, - # 不能当作有效指针(否则 _format_func_type_str/TypePrint 会沿垃圾链 - # 无限分配导致 OOM)。 - # 注意:chk_stack 容量满(64K)后 alloc 不再记录新块(chk_depth==chk_cap), - # 此时无法区分,跳过校验(退化到仅 header 校验,避免误判正常 free)。 + # Live-set check: every alloc pushes its block onto chk_stack, and reset_to(mark) + # pops [mark, chk_depth) and frees those blocks. A stale pointer with a still-odd + # header would pass the checks above, so require the block to sit in the live + # window [0, chk_depth). Once chk_stack is full (chk_depth == chk_cap) new blocks + # are no longer recorded, so the check is skipped to avoid rejecting valid frees. if self.chk_stack is not None and self.chk_depth < self.chk_cap: - alive: t.CInt = 0 + alive: int = 0 ci: sizet = 0 while ci < self.chk_depth: - if self.chk_stack[ci] == t.CUInt64T(block): + if self.chk_stack[ci] == uint64(block): alive = 1 break ci += 1 @@ -1387,140 +1430,129 @@ class MemBuddy(MemManager): # === Spinlock === def _lock(self): # can use the spinlock library for decoupling - # 单线程编译(MEMBUDDY_THREAD_SAFE=0)下本 if 被常量折叠,函数体为空; - # 多线程(=1)下保留原有原子自旋加锁,行为与历史实现逐字节一致。 + # With MEMBUDDY_THREAD_SAFE = 0 this if folds away and the body is empty; with 1 the atomic + # spinlock is kept as before. if MEMBUDDY_THREAD_SAFE != 0: - while atom.__atomic_test_and_set(c.Addr(self.lock_val), atom.ATOMIC_ACQUIRE): + while atom.__atomic_test_and_set(&self.lock_val, atom.ATOMIC_ACQUIRE): pass def _unlock(self): if MEMBUDDY_THREAD_SAFE != 0: - atom.__atomic_clear(c.Addr(self.lock_val), atom.ATOMIC_RELEASE) + atom.__atomic_clear(&self.lock_val, atom.ATOMIC_RELEASE) # === Virtual function overrides === def alloc(self, size: sizet) -> t.CPtr: self._lock() result: t.CPtr = None if size != 0 and self.usable is not None: - # +MEMBUDDY_CANARY:预留块尾 8 字节哨兵(用户数据区之外),越界写定位用 + # OOM 诊断:≥8KB 的请求计数。此字段此前从不自增,报告里恒为 big=0, + # 无法据此分辨“多次小块”与“少量大块”。阈值与字段注释口径一致。 + if size >= 8192: + self.big_alloc_count += 1 + # +CANARY: reserve 8 sentinel bytes past the user area for overflow detection. needed: sizet = size + MEMBUDDY_HEADER_SIZE + MEMBUDDY_CANARY if self.detection != 0: - # [DET] 前后各 2 字节 0xCDCD 标记,实际比请求多 4 字节 + # [DET] 2 bytes of 0xCDCD on each side: 4 bytes more than requested. needed = needed + 4 - # 尺寸溢出守卫(fail-fast):size 近 u64 上限时 `size + 16 (+4)` 回绕成 - # 极小值 ⇒ order 变小 ⇒ 会"成功"返回一个远小于请求的块,调用方按请求 - # 尺寸写入即越界写穿相邻块——这是堆损坏最上游的来源之一(free list - # 槽位/块首字被写成垃圾,最终在别处表现为 ASAN)。合法请求不可能回绕, - # 故就地响亮终止;不回绕、不静默返回小块、不改动既有的"超池→None"契约 - # (后者仍由下方 `order <= self.max_order` 分支按原样处理)。 + # Size-overflow guard (fail-fast): when size is near the u64 limit, `size + 16 (+4)` + # wraps to a tiny value => a smaller order => a block far smaller than requested is + # "successfully" returned and the caller overflows the neighbours. A valid request can + # never wrap, so terminate loudly here; never wrap, never silently return a small block, + # and leave the existing "too big -> None" contract (the `order <= self.max_order` + # branch below) untouched. if needed < size: stdio.printf("[MEM] alloc 调用者=上层 size 溢出: size=%zu needed=%zu(size+HEADER+CANARY 回绕,拒绝分配)\n", size, needed) stdio.fflush(None) stdlib.exit(1) - order: t.CInt = self._order_for_size(needed) + order: int = self._order_for_size(needed) if order <= self.max_order: block: t.CPtr = self._split_to_order(order) if block is not None: - c.DerefAs(block, t.CPtr(t.CUInt64T((order << 1) | 1))) - result = t.CPtr(t.CUInt64T(block) + MEMBUDDY_HEADER_SIZE) - # 写块尾 canary(block + block_size - CANARY) - # order 恒 ≥ 0(_order_for_size 返回值),内联左移等价于 - # _block_size_at_order(order),省去每次 alloc 的 call/ret。 + *((uint64*)(block)) = uint64((order << 1) | 1) # header: odd = allocated + result = block + MEMBUDDY_HEADER_SIZE bs: sizet = MEMBUDDY_MIN_BLOCK << sizet(order) - c.DerefAs(t.CPtr(t.CUInt64T(block) + bs - MEMBUDDY_CANARY), - t.CPtr(MEMBUDDY_CANARY_PATTERN)) - # 分配守卫:填充用户数据区 0xCD 哨兵(未初始化检测)。 - # 调用方初始化(list.__init__ 等)覆盖前,字段保持 0xCDCD... - # CTORDBG 打印 d0-d40 时:0xCDCD...=未初始化,其他=被覆盖。 + # Write the block-tail canary (just past the user area). + *((uint64*)(block + bs - MEMBUDDY_CANARY)) = uint64(MEMBUDDY_CANARY_PATTERN) if MEMBUDDY_FILL_GUARD != 0: + # Diagnostic fill: uninitialised fields read back as 0xCD. fill_size: sizet = bs - MEMBUDDY_HEADER_SIZE - MEMBUDDY_CANARY if fill_size > 0: string.memset(result, MEMBUDDY_FILL_PATTERN, fill_size) - # 记录到 checkpoint 栈(供 reset_to 回收) - # 栈满时 stdlib.realloc 翻倍扩容(防爆满后记录缺失 - # → reset 不回收 + _is_valid_ptr 栈校验退化的连锁问题) + # Record on the checkpoint stack (for reset_to reclamation). When full, double + # it with stdlib.realloc (a full stack would stop recording -> reset misses + # blocks and _is_valid_ptr's stack check degrades). if self.chk_stack is not None: if self.chk_depth >= self.chk_cap: new_cap: sizet = self.chk_cap * 2 new_stack: t.CPtr = t.CPtr(stdlib.realloc(self.chk_base, new_cap * 8)) if new_stack is not None: self.chk_base = new_stack - self.chk_stack = (t.CUInt64T | t.CPtr)(t.CPtr(new_stack)) + self.chk_stack = (uint64*)(new_stack) self.chk_cap = new_cap else: - # realloc 失败:块不再记录 → reset_to 无法回收该块 → - # 渐进泄漏最终 1GB 主池 OOM。fail-fast 暴露而非静默。 + # realloc failed: the block is not recorded -> reset_to cannot + # reclaim it -> a gradual leak ending in 1 GB main-pool OOM. + # fail-fast instead of silent. stdio.printf( "[MEM] 记录栈 realloc 失败 cap=%zu depth=%zu(reset 将失效,终止)\n", self.chk_cap, self.chk_depth) if self.chk_depth < self.chk_cap: - self.chk_stack[self.chk_depth] = t.CUInt64T(block) + self.chk_stack[self.chk_depth] = uint64(block) self.chk_depth += 1 if result is not None and self._det_records is not None: - base_ret_d: t.CUInt64T = t.CUInt64T(result) - # 返回指针(detection 布局下 = 用户区 + 2,前标记之后) - rec_ptr_d: t.CUInt64T = base_ret_d + base_ret_d: uint64 = uint64(result) + # Returned pointer (in detection layout = user area + 2, past the front marker). + rec_ptr_d: uint64 = base_ret_d if self.detection != 0: - # [DET] 布局 [0xCDCD][请求大小内存][0xCDCD]:返回指针 = 用户区 + 2 - # (前标记之后);记录 {返回 ptr, size} 到独立记录区;随后探测全部 - # 存活块的后标记——被破坏 = 越界写,立即汇报并终止。 - m_f_d: t.CUInt16T | t.CPtr = (t.CUInt16T | t.CPtr)(t.CPtr(base_ret_d)) - m_f_d[0] = t.CUInt16T(MEMBUDDY_DET_MARKER) - tail_d: t.CUInt64T = base_ret_d + 2 + t.CUInt64T(size) - m_t_d: t.CUInt16T | t.CPtr = (t.CUInt16T | t.CPtr)(t.CPtr(tail_d)) - m_t_d[0] = t.CUInt16T(MEMBUDDY_DET_MARKER) + # [DET] Layout [0xCDCD][user][0xCDCD]: returned ptr = user area + 2. Record + # {ptr, size} and stamp both markers; a broken marker = out-of-bounds write. + m_f_d: uint16* = (uint16*)(base_ret_d) + m_f_d[0] = uint16(MEMBUDDY_DET_MARKER) + tail_d: uint64 = base_ret_d + 2 + uint64(size) + m_t_d: uint16* = (uint16*)(tail_d) + m_t_d[0] = uint16(MEMBUDDY_DET_MARKER) rec_ptr_d = base_ret_d + 2 - # 采样:每 MEMBUDDY_DET_PROBE_PERIOD 次 alloc 做一次全表扫描 - # (分配重叠 + [MEMDBG] 残留探测),其余只追加记录(O(1))。损坏是 - # 持久性的,不依赖即时扫描——若每次全表 O(n) 则整体 O(n²)。该采样块 - # 即 MemBuddy.alloc 的 O(n) 热点(量级见 MEMBUDDY_PERIOD_SCAN 定义), - # 故整体挂在编译期总开关下,默认 0 = 不执行该分支(历史行为)。 - # 运行期重诊断(--detection-deep → self.deep_detect != 0)额外开此 - # 扫描,并把采样掩码短路成"每次都扫":两个判定点(此处总开关 + - # 下方采样)各加一个 deep_detect 分支,deep_detect 恒 0 时两条 - # 表达式与历史实现逐字节相同。 + # Sampling: run the O(n) full-table scan every MEMBUDDY_DET_PROBE_PERIOD allocs + # (allocs otherwise only append a record, O(1)); corruption is persistent so a + # delayed scan still finds it. Gated by the compile-time switch (default 0 = + # never run) and by deep_detect (--detection-deep = scan on every alloc). if MEMBUDDY_PERIOD_SCAN != 0 or self.deep_detect != 0: if (self._det_count & (MEMBUDDY_DET_PROBE_PERIOD - 1)) == 0 or self.deep_detect != 0: - # 分配重叠即时检测:新块地址是否已在 active 记录(同一块重复 - # 分配实锤——SlotsBugTest 7B buf 与 72B Token 同 ptr、cargs - # 只写 0/40 根因)。重叠 = MemBuddy free list 损坏(active 块 - # 混入 free list),在第一次重叠处拦截并打印两个 size。 - ff_d: t.CUInt64T = t.CUInt64T(1) << 63 + # Overlap check: the new block's address must not already be an active + # record (same block allocated twice = free list corruption). + ff_d: uint64 = uint64(1) << 63 dup_i: sizet = 0 while dup_i < self._det_count: - drp: t.CUInt64T = self._det_records[dup_i * 2] - drs: t.CUInt64T = self._det_records[dup_i * 2 + 1] + drp: uint64 = self._det_records[dup_i * 2] + drs: uint64 = self._det_records[dup_i * 2 + 1] if drp == rec_ptr_d and (drs & ff_d) == 0: stdio.printf("[DET] 分配重叠: 新alloc(size=%llu)与记录[%zu](sz=%llu)同地址 ptr=%llu(同一块重复分配,free list 损坏)\n", - t.CUInt64T(size), dup_i, drs & (ff_d - 1), rec_ptr_d) + uint64(size), dup_i, drs & (ff_d - 1), rec_ptr_d) stdio.fflush(None) stdlib.exit(1) dup_i += 1 if _memdbg_enable != 0: - # [MEMDBG] 新出块是否仍残留于同 order free list(此前的重复 put - # 使链表含两条本块 → pop 后还有残条 → 本块将被二次分配,双向写穿)。 - # 记录表重叠检测要等下次 pop 同地址才撞见,这里在出块当场提前拦截。 - dbg_block: t.CPtr = t.CPtr(base_ret_d - MEMBUDDY_HEADER_SIZE) - dbg_hdr: t.CPtr = t.CPtr(c.Deref((t.CUInt64T | t.CPtr)(dbg_block))) - dbg_order: t.CInt = t.CInt(t.CUInt64T(dbg_hdr) >> 1) - # 取值校验(fail-fast,与 _fl_pop/_coalesce 同口径):dbg_order - # 直接取自刚出块的块头(正常 = (order<<1)|1 的 order),但块头若 - # 已被越界写穿(本分支正是为查这类损坏而开),`free_lists[dbg_order]` - # 就是越界读;读到的垃圾再被当块地址解引用 ⇒ 崩溃点漂移。合法 - # 路径下 dbg_order ∈ [0, max_order] ⇒ 只在已损坏时改变行为。 + # [MEMDBG] A freshly popped block must not still be on its own free + # list (a duplicate put would leave a residual node -> reallocated). + dbg_block: t.CPtr = result - MEMBUDDY_HEADER_SIZE + dbg_hdr: t.CPtr = t.CPtr(*((uint64*)(dbg_block))) + dbg_order: int = int(uint64(dbg_hdr) >> 1) + # dbg_order comes from the block header; if the header were corrupted + # (the very case this branch hunts), indexing free_lists[dbg_order] + # would read out of bounds and feed garbage to a deref. fail-fast. if dbg_order < 0 or dbg_order > MEMBUDDY_MAX_ORDERS: stdio.printf("[MEMDBG] alloc dbg_order 非法(块头被写穿): hdr=0x%llX dbg_order=%d 容量=%d block=%llu(终止)\n", - t.CUInt64T(dbg_hdr), dbg_order, MEMBUDDY_MAX_ORDERS + 1, - t.CUInt64T(dbg_block)) + uint64(dbg_hdr), dbg_order, MEMBUDDY_MAX_ORDERS + 1, + uint64(dbg_block)) stdio.fflush(None) stdlib.exit(1) dbg_cur: t.CPtr = t.CPtr(self.free_lists[dbg_order]) dbg_n: sizet = 0 - while t.CUInt64T(dbg_cur) != 0: - if t.CUInt64T(dbg_cur) == t.CUInt64T(dbg_block): + while uint64(dbg_cur) != 0: + if uint64(dbg_cur) == uint64(dbg_block): stdio.printf("[MEMDBG] alloc 重复出块: size=%llu block=%llu 仍残留 free list order=%d(重复 put 实锤,终止)\n", - t.CUInt64T(size), t.CUInt64T(dbg_block), dbg_order) + uint64(size), uint64(dbg_block), dbg_order) stdio.fflush(None) stdlib.exit(1) dbg_n += 1 @@ -1528,141 +1560,165 @@ class MemBuddy(MemManager): stdio.printf("[MEMDBG] alloc 扫描 free list order=%d 成环/超长(自检异常,终止)\n", dbg_order) stdio.fflush(None) stdlib.exit(1) - dbg_cur = t.CPtr(c.Deref((t.CUInt64T | t.CPtr)(dbg_cur))) + dbg_cur = t.CPtr(*((uint64*)(dbg_cur))) if self._det_count >= self._det_cap: - # 记录表满:stdlib.realloc 翻倍扩容(防 is_alive/_det_probe - # 因容量耗尽漏检——SlotsBugTest 编译 includes 超 2M 次 alloc, - # 满后新块无记录 → is_alive 误报"读取悬空") + # Record table full: double it so is_alive/_det_probe never miss live blocks. new_cap: sizet = self._det_cap * 2 new_rec: t.CPtr = t.CPtr(stdlib.realloc(self._det_arena, new_cap * 16)) if new_rec is not None: self._det_arena = new_rec - self._det_records = (t.CUInt64T | t.CPtr)(t.CPtr(new_rec)) + self._det_records = (uint64*)(new_rec) self._det_cap = new_cap if self._det_count < self._det_cap: self._det_records[self._det_count * 2] = rec_ptr_d - self._det_records[self._det_count * 2 + 1] = t.CUInt64T(size) + self._det_records[self._det_count * 2 + 1] = uint64(size) self._det_count += 1 - # 探测判定:detection 关闭时不进入(故未开 --detection 的默认路径 - # 不额外读 deep_detect);开 detection 时 deep_detect != 0 → 每次 - # alloc 都探测,否则退回历史采样(每 MEMBUDDY_DET_PROBE_PERIOD 次)。 + # Probe decision: never entered when detection is off; with it on, deep_detect + # probes every alloc, otherwise the historical sampling is used. if self.detection != 0 and ((self._det_count & (MEMBUDDY_DET_PROBE_PERIOD - 1)) == 0 or self.deep_detect != 0): self._det_probe() if self.detection != 0: - result = t.CPtr(base_ret_d + 2) + result = result + 2 if result is None: - # 分配失败诊断(定位"异常 size/池耗尽导致 None → 调用方解引用段错误") - # size == 0 与"池耗尽"是两回事:alloc 入口的 `size != 0` 守卫会直接 - # 跳过 → 返回 NULL,调用方却只看到"分配失败",无从判断真因。此处 - # 显式区分:0 尺寸几乎总来自调用处的尺寸表达式被静默求值为 0 - # (如未解析/被遮蔽的 t.CDefine:Phase1.MAX_CLASSES * Phase1.SHA1_LEN - # → 0)。本项目禁止静默回退,故给出可区分的诊断(不 exit,沿用本函数 - # "alloc 失败返回 None 由调用方处理"的既有契约)。 + # Allocation-failure diagnostics. A zero size and "pool exhausted" are different + # cases: the entry `size != 0` guard skips the former and returns NULL. A zero + # size almost always means the caller's size expression evaluated to 0 + # (unresolved or shadowed constant). Never exit here: alloc failures return None + # for the caller to handle (tests rely on free-and-retry after OOM). if size == 0: stdio.printf("[MEM] alloc 请求尺寸为 0:返回 NULL(非池耗尽!)——" "请检查调用处尺寸表达式是否被静默求值为 0" "(常量未解析/被同名符号遮蔽/被算成 0)\n") + # Fields below are read from existing state only (this branch runs only on + # failure): site tag, pool alloc sequence, zero-size hit count, pool state, + # and the stage/detail context set by TPV. + usable_addr: uint64 = 0 + if self.usable is not None: + usable_addr = uint64(self.usable) + marked: int = 0 + if self.chk_stack is not None: + marked = 1 + zero_hits: sizet = _zero_alloc_note() + stdio.printf("[MEM] site=%s ctx=%s/%s marked=%d 本池分配序号=%zu 0尺寸累计=%zu " + "池基址=%llu 池容量=%zu 已用=%zu 空闲=%zu " + "max_order=%d detection=%d deep=%d\n", + _det_site_name(_alloc_site_current()), + _alloc_ctx_stage_str(), _alloc_ctx_detail_str(), marked, + self.chk_depth, + zero_hits, usable_addr, self.usable_size, self.used_bytes(), + self.free_bytes(), self.max_order, + self.detection, self.deep_detect) stdio.fflush(None) elif self.usable is not None: needed2: sizet = size + MEMBUDDY_HEADER_SIZE + MEMBUDDY_CANARY - order2: t.CInt = self._order_for_size(needed2) + order2: int = self._order_for_size(needed2) stdio.printf("[MEM] alloc 失败 size=%zu needed=%zu order=%d max_order=%d used=%zu pool_base=%llu\n", size, needed2, order2, self.max_order, self.used_bytes(), - t.CUInt64T(self.usable)) - # 越界写诊断:free list 结构损坏(self_check!=0)说明有越界写穿 - # 破坏块头/free list next 指针 → alloc 找不到块(used 虚高非真实耗尽) - sc: t.CInt = self.self_check() + uint64(self.usable)) + # 归属(always-on,无需 --detection):site 来自 alloc_tagged 维护的站点 tag, + # ctx=阶段/当前文件由 TPV 的 SetAllocCtx 维护(未设置则打印 ?)。只在失败路径 + # 打印,不影响正常流程;缺这一行时 [MEM] 只给池基址,无法判断谁吃满了池子。 + stdio.printf("[MEM] 归属 site=%s ctx=%s/%s 池容量=%zu\n", + _det_site_name(_alloc_site_current()), + _alloc_ctx_stage_str(), _alloc_ctx_detail_str(), + self.usable_size) + # 尺寸画像:池被谁吃满,看存活块的 order 分布(只读既有状态,无需 --detection) + self.dump_alive_orders() + # self_check != 0 means the free list structure is damaged (a block header or + # next pointer was written through), so alloc finds no block despite free space. + sc: int = self.self_check() stdio.printf("[MEM] alloc 失败 self_check=%d free_bytes=%zu free_count=%zu chk_depth=%zu big=%zu\n", sc, self.free_bytes(), self.free_count(), self.chk_depth, self.big_alloc_count) - # chk_depth 巨大(数百万)→ 分配次数爆炸(每语句/每调用反复分配); - # chk_depth 不大但 used 巨大 → 单次大分配(缓冲/表扩容)反复发生 + # Huge chk_depth => allocation storm (per-statement/per-call); small chk_depth + # with huge used => repeated single large allocations (buffers/table growth). if order2 > self.max_order: stdio.printf("[MEM] 原因:请求 size 超 pool 最大块(order %d > max %d)\n", order2, self.max_order) else: stdio.printf("[MEM] 原因:pool 耗尽(空闲 %zu 字节不足)\n", self.free_bytes()) - # 分配失败(alloc(0) 或池耗尽):解锁并返回 None,由调用者决定后续处理。 - # 不能 exit——测试依赖 alloc 失败返回 None 后继续 free+重试 - # (如 OOM section: 耗尽后 free 一块再 alloc 应成功)。 + # Failure (alloc(0) or exhausted): unlock and return None; the caller decides. + # Must not exit -- tests free a block and retry after an OOM. self._unlock() return None self._unlock() return result def alloc_tagged(self, size: sizet, tag: sizet) -> t.CPtr: - """带调用点 tag 的 alloc 包装(供 [FREEFRAME] 直接指名写入者申请点)。 + """alloc wrapper that records the caller's site tag for [FREEFRAME] / zero-size reports. - 语义与 alloc 完全一致,仅多一步登记:detection != 0 时,转发 - self.alloc(size) 后把 (返回 ptr, tag) 写入模块级平行表 _det_site_arena, - 索引 = 调用前 self._det_count(= 本次新记录下标)。_ff_report 据此打印 - site=文件名:行。tag 由调用点以稳定常量(DETSITE_*)传入。 - - detection == 0(或记录表未建 / tag==0)时直接转发 self.alloc(size): - 不读写任何 tag 状态,MemBuddy.alloc 函数体未改动 ⇒ 分配器默认路径 - 逐字节等价(本包装对该路仅是一次尾转发)。记录表满且 realloc 失败时 - _det_count 未增 ⇒ 不登记(报文打 site=?),不伪造。 + Same semantics as alloc, plus two registrations: + 1. site tag (independent of detection): set _alloc_pending_site before forwarding and + restore it after, so alloc's zero-size / failure reports print site=file:line; + 2. when detection != 0: after self.alloc(size), store (returned ptr, tag) into the + module-level side table _det_site_arena at index = the pre-call self._det_count. + If the record table is full and realloc fails, _det_count does not advance, nothing is + registered, and the report prints site=? instead of fabricating one. """ + prev: uint64 = _alloc_site_enter(uint64(tag)) if self.detection == 0 or self._det_records is None or tag == 0: - return self.alloc(size) + r_plain: t.CPtr = self.alloc(size) + _alloc_site_leave(prev) + return r_plain before: sizet = self._det_count r: t.CPtr = self.alloc(size) if r is not None and self._det_count == before + 1: - _det_site_put(before, t.CUInt64T(r), t.CUInt64T(tag)) + _det_site_put(before, uint64(r), uint64(tag)) + _alloc_site_leave(prev) return r - def free(self, ptr: t.CPtr) -> t.CInt: + def free(self, ptr: t.CPtr) -> int: self._lock() if ptr is not None: p_eff: t.CPtr = ptr if self.detection != 0: - # [DET] 返回指针比实际用户区靠后 2 字节,回收时向前偏移 2 - p_eff = t.CPtr(t.CUInt64T(ptr) - 2) - # 记录表置 free 标志(解耦于 detection,2026-09-04):非 detection - # 也需置位,否则 free 后同地址再 alloc(正常 reuse)被误报重叠。 + # [DET] Returned ptr is 2 bytes past the user area; step back on free. + p_eff = ptr - 2 + # Mark the record free (independent of detection): otherwise a later alloc of the + # same address (normal reuse) would be misreported as an overlap. if self._det_records is not None: - # 从记录表取本条真实 size(detection 下 free 后填 0xCD,UAF - # 写入检测:free 后块被写穿 → 探测时 0xCD 被破坏 → 报警); - # 记录保留并置 free 标志(探测时对仍 free 的块验证 0xCD 完整)。 + # Read this record's real size (detection fills 0xCD after free, so a later + # write-through breaks the fill = use-after-free). Keep the record and set the + # free bit; the probe checks the 0xCD fill of still-free blocks. ri: sizet = 0 while ri < self._det_count: - rp: t.CUInt64T = self._det_records[ri * 2] - rs: t.CUInt64T = self._det_records[ri * 2 + 1] - if rp == t.CUInt64T(ptr) and (rs & (t.CUInt64T(1) << 63)) == 0: + rp: uint64 = self._det_records[ri * 2] + rs: uint64 = self._det_records[ri * 2 + 1] + if rp == uint64(ptr) and (rs & (uint64(1) << 63)) == 0: if self.detection != 0 and rs > 0: string.memset(ptr, MEMBUDDY_FILL_PATTERN, rs) - self._det_records[ri * 2 + 1] = rs | (t.CUInt64T(1) << 63) + self._det_records[ri * 2 + 1] = rs | (uint64(1) << 63) break ri += 1 if self._is_valid_ptr(p_eff) != 0: - block: t.CPtr = t.CPtr(t.CUInt64T(p_eff) - MEMBUDDY_HEADER_SIZE) - stored: t.CPtr = t.CPtr(c.Deref((t.CUInt64T | t.CPtr)(block))) - stored_val: t.CUInt64T = t.CUInt64T(stored) - order: t.CInt = t.CInt(stored_val >> 1) + block: t.CPtr = p_eff - MEMBUDDY_HEADER_SIZE + stored: t.CPtr = t.CPtr(*((uint64*)(block))) + stored_val: uint64 = uint64(stored) + order: int = int(stored_val >> 1) if (stored_val & 1) == 0: - # 双重 free:块已释放(header 偶)。debug 关 = 按原语义静默忽略 - # (不再次 coalesce,避免 free list 重复 put 污染);debug 开 = - # 终止并暴露悬空/重复 free 调用点。 + # Double free (even header = already freed): silent no-op when debug is + # off (never coalesce again, to avoid a duplicate free-list put); with + # debug on, terminate and expose the stale/duplicate free call site. if _memdbg_enable != 0: stdio.printf("[MEMDBG] 悬空/重复 free: ptr=%llu block=%llu header=0x%llX(偶数=已释放/未初始化,终止)\n", - t.CUInt64T(ptr), t.CUInt64T(block), stored_val) + uint64(ptr), uint64(block), stored_val) stdio.fflush(None) stdlib.exit(1) self._unlock() return 1 - # [FREEFRAME] 释放前所有者检查(仅 detection):活块的块尾 canary 被 - # 写穿 ⇒ 本块所有者就是"写穿右邻空闲块首字"的写入者(canary 后 8 字节 - # 即右邻块头)。放在下面 [MEMDBG] 扫描之前:重复归还(double-put)与 - # 越界写是两类不同损坏,先报更上游的那类。detection == 0 时立即返回。 + # [FREEFRAME] Pre-free owner check (detection only): a corrupted block-tail + # canary means this block's owner is the writer that ran over into the right + # neighbour's first word (the neighbour header sits 8 bytes past the canary). + # Done before the [MEMDBG] scan: double-put and overflow are different faults. self._ff_owner_check(block, order) if _memdbg_enable != 0: - # [MEMDBG] 重复归还检测:活跃块(header 奇数)绝不该已在本 order - # free list;命中 = 同块被 put 两次(free list 重复 put 实锤)。 + # [MEMDBG] Double-return check: a live block (odd header) must never + # already be on this order's free list; a hit = the same block put twice. dbg_f: t.CPtr = t.CPtr(self.free_lists[order]) dbg_fn: sizet = 0 - while t.CUInt64T(dbg_f) != 0: - if t.CUInt64T(dbg_f) == t.CUInt64T(block): + while uint64(dbg_f) != 0: + if uint64(dbg_f) == uint64(block): stdio.printf("[MEMDBG] 重复归还: free(ptr=%llu) block=%llu 已在 free list order=%d(double-put 实锤,终止)\n", - t.CUInt64T(ptr), t.CUInt64T(block), order) + uint64(ptr), uint64(block), order) stdio.fflush(None) stdlib.exit(1) dbg_fn += 1 @@ -1670,56 +1726,71 @@ class MemBuddy(MemManager): stdio.printf("[MEMDBG] free 扫描 free list order=%d 成环/超长(自检异常,终止)\n", order) stdio.fflush(None) stdlib.exit(1) - dbg_f = t.CPtr(c.Deref((t.CUInt64T | t.CPtr)(dbg_f))) - c.DerefAs(block, t.CPtr(0)) + dbg_f = t.CPtr(*((uint64*)(dbg_f))) + *((uint64*)(block)) = 0 self._coalesce(block, order) elif _memdbg_enable != 0: - # [MEMDBG] 无效/悬空 free(原实现静默忽略):块在本池范围内但 header - # 为偶 = 已被释放/未初始化 → 悬空重复 free(stale 指针)。范围外 - # (非本池指针)是正常多池场景,不报。 - dbg_eff: t.CUInt64T = t.CUInt64T(p_eff) - dbg_blk: t.CUInt64T = dbg_eff - MEMBUDDY_HEADER_SIZE - if (dbg_eff >= MEMBUDDY_HEADER_SIZE and dbg_blk >= t.CUInt64T(self.usable) - and dbg_blk < t.CUInt64T(self.usable) + self.usable_size): - dbg_hv: t.CUInt64T = t.CUInt64T(t.CPtr(c.Deref((t.CUInt64T | t.CPtr)(dbg_blk)))) + # [MEMDBG] Invalid/stale free (previously silent): the block is inside this pool + # but the header is even = already freed / never initialised => stale free. + # Pointers outside the pool belong to another pool and are not reported. + dbg_eff: uint64 = uint64(p_eff) + dbg_blk: uint64 = dbg_eff - MEMBUDDY_HEADER_SIZE + if (dbg_eff >= MEMBUDDY_HEADER_SIZE and dbg_blk >= uint64(self.usable) + and dbg_blk < uint64(self.usable) + self.usable_size): + dbg_hv: uint64 = *((uint64*)(dbg_blk)) if (dbg_hv & 1) == 0: stdio.printf("[MEMDBG] 悬空/重复 free: ptr=%llu block=%llu header=0x%llX(偶数=已释放/未初始化,终止)\n", - t.CUInt64T(ptr), dbg_blk, dbg_hv) + uint64(ptr), dbg_blk, dbg_hv) stdio.fflush(None) stdlib.exit(1) + else: + # _is_valid_ptr 判无效 = 该块不会被回收到空闲链(静默 no-op 是"池漏满却 + # 无线索"的元凶)。首次出现即打印一行现场,之后不再刷屏。 + global _rej_free_reported + if _rej_free_reported == 0: + _rej_free_reported = 1 + e_eff: uint64 = uint64(p_eff) + e_blk: uint64 = e_eff - MEMBUDDY_HEADER_SIZE + e_hdr: uint64 = 0 + if (e_eff >= MEMBUDDY_HEADER_SIZE and self.usable is not None + and e_blk >= uint64(self.usable) + and e_blk < uint64(self.usable) + self.usable_size): + e_hdr = *((uint64*)(e_blk)) + stdio.printf("[MEM] free 被拒绝(不在记录窗口 / 块头非存活(偶数) / 越界 / 未对齐),该块不会回收到空闲链: ptr=%llu block=%llu header=0x%llX chk_depth=%zu\n", + e_eff, e_blk, e_hdr, self.chk_depth) + stdio.fflush(None) self._unlock() return 1 def _rebuild_free_lists(self): - """按二进制分解重建空闲链表(reset 恢复初始分配格局) + """Rebuild the free lists by binary decomposition (reset restores the initial layout). - 与 __init__ 的初始化分解一致:非 2 的幂 arena 会拆成多个 - 2 的幂空闲块(buddy 合并跨分解边界时因 order 不匹配自然 - 停止,安全);2 的幂 arena 等价于单个 max_order 块。 + Same decomposition as __init__: a non-power-of-two arena splits into power-of-two free + blocks (buddy coalescing naturally stops at decomposition boundaries because the orders + no longer match); a power-of-two arena is a single max_order block. """ - i: t.CInt + i: int for i in range(MEMBUDDY_MAX_ORDERS + 1): self.free_lists[i] = 0 pos: sizet = 0 rem: sizet = self.usable_size while rem >= MEMBUDDY_MIN_BLOCK: blk: sizet = _largest_pow2_le(rem) - o: t.CInt = 0 + o: int = 0 ob: sizet = MEMBUDDY_MIN_BLOCK while ob < blk: ob = ob << 1 o += 1 - self._fl_push(o, t.CPtr(t.CUInt64T(self.usable) + pos)) + self._fl_push(o, self.usable + pos) pos += blk rem -= blk - def reset(self) -> t.CInt: + def reset(self) -> int: if self.usable is None: return 0 self._rebuild_free_lists() - # [DET] reset 重建全部 free list = 全部块释放:记录表同步清空, - # 防止后续 alloc 复用旧块被误报(与 reset_to 截断同因)。 - # 2026-09-04 记录表已与 detection 解耦,清空条件同样不能挂 detection。 + # [DET] reset frees every block, so clear the record table too; otherwise a later + # alloc reusing an old block would be misreported (same reason as reset_to's truncation). if self._det_records is not None: self._det_count = 0 return 1 @@ -1733,25 +1804,23 @@ class MemBuddy(MemManager): return None p_eff: t.CPtr = ptr if self.detection != 0: - # [DET] 与 free 一致:先向前偏移 2 字节再做块级计算 - p_eff = t.CPtr(t.CUInt64T(ptr) - 2) + # [DET] Same as free: step back 2 bytes before block-level math. + p_eff = ptr - 2 if self._is_valid_ptr(p_eff) == 0: return None - block: t.CPtr = t.CPtr(t.CUInt64T(p_eff) - MEMBUDDY_HEADER_SIZE) - stored: t.CPtr = t.CPtr(c.Deref((t.CUInt64T | t.CPtr)(block))) - stored_val: t.CUInt64T = t.CUInt64T(stored) - old_order: t.CInt = t.CInt(stored_val >> 1) - # 容量口径必须与 alloc **逐字同源**(唯一真相源):alloc 的块容量判据是 - # `needed = size + HEADER + CANARY (+4 检测标记)`,块内真实可写 = bs-16。 - # 本函数原缺 CANARY(`needed = new_size + HEADER`)⇒ 原地判定宽一整个边界: - # new_size ∈ (bs-16, bs-8] 时 new_order ≤ old_order 而原地返回,调用方按契约 - # 写 new_size 字节即越界写进自己的 **canary**(例:order-1 bs=64,可写 48; - # 旧口径放行到 56 ⇒ 多写 8 字节入 canary)。补齐 CANARY 后两侧一致: - # 48(=bs-16)是块内真实上限、56(=bs-8)是错的口径;>48 必须升阶(order2=128)。 + block: t.CPtr = p_eff - MEMBUDDY_HEADER_SIZE + stored: t.CPtr = t.CPtr(*((uint64*)(block))) + stored_val: uint64 = uint64(stored) + old_order: int = int(stored_val >> 1) + # Capacity must match alloc exactly (single source of truth): alloc's block capacity is + # `needed = size + HEADER + CANARY (+4 for detection markers)`, so the real writable + # bytes are bs-16. Without CANARY the in-place test would be one boundary too wide + # (new_size in (bs-16, bs-8] would return in place and let the caller overflow its own + # canary), so CANARY is required here for both sides to agree. needed: sizet = new_size + MEMBUDDY_HEADER_SIZE + MEMBUDDY_CANARY if self.detection != 0: - needed = needed + 4 # [DET] 前后标记 - new_order: t.CInt = self._order_for_size(needed) + needed = needed + 4 # [DET] front/back markers + new_order: int = self._order_for_size(needed) if new_order <= old_order: return ptr new_ptr: t.CPtr = self.alloc(new_size) @@ -1761,103 +1830,102 @@ class MemBuddy(MemManager): old_data_size: sizet = old_block_size - MEMBUDDY_HEADER_SIZE cp_len: sizet = old_data_size if self.detection != 0: - cp_len = old_data_size - 4 # [DET] 排除前后 2 字节标记 + cp_len = old_data_size - 4 # [DET] exclude the 2-byte front/back markers string.memcpy(new_ptr, ptr, cp_len) self.free(ptr) return new_ptr # ============================================================ - # [DET] is_alive - 读取侧存活校验:ptr 是否在当前记录表的 active - # 记录中(alloc 后未 free)。返回 1=存活 / 0=悬空(不在记录或已 - # free)。detection 关闭或记录表不可用时返回 1(不拦截)。 - # 补 _det_probe 盲区:写入侧检测(标记/0xCD)拦不住 use-after-free - # 读取(如 SlotsBugTest KwEntry cargs=cl.args 悬空指向复用块)。 + # [DET] is_alive - read-side liveness check: is ptr an active record in + # the current record table (allocated, not freed)? 1 = alive / 0 = dangling. + # Returns 1 when detection is off or the table is unavailable (no blocking). + # Covers _det_probe's blind spot: write-side markers cannot catch use-after-free + # reads (a dangling pointer into a reused block). # ============================================================ - def is_alive(self, ptr: t.CPtr) -> t.CInt: + def is_alive(self, ptr: t.CPtr) -> int: if self.detection == 0: return 1 if self._det_records is None: return 1 - target: t.CUInt64T = t.CUInt64T(ptr) - free_flag: t.CUInt64T = t.CUInt64T(1) << 63 + target: uint64 = uint64(ptr) + free_flag: uint64 = uint64(1) << 63 i: sizet = 0 while i < self._det_count: - rp: t.CUInt64T = self._det_records[i * 2] - rs: t.CUInt64T = self._det_records[i * 2 + 1] + rp: uint64 = self._det_records[i * 2] + rs: uint64 = self._det_records[i * 2 + 1] if rp == target and (rs & free_flag) == 0: return 1 i += 1 return 0 # ============================================================ - # [DET] _det_probe - 分配检测探测:遍历记录表,验证每个存活块的 - # 后标记(返回 ptr + size 处 2 字节)仍为 0xCDCD。 - # 被破坏 = 越界写(数据区写穿),立即汇报 ptr/size 并终止—— - # 把"随机崩溃/字段被覆盖"变成"确定性定位到越界写发生的分配块"。 + # [DET] _det_probe - walk the record table and check every live block's tail + # marker (2 bytes at ptr + size), which must still read 0xCDCD. A broken + # marker = out-of-bounds write: report ptr/size and terminate, turning a + # random crash into a deterministic "which allocation overflowed". # ============================================================ def _det_probe(self): if self._det_records is None: return - free_flag: t.CUInt64T = t.CUInt64T(1) << 63 + free_flag: uint64 = uint64(1) << 63 i: sizet = 0 while i < self._det_count: - p: t.CUInt64T = self._det_records[i * 2] - s: t.CUInt64T = self._det_records[i * 2 + 1] + p: uint64 = self._det_records[i * 2] + s: uint64 = self._det_records[i * 2 + 1] if (s & free_flag) != 0: - # free 记录:跳过——buddy 分配器合并/重分配使 free 块内存不排他 - # (块合并进更大块后,新分配的用户区可覆盖旧记录的前标记区而旧块 - # 头仍为 0),前标记校验会误报(SlotsBugTest 1024B 块 hdr=0x0、 - # 前标记被 2048 合并分配数据写零的误报例,与 w0/w1 0xCD 校验同类)。 - # 只校验 active 记录(存活块独占,header LSB=1 不会合并, - # 前/后标记可靠)。 + # Free record: skip. Buddy coalescing/reallocation makes a freed block's memory + # non-exclusive (a merged-in neighbour can overwrite the old front marker while + # the old header stays 0), so marker checks would false-positive. Only active + # records are checked: a live block is exclusive (odd header => never merged), + # so its front/tail markers are reliable. i += 1 continue if s == 0: i += 1 - continue # 无 size 记录(理论不发生,防御) - # active 记录:前标记(下溢检测)+ 后标记(上溢检测) - m_fa: t.CUInt16T | t.CPtr = (t.CUInt16T | t.CPtr)(t.CPtr(p - 2)) - mfv_a: t.CUInt16T = m_fa[0] - if mfv_a != t.CUInt16T(MEMBUDDY_DET_MARKER): + continue # no size recorded (defensive; should not happen) + # Active record: front marker (underflow) + tail marker (overflow). + m_fa: uint16* = (uint16*)(p - 2) + mfv_a: uint16 = m_fa[0] + if mfv_a != uint16(MEMBUDDY_DET_MARKER): stdio.printf("[DET] 下溢写: ptr=%llu size=%llu 前标记=0x%04X(期望 0xCDCD)\n", - p, s, t.CUInt32T(mfv_a)) + p, s, uint32(mfv_a)) stdio.fflush(None) stdlib.exit(1) - m16: t.CUInt16T | t.CPtr = (t.CUInt16T | t.CPtr)(t.CPtr(p + s)) - mv: t.CUInt16T = m16[0] - if mv != t.CUInt16T(MEMBUDDY_DET_MARKER): + m16: uint16* = (uint16*)(p + s) + mv: uint16 = m16[0] + if mv != uint16(MEMBUDDY_DET_MARKER): stdio.printf("[DET] 越界写: ptr=%llu size=%llu 后标记=0x%04X(期望 0xCDCD)块内容前16B: ", - p, s, t.CUInt32T(mv)) - bi3: t.CInt = 0 + p, s, uint32(mv)) + bi3: int = 0 while bi3 < 16: - bv3: t.CUInt8T = (t.CUInt8T | t.CPtr)(t.CPtr(p + t.CUInt64T(bi3)))[0] - stdio.printf("%02X ", t.CUInt32T(bv3)) + bv3: uint8 = *((uint8*)(p + uint64(bi3))) + stdio.printf("%02X ", uint32(bv3)) bi3 += 1 - # 越界区(ptr+size 起 24 字节):0x00=被写穿 0xCD=未写 其它=写入值, - # 用于判断写入者写多长、往哪个方向 + # Overflow region (24 bytes from ptr+size): 0x00 = written through, 0xCD = not + # written, other = written value; shows how far/direction the writer went. stdio.printf("越界区: ") - bj3: t.CInt = 0 + bj3: int = 0 while bj3 < 24: - bvj: t.CUInt8T = (t.CUInt8T | t.CPtr)(t.CPtr(p + s + t.CUInt64T(bj3)))[0] - stdio.printf("%02X ", t.CUInt32T(bvj)) + bvj: uint8 = *((uint8*)(p + s + uint64(bj3))) + stdio.printf("%02X ", uint32(bvj)) bj3 += 1 - # 记录表上下文:该条前 3 + 后 3 的 {ptr,size},推断越界块分配阶段 + # Record-table context: the 3 records before/after, to infer the phase. stdio.printf("\n记录上下文: ") - ci2: t.CInt = -3 + ci2: int = -3 while ci2 <= 3: - idx2: sizet = sizet(t.CInt(i) + ci2) + idx2: sizet = sizet(int(i) + ci2) if idx2 < self._det_count: - cip: t.CUInt64T = self._det_records[idx2 * 2] - cis: t.CUInt64T = self._det_records[idx2 * 2 + 1] + cip: uint64 = self._det_records[idx2 * 2] + cis: uint64 = self._det_records[idx2 * 2 + 1] stdio.printf("[%d]ptr=%llu sz=%llu ", ci2, cip, cis & (free_flag - 1)) ci2 += 1 - # 重叠检测:同一 ptr 是否有多条记录(alloc 重叠实锤——不同 size - # 请求拿到同一块 → 互相覆盖;如 Token(72B) 与字符串 buf(7B) 共用块) + # Overlap check: a ptr with several records means alloc handed the same block to + # different size requests (they overwrite each other). stdio.printf("\n同ptr记录: ") ci3: sizet = 0 while ci3 < self._det_count: - cp3: t.CUInt64T = self._det_records[ci3 * 2] - cs3: t.CUInt64T = self._det_records[ci3 * 2 + 1] + cp3: uint64 = self._det_records[ci3 * 2] + cs3: uint64 = self._det_records[ci3 * 2 + 1] if cp3 == p and (cs3 & free_flag) == 0: stdio.printf("[%zu]sz=%llu ", ci3, cs3 & (free_flag - 1)) ci3 += 1 @@ -1878,52 +1946,52 @@ class MemBuddy(MemManager): return self.usable_size def free_count(self) -> sizet: - # Count total blocks in all order lists for status statistics - # 环检测(与 self_check 同界):本函数是诊断/OOM 报错路径的取数口, - # free list 成环时无上限会"打点自身卡死"(Phase1Main 的 dump_stats 前哨 - # 已按同一理由先调 self_check)。超限即停止该阶遍历,返回已数到的部分值。 + # Count blocks in all order lists for status statistics. + # Cycle guard (same bound as self_check): this is a diagnostic/OOM data path, so a cyclic + # free list must not hang the reporter; on overrun stop that order's walk and return the + # partial count. count: sizet = 0 - i: t.CInt + i: int max_steps: sizet = self.usable_size / MEMBUDDY_MIN_BLOCK + 2 for i in range(MEMBUDDY_MAX_ORDERS + 1): - head_val: t.CUInt64T = self.free_lists[i] + head_val: uint64 = self.free_lists[i] cur: t.CPtr = t.CPtr(head_val) steps: sizet = 0 - while t.CUInt64T(cur) != 0: + while uint64(cur) != 0: steps += 1 if steps > max_steps: stdio.printf("[MEM] free_lists[%d] 链成环或节点数超上限(next 指针被破坏): head=%llu;停止计数避免自旋,堆已损坏\n", i, head_val) break count += 1 - cur = t.CPtr(c.Deref((t.CUInt64T | t.CPtr)(cur))) + cur = t.CPtr(*((uint64*)(cur))) return count def free_bytes(self) -> sizet: - # Count total bytes in all free blocks for status statistics - # 环检测(与 self_check 同界):语义同 free_count。 + # Count bytes in all free blocks for status statistics. + # Cycle guard (same bound as self_check): same as free_count. total: sizet = 0 - i: t.CInt + i: int max_steps: sizet = self.usable_size / MEMBUDDY_MIN_BLOCK + 2 for i in range(MEMBUDDY_MAX_ORDERS + 1): - head_val: t.CUInt64T = self.free_lists[i] + head_val: uint64 = self.free_lists[i] cur: t.CPtr = t.CPtr(head_val) steps: sizet = 0 - while t.CUInt64T(cur) != 0: + while uint64(cur) != 0: steps += 1 if steps > max_steps: stdio.printf("[MEM] free_lists[%d] 链成环或节点数超上限(next 指针被破坏): head=%llu;停止统计避免自旋,堆已损坏\n", i, head_val) break total += _block_size_at_order(i) - cur = t.CPtr(c.Deref((t.CUInt64T | t.CPtr)(cur))) + cur = t.CPtr(*((uint64*)(cur))) return total def used_bytes(self) -> sizet: # Used bytes = total usable region size - free bytes in all free blocks return self.usable_size - self.free_bytes() - def dump_stats(self, label: char | t.CPtr): + def dump_stats(self, label: char*): # Print memory usage statistics fb: sizet = self.free_bytes() ub: sizet = self.usable_size - fb @@ -1932,45 +2000,91 @@ class MemBuddy(MemManager): (ub * 100) / self.usable_size if self.usable_size > 0 else 0, fb, self.free_count()) - def self_check(self) -> t.CInt: + def dump_alive_orders(self): + """OOM 归属画像:按 order 统计「记录在案且仍存活」的块,给出内存花在哪些尺寸上。 + + 数据全部来自既有状态,无需 --detection: + - `chk_stack[0, chk_depth)` 是 alloc 时压栈的块地址; + - 块头为 `(order << 1) | 1` 表示存活,偶数表示已被单独 free(跳过,与 + reset_to / check_alive_blocks 同判据)。 + 块大小 = MEMBUDDY_MIN_BLOCK << order,故这份分布能直接区分 + “大量小块” 与 “少量大块”,是定位池被谁吃满的关键画像。 + """ + if self.chk_stack is None or self.usable is None: + return + max_o: int = MEMBUDDY_MAX_ORDERS + counts: uint64* = (uint64*)(stdlib.malloc((max_o + 1) * 8)) + if counts is None: + return + string.memset(counts, 0, (max_o + 1) * 8) + live: sizet = 0 + ci: sizet = 0 + while ci < self.chk_depth: + block: t.CPtr = t.CPtr(self.chk_stack[ci]) + ci += 1 + if block is None: + continue + stored: t.CPtr = t.CPtr(*((uint64*)(block))) + stored_val: uint64 = uint64(stored) + if (stored_val & 1) == 0: + continue + order: int = int(stored_val >> 1) + if order < 0 or order > max_o: + continue + counts[order] = counts[order] + 1 + live += 1 + stdio.printf("[MEM] 存活块 order 分布(记录在案共 %zu 块;单块=%llu< int: # Validate buddy allocator internal consistency, return 0=OK, non-0=damage if self.usable is None: return 0 - # 环检测:free list 被越界写破坏 next 指针后可能成环, - # 无步数上限会导致无限循环(崩溃点漂移排查哨兵必须能终止)。 + # Cycle guard: a corrupted next pointer can make the free list cyclic; without a step + # bound the walk would spin forever (a corruption sentinel must terminate). max_steps: sizet = self.usable_size / MEMBUDDY_MIN_BLOCK + 2 - i: t.CInt + i: int for i in range(MEMBUDDY_MAX_ORDERS + 1): - head_val: t.CUInt64T = self.free_lists[i] + head_val: uint64 = self.free_lists[i] cur: t.CPtr = t.CPtr(head_val) steps: sizet = 0 - while t.CUInt64T(cur) != 0: + while uint64(cur) != 0: steps += 1 if steps > max_steps: - return 4 # free list 成环或节点数超上限(next 指针被破坏) + return 4 # free list cyclic or over the node limit (next pointer damaged) # Check pointer is in usable region range - if t.CUInt64T(cur) < t.CUInt64T(self.usable): + if uint64(cur) < uint64(self.usable): return 1 - if t.CUInt64T(cur) >= t.CUInt64T(self.usable) + self.usable_size: + if uint64(cur) >= uint64(self.usable) + self.usable_size: return 2 # Check alignment: order i blocks must be aligned to size _block_size_at_order(i) - offset: sizet = t.CUInt64T(cur) - t.CUInt64T(self.usable) + offset: sizet = uint64(cur) - uint64(self.usable) bs: sizet = _block_size_at_order(i) if offset % bs != 0: return 3 - cur = t.CPtr(c.Deref((t.CUInt64T | t.CPtr)(cur))) + cur = t.CPtr(*((uint64*)(cur))) return 0 - def check_alive_blocks(self, from_mark: sizet) -> t.CInt: - # 检查 [from_mark, chk_depth) 记录的所有已分配块 header 完整性。 - # 越界写(参数错位/下标 GEP 越界)会破坏相邻块 header(奇偶位/order 错乱), - # 返回 0=OK,非 0=损坏(rc 语义见调用方注释)。 - # 用于 reset_to 前哨兵:把"崩溃点漂移"变成"确定性定位到损坏发生的文件"。 + def check_alive_blocks(self, from_mark: sizet) -> int: + # Check the header integrity of every allocated block in [from_mark, chk_depth). + # An out-of-bounds write corrupts a neighbour's header (parity/order), so this returns + # 0=OK / non-zero=damage (rc meanings in the caller). + # Used as a reset_to sentinel: turns a drifting crash into a deterministic file. if self.chk_stack is None: return 0 if from_mark > self.chk_depth: - # from_mark 大于当前深度:调用方 mark 值异常(可能读错字段/跨实例), - # 校验将 no-op → 越界写漏检。fail-fast 暴露。 + # from_mark > chk_depth: the caller's mark is bogus (wrong field / cross-instance), + # which would make the check a no-op and miss overflows. fail-fast. stdio.printf( "[MEM] check_alive_blocks from_mark=%zu > chk_depth=%zu(mark 异常)\n", from_mark, self.chk_depth) @@ -1983,93 +2097,63 @@ class MemBuddy(MemManager): while ci < self.chk_depth: block: t.CPtr = t.CPtr(self.chk_stack[ci]) if block is not None: - if t.CUInt64T(block) < t.CUInt64T(self.usable): - # [TEMP-DIAG] 现场:块地址低于池起点(%llu/%zu 格式先例见 L861/L1216) + if uint64(block) < uint64(self.usable): + # Block address below the pool start. stdio.printf("[MEM] 存活块损坏 rc=1(块地址低于 usable): block=%llu usable=%llu ci=%zu from_mark=%zu\n", - t.CUInt64T(block), t.CUInt64T(self.usable), ci, from_mark) + uint64(block), uint64(self.usable), ci, from_mark) return 1 - if t.CUInt64T(block) >= t.CUInt64T(self.usable) + self.usable_size: - # [TEMP-DIAG] 现场:块地址超出池末尾 + if uint64(block) >= uint64(self.usable) + self.usable_size: + # Block address past the pool end. stdio.printf("[MEM] 存活块损坏 rc=2(块地址超出池末尾): block=%llu usable=%llu ci=%zu from_mark=%zu\n", - t.CUInt64T(block), t.CUInt64T(self.usable), ci, from_mark) + uint64(block), uint64(self.usable), ci, from_mark) return 2 - stored: t.CPtr = t.CPtr(c.Deref((t.CUInt64T | t.CPtr)(block))) - stored_val: t.CUInt64T = t.CUInt64T(stored) + stored: t.CPtr = t.CPtr(*((uint64*)(block))) + stored_val: uint64 = uint64(stored) if (stored_val & 1) != 0: - # 活块:header 必须是 (order<<1)|1,order 合法且块对齐到该 order 大小 - order: t.CInt = t.CInt(stored_val >> 1) + # Live block: header must be (order<<1)|1 with a valid order and alignment. + order: int = int(stored_val >> 1) if order < 0: - # [TEMP-DIAG] 现场:order 为负(header 被写穿) + # Negative order (header written through). stdio.printf("[MEM] 存活块损坏 rc=3(order 为负): block=%llu order=%d ci=%zu from_mark=%zu\n", - t.CUInt64T(block), order, ci, from_mark) + uint64(block), order, ci, from_mark) return 3 if order > self.max_order: - # [TEMP-DIAG] 现场:order 超 max_order(%d 先例见 L861 max_order=%d) + # order beyond max_order. stdio.printf("[MEM] 存活块损坏 rc=4(order 超 max_order): block=%llu order=%d max_order=%d ci=%zu from_mark=%zu\n", - t.CUInt64T(block), order, self.max_order, ci, from_mark) + uint64(block), order, self.max_order, ci, from_mark) return 4 - offset: sizet = t.CUInt64T(block) - t.CUInt64T(self.usable) + offset: sizet = uint64(block) - uint64(self.usable) bs: sizet = _block_size_at_order(order) if offset % bs != 0: - # [TEMP-DIAG] 现场:块未对齐到该 order 的块大小 + # Block not aligned to its order's block size. stdio.printf("[MEM] 存活块损坏 rc=5(块未对齐 order 大小): block=%llu order=%d bs=%zu offset=%zu ci=%zu from_mark=%zu\n", - t.CUInt64T(block), order, bs, offset, ci, from_mark) + uint64(block), order, bs, offset, ci, from_mark) return 5 - # 块尾 canary 校验:越界写穿数据区会破坏块尾哨兵, - # 立即定位到越界写发生的文件(崩溃点漂移的关键哨兵)。 - # 与 alloc 写入点统一使用编译期常量 MEMBUDDY_CANARY_PATTERN(同值同宽)。 - # 【地址口径】必须与 alloc 的写入点逐字一致:先把 block 转成整数, - # 再做整数加/减(t.CUInt64T + i64 = 字节语义),最后才转指针。 - # 不可写成 (t.CUInt64T | t.CPtr)(block) + bs - MEMBUDDY_CANARY: - # 联合转换 (T | t.CPtr)(x) 按 LLVMTypeMapper 映射为 T*(此处 i64*), - # 指针 + 整数走 GEP ⇒ 每单位前进 sizeof(T)=8 字节 - # ⇒ 实际读到 block + 8*bs - 64(order0/bs=32 时 = block+192), - # 读到的是别的块(池内其它路径缓冲)的数据 ⇒ 假阳性 rc=6。 - # 宽度只由指针的元素类型决定:canary 是 8 字节量,故必须给出 - # 有元素类型的指针视图 t.CUInt64T(x, t.CPtr)(→ i64*;本文件 - # 多处同款)。i8* 现在恒读 1 字节,不能再靠赋值目标注解反推。 - # 地址口径与原写法一致:先整数加减,再转指针。 - can_stored: t.CPtr = t.CPtr(c.Deref( - t.CUInt64T(t.CUInt64T(block) + bs - MEMBUDDY_CANARY, t.CPtr))) - if t.CUInt64T(can_stored) != t.CUInt64T(MEMBUDDY_CANARY_PATTERN): - # [TEMP-DIAG] 越界写现场:块尾 canary 被写穿(rc=6 的判据)。 - # 原实现只回 rc(int),调用方拿不到块/阶/实际值 → 无法定位。 - # 此处补齐现场字段(全部为当前作用域既有变量,不新增取值路径): - # block/order/bs 见上方 L1241-1253 计算;ci/from_mark 为 - # 校验下标与窗口起点;canary vs 期望均以十六进制打印。 - # printf 形式与格式符沿用本文件既有先例: - # stdio.printf(...) / stdio.fflush(None) 见 L800-802、L816-817 - # %zu ← sizet(L1216 from_mark/chk_depth、L765 chk_cap/depth) - # %llu ← t.CUInt64T(L800 size/ptr、L861 pool_base) - # %d ← t.CInt(L816 dbg_order、L822) - # %llx ← t.CUInt64T 十六进制(L917 header=0x%llX) + # Block-tail canary check: the key sentinel for overflow damage. The canary is + # an 8-byte value, so it must be read through a uint64* view, and the byte + # address is computed on the byte pointer first (arithmetic on a typed pointer + # scales by sizeof(T)=8 and would read a different block). + can_stored: t.CPtr = t.CPtr(*((uint64*)(block + bs - MEMBUDDY_CANARY))) + if uint64(can_stored) != uint64(MEMBUDDY_CANARY_PATTERN): + # Overflow scene: the block-tail canary was overwritten (rc=6). Report the + # block/order/bs plus the index and window start for location. stdio.printf( "[MEM] canary 损坏: block=%llu order=%d bs=%zu ci=%zu from_mark=%zu canary=0x%llx 期望=0x%llx\n", - t.CUInt64T(block), order, bs, ci, from_mark, - t.CUInt64T(can_stored), t.CUInt64T(MEMBUDDY_CANARY_PATTERN)) - # [TEMP-DIAG] 位宽诊断:把上面同一对值(canary 实际读回值 / 期望 - # 常量)再按高 32 位、低 32 位分别打印,用于在现场直接分辨: - # (i) 写入端只落了低 32 位(高 32 位为 0):canary_hi == 0x0; - # (ii) 比较端把 32 位常量做了符号扩展:期望_hi == 0xffffffff; - # 若 canary_hi 与 期望_hi 不一致,即"同一个编译期常量被两种不同 - # 扩展方式抬到 64 位"的代码生成缺陷实锤(既有字段与 rc 不变)。 - # 写法先例(均为本文件既有): - # t.CUInt64T 局部变量 + 移位:L806/L1056 ff_d: t.CUInt64T = t.CUInt64T(1) << 63 - # CUInt64T 域内算术:L916 (rs & (t.CUInt64T(1) << 63))、L919 rs | ... - # %llx ← t.CUInt64T 十六进制:上行 canary=0x%llx / L932 header=0x%llX - # 低 32 位用"减去高半部"求得,不引入 >0x7FFFFFFF 的大字面量 - # (本仓库踩过大字面量被解析截断的坑,见本文件 L120-122)。 - can_d: t.CUInt64T = t.CUInt64T(can_stored) - exp_d: t.CUInt64T = t.CUInt64T(MEMBUDDY_CANARY_PATTERN) - can_hi: t.CUInt64T = can_d >> 32 - can_lo: t.CUInt64T = can_d - (can_hi << 32) - exp_hi: t.CUInt64T = exp_d >> 32 - exp_lo: t.CUInt64T = exp_d - (exp_hi << 32) + uint64(block), order, bs, ci, from_mark, + uint64(can_stored), uint64(MEMBUDDY_CANARY_PATTERN)) + # Width diagnostic: print the high/low 32-bit halves of the read value and + # the expectation, to tell a low-32-only write from a sign-extended compare + # (low halves derived by subtracting the high half, no large literal). + can_d: uint64 = uint64(can_stored) + exp_d: uint64 = uint64(MEMBUDDY_CANARY_PATTERN) + can_hi: uint64 = can_d >> 32 + can_lo: uint64 = can_d - (can_hi << 32) + exp_hi: uint64 = exp_d >> 32 + exp_lo: uint64 = exp_d - (exp_hi << 32) stdio.printf( "[MEM] canary 位宽诊断: canary_lo=0x%llx canary_hi=0x%llx 期望_lo=0x%llx 期望_hi=0x%llx\n", can_lo, can_hi, exp_lo, exp_hi) - # 调用方(Phase1Main)随后打印并 exit;此处先 fflush 保证 - # 重定向下现场不丢(同 [DET] 终止路径,本文件 L802/L818)。 + # Flush so the scene survives a redirected stdout (the caller prints then exits). stdio.fflush(None) return 6 ci += 1 diff --git a/mlirlite/README.md b/mlirlite/README.md index 8f1dc40..250a7a2 100644 --- a/mlirlite/README.md +++ b/mlirlite/README.md @@ -7,6 +7,12 @@ 目的:让 TransPyV 侧只需把 `llvmlite.` 前缀重命名为 `mlirlite.` 即可编译, 从而在不改内部模型、不改 builders 的前提下切换到 MLIR 后端。 +> 实际切换方式(2026-09):不动 TransPyV 侧任何 `llvmlite.` 调用点,也不动 +> `includes/llvmlite/**`——工程文件 `project.vpj` 的 `options.target = "mlir"` 会让 +> 模块名 `llvmlite` 在名称→SHA1 解析处改写为 `mlirlite`(唯一改动点是 +> `StubMergerIR._FindSha1ByModName`)。开关、回退与缓存标签要求见 +> [Debugger/BackendSwitch.md](../../Debugger/BackendSwitch.md)。 + > 术语约定:文中"中间文本"指 builders 写进 `Line` 的**指令文本**(仍是 LLVM 语法, > 如 `%0 = add i32 %a, %b`);"MLIR 文本"指 `FunctionPrint` / `LLVMModulePrint` > 最终输出、可喂给 `mlir-translate` / `mlir-opt` 的文本。 @@ -20,11 +26,11 @@ | 文件 | 内容 | 与 llvmlite 的关系 | |------|------|--------------------| | `__init__.vp` | 公共导出 + 便捷工厂(`create_function`/`const_int*`/`create_global_*` 等)+ `LLVMTypeCore`/`LLVMModuleCore` | **导入清单逐名一致(208 个名字),代码逐行一致**,仅注释不同 | -| `__types.vp` | `LLVMType`(REnum) / `ParamNode` / 类型工厂 / `TypePrint` / `DeepCopyType` | 模型与工厂**逐名同形**;`TypePrint` 改为 MLIR 语义,`PrintStructDefinition` 变为空操作 | +| `__types.vp` | `LLVMType`(REnum) / `ParamNode` / 类型工厂 / `TypePrint` / `DeepCopyType` | 模型与工厂**逐名同形**;`TypePrint` 改为 MLIR 语义(具名结构体内联为 `!llvm.struct<"name", (…)>`),`PrintStructDefinition` 变为空操作,新增打印期命名结构体登记表(§2.10) | | `__values.vp` | `Value` / `ConstInt`/`ConstNull`/`ConstZero`/`ConstFloat`/`SSAValue` / `ValuePrint` | 模型与工厂同形;仅 `ValuePrint` 改为 MLIR 语义 | | `__function.vp` | `Line`/`BasicBlock`/`Param`/`Function` + 工厂 + `RenumberSSA` + 窥孔 + `FunctionPrint` | 模型、`RenumberSSA`、本地窥孔**逐行一致**;`FunctionPrint` 重写为 MLIR 渲染 | | `__builder.vp` | `IRBuilder` + 全部 `build_*` | **逐行一致**(仅 `_type_str` 的 fail-fast 校验字符集适配 MLIR 类型首字符) | -| `__module.vp` | `LLVMModule`/`GlobalVariable`/`NamedTypeNode` + `LLVMModulePrint` | 模型同形;`PrintModule` 与全局渲染改为 MLIR | +| `__module.vp` | `LLVMModule`/`GlobalVariable`/`NamedTypeNode` + `LLVMModulePrint` | 模型同形;`PrintModule` 与全局渲染改为 MLIR(定义行丢弃 + 打印期登记表发布,§2.10) | | `__verify.vp` | `VerifyResult`/`verify_function`/`verify_module` | **逐行一致**(校验的是内部模型 + 中间文本,与 MLIR 文本无关) | ### 1.2 差异只在打印阶段(设计红线) @@ -53,8 +59,11 @@ builders 未改,而它们内嵌的类型一律来自 `IRBuilder._type_str` → - `MlirTypeText`(中间类型文本 → MLIR 类型文本)以 **`!llvm.…` / `none` 直通**为主路径, 同时保留 LLVM 语法分支(`i8*`、`[N x x]`、`{…}`)以兼容手工构造的中间文本; -- 结构体类型在中间文本里就已是完整内联形态 `!llvm.struct<(…)>`, - 因此**不需要**命名结构体类型登记表,也不存在 `%"name"`(出现即响亮报错); +- 结构体类型在中间文本里就已是完整内联形态(匿名 `!llvm.struct<(…)>`、 + 具名 `!llvm.struct<"name", (…)>`,见 §2.10),因此**不需要**在中间文本里 + 出现 `%"name"` 这种"只有名字"的形态;若出现(手工构造 / TPV 注入), + `MlirTypeText` 按**模块打印期发布的登记表**回查完整定义后展开为具名内联形态, + 查不到 / 仍是 opaque ⇒ 响亮报错; - `_mlir_read_type` 的类型读取按 `[] {} () <>` 统一做括号平衡, 以正确读出含空格/逗号的 `!llvm.array<…>` / `!llvm.struct<(…)>`。 @@ -70,7 +79,7 @@ builders 未改,而它们内嵌的类型一律来自 `IRBuilder._type_str` → | `Half/Float/Double/FP128` | `f16/f32/f64/f128` | | `Ptr(_)` | `!llvm.ptr`(不透明指针;pointee 只保留在内部模型里) | | `Array(elem,N)` | `!llvm.array`(嵌套:`!llvm.array<2 x !llvm.array<3 x i8>>`) | -| `Struct(fields,N,name)` | `!llvm.struct<(f0, f1, …)>`;空 → `!llvm.struct<()>`(**名字不参与渲染**) | +| `Struct(fields,N,name)` | `!llvm.struct<(f0, f1, …)>`;空 → `!llvm.struct<()>`。**具名**(`name` 非 None)→ `!llvm.struct<"name", (f0, f1, …)>`(内联具名体,见 §2.10) | | `Func(ret,params)` | `!llvm.func` | | `Void` | `none`(仅必须打印时;函数返回值位置**省略**) | | `Label` / `Metadata` | **v1 不支持 → 响亮报错** | @@ -86,7 +95,7 @@ builders 未改,而它们内嵌的类型一律来自 `IRBuilder._type_str` → | 任意以 `*` 结尾(`i8*`、`i8**`) | `!llvm.ptr` | | `[N x T]` | `!llvm.array` | | `{T1, T2}` | `!llvm.struct<(T1', T2')>` | -| `%name`(只有名字的结构体类型) | **v1 不支持 → 响亮报错**(中间文本不应出现该形态) | +| `%name`(只有名字的结构体类型) | 按打印期登记表回查完整定义 → `!llvm.struct<"name", (f0, …)>`;未登记 / 仍是 opaque → **响亮报错**(见 §2.10) | | `T (P0, P1)` | `!llvm.func` | ### 2.2 函数 / 块 / 指令 @@ -99,6 +108,8 @@ llvm.func @add(%a: i32, %b: i32) -> i32 { ``` - void 返回:`llvm.func @noop() { … llvm.return }`;declare(无体):`llvm.func @f(%x: i32) -> i32` +- 变参函数(`func.IsVarArg == 1`):参数表尾追加 `...` —— + `llvm.func @printf(%p: !llvm.ptr, ...) -> i32`;无固定参数时是 `llvm.func @f(...) -> i32` - **entry 块不带标签**;后续块 `^:`(块名去掉前导 `%`) - 块参数:`^merge(%7: i32):`(块首连续 phi 变成块参数,phi 行不再输出) @@ -110,6 +121,7 @@ llvm.func @add(%a: i32, %b: i32) -> i32 { | `%N = icmp sgt i32 %a, %b` | `%N = llvm.icmp "sgt" %a, %b : i32` | | `%N = fcmp oeq f64 %a, %b` | `%N = llvm.fcmp "oeq" %a, %b : f64` | | `%N = alloca i32` | `%N = llvm.alloca %c0 x i32 : (i32) -> !llvm.ptr`(个数按常量 op 物化) | +| `%N = alloca i32, align N` | `%N = llvm.alloca %c0 x i32 {alignment = N : i64} : (i32) -> !llvm.ptr`(对齐必须是 2 的幂常量,见 §2.7) | | `%N = load i32, !llvm.ptr %p` | `%N = llvm.load %p : !llvm.ptr -> i32` | | `store i32 %v, !llvm.ptr %p` | `llvm.store %v, %p : i32, !llvm.ptr` | | `%N = getelementptr i32, !llvm.ptr %p, i64 %i` | `%N = llvm.getelementptr %p[%i] : (!llvm.ptr, i64) -> !llvm.ptr, i32` | @@ -121,13 +133,20 @@ llvm.func @add(%a: i32, %b: i32) -> i32 { | `%N = call i32 @f(i32 %a)` | `%N = llvm.call @f(%a) : (i32) -> i32` | | `call none @f(i32 %a)` | `llvm.call @f(%a) : (i32) -> ()`(`Void` 的中间类型文本是 `none`) | | `%N = call i32 %fp(i32 %a)` | `%N = llvm.call %fp(%a) : !llvm.ptr, (i32) -> i32` | +| `call none asm sideeffect "nop", ""()` | `llvm.inline_asm has_side_effects "nop", "" : () -> ()`(见 §2.8) | +| `%N = call i32 asm sideeffect inteldialect "…", "=r,r"(i32 %v)` | `%N = llvm.inline_asm has_side_effects asm_dialect = intel "…", "=r,r" %v : (i32) -> i32`(见 §2.8) | | `br label %blk` | `llvm.br ^blk`(目标有块参数时 `llvm.br ^merge(%c0 : i32)`) | | `br i1 %c, label %t, label %e` | `llvm.cond_br %c, ^t, ^e` | | `ret i32 %v` / `ret void` | `llvm.return %v : i32` / `llvm.return` | | `unreachable` | `llvm.unreachable` | | `%N = select i1 %c, i32 %a, i32 %b` | `%N = llvm.select %c, %a, %b : i1, i32` | -| `switch i32 %v, label %d [i32 0, label %c0]` | `llvm.switch %v, ^d [0: ^c0]`(case 值是属性,不物化) | +| `switch i32 %v, label %d [i32 0, label %c0]` | `llvm.switch %v : i32, ^d [0: ^c0]`(**值类型紧随值**;case 值是属性,不物化) | +| 目标块带 phi 的 switch | `llvm.switch %v : i32, ^d(%c1 : i32) [0: ^c0(%c0 : i32)]`(与 `br` 同一块实参设施,见 §2.4) | | `%N = atomicrmw add !llvm.ptr %p, i32 %v monotonic` | `%N = llvm.atomicrmw add %p, %v monotonic : !llvm.ptr, i32` | +| `%N = va_arg !llvm.ptr %ap, i32` | `%N = "llvm.va_arg"(%ap) : (!llvm.ptr) -> i32`(**generic 形态**,见 §2.9) | +| `%N = cmpxchg !llvm.ptr %p, i32 %c, i32 %n acq_rel monotonic` | `%N = llvm.cmpxchg %p, %c, %n acq_rel monotonic : !llvm.ptr, i32` + 两条 `llvm.extractvalue`(见 §2.9) | +| `%r = extractvalue {i32, i32} %v, 0` | `%r = llvm.extractvalue %v[0] : !llvm.struct<(i32, i32)>`(**索引写在 `[]` 内**;多级索引 `%v[1, 0]` 同理,见 §2.8) | +| 变参调用 `%N = call i32 (i8*, i32, ...) @printf(i8* %p, i32 %n)` | `%N = llvm.call @printf(%p, %n) vararg(!llvm.func) : (!llvm.ptr, i32) -> i32`(见 §2.9) | ### 2.3 常量必须物化为 op(关键) @@ -162,16 +181,29 @@ MLIR 的操作数不能是字面量。**扫描阶段**(pass 1)遍历函数 |----------|-----------| | 字符串常量(`c"hello\00"`,`[6 x i8]`) | `llvm.mlir.global private unnamed_addr constant @".str"("hello\00") : !llvm.array<6 x i8>` | | 整数全局(`0`,`i32`) | `llvm.mlir.global @counter(0 : i32) : i32` | -| stub 模式(无初值) | `llvm.mlir.global external @x : i32` | +| stub 模式(无初值) | `llvm.mlir.global external @x() : i32`(**值括号必须保留**,实测见下) | | linkage / constant / unnamed_addr | 对应关键字(`private`/`internal`/`linkonce_odr`、`constant`、`unnamed_addr`) | -| section / align | 属性字典 `{section = "…", alignment = N : i64}`(**形态未经 mlir-opt 验证**) | +| section / align | 属性字典 `{section = "…", alignment = N : i64}`,**位置在值括号之后、类型之前**(已实测,见下) | | 整型/浮点数组 `[i8 1, i8 2]` | `[1 : i8, 2 : i8]`(元素**带类型注解**;`dense<…>` 对 `!llvm.array` **非法**) | -| `null` / `zeroinitializer` | `zeroinitializer` | +| 根级裸 `null` / `zeroinitializer` | **initializer region**:`@p() : !llvm.ptr` + `%v0 = llvm.mlir.zero : <顶层类型>` + `llvm.return`(属性形态 `(zeroinitializer)` 被 mlir-opt 拒绝:`expected attribute value`,实测 22.1.8) | +| 结构体 / vtable `{ i8* null, i8* bitcast (… @m0 …) }` | **initializer region**:`@vt()` + `: !llvm.struct<(…)>` + `{ <叶子 op>; undef; insertvalue 链; llvm.return }` | +| 指针数组 + GEP `[i8* getelementptr … @s0 …]` | region;GEP/bitcast 常量表达式整体等价 `llvm.mlir.addressof @s0 : !llvm.ptr` | +| 指针型初值 `i8* @s0` / `i8* null` | region(顶层叶子:无 undef/insertvalue,直接 `llvm.return`) | + +**初值两条路径**(判定见 `__module.vp:_MlirGlobalInitNeedsRegion`): +`c"…"` / `"…"` / 标量字面量 / **纯标量数组**走**属性路径**; +根级裸 `null` / `zeroinitializer`、结构体 `{…}`、含符号引用(`@`)或结构体的数组、 +顶层 `@name`、指针/聚合类型前缀开头的初值 +(`i8* null`、`i8* bitcast (…)`、`i8* getelementptr …`)走 **region 路径**。 +region 内元素 MLIR 类型并行取自**全局类型树**(`Struct` 字段链 / `Array` 元素类型), +下标为多下标路径(嵌套聚合如 `[0, 1]`),`insertvalue`/`llvm.return` 的类型后缀始终是**顶层聚合类型**。 +符号名一律走 `_MlirSymRefAppend`(先剥 LLVM 口径引号,再按 MLIR 口径重加)。 +两条路径都覆盖不到的叶子形态一律 `MlirFail`(带原始文本)。 - `module_set_target` / `module_set_datalayout`:MLIR 模块不携带 triple/datalayout ⇒ **记录但不输出**,模块输出里给一行注释标明"由 mlir-translate / llc 阶段提供"。 -**⚠ 三条由 mlir-opt 22.1.8 实测确立的硬约束(违反即解析/校验失败)** +**⚠ 五条由 mlir-opt 22.1.8 实测确立的硬约束(违反即解析/校验失败)** 1. **符号名判据与 LLVM 不同**:MLIR 裸标识符**首字符必须是字母或 `_`**,且 **不允许 `-`**(LLVM 允许)。⇒ TPV 的 `{sha1}.{name}`(sha1 以数字开头)与 @@ -183,6 +215,15 @@ MLIR 的操作数不能是字面量。**扫描阶段**(pass 1)遍历函数 3. **`dense<[…] : !llvm.array<…>>` 非法**(`dense` 只接受 builtin shaped 类型 tensor/vector);数组属性必须是 `值 : 类型` 的元素列表形态,且元素类型与 全局类型一致(元素不注解时按 i64 解释)。 +4. **属性字典必须在类型之前**(紧跟值括号):正确形态 + `llvm.mlir.global internal constant @s("ab\00") {section = "data", alignment = 16 : i64} : !llvm.array<3 x i8>` + exit 0;把字典放到**类型之后**(`… : !llvm.array<3 x i8> {section = "data"}`) + 会把 `section` 当成新 op,报 ``Dialect `' not found for custom op 'section'``。 + 同理 `unnamed_addr` 是**关键字**(输出在名字之前),**不能**放进字典 —— + 放进字典报 `attribute 'unnamed_addr' occurs more than once`。 +5. **值括号必须存在**:stub 的 `external` 声明没有初值,但仍要写空括号 —— + `llvm.mlir.global external @x() : i32` exit 0; + `llvm.mlir.global external @x : i32` 报 `expected '('`。 **⚠ 验收口径**:类型文本、值文本、**裸函数**都不是合法的 MLIR 顶层单元 (类型/值不是顶层 op;裸函数父级不是 SymbolTable,其外部符号引用无法解析) @@ -207,22 +248,242 @@ module { `ValuePrint`:SSA → `%0`;常量 → `42 : i32`(**仅供诊断**)。 指令渲染**不依赖** `ValuePrint`,`FunctionPrint` 用自己的操作数解析。 +### 2.7 alloca 对齐(`build_alloca_aligned`) + +中间文本 `%N = alloca , align N` → `%N = llvm.alloca %c0 x i32 {alignment = N : i64} : (i32) -> !llvm.ptr` + +- 属性形态**由 mlir-opt 22.1.8 实测确定**(不得凭记忆): + `{alignment = 16 : i64}` exit 0;`align(16)` / `, align 16`(LLVM 词法)报 `expected ':'`; + `{alignment = 16 : i32}` 报 `attribute 'alignment' failed to satisfy constraint: + 64-bit signless integer attribute`(必须是 **i64**)。 +- 对齐值只接受**常量十进制 + 2 的幂**:非常量(SSA)/非幂/超范围(> 2^30)一律 `MlirFail` + —— mlir-opt 自身不校验幂次(实测 `{alignment = 3 : i64}` exit 0),故由本层把关。 + +### 2.8 内联汇编(`build_inline_asm` / TPV 注入文本) + +两条来源的中间文本 opcode 都是 `call`,由 `_MlirCall` 转交 `_MlirAsm`: + +| 中间文本(LLVM 词法) | MLIR 文本 | +|---|---| +| `call none asm sideeffect "nop", ""()` | `llvm.inline_asm has_side_effects "nop", "" : () -> ()` | +| `%N = call i32 asm sideeffect "…", "=r,r"()` | `%N = llvm.inline_asm has_side_effects "…", "=r,r" : () -> i32` | +| `%N = call i32 asm sideeffect inteldialect "…", "=r,r"(i32 %v)` | `%N = llvm.inline_asm has_side_effects asm_dialect = intel "…", "=r,r" %v : (i32) -> i32` | +| `%N = call {i32, i32} asm sideeffect "…", "=r,r"()` | `%N = llvm.inline_asm has_side_effects "…", "=r,r" : () -> !llvm.struct<(i32, i32)>`(**多结果 = 单结果 + 结构体类型**) | + +- 方言关键字:LLVM 的 `inteldialect` → MLIR 的 **`asm_dialect = intel`** + (裸 `inteldialect` 实测报 `expected attribute value`; + `asm_dialect = inteldialect` 报 `expected … enum values … [att, intel]`)。 +- **输入操作数在 MLIR 里是逗号分隔的裸操作数、不带括号**(LLVM 词法的 `"…"(%v)` + 实测报 `expected ':'`);操作数类型串位于 `: (<操作数类型…>) -> <返回类型|()>`。 +- 汇编/约束串按引号串整体透传:TPV 的 `\22`/`\5C`/`\0A` 转义对 MLIR 词法同样合法(实测 exit 0)。 +- 只认 `sideeffect` / `inteldialect` 两个关键字;其它记号(含 LLVM 的 + `attdialect`/`alignstack`/`unwind`)、缺约束串、实参不成对、有返回值却无结果名 + ⇒ `MlirFail` 响亮报错。 + (`attdialect`/`alignstack` 在 1024 份真实 `.ll` 语料中 **0 命中** ⇒ **不列入支持计划**, + 此处的 `MlirFail` 只是兜底,不是待补齐缺口。) + +**多结果内联汇编 + `extractvalue`**(本轮补齐;TPV 全量产物 4163 份 `.ll` 的 +"指令位置"扫描中 `extractvalue` **70 处**,且是**唯一**未支持却真实使用的 opcode) + +真实形态(`Test/DefaultArgTest/output/*.ll` 等,多结果内联汇编是 `extractvalue` +的**唯一来源**——`rdrand`/`rdseed` 类 builtin): + +``` +%asm_result = call {i32, i32} asm inteldialect "rdrand $1", "=r,=r,~{cc}"() +%asm_out_0 = extractvalue {i32, i32} %asm_result, 0 +%asm_out_1 = extractvalue {i32, i32} %asm_result, 1 +``` + +(语料实测结构体结果有 `{i32, i32}` / `{i64, i64}` / `{i32, i32, i32, i32}` / +`{i64, i32, i64, i32}`,索引一律**单级**。) + +| 中间文本(LLVM 词法) | MLIR 文本 | +|---|---| +| `%N = call {i32, i32} asm sideeffect "…", "=r,r"()` | `%N = llvm.inline_asm has_side_effects "…", "=r,r" : () -> !llvm.struct<(i32, i32)>` | +| `%r = extractvalue {i32, i32} %N, 0` | `%r = llvm.extractvalue %N[0] : !llvm.struct<(i32, i32)>` | +| `%r = extractvalue {i32, {i32, i32}} %N, 1, 0` | `%r = llvm.extractvalue %N[1, 0] : !llvm.struct<(i32, !llvm.struct<(i32, i32)>)>` | + +- **多结果 = "单结果 + 结构体返回类型"**:`_MlirAsm` **无需特殊分支**(返回类型 + 文本经 `MlirTypeText` 即 `{…}` → `!llvm.struct<(...)>`,嵌套结构体亦同)。 +- `extractvalue` 由 `_MlirExtractValue` 渲染(`_MlirRenderLine` 分派):聚合类型 + 文本在**值之前**(LLVM 词法)→ `MlirTypeText` 转 MLIR 类型;**索引必须从 LLVM 的 + `<聚合>, i0, i1` 转写为 MLIR 的 `[i0, i1]`**(MLIR 实测拒绝 LLVM 词法:把 `<聚合>` + 当结果类型、`%v,` 当多余记号)。 +- 边界:SSA 索引 / 缺索引 / 索引后有多余记号 ⇒ `MlirFail` 响亮报错。 + **多级索引**(`%v[1, 0]`)经 mlir-opt 22.1.8 实测**合法** ⇒ 一并支持。 +- 实测(mlir-opt / mlir-translate 22.1.8):`%v[0]` / `%v[1, 0]` / 具名结构体 + `!llvm.struct<"n", (i32, i32)>` / 数组 `!llvm.array<2 x i32>` 作聚合类型均 exit 0; + 降级得 `call { i32, i32 } asm sideeffect "…", "=r,r"()` + + `extractvalue { i32, i32 } %1, 0` / `, 1`(与 TPV 产物逐字同形)。 +- `insertvalue` / `invoke` / `landingpad` / `resume` / `freeze` / `fence` / + `shufflevector` / `insertelement` / `extractelement` / `callbr` 在 4163 份真实产物中 + **全部 0 命中** ⇒ **不实现**,仍走"未知指令 opcode"响亮报错。 + +### 2.9 变参(`...`)/ `va_arg` / `cmpxchg` + +三者形态均由 mlir-opt 22.1.8 实测 exit 0 确立(候选与 exit 码见 +`__function.vp` 的 `_MlirFuncSig` / `_MlirCall` / `_MlirVaArg` / `_MlirCmpXchg`); +选做依据是真实 `.ll` 语料频次(变参 call 8831 行/627 文件、`va_arg` 1994/397、 +`cmpxchg` 547/371)。 + +**① 变参函数声明**(`func.IsVarArg == 1`):参数表尾追加 `...` +(有固定参数 ⇒ `, ...`,否则裸 `...`)。 + +**② 变参调用**:中间文本 `call (<实参类型…>, ...) @f(<实参>)` +(builders 的 `build_call(is_variadic=1)`)在操作数括号后追加 +`vararg(<被调函数的完整 MLIR 函数类型>)`: + +``` +%0 = llvm.call @printf(%p, %n) vararg(!llvm.func) : (!llvm.ptr, i32) -> i32 +``` + +- `vararg(...)` 内是**完整函数类型**(返回类型 + 中间文本括号里的参数类型串,含 `...`); +- **void 返回时该处返回类型写 `void`** —— 写 `none` 实测报 + `expected var_callee_type to return void`(普通 `-> ()` 位置仍用 `()`); +- 降级验证:`mlir-translate --mlir-to-llvmir` 得 + `%3 = call i32 (ptr, ...) @printf(ptr %0, i32 %1)` ✓。 + +**③ `va_arg` —— 用 generic(通用 op)形态是刻意选择**: + +``` +%0 = "llvm.va_arg"(%ap) : (!llvm.ptr) -> i32 +``` + +- 自定义汇编语法 `llvm.va_arg %ap, i32` 要求显式给出 va_list 的类型(LLVM 里的 + `ptr`),而 22.x **不透明指针**下该类型无法表达 ⇒ mlir-opt 报 + `expected builtin.function`。故**刻意改用通用 op 语法**,只用操作数/结果类型列表描述。 +- 降级验证:`mlir-translate --mlir-to-llvmir` 得 `%2 = va_arg ptr %0, i32` ✓ + (与 LLVM 的 va_arg 完全同形)。 + +**④ `cmpxchg` —— MLIR 里是单结果 `struct{T, i1}`,不是两个 SSA 值**: +中间文本两态(builder 的 `%N = cmpxchg …` 与 `%a, %b = cmpxchg …`)都渲染为 +**一条 op + 两条 `extractvalue`**: + +``` +%N = llvm.cmpxchg %p, %c, %n acq_rel monotonic : !llvm.ptr, i32 +%_N_v = llvm.extractvalue %N[0] : !llvm.struct<(i32, i1)> ; [0] = 旧值(比较值) +%_N_ok = llvm.extractvalue %N[1] : !llvm.struct<(i32, i1)> ; [1] = i1 成功位 +``` + +- 结果名绑定:`%N`(单结果)/ `%a`(双结果首名)→ 旧值 `[0]`;双结果次名 `%b` → 成功位 `[1]`。 + 绑定复用 bitcast no-op 的**别名设施**(`_MlirAliasAdd`),后续引用经 `_MlirResolve` + 自动改写为上述两条 `extractvalue` 的结果名 ⇒ builder 端 `build_cmpxchg` 返回的 + 「T 类型 Value」(旧值)语义保持。 +- 两条 `extractvalue` 结果名由 struct 结果名**确定性派生**(`_MlirCxResName`): + `%0` → `%_0_v`(旧值)/ `%_0_ok`(成功位),`%a` → `%_a_v`/`%_a_ok`。 + `_` 前缀是必需的——MLIR 裸 SSA 名首字符须为字母或 `_`,`%0_v` 被 mlir-opt 22.1.8 + 拒绝(`expected '=' after SSA name`)。名字与结果名一一对应、**不含计数器** + ⇒ pass1(登记别名)与 pass2(发射)必然同名,引用不会悬空。 + 取 `[0]`(旧值)在前、`[1]`(成功位)在后。 +- 降级验证:`mlir-translate --mlir-to-llvmir` 得 + `cmpxchg ptr %0, i32 %1, i32 %2 acq_rel monotonic, align 4` + + `extractvalue { i32, i1 } %4, 0` / `, 1` ✓。 + +### 2.10 具名结构体(`%"name" = type {…}`) + +**背景(实测语料)**:TPV 对每个类都调 `module_add_named_type`(18 处调用点: +`ClassGenDef.vp:522`、`ClassGenEnum.vp:720/1000`、`StructGenMixin.vp:1802`、 +`AsyncGenMixin.vp:733`、`VpiReader.vp:2705/4059` 等),llvmlite 侧由 +`PrintStructDefinition` 输出 `%"sha1.Cls" = type {…}` 定义行 +(`.tpv_build/**/*.ll` 里 `^%"[^"]+" = type` 命中 78319 行 / 1815 文件)。 + +**MLIR 侧两条硬约束(实测 mlir-opt 22.1.8)**: + +1. **无独立类型定义语法** ⇒ 定义行**整体丢弃**(`PrintStructDefinition` 输出空串, + `PrintModule` 完全不遍历 `NamedTypeHead` 输出文本); +2. **不允许无体前向声明** ⇒ `!llvm.struct<"x">`(只有名字、无字段表)报 + `struct without a body only allowed in a recursive struct`;opaque + (`fcount < 0` 哨兵)**无法表达**。 + +**唯一可表达形态 = 使用处的内联具名体**(四种均实测 exit 0): + +| 场景 | MLIR 文本 | +|------|-----------| +| 按值(形参 / 返回 / 全局 / 字段 / alloca 元素) | `!llvm.struct<"aa11bb22cc33dd44.Cls", (i32, !llvm.ptr)>` | +| 名字含数字 / `.` / `[` / `]`(泛型特化名) | `!llvm.struct<"01c70390f01048df.Lock[0]", (i32)>`(**直接作字符串字面量,不额外转义/加引号**) | +| 嵌套具名(字段是另一个具名结构体) | `!llvm.struct<"A.Lock", (!llvm.struct<"A.CRIT", (i32)>)>` | +| 空体(无字段的类) | `!llvm.struct<"x.Empty", ()>` | +| packed(`@c.Attribute(t.attr.packed)`) | `!llvm.struct<"a.P", packed (i32, i8)>`(关键字在字段表**前**;降级 `%a.P = type <{ i32, i8 }>`;空体 `packed ()` 亦 exit 0) | +| `Ptr(具名结构体)`(`%"name"*`) | `!llvm.ptr`(不透明指针,既有路径**不变**) | + +**定义行由 MLIR 自动生成(实测降级证据)**:`mlir-translate --mlir-to-llvmir` +会把使用处的内联形态合成为定义行,故模块里**不需要**(也不能)手写定义行: + +``` +%aa11bb22cc33dd44.Cls = type { i32, ptr } ; ← 模块文本里没有这一行 +%x.Empty = type {} +%"01c70390f01048df.Lock[0]" = type { i32 } +@per_cpu = internal global %aa11bb22cc33dd44.Cls { i32 undef, ptr null } +``` + +**实现位点**: + +- `__types.vp:_MlirNamedStructAppend` —— 具名内联体渲染(`TypePrint` / + `_MlirTypePrintDepth` 的具名分支;匿名分支 `_MlirStructFields` 不变); + `packed` 非 0 时在字段表**前**多输出 `packed `; +- `__types.vp:_MlirNamedStructText` —— 中间文本 `%"name"` → 查登记表展开 + (packed 标记与类型同槽取出,故文本路径同样输出 `packed (…)`); +- `__module.vp:_MlirPublishNamedTypes` —— `PrintModule` 开始时按 `NamedTypeHead` + 发布登记表(`MlirNamedTypesBegin(pool, cnt)` + + `MlirNamedTypeRegister(ty, is_packed)`,`cnt` 取自链表实际长度,`is_packed` + 取自 `NamedTypeNode.IsPacked`),打印结束时 `MlirNamedTypesClear`(表内类型 + 指针只在当次打印期间有效)。 + +**响亮报错(绝不静默输出错误布局)**: + +| 情形 | 报文 | +|------|------| +| 打印时该具名结构体仍是 opaque(`fcount < 0`,跨模块前向声明未被完整定义替换) | `命名结构体在打印时仍为 opaque(无字段体)——MLIR 不允许无体前向声明(类型名 '…')` | +| 具名但字段链缺失(`FCount` 非 0 而 `Fields == None`) | `命名结构体字段链缺失(FCount 非 0 但无字段)` | +| 名字含 `"` / `\` / 控制字符 | `命名结构体名字含 MLIR 字符串字面量非法字符` | +| 名字为空 / 字段链成环(> 4096) | `命名结构体名字为空` / `命名结构体字段过多或字段链成环` | +| 中间文本 `%"name"` 未登记(含"表未发布"的独立 `FunctionPrint`) | `命名结构体类型文本未在模块中登记完整定义(MLIR 不允许无体前向声明…)` | + +**opaque 风险查证(结论:按值使用处不会出现 opaque)**:TPV 的 opaque 只由 +`_scan_type_for_cross_module_refs`(`Ptr(Struct(name))` 分支)与 REPL 专用的 +`_repl_opaque_class_ty` 产生,两者都服务于**指针**引用;按值位置(C ABI 值形参) +另有显式判据 `_AggregateParamTy`:命中注册表且非 opaque 才按值、否则**降级 i64**; +值类型的全局 / 返回类型先经 `_ensure_full_value_types` 注册完整定义。故打印时 +按值出现的具名结构体字段都是完整的;一旦不完整即按上表响亮报错(旧行为是静默 +输出空体 `!llvm.struct<()>` ⇒ 错误布局)。 + +**packed 结构体(已接入)**:`@c.Attribute(t.attr.packed)` → +`module_set_named_type_packed` → `NamedTypeNode.IsPacked`(**唯一设置点**)。 +packed 标记只挂在**具名**节点上(`LLVMType.Struct` 变体没有该字段; +`module_set_named_type_packed` 对 `Name == None` 直接返回),故匿名结构体 +(`{…}` → `!llvm.struct<(...)>`)**结构上无法**携带 packed —— 不存在"无名却要求 +packed"的组合,无需(也无处)为其单独报错。登记表每槽 16 字节 +(`{ LLVMType*, IsPacked }`),打印期由 `_MlirPublishNamedTypes` 从节点读出; +渲染侧(`TypePrint` 具名分支经 `_MlirNamedPackedOf` 回查;文本路径 +`_MlirNamedStructText` 与类型同槽取出)据标记在字段表前输出 `packed `。 +实测 mlir-opt / mlir-translate 22.1.8 exit 0: +`!llvm.struct<"a.P", packed (i32, i8)>` → `%a.P = type <{ i32, i8 }>`; +空体 `!llvm.struct<"x.Empty", packed ()>` → `%x.Empty = type <{}>`。 + --- ## 3. v1 支持 / 不支持清单 ### 3.1 支持(已实现) -类型:`iN` 全整数、`f16/f32/f64/f128`、不透明指针、数组、结构体、函数类型、`Void`。 +类型:`iN` 全整数、`f16/f32/f64/f128`、不透明指针、数组、结构体(含**具名结构体** +`!llvm.struct<"name", (…)>` 与 **packed 具名结构体** `!llvm.struct<"name", packed (…)>`, +见 §2.10)、函数类型、`Void`。 指令:`add/sub/mul/sdiv/udiv/srem/urem/and/or/xor/shl/lshr/ashr`、 -`fadd/fsub/fmul/fdiv/frem/fneg`、`icmp/fcmp`、`alloca`、`load/store`(含 `volatile`)、 +`fadd/fsub/fmul/fdiv/frem/fneg`、`icmp/fcmp`、`alloca`(含 `, align N` 对齐属性,见 §2.7)、 +`load/store`(含 `volatile`)、 `getelementptr`(单索引 / 数组 / 结构体)、`bitcast`(ptr→ptr 折叠为 no-op)、 `sext/trunc/zext/ptrtoint/inttoptr/fpext/fptrunc/fptosi/sitofp/fptoui/uitofp`、 -`call`(直接 / 间接)、`br/cond_br`(含块实参)、`ret/ret void`、`unreachable`、 -`select`、`phi`(→ 块参数)、`switch`、`atomicrmw`。 +`call`(直接 / 间接 / **变参**)、`inline_asm`(`sideeffect` / `inteldialect`,**含多结果**,见 §2.8)、 +`br/cond_br`(含块实参)、`ret/ret void`、`unreachable`、 +`select`、`phi`(→ 块参数)、`switch`(含目标块带 phi 的块实参)、`atomicrmw`、 +**`va_arg`**、**`cmpxchg`**(三者见 §2.9)、**`extractvalue`**(多结果内联汇编的结果抽取, +含多级索引,见 §2.8);**变参函数声明**(`llvm.func @f(…, ...)`)。 -全局初值:整数、浮点、`null`、`zeroinitializer`、cstring、整型数组(含嵌套数组)。 +全局初值:整数、浮点、cstring、整型数组(含嵌套数组)走**属性路径**; +根级裸 `null` / `zeroinitializer`、结构体/vtable、含符号引用的聚合、指针型初值 +走 **initializer region**(叶子 op + undef + insertvalue 链 + return;见 §2.5)。 ### 3.2 不支持 —— 一律**响亮报错**(`MlirFail`:打印定位信息后 `exit(1)`) @@ -230,19 +491,25 @@ module { |------|------------------|------| | `Label` 类型 | `TypePrint` / `_MlirTypePrintDepth` | `TypePrint(Label): MLIR 无 label 类型,v1 不支持` | | `Metadata` 类型 | `TypePrint` / `_MlirTypePrintDepth` | `TypePrint(Metadata): MLIR llvm 方言 v1 不支持 metadata 类型` | -| `alloca …, align N` | `_MlirAlloca` | `alloca 对齐(build_alloca_aligned)v1 不支持` | -| 变参函数 `...` | `_MlirFuncSig` | `变参函数(...)的 llvm.func 形态未固化,v1 不支持` | -| 变参 `call`(builder 的 `call () @f(...)`) | `_MlirCall` | `变参 call 的 MLIR 形态未固化,v1 不支持` | -| `cmpxchg` | `_MlirRenderLine` | `cmpxchg 的 MLIR 形态未固化,v1 不支持` | -| `va_arg` | `_MlirRenderLine` | `va_arg v1 不支持` | -| 内联汇编(中间文本为 `call asm …`) | `_MlirCall` | `inline_asm 的 MLIR 形态未固化,v1 不支持` | -| `{ … }` 结构体 / vtable 全局初始化串 | `_MlirGlobalInit` | `全局初值形态 v1 不支持(结构体/vtable 初始化串需改用结构化初始化)` | +| `alloca` 对齐值非常量/非 2 的幂/超范围(> 2^30) | `_MlirAlloca` | `alloca 对齐值必须是常量(常量十进制,非 SSA)` / `alloca 对齐值必须是 2 的幂` / `alloca 对齐值超范围(须为 2 的幂且 <= 2^30)` | +| `alloca` 的 `, <非 align 属性>` | `_MlirAlloca` | `alloca 只支持 align 属性(未知属性)` | +| `inline_asm` 的未知关键字(含 LLVM 的 `attdialect`/`alignstack`/`unwind`)或非字符串汇编文本 | `_MlirAsm` | `inline_asm 缺少汇编文本(或含未知关键字)` | +| `inline_asm` 缺约束串 / 约束串非字符串 / 缺实参列表 / 实参后有多余记号 | `_MlirAsm` | `inline_asm 缺少约束串(形态须为 "asm", "constraints")` 等 | +| `inline_asm` 有返回值却无结果名(中间文本缺 `%N = `) | `_MlirAsm` | `inline_asm 有返回值但缺少结果名(%N = call …)` | +| 双结果前缀 `%a, %b =` 用于非 `cmpxchg`,或缺 `=` | `_MlirRenderLine` | `双结果前缀(%a, %b =)只支持 cmpxchg` / `双结果前缀(%a, %b =)缺少 '='` | +| `cmpxchg` 缺结果名 / extractvalue 结果名分配失败 | `_MlirCmpXchg` / `_MlirCxResName` | `cmpxchg 缺少结果名` / `cmpxchg 结果名分配失败` | +| `va_arg` 缺结果名 | `_MlirVaArg` | `va_arg 缺少结果名` | +| `extractvalue` 缺索引 / SSA 索引 / 索引后有多余记号 / 索引过多(> 32) | `_MlirExtractValue` | `extractvalue 缺少索引` / `extractvalue 索引必须是常量(SSA 索引不支持)` / `extractvalue 索引之后出现多余记号` / `extractvalue 索引过多` | +| 变参 `call` 的函数类型参数过多(> 256) | `_MlirCall` | `变参 call 的函数类型参数过多` | +| 全局初值叶子形态非 `符号引用`/`null`/`zeroinitializer`/`标量字面量`(如聚合里的字符串常量) | `_MlirGvWalkLeaf` | `全局初值叶子形态 v1 不支持:'<原始文本>'` | +| 初值树与全局类型树不一致(初值为结构体/数组但全局类型不匹配) | `_MlirGvWalkStruct` / `_MlirGvWalkArray` | `初值为结构体,但全局类型不是结构体` / `初值为数组,但全局类型不是数组` | +| 初值嵌套超过 64 层 | `_MlirGvWalk` | `全局初值嵌套过深(> 64 层)` | | phi 不在块首(非连续) | `_MlirRenderLine` | `phi 不在块首(v1 只支持块首连续 phi → 块参数)` | | entry 块出现 phi | `_MlirCheckEntryNoPhi` | `entry 块不允许 phi(MLIR entry 块不接受块参数)` | -| 跳转目标带 phi 但缺匹配入边 | `_MlirBlockOperands` | `跳转目标块带 phi,但找不到与源块匹配的入边` | -| `switch` 目标块带 phi | `_MlirSwitch` | `switch 目标块带 phi(携带块实参)v1 不支持` | +| 跳转目标(含 `switch` 的 case/default 目标)带 phi 但缺匹配入边 | `_MlirBlockOperands` | `跳转目标块带 phi,但找不到与源块匹配的入边` | | `switch` case 值是 SSA | `_MlirSwitch` | `switch case 值必须是常量(SSA 值不支持)` | -| `%name` 命名结构体类型文本(中间文本里不应出现) | `_MlirNamedStructText` | `命名结构体类型文本 v1 不支持(应使用内联 !llvm.struct<(…)>)` | +| 具名结构体在打印时仍为 opaque / 字段链缺失;名字含 `"`、`\`、控制字符;字段链成环 | `_MlirNamedStructAppend` / `_MlirNamedCheckName` | 见 §2.10 报错表 | +| 中间文本 `%"name"` 未登记完整定义(含独立 `FunctionPrint` 时登记表未发布) | `_MlirNamedStructText` | `命名结构体类型文本未在模块中登记完整定义(MLIR 不允许无体前向声明…)` | | 中间文本里出现未知 opcode | `_MlirRenderLine` | `未知指令 opcode` | | 无法转换的类型文本 | `_MlirTypeAppend` | `无法转换为 MLIR 类型文本(v1 未覆盖该形态)` | | 常量操作数形态非 整数/浮点/null/符号 | `_MlirEmitOneConst` | `不支持的常量操作数形态` | @@ -256,8 +523,11 @@ module { v1 **记录但不渲染**(模型字段保留,`function_set_attrs` / `function_set_attr_group` 同名可用)。 - `Param.Attrs`(`noundef`/`nocapture` 等):同上。 - `NamedTypeHead` 类型定义链表:MLIR 无独立类型定义语法,结构体在使用处内联为 - `!llvm.struct<(…)>`;`module_add_named_type` 保留模型语义(去重/opaque 替换)但 - 不再产生输出文本,`PrintStructDefinition` 输出空串(保留 API 同形)。 + `!llvm.struct<"name", (…)>`(具名)/ `!llvm.struct<(…)>`(匿名),packed 具名 + 再加关键字(`!llvm.struct<"name", packed (…)>`);**不输出**任何 + 类型定义行。`module_add_named_type` 保留模型语义(去重 / opaque 就地替换)并作为 + 打印期登记表的来源(`_MlirPublishNamedTypes`,见 §2.10),`PrintStructDefinition` + 输出空串(保留 API 同形)。 --- @@ -265,34 +535,44 @@ module { | # | 同形点 | 语义差异 | |---|--------|----------| -| 1 | `TypePrint(buf,size,ty,pool)` | 输出 MLIR 类型而非 LLVM 类型;`Ptr(_)` 恒为 `!llvm.ptr`(丢失 pointee),`Void` 为 `none`,`Label` 报错 | +| 1 | `TypePrint(buf,size,ty,pool)` | 输出 MLIR 类型而非 LLVM 类型;`Ptr(_)` 恒为 `!llvm.ptr`(丢失 pointee)、`Struct(…,name)` 内联为 `!llvm.struct<"name", (…)>`(丢失"定义行"这一概念,见 §2.10),`Void` 为 `none`,`Label` 报错 | | 2 | `ValuePrint(buf,size,val,pool)` | SSA 只输出 `%0`(不带类型);常量输出 `42 : i32`;llvmlite 版是 `i32 42` | | 3 | `FunctionPrint(buf,size,func,pool)` | 输出 `llvm.func`/`llvm.*` op;**入口常量物化**、**phi→块参数**、**no-op bitcast 折叠** | | 4 | `LLVMModulePrint(buf,size,mod,pool,mode)` | 输出 `module { … }`;`OUTPUT_STUB/TEXT` 只能用注释标示(MLIR 无此二分);triple/datalayout 不输出 | | 5 | `IRBuilder.build_bitcast(val,to_ty)` | 返回的 `Value` 名字仍会出现在中间文本里,但打印时若两端都是指针则**整个指令消失**、名字被别名替换(`Value` 对象本身不变) | | 6 | `IRBuilder.build_phi` / `build_br` | builder 语义不变(仍写 phi 行);MLIR 打印时 phi 行消失、br 追加实参 | -| 7 | `IRBuilder.build_alloca` | 打印时个数按常量 `1`(i32)物化;`build_alloca_aligned` 报错 | +| 7 | `IRBuilder.build_alloca` | 打印时个数按常量 `1`(i32)物化;`build_alloca_aligned` 的 `, align N` 转写为 `{alignment = N : i64}` 属性(N 必须是 2 的幂常量,否则响亮报错) | | 8 | `const_int` / `ConstFloat` | `Name` 文本仍是 llvmlite 形态(十进制 / `0x` 位模式);**转换发生在打印阶段**(浮点位模式→十进制科学计数法) | | 9 | `create_global_string` | `Initializer` 仍是 `c"…\00"`;打印时转成 `"…"`(**去掉尾随 `\00`**,见 §5 待确认) | | 10 | `verify_function` / `verify_module` | 校验内部模型 + 中间文本(与 llvmlite 完全一致);**不**校验 MLIR 文本合法性(交给 mlir-opt) | -| 11 | `PrintStructDefinition` | 输出空串(MLIR 无类型定义行),llvmlite 版输出 `%"name" = type {…}` | +| 11 | `PrintStructDefinition` | 输出空串(MLIR 无类型定义行),llvmlite 版输出 `%"name" = type {…}`;具名结构体只在**使用处**内联出现(§2.10) | | 12 | `G_PeepEnable` | 语义不变:窥孔作用在**中间文本**(`ret zeroinitializer→0/null`、入口槽 store→load 转发、no-op bitcast 消除),与 MLIR 渲染正交(T3 与 MLIR 的 no-op 折叠叠加,结果一致) | -| 13 | `module_set_named_type_packed` | 语义不变(记录 packed);MLIR 侧 packed 布局未参与渲染(字段列表渲染不区分 packed) | +| 13 | `module_set_named_type_packed` | 语义不变(记录 `NamedTypeNode.IsPacked`);MLIR 侧**已参与渲染**:具名结构体在使用处内联为 `!llvm.struct<"name", packed (…)>`(见 §2.10) | | 14 | 中间指令文本(`Line.Buf`) | **内容与 llvmlite 不同**:builders 内嵌类型经 `_type_str` → `TypePrint` 已是 MLIR 形态(`load i32, !llvm.ptr %p` 而非 `load i32, i32* %p`)。文本层使用者(窥孔、verify)按名称/结构匹配,语义不受影响 | --- ## 5. 待确认(不做静默假设,逐条标注) -1. `switch` 的方言形态按设计规范输出 `llvm.switch %v, ^default [0: ^case0, …]`, - 未用 `mlir-opt` 校验(真实 MLIR 可能要求 `%v : i32, ^default [ … ]` 形式)。 +1. ~~`switch` 的方言形态按设计规范输出 `llvm.switch %v, ^default [0: ^case0, …]`~~ —— + **已由 mlir-opt 22.1.8 实测纠正**:规范里的形态被拒(`expected ':'`), + 实际采用 `llvm.switch %v : i32, ^default [0: ^case0]`(值类型紧随值), + 目标块实参写作 `^d(%c1 : i32)` / `[0: ^c0(%c0 : i32)]`(见 §2.2)。 + 本轮同样实测了 `alloca {alignment = N : i64}`(§2.7)与 + `inline_asm … asm_dialect = intel …`(§2.8),三者均以实测形态为准。 2. `alloca` / `load` / `store` 的 `volatile`、`atomicrmw` 的 ordering 关键字按 - LLVM 词法直译为 MLIR 词法,未用 `mlir-opt` 校验。 -3. 全局 `{section = "…", alignment = N : i64}` 属性字典的**位置**(值之后、类型之前) - 未用 `mlir-opt` 校验。 -4. `c"hello\00"` → `"hello"`:按设计规范**去掉尾随 `\00`**(设计规范样例为 - `@.str("hello") : !llvm.array<6 x i8>`)。严格 MLIR 语义下 `!llvm.array<6 x i8>` - 需要 6 字节;若工具链要求显式 NUL,去掉 `_MlirCStringToMlir` 里的剥离逻辑即可。 + LLVM 词法直译为 MLIR 词法。其中 `acq_rel` / `monotonic` 已随本轮的 `cmpxchg` + 用例一并由 `mlir-opt` 实测(§2.9);`volatile` 仍未单独校验。 +3. ~~全局 `{section = "…", alignment = N : i64}` 属性字典的**位置**(值之后、类型之前) + 未用 `mlir-opt` 校验~~ —— + **已由 mlir-opt 22.1.8 实测确立**:字典**必须在类型之前**;放到类型之后会被当成 + 新 op(``Dialect `' not found for custom op 'section'``),`unnamed_addr` 只能作 + 关键字(进字典报 `occurs more than once`);stub 的 `external` 声明必须带空括号 + `()`。详见 §2.5 的硬约束 4/5。 +4. ~~`c"hello\00"` → `"hello"`:按设计规范**去掉尾随 `\00`**~~ —— + **与实测冲突,已按实测推翻**:MLIR 要求字符串属性长度**等于** i8 数组长度, + 而 TPV 的类型含尾随 NUL(`[N+1 x i8]`)⇒ 属性里**必须保留 `\00`** + (`_MlirCStringToMlir` 只剥 `c` 前缀,见 §2.5 硬约束 2)。 5. `Func(Void, …)` 的类型文本渲染为 `!llvm.func`(由 `Void → none` 规则 组合而来);函数签名位置的 void 返回值则是**省略** `-> ty`(不影响)。 6. 渲染期的常量表 / 别名表 / 解析 scratch 都是**模块级全局**(每次 `FunctionPrint` @@ -303,6 +583,26 @@ module { 放大编译器内存池;后续若要降内存,可改为"同一池内一次性分配并复用"。 8. `MlirFail` 采用 `stdio.printf` + `stdlib.exit(1)` 的 fail-fast 风格(对齐 llvmlite 的 `[IR-DUMP]`/`[IR-FAIL]`):不支持形态会**终止进程**而非返回错误码。 + 拿不到调用栈,故"类型文本转换失败"这条报文会**追加当前正在渲染的中间文本行** + (`… :'<类型片断>' [行: %1 = call …]`);行上下文由 `__function.vp` 的 + `_MlirRenderLine` 入口经 `__types.vp` 的 `MlirSetContextLine` 寄存 + (`FunctionPrint` / `PrintModule` 入口清空,避免带上无关旧行)。 + 具名结构体的报错同样带类型名与行上下文(`_MlirFailNamed`,见 §2.10)。 +9. 具名结构体登记表(§2.10)是**打印期**设施:只在 `PrintModule` 期间有效 + (先数 `NamedTypeHead` 实际长度 → `MlirNamedTypesBegin(pool, cnt)` 精确分配 + `cnt * 16` 字节,每槽 `{ LLVMType*, IsPacked }` → 逐项 + `MlirNamedTypeRegister(ty, is_packed)` → 打印结束 `MlirNamedTypesClear`)。 + **无具名类型的模块零分配**;真实模块的具名类型可达数千个(实测单文件最多 7837 个), + 故不设固定容量上限。**独立调用 `FunctionPrint`** 时表未发布 ⇒ 中间文本里的 + `%"name"` 会响亮报错(builders 不会产生该形态,故正常路径不受影响)。 +10. `_mlir_read_type` 按 `[] {} () <>` 做**字符级**括号平衡来读 `!llvm.struct<…>`; + 具名结构的名字是**带引号字符串**,含 `[`/`]`(泛型特化名如 + `100000000000000a.GSList[100000000000000b.BasicBlock]`)时不影响平衡(成对出现)。 + 实测语料(全仓 1462 个 `.ll`/`.vpi`,26476 个唯一具名类型):TPV 名字**全部成对**、 + 无空格、无 `"`/`\`;仅 19 个非 TPV 名(手工 IR 里的 `<<.1` / `>>.10` 类移位临时名) + 含不配对 `<>` —— 它们不经 mlirlite(本库是**生成器**,只打印 TPV 模型)。 + MLIR 侧对这些名字的承载已实测:`!llvm.struct<">>.1", (i32)>` exit 0, + 降级得 `%">>.1" = type { i32 }`。 --- @@ -321,4 +621,8 @@ includes/mlirlite/ ``` 测试:`Test/MLIRLiteTest/`(`App/main.vp` + `project.json`),用 `testcheck` -断言各渲染规则(类型 / 函数 / 模块 / 常量物化 / phi→块参数 / 内存·调用·转换 / 全局 / verify)。 +断言各渲染规则(类型 / 函数 / 模块 / 常量物化 / phi→块参数 / 内存·调用·转换 / 全局 / verify / +alloca 对齐·switch·inline_asm / 变参·va_arg·cmpxchg·全局属性字典(T14)/ +**具名结构体:按值·指针·文本 `%"name"` 展开·定义行丢弃(T15,见 §2.10)**/ +**多结果内联汇编·`extractvalue`(含多级索引,T16,见 §2.8)**)。 +其中 `output/*.mlir` 是 mlir-opt 的离线校验集(裸函数/类型/值文本落 `.txt`)。 diff --git a/mlirlite/__function.vp b/mlirlite/__function.vp index e0f178c..0e5cef2 100644 --- a/mlirlite/__function.vp +++ b/mlirlite/__function.vp @@ -8,7 +8,7 @@ import stdlib from linkedlist import GSListNode, GSList from .__types import ( LLVMType, ParamNode, TypePrint, - MlirTypeText, MlirTypeTextIsPointer, MlirFail, + MlirTypeText, MlirTypeTextIsPointer, MlirFail, MlirSetContextLine, ) @@ -1160,11 +1160,17 @@ _MLIR_ConstVal: t.CPtr = None _MLIR_ConstName: t.CPtr = None _MLIR_ConstCount: int = 0 -# bitcast no-op 别名表:结果名 -> 源操作数(ptr→ptr bitcast 在 MLIR 无意义) +# bitcast no-op 别名表 + cmpxchg 结果绑定表:结果名 -> 源操作数 / extractvalue 结果名 +# (cmpxchg 的 extractvalue 结果名由结果名确定性派生,不再用计数器,见 _MlirCxResName) _MLIR_AliasFrom: t.CPtr = None _MLIR_AliasTo: t.CPtr = None _MLIR_AliasCount: int = 0 +# 当前正在渲染的中间指令文本行(_MlirRenderLine 入口赋值)。 +# 供类型文本转换失败的报文追加行上下文:MlirFail 是即时终止、拿不到调用栈, +# 故把行文本寄存到 __types.vp(经 MlirSetContextLine),由报错处一并打印。 +_MLIR_CurLine: str = None + # 解析期 scratch 缓冲(每次 FunctionPrint 分配一次,全程复用; # 所有需要跨行存活的文本在入表/入别名表时都会 dup,见 _mlir_dup) _SC_Op: str = None @@ -1334,6 +1340,26 @@ def _MlirConstGet(tytext: str, valtext: str) -> str: return nm +def _MlirCxResName(resname: str, suffix: str) -> str: + """cmpxchg 的 extractvalue 结果名:由 struct 结果名**确定性派生** + + MLIR 裸 SSA 名首字符必须是字母或 `_`(mlir-opt 22.1.8 实测:`%0_v` 报 + "expected '=' after SSA name"),故统一在结果名正文前加 `_`: + `%0` → `%_0_v`(旧值 [0])/ `%_0_ok`(成功位 [1]) + `%a` → `%_a_v` / `%_a_ok` + 名字与结果名一一对应、不含计数器 ⇒ pass1(登记别名)与 pass2(发射) + 生成的名字必然逐字相同,引用不会悬空。 + """ + nm: str = _MLIR_Pool.alloc(64) + if nm is None: + MlirFail("MLIR 渲染: cmpxchg 结果名分配失败") + nm[0] = '\0' + _append_cstr(nm, 64, "%_") + _append_cstr(nm, 64, resname + 1) + _append_cstr(nm, 64, suffix) + return nm + + def _MlirParseHexU64(s: str) -> t.CUInt64T: """解析 0x 十六进制字面量为 u64""" p: str = s @@ -1481,11 +1507,15 @@ def _mlir_read_value(p: str, out: str, outsz: sizet) -> str: return p -def _mlir_read_type(p: str, out: str, outsz: sizet) -> str: +def _mlir_read_type(p: str, out: str, outsz: sizet, nofunc: int = 0) -> str: """读一个类型记号(MLIR 形态 !llvm.… 或标量 iN/fN,兼容 LLVM 聚合形态) 括号平衡覆盖 `[] {} () <>`:MLIR 的 !llvm.array<…>/!llvm.struct<(…)> 里含 空格与逗号,必须靠深度计数整体读出;无括号类型(!llvm.ptr)遇到顶层分隔符结束。 + + nofunc=1 时**不吞并**紧随其后的 ` (…)` 函数类型后缀:call 的返回类型位置 + 上,`(…)`(LLVM 词法)是**被调函数的参数类型表**而不是返回类型的一部分, + 必须留给 _MlirCall 的变参分支处理(见该处注释)。 """ p = _mlir_ws(p) n: sizet = 0 @@ -1539,9 +1569,9 @@ def _mlir_read_type(p: str, out: str, outsz: sizet) -> str: out[n] = p[0] n += 1 p = p + 1 - # 函数类型后缀 " ( … )" + # 函数类型后缀 " ( … )"(nofunc=1 时不吞并,见函数 docstring) q: str = _mlir_ws(p) - if q[0] == '(': + if nofunc == 0 and q[0] == '(': if n + 1 < outsz: out[n] = ' ' n += 1 @@ -1973,13 +2003,51 @@ def _MlirCmp(p: str, resname: str, ob: str, out: str, outsz: sizet): def _MlirAlloca(p: str, resname: str, out: str, outsz: sizet): - """alloca: llvm.alloca %cN x ty : (i32) -> !llvm.ptr""" + """alloca: llvm.alloca %cN x ty {alignment = N : i64} : (i32) -> !llvm.ptr + + 对齐属性的 MLIR 形态由 mlir-opt 22.1.8 实测确定(**不得凭记忆**): + `{alignment = 16 : i64}` → exit 0 ✓(采用) + `align(16)` / `, align 16`(LLVM 词法) → exit 1(expected ':') + `{alignment = 16 : i32}` → exit 1 + ("attribute 'alignment' failed to satisfy constraint: 64-bit signless + integer attribute"——必须是 **i64**) + 故中间文本 `, align N` 必须转写为 `{alignment = N : i64}`。 + + 对齐值只接受**常量十进制**且为 **2 的幂**:mlir-opt 不校验幂次 + (实测 `{alignment = 3 : i64}` exit 0),但非 2 的幂在 LLVM 语义里非法, + 故本层把关,非常量/非幂/超范围(> 2^30)一律响亮报错。 + """ tyb: str = _SC_Ty p = _mlir_read_type(p, tyb, 512) p = _mlir_ws(p) + aligntxt: str = _SC_P2 + aligntxt[0] = '\0' if p[0] == ',': - # ", align N" —— MLIR 对齐属性形态未经 mlir-opt 验证,v1 响亮报错 - MlirFail("MLIR 渲染: alloca 对齐(build_alloca_aligned)v1 不支持") + # ", align N"(build_alloca_aligned 的中间文本形态) + p = _mlir_ws(p + 1) + if string.strncmp(p, "align", 5) != 0: + MlirFail("MLIR 渲染: alloca 只支持 align 属性(未知属性)") + p = _mlir_ws(p + 5) + nd: int = 0 + while p[0] >= '0' and p[0] <= '9': + if nd + 1 >= 512: + MlirFail("MLIR 渲染: alloca 对齐值过长(超范围)") + aligntxt[nd] = p[0] + nd += 1 + p = p + 1 + aligntxt[nd] = '\0' + p = _mlir_ws(p) + if p[0] != '\0': + MlirFail("MLIR 渲染: alloca 的 align 之后出现多余记号") + if nd == 0: + MlirFail("MLIR 渲染: alloca 对齐值必须是常量(常量十进制,非 SSA)") + if nd > 10: + MlirFail("MLIR 渲染: alloca 对齐值超范围(须为 2 的幂且 <= 2^30)") + av: int = string.atoi(aligntxt) + if av <= 0 or av > 1073741824: + MlirFail("MLIR 渲染: alloca 对齐值超范围(须为 2 的幂且 <= 2^30)") + if (av & (av - 1)) != 0: + MlirFail("MLIR 渲染: alloca 对齐值必须是 2 的幂") mt: str = _SC_Mt mt[0] = '\0' MlirTypeText(mt, 512, tyb, _MLIR_Pool) @@ -1989,6 +2057,10 @@ def _MlirAlloca(p: str, resname: str, out: str, outsz: sizet): _append_cstr(out, outsz, cnt) _append_cstr(out, outsz, " x ") _append_cstr(out, outsz, mt) + if aligntxt[0] != '\0': + _append_cstr(out, outsz, " {alignment = ") + _append_cstr(out, outsz, aligntxt) + _append_cstr(out, outsz, " : i64}") _append_cstr(out, outsz, " : (i32) -> !llvm.ptr\n") @@ -2157,16 +2229,68 @@ def _MlirUnreachable(out: str, outsz: sizet): def _MlirCall(p: str, resname: str, out: str, outsz: sizet): - """call: llvm.call @f(%a) : (i32) -> i32 / 间接 llvm.call %fp(%a) : !llvm.ptr, (i32) -> i32""" + """call: llvm.call @f(%a) : (i32) -> i32 / 间接 llvm.call %fp(%a) : !llvm.ptr, (i32) -> i32 + + 变参调用(中间文本 `call (…) @f(…)`)追加 `vararg(!llvm.func<…>)`。 + """ p = _mlir_ws(p) + variadic: int = 0 + vftys: str = _SC_Tmp + vftys[0] = '\0' rtyb: str = _SC_Ty - p = _mlir_read_type(p, rtyb, 512) + # 返回类型必须用 nofunc=1 读:变参调用的中间文本里,返回类型之后的 `(…)` + # 是**被调函数的参数类型表**(LLVM 词法),不是返回类型的一部分。若按默认 + # 行为把它当"函数类型后缀"吞进 rtyb,则 p 落到 `@`/`%`、下面的变参分支 + # 永远进不去,rtyb 变成 `i32 (!llvm.ptr, i32, ...)` 送进 MlirTypeText → + # _MlirFuncText 按逗号切参数,末片 `...` 无对应形态 → 误报 + # "无法转换为 MLIR 类型文本(v1 未覆盖该形态):'...'"。 + p = _mlir_read_type(p, rtyb, 512, 1) p = _mlir_ws(p) if string.strncmp(p, "asm", 3) == 0: - # 内联汇编按 builder 形态渲染为 "call asm ..." - MlirFail("MLIR 渲染: inline_asm 的 MLIR 形态未固化,v1 不支持") + # 内联汇编:中间文本是 `[%N = ]call asm [sideeffect] + # [inteldialect] "", ""( %v, …)`(LLVM 词法), + # MLIR 形态由 _MlirAsm 转写。 + _MlirAsm(p, resname, rtyb, out, outsz) + return if p[0] == '(': - MlirFail("MLIR 渲染: 变参 call 的 MLIR 形态未固化,v1 不支持") + # 变参调用:中间文本是 `call (<实参类型…>, ...) @f(<实参>)` + # (builder 的 build_call is_variadic=1 形态)。 + # MLIR 形态(mlir-opt 22.1.8 实测 exit 0): + # %0 = llvm.call @printf(%p, %n) vararg(!llvm.func) + # : (!llvm.ptr, i32) -> i32 + # `vararg(...)` 里是**被调函数的完整 MLIR 函数类型**(含 `...`); + # void 返回时该处返回类型写 `void`(写 `none` 实测报 + # "expected var_callee_type to return void")。 + # 注:vftys/calb 等缓冲用非 _SC_Ty(rtyb 占着)/非 _SC_Tmp2(实参循环用)。 + variadic = 1 + p = p + 1 + g0: int = 0 + while p[0] != '\0': + g0 += 1 + if g0 > 256: + MlirFail("MLIR 渲染: 变参 call 的函数类型参数过多") + p = _mlir_ws(p) + if p[0] == ')': + p = p + 1 + break + if vftys[0] != '\0': + _append_cstr(vftys, 4096, ", ") + if p[0] == '.': + # `...` 变参记号直通(不是类型,MlirTypeText 不认该记号) + _append_cstr(vftys, 4096, "...") + while p[0] == '.': + p = p + 1 + else: + ftb: str = _SC_V2 + ftb[0] = '\0' + p = _mlir_read_type(p, ftb, 512) + fmt: str = _SC_IdxT + fmt[0] = '\0' + MlirTypeText(fmt, 1024, ftb, _MLIR_Pool) + _append_cstr(vftys, 4096, fmt) + p = _mlir_ws(p) + if p[0] == ',': + p = p + 1 calb: str = _SC_Callee p = _mlir_read_value(p, calb, 512) p = _mlir_ws(p) @@ -2214,7 +2338,21 @@ def _MlirCall(p: str, resname: str, out: str, outsz: sizet): _append_cstr(out, outsz, calb) _append_cstr(out, outsz, "(") _append_cstr(out, outsz, args) - _append_cstr(out, outsz, ") : ") + _append_cstr(out, outsz, ")") + if variadic != 0: + # 变参调用:vararg(被调函数的完整 MLIR 函数类型)(含 `...`) + _append_cstr(out, outsz, " vararg(!llvm.func<") + if string.strcmp(rmt, "none") == 0: + # void 返回的变参函数类型:返回类型写 `void`,不能写 `none` + _append_cstr(out, outsz, "void") + else: + _append_cstr(out, outsz, rmt) + _append_cstr(out, outsz, " (") + _append_cstr(out, outsz, vftys) + # 依次闭合三层:参数表 `)`、`!llvm.func<` 的 `>`、`vararg(` 的 `)` + # (此前漏掉 vararg 的 `)`,mlir-opt 22.1.8 逐字报 "expected ')'") + _append_cstr(out, outsz, ")>)") + _append_cstr(out, outsz, " : ") if calb[0] != '@': # 间接调用:被调用者是 !llvm.ptr 操作数 _append_cstr(out, outsz, "!llvm.ptr, ") @@ -2228,6 +2366,131 @@ def _MlirCall(p: str, resname: str, out: str, outsz: sizet): _append_cstr(out, outsz, "\n") +def _MlirAsm(p: str, resname: str, rtyb: str, out: str, outsz: sizet): + """内联汇编: llvm.inline_asm [has_side_effects] [asm_dialect = intel] "…", "…" %a, %b : (i32, i32) -> i32 + + 中间文本(LLVM 词法)有两条来源: + builders 的 build_inline_asm → `call asm sideeffect "…", "…"()` + TPV CallGenCBuiltin 注入文本 → `call asm sideeffect inteldialect "…", "…"( %v, …)` + MLIR(llvm 方言) 形态由 mlir-opt 22.1.8 实测确定(**不得凭记忆**): + * 方言关键字是 `asm_dialect = intel`:LLVM 的裸 `inteldialect` 报 + "expected attribute value";`asm_dialect = inteldialect` 报 + "expected string or keyword containing one of the following enum values + for attribute 'asm_dialect' [att, intel]"。 + * 输入操作数是**逗号分隔的裸操作数、不带括号**:`"…", "=r,r"(%x) : (i32) -> i32` + 报 "expected ':'"(那正是 LLVM 词法,须改写)。 + * 无返回 ⇒ `: (<操作数类型…>) -> ()`;有返回 ⇒ `-> ty`。 + * **多结果**内联汇编在 MLIR 里是**单结果 + 结构体类型**:TPV 形态 + `%N = call {i32, i32} asm sideeffect "…", "=r,r"()` 渲染为 + `%N = llvm.inline_asm has_side_effects "…", "=r,r" : () -> !llvm.struct<(i32, i32)>` + (mlir-opt / mlir-translate 22.1.8 实测 exit 0,降级得 + `call { i32, i32 } asm sideeffect …`),随后由 `llvm.extractvalue` + 逐字段抽取(见 _MlirExtractValue)。此处无需特殊分支:返回类型文本经 + MlirTypeText(`{…}` → `!llvm.struct<(...)>`)即可,嵌套结构体亦同。 + * 实测通过组合:有/无 has_side_effects × 有/无 asm_dialect = intel × + 0/1/2 个操作数 × 有/无返回值。 + 非法形态一律响亮报错:除 `sideeffect`/`inteldialect` 之外的未知记号、 + 缺约束串、非字符串字面量、实参不成对、有返回值却无结果名。 + """ + p = _mlir_ws(p + 3) + side: int = 0 + intel: int = 0 + g: int = 0 + while g < 8: + g += 1 + if string.strncmp(p, "sideeffect", 10) == 0 and (p[10] == ' ' or p[10] == '\t'): + side = 1 + p = _mlir_ws(p + 10) + continue + if string.strncmp(p, "inteldialect", 12) == 0 and (p[12] == ' ' or p[12] == '\t'): + intel = 1 + p = _mlir_ws(p + 12) + continue + break + # 汇编文本可长达数 KB(TPV 的 ASM_LINE_BUF_SIZE=4096),用 4096 缓冲承载, + # 不落小缓冲以免截断;转义对(\22/\5C/\0A)原样透传——MLIR 词法同样接受。 + asmb: str = _SC_Tmp + p = _mlir_read_value(p, asmb, 4096) + if asmb[0] != '"': + MlirFail("MLIR 渲染: inline_asm 缺少汇编文本(或含未知关键字)") + p = _mlir_ws(p) + if p[0] != ',': + MlirFail("MLIR 渲染: inline_asm 缺少约束串(形态须为 \"asm\", \"constraints\")") + p = _mlir_ws(p + 1) + conb: str = _SC_Args + p = _mlir_read_value(p, conb, 2048) + if conb[0] != '"': + MlirFail("MLIR 渲染: inline_asm 约束串必须是字符串字面量") + p = _mlir_ws(p) + if p[0] != '(': + MlirFail("MLIR 渲染: inline_asm 缺少实参列表 '(...)'") + p = p + 1 + ops: str = _SC_ArgTs + ops[0] = '\0' + tys: str = _SC_IdxV + tys[0] = '\0' + nops: int = 0 + g = 0 + while p[0] != '\0': + g += 1 + if g > 256: + MlirFail("MLIR 渲染: inline_asm 实参过多") + p = _mlir_ws(p) + if p[0] == ')': + break + atyb: str = _SC_Ty2 + p = _mlir_read_type(p, atyb, 512) + p = _mlir_ws(p) + avb: str = _SC_V1 + p = _mlir_read_value(p, avb, 512) + if nops > 0: + _append_cstr(ops, 2048, ", ") + _append_cstr(tys, 1024, ", ") + _MlirOperand(avb, atyb, ops, 2048) + amt: str = _SC_Mt + amt[0] = '\0' + MlirTypeText(amt, 512, atyb, _MLIR_Pool) + _append_cstr(tys, 1024, amt) + nops += 1 + p = _mlir_ws(p) + if p[0] == ',': + p = p + 1 + if p[0] == ')': + p = p + 1 + p = _mlir_ws(p) + if p[0] != '\0': + MlirFail("MLIR 渲染: inline_asm 实参列表之后出现多余记号") + rmt: str = _SC_Buf + rmt[0] = '\0' + MlirTypeText(rmt, 512, rtyb, _MLIR_Pool) + if string.strcmp(rmt, "none") == 0: + _MlirEmitRes(out, outsz, None) + else: + if resname is None: + MlirFail("MLIR 渲染: inline_asm 有返回值但缺少结果名(%N = call …)") + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "llvm.inline_asm") + if side != 0: + _append_cstr(out, outsz, " has_side_effects") + if intel != 0: + _append_cstr(out, outsz, " asm_dialect = intel") + _append_cstr(out, outsz, " ") + _append_cstr(out, outsz, asmb) + _append_cstr(out, outsz, ", ") + _append_cstr(out, outsz, conb) + if nops > 0: + _append_cstr(out, outsz, " ") + _append_cstr(out, outsz, ops) + _append_cstr(out, outsz, " : (") + _append_cstr(out, outsz, tys) + _append_cstr(out, outsz, ") -> ") + if string.strcmp(rmt, "none") == 0: + _append_cstr(out, outsz, "()") + else: + _append_cstr(out, outsz, rmt) + _append_cstr(out, outsz, "\n") + + def _MlirCast(p: str, resname: str, ob: str, out: str, outsz: sizet): """转换指令: llvm.zext %v : i32 to i64""" ftyb: str = _SC_Ty @@ -2407,7 +2670,21 @@ def _MlirAppendCommaTypes(out: str, outsz: sizet, tylist: str): def _MlirSwitch(p: str, out: str, outsz: sizet): - """switch: llvm.switch %v, ^default [0: ^case0, 1: ^case1]""" + """switch: llvm.switch %v : i32, ^default [0: ^case0, 1: ^case1] + + 形态由 mlir-opt 22.1.8 实测确定(设计规范里的 + `llvm.switch %v, ^default [0: ^case0] : i32` **被拒绝**:报 "expected ':'"): + `llvm.switch %v : i32, ^default [0: ^c0, 1: ^c1]` → exit 0 ✓(采用) + `llvm.switch %v : i32, ^default(%r : i32) [ … ]` → exit 0 ✓(default 块实参) + `llvm.switch %v : i32, ^default [0: ^c0(%r : i32)]` → exit 0 ✓(case 块实参) + `llvm.switch %v : i64, ^default [4294967296: ^c0]` → exit 0 ✓(case 值随值类型) + `llvm.switch %v : i32, ^default [-1: ^c0]` → exit 0 ✓ + `llvm.switch %v : i32, ^default []` → exit 0 ✓(空 case 表合法) + 即**值类型紧跟值之后**(`%v : i32,`),块实参写法与 br/cond_br 完全同构, + 故 `_MlirBlockOperands` 的 phi→块实参设施在此原样复用 ⇒ **支持目标块带 phi** + (匹配不到入边会响亮报错,绝不静默丢实参)。 + case 值仍必须是常量:MLIR 里 case 值是属性(case_values),SSA 值不可用。 + """ tyb: str = _SC_Ty p = _mlir_read_type(p, tyb, 512) p = _mlir_ws(p) @@ -2423,6 +2700,14 @@ def _MlirSwitch(p: str, out: str, outsz: sizet): p = _mlir_ws(p) if p[0] == '[': p = p + 1 + # 先物化值操作数与其 MLIR 类型文本:case 循环里的 _MlirBlockOperands 会复用 + # _SC_Ty/_SC_Buf/_SC_P1/_SC_P2 等 scratch,捕获到独立缓冲后才安全。 + vop: str = _SC_IdxT + vop[0] = '\0' + _MlirOperand(vb, tyb, vop, 1024) + vmt: str = _SC_IdxV + vmt[0] = '\0' + MlirTypeText(vmt, 1024, tyb, _MLIR_Pool) cases: str = _SC_Tmp2 cases[0] = '\0' ncase: int = 0 @@ -2448,23 +2733,27 @@ def _MlirSwitch(p: str, out: str, outsz: sizet): p = _mlir_read_value(p, cb, 512) if cvb[0] == '%': MlirFail("MLIR 渲染: switch case 值必须是常量(SSA 值不支持)") + cbr: str = _MlirBlkRef(cb) if ncase > 0: _append_cstr(cases, 1024, ", ") _append_cstr(cases, 1024, cvb) _append_cstr(cases, 1024, ": ^") - _append_cstr(cases, 1024, _MlirBlkRef(cb)) + _append_cstr(cases, 1024, cbr) + # 目标块带 phi ⇒ 追加块实参(与 br/cond_br 同一设施) + _MlirBlockOperands(cases, 1024, _MlirFindBlock(_MLIR_Func, cbr), _MLIR_SrcBlk) ncase += 1 p = _mlir_ws(p) if p[0] == ',': p = p + 1 dr: str = _MlirBlkRef(db) - if _MlirBlockHasPhi(_MlirFindBlock(_MLIR_Func, dr)) != 0: - MlirFail("MLIR 渲染: switch 目标块带 phi(携带块实参)v1 不支持") _MlirEmitRes(out, outsz, None) _append_cstr(out, outsz, "llvm.switch ") - _MlirOperand(vb, tyb, out, outsz) + _append_cstr(out, outsz, vop) + _append_cstr(out, outsz, " : ") + _append_cstr(out, outsz, vmt) _append_cstr(out, outsz, ", ^") _append_cstr(out, outsz, dr) + _MlirBlockOperands(out, outsz, _MlirFindBlock(_MLIR_Func, dr), _MLIR_SrcBlk) _append_cstr(out, outsz, " [") _append_cstr(out, outsz, cases) _append_cstr(out, outsz, "]\n") @@ -2513,18 +2802,227 @@ def _MlirAtomicRmw(p: str, resname: str, out: str, outsz: sizet): _append_cstr(out, outsz, "\n") +def _MlirCmpXchg(p: str, resname: str, resname2: str, + out: str, outsz: sizet): + """cmpxchg: MLIR 单结果 struct{T, i1} + 两条 extractvalue 导出旧值/成功位 + + 中间文本(LLVM 词法,两态:builder 单结果 / 双结果前缀) + %N = cmpxchg , , + %a, %b = cmpxchg … + MLIR 形态(mlir-opt 22.1.8 实测 exit 0;cmpxchg 在 MLIR 里是**单结果**, + 结果为 struct{T, i1},不是两个 SSA 值): + %N = llvm.cmpxchg %p, %c, %n acq_rel monotonic : !llvm.ptr, i32 + %_N_v = llvm.extractvalue %N[0] : !llvm.struct<(i32, i1)> ; 旧值(比较值) + %_N_ok = llvm.extractvalue %N[1] : !llvm.struct<(i32, i1)> ; i1 成功位 + 两条 extractvalue 的结果名由 struct 结果名**确定性派生** + (_MlirCxResName,如 `%0` → `%_0_v`/`%_0_ok`)。 + 两个结果名绑定:`%N`(单结果)/ `%a`(双结果首名)→ 旧值 `[0]`; + 双结果次名 `%b` → 成功位 `[1]`。绑定走 bitcast no-op 同一别名设施 + (_MlirAliasAdd),后续引用由 _MlirResolve 自动改写为 extractvalue 结果名。 + 这样 builder 端 `build_cmpxchg` 返回的「T 类型 Value」语义(旧值)得以保持。 + """ + ptyb: str = _SC_Ty + p = _mlir_read_type(p, ptyb, 512) + p = _mlir_ws(p) + pb: str = _SC_V1 + p = _mlir_read_value(p, pb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + ctyb: str = _SC_Ty2 + p = _mlir_read_type(p, ctyb, 512) + p = _mlir_ws(p) + cb: str = _SC_V2 + p = _mlir_read_value(p, cb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + ntyb: str = _SC_B1 + p = _mlir_read_type(p, ntyb, 512) + p = _mlir_ws(p) + nb: str = _SC_B2 + p = _mlir_read_value(p, nb, 512) + p = _mlir_ws(p) + succ: str = _SC_P1 + p = _mlir_read_value(p, succ, 512) + p = _mlir_ws(p) + fail: str = _SC_P2 + _mlir_read_value(p, fail, 512) + if resname is None: + MlirFail("MLIR 渲染: cmpxchg 缺少结果名") + # 两条 extractvalue 的结果名由 struct 结果名确定性派生(两遍必然同名) + onm: str = _MlirCxResName(resname, "_v") + snm: str = _MlirCxResName(resname, "_ok") + _MlirAliasAdd(resname, onm) + if resname2 is not None: + _MlirAliasAdd(resname2, snm) + # MLIR 类型文本(用非 _SC_Mt 缓冲:_MlirOperand 对字面量会占用 _SC_Mt) + pmt: str = _SC_Tmp2 + pmt[0] = '\0' + MlirTypeText(pmt, 1024, ptyb, _MLIR_Pool) + vmt: str = _SC_IdxV + vmt[0] = '\0' + MlirTypeText(vmt, 1024, ctyb, _MLIR_Pool) + stmt: str = _SC_IdxT + stmt[0] = '\0' + _append_cstr(stmt, 1024, "!llvm.struct<(") + _append_cstr(stmt, 1024, vmt) + _append_cstr(stmt, 1024, ", i1)>") + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "llvm.cmpxchg ") + _MlirOperand(pb, ptyb, out, outsz) + _append_cstr(out, outsz, ", ") + _MlirOperand(cb, ctyb, out, outsz) + _append_cstr(out, outsz, ", ") + _MlirOperand(nb, ntyb, out, outsz) + _append_cstr(out, outsz, " ") + _append_cstr(out, outsz, succ) + _append_cstr(out, outsz, " ") + _append_cstr(out, outsz, fail) + _append_cstr(out, outsz, " : ") + _append_cstr(out, outsz, pmt) + _append_cstr(out, outsz, ", ") + _append_cstr(out, outsz, vmt) + _append_cstr(out, outsz, "\n") + _MlirEmitRes(out, outsz, onm) + _append_cstr(out, outsz, "llvm.extractvalue ") + _append_cstr(out, outsz, resname) + _append_cstr(out, outsz, "[0] : ") + _append_cstr(out, outsz, stmt) + _append_cstr(out, outsz, "\n") + _MlirEmitRes(out, outsz, snm) + _append_cstr(out, outsz, "llvm.extractvalue ") + _append_cstr(out, outsz, resname) + _append_cstr(out, outsz, "[1] : ") + _append_cstr(out, outsz, stmt) + _append_cstr(out, outsz, "\n") + + +def _MlirVaArg(p: str, resname: str, out: str, outsz: sizet): + """va_arg: 用**通用 op 语法**(刻意选择,见下) + + 中间文本(LLVM 词法): %N = va_arg , + MLIR 文本: %N = "llvm.va_arg"(%ap) : (!llvm.ptr) -> i32 + + 为什么必须用 generic 形态(mlir-opt 22.1.8 实测):自定义汇编语法 + `llvm.va_arg %ap, i32` 要求显式给出 va_list 的类型(LLVM 里是 `ptr`), + 而不透明指针下该类型无法表达 —— mlir-opt 报 "expected builtin.function", + 故改用通用 op 语法(generic form),只用操作数类型列表描述。 + 降级验证(mlir-translate --mlir-to-llvmir)实测得到 + %2 = va_arg ptr %0, i32 + 与 LLVM 的 va_arg 完全同形。 + """ + vtyb: str = _SC_Ty + p = _mlir_read_type(p, vtyb, 512) + p = _mlir_ws(p) + vb: str = _SC_V1 + p = _mlir_read_value(p, vb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + atyb: str = _SC_Ty2 + _mlir_read_type(p, atyb, 512) + if resname is None: + MlirFail("MLIR 渲染: va_arg 缺少结果名") + # 操作数类型 / 结果类型(_MlirOperand 只用 _SC_Mt,故此处先取独立缓冲) + omt: str = _SC_Tmp2 + omt[0] = '\0' + MlirTypeText(omt, 1024, vtyb, _MLIR_Pool) + rmt: str = _SC_IdxV + rmt[0] = '\0' + MlirTypeText(rmt, 1024, atyb, _MLIR_Pool) + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "\"llvm.va_arg\"(") + _MlirOperand(vb, vtyb, out, outsz) + _append_cstr(out, outsz, ") : (") + _append_cstr(out, outsz, omt) + _append_cstr(out, outsz, ") -> ") + _append_cstr(out, outsz, rmt) + _append_cstr(out, outsz, "\n") + + +def _MlirExtractValue(p: str, resname: str, out: str, outsz: sizet): + """extractvalue: llvm.extractvalue %v[i0, i1] : <聚合类型> + + 中间文本是 LLVM 语法(**聚合类型文本在值之前,索引在逗号之后**): + %r = extractvalue {i32, i32} %v, 0 + %r = extractvalue {i32, {i32, i32}} %v, 1, 0 ; 多级索引 + MLIR 形态(mlir-opt 22.1.8 实测 exit 0): + %r = llvm.extractvalue %v[0] : !llvm.struct<(i32, i32)> + %r = llvm.extractvalue %v[1, 0] : !llvm.struct<(i32, !llvm.struct<(i32, i32)>)> + 索引列表在 MLIR 里写在 `[]` 内、以 `, ` 分隔(与 LLVM 的 `<聚合>, i0, i1` + 词法不同,必须转写);聚合类型文本经 MlirTypeText 转成 MLIR 类型 + (匿名 `{…}` → !llvm.struct<(...)>、具名 `%"name"` → 内联具名体、 + 数组 `[N x T]` → !llvm.array,三者均已实测 exit 0)。 + 真实来源:TPV 的**多结果内联汇编**(`%asm_result = call {i32, i32} asm …`) + 之后逐字段抽取(`%asm_out_0 = extractvalue {i32, i32} %asm_result, 0`, + 语料实测均为单索引);多级索引同样经 mlir-opt 实测合法,故一并支持。 + 索引必须是常量字面量(LLVM 词法如此)——SSA 索引 ⇒ 响亮报错。 + """ + atyb: str = _SC_Ty + p = _mlir_read_type(p, atyb, 512) + p = _mlir_ws(p) + vb: str = _SC_V1 + p = _mlir_read_value(p, vb, 512) + idxs: str = _SC_IdxV + idxs[0] = '\0' + nidx: int = 0 + g: int = 0 + # 索引列表:LLVM 词法里逗号分隔(`<聚合>, i0, i1`),逐个读出后转写; + # 循环上限与文件内其它解析循环同款(有界,超出即响亮报错)。 + while g < 64: + p = _mlir_ws(p) + if p[0] != ',': + break + g += 1 + if g > 32: + MlirFail("MLIR 渲染: extractvalue 索引过多") + p = _mlir_ws(p + 1) + ib: str = _SC_IdxT + p = _mlir_read_value(p, ib, 512) + if ib[0] == '\0': + MlirFail("MLIR 渲染: extractvalue 缺少索引") + if ib[0] == '%': + MlirFail("MLIR 渲染: extractvalue 索引必须是常量(SSA 索引不支持)") + if nidx > 0: + _append_cstr(idxs, 1024, ", ") + _append_cstr(idxs, 1024, ib) + nidx += 1 + if nidx == 0: + MlirFail("MLIR 渲染: extractvalue 缺少索引") + if p[0] != '\0': + MlirFail("MLIR 渲染: extractvalue 索引之后出现多余记号") + # MLIR 聚合类型文本先落独立缓冲(_MlirOperand 只占 _SC_Mt,不冲突) + amt: str = _SC_Tmp2 + amt[0] = '\0' + MlirTypeText(amt, 1024, atyb, _MLIR_Pool) + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "llvm.extractvalue ") + _MlirOperand(vb, atyb, out, outsz) + _append_cstr(out, outsz, "[") + _append_cstr(out, outsz, idxs) + _append_cstr(out, outsz, "] : ") + _append_cstr(out, outsz, amt) + _append_cstr(out, outsz, "\n") + + # ============================================================ # 指令行 → MLIR 分派 # ============================================================ def _MlirRenderLine(text: str, out: str, outsz: sizet): """解析一条中间指令文本并渲染为 MLIR(out=None 时仅登记/校验)""" - global _MLIR_Pass + global _MLIR_Pass, _MLIR_CurLine if text is None: return + # 入口寄存"当前行":类型文本转换失败时随报文打印(两次 pass 都是同一行, + # pass1 失败也能定位到具体中间文本)。text 指向 Line.Buf,生命周期覆盖整次渲染。 + _MLIR_CurLine = text + MlirSetContextLine(_MLIR_CurLine) p: str = _mlir_ws(text) if p[0] == '\0': return resname: str = None + resname2: str = None if p[0] == '%': tb: str = _SC_Tb q: str = _mlir_read_value(p, tb, 512) @@ -2532,10 +3030,24 @@ def _MlirRenderLine(text: str, out: str, outsz: sizet): if q2[0] == '=': resname = _mlir_dup(tb) p = _mlir_ws(q2 + 1) + elif q2[0] == ',': + # 双结果前缀 `%a, %b = `(仅 cmpxchg):MLIR 的 cmpxchg 是**单结果** + # struct{T, i1},两个名字分别绑定到两条 extractvalue(见 _MlirCmpXchg)。 + resname = _mlir_dup(tb) + q3: str = _mlir_ws(q2 + 1) + tb2: str = _SC_V3 + q3 = _mlir_read_value(q3, tb2, 512) + q4: str = _mlir_ws(q3) + if q4[0] != '=': + MlirFail("MLIR 渲染: 双结果前缀(%a, %b =)缺少 '='") + resname2 = _mlir_dup(tb2) + p = _mlir_ws(q4 + 1) ob: str = _SC_Op p = _mlir_read_value(p, ob, 64) if ob[0] == '\0': MlirFail("MLIR 渲染: 空指令") + if resname2 is not None and string.strcmp(ob, "cmpxchg") != 0: + MlirFail("MLIR 渲染: 双结果前缀(%a, %b =)只支持 cmpxchg") if _mlir_in_set(ob, "|add|sub|mul|sdiv|udiv|srem|urem|and|or|xor|shl|lshr|ashr|") != 0: _MlirBinop(p, resname, ob, out, outsz) return @@ -2597,11 +3109,19 @@ def _MlirRenderLine(text: str, out: str, outsz: sizet): _MlirAtomicRmw(p, resname, out, outsz) return if string.strcmp(ob, "cmpxchg") == 0: - MlirFail("MLIR 渲染: cmpxchg 的 MLIR 形态未固化,v1 不支持") + _MlirCmpXchg(p, resname, resname2, out, outsz) + return if string.strcmp(ob, "va_arg") == 0: - MlirFail("MLIR 渲染: va_arg v1 不支持") - # 注:内联汇编在中间文本里 opcode 是 "call"(call asm ...), - # 由 _MlirCall 内部识别并响亮报错。 + _MlirVaArg(p, resname, out, outsz) + return + if string.strcmp(ob, "extractvalue") == 0: + # 真实来源:TPV 的多结果内联汇编结果抽取(见 _MlirExtractValue)。 + # 注:`insertvalue` 等其它未支持 opcode 在语料中 0 命中, + # 落到下面的兜底响亮报错(保持"未知指令 opcode"即可)。 + _MlirExtractValue(p, resname, out, outsz) + return + # 注:内联汇编在中间文本里 opcode 是 "call"(call asm …), + # 由 _MlirCall 内部识别并转交 _MlirAsm 渲染(llvm.inline_asm)。 MlirFail("MLIR 渲染: 未知指令 opcode") @@ -2643,10 +3163,14 @@ def _MlirFuncSig(buf: str, size: sizet, _append_cstr(buf, size, ty_buf) cur = cur.Next first = 0 - _append_cstr(buf, size, ")") - # 变参函数:MLIR 形态未固化 → 响亮报错(见 README v1 不支持清单) + # 变参函数:参数表尾追加 `...`(mlir-opt 22.1.8 实测 exit 0: + # `llvm.func @f(%p: !llvm.ptr, ...) -> i32` / 无固定参数时 `llvm.func @f(...) -> i32`) if func.IsVarArg == 1: - MlirFail("MLIR 渲染: 变参函数(...)的 llvm.func 形态未固化,v1 不支持") + if params is not None and params.Head is not None: + _append_cstr(buf, size, ", ...") + else: + _append_cstr(buf, size, "...") + _append_cstr(buf, size, ")") # 返回类型(Void 省略) is_void: int = 0 match func.RetTy: @@ -2669,7 +3193,7 @@ def FunctionPrint(buf: str, size: sizet, 注意:与 llvmlite 版一致,本函数为追加写(供模块打印连续拼接多个函数), 调用方若要单函数文本需自行先置 buf[0]='\\0'。 """ - global _MLIR_Pass, _MLIR_Func, _MLIR_SrcBlk + global _MLIR_Pass, _MLIR_Func, _MLIR_SrcBlk, _MLIR_CurLine if func is None or buf is None or size == 0: return @@ -2683,6 +3207,10 @@ def FunctionPrint(buf: str, size: sizet, _MlirTablesInit(pool) _MLIR_Func = func + # 清空上一函数的行上下文:签名/declare 等非"行驱动"位置若报类型错, + # 不该带上一个已经无关的旧行,避免诊断误导(行上下文只在 _MlirRenderLine 赋值)。 + _MLIR_CurLine = None + MlirSetContextLine(None) if func.IsDeclared == 1: # declare(无函数体) diff --git a/mlirlite/__module.vp b/mlirlite/__module.vp index e406b00..0cde7b8 100644 --- a/mlirlite/__module.vp +++ b/mlirlite/__module.vp @@ -8,11 +8,13 @@ import memhub from linkedlist import GSList, GSListNode from .__types import ( LLVMType, TypePrint, Array, PrintStructDefinition, get_struct_name, Struct, - ParamNode, MlirFail, MlirTypeText, + ParamNode, MlirFail, MlirTypeText, MlirSetContextLine, + MlirNamedTypesBegin, MlirNamedTypesClear, MlirNamedTypeRegister, ) from .__function import ( Function, FunctionPrint, _ll_name_needs_quote, Param, param_get_next, param_get_ty, _mlir_read_type, _mlir_read_value, _mlir_ws, + _MlirSymRefAppend, ) @@ -43,7 +45,7 @@ class GlobalVariable(GSListNode[GlobalVariable]): @t.NoVTable class NamedTypeNode(GSListNode[NamedTypeNode]): Ty: rawptr[LLVMType] # 命名结构体类型指针 - IsPacked: int # 1=packed 结构体(输出 <{...}>,无字段填充) + IsPacked: int # 1=packed 结构体(MLIR: !llvm.struct<"name", packed (…)>,无字段填充) # ============================================================ @@ -218,6 +220,16 @@ class LLVMModule: """ if self is None or buf is None or size == 0: return + # 清空行上下文:全局段不是"行驱动"渲染,报类型错时不该带上一函数残留的旧行 + # (行上下文由 __function.vp 的 _MlirRenderLine 赋值,见 __types.vp 的 _MlirCtxLine)。 + MlirSetContextLine(None) + + # 发布本模块的命名结构体登记表:中间文本里"只有名字"的类型(`%"name"`) + # 经它回查完整定义、展开为使用处的内联具名形态(!llvm.struct<"name", (…)>)。 + # 表随本次打印重建,打印结束(本函数末尾)即清空 —— 表内类型指针只在本次 + # 打印期间有效,清空后不再解引用(避免 pool reset 后悬空)。 + _MlirPublishNamedTypes(self, pool) + _append_cstr(buf, size, "module {\n") # 模式说明注释(MLIR 无 stub/text 二分) @@ -280,7 +292,56 @@ class LLVMModule: _MlirAppendIndented(buf, size, tmp, 2, pool) func = func.Next - _append_cstr(buf, size, "}\n") + _append_cstr(buf, size, "}\n") + # 打印结束:清空命名结构体登记表(表内类型指针此后不再解引用) + MlirNamedTypesClear() + + +# ============================================================ +# _MlirPublishNamedTypes - 把模块的命名结构体链表发布给 __types.vp 的登记表 +# +# 用途:类型文本里"只有名字"的形态(中间文本 `%"sha1.Cls"`)在 MLIR 侧必须 +# 展开成使用处的内联具名体(MLIR 无定义行、且不允许无体前向声明),故需要一个 +# 名字 → 完整定义的查表设施。NamedTypeHead 由 module_add_named_type 维护 +# (同名至多一项,opaque 会被后到的完整定义就地替换),这里在模块打印开始时 +# 逐项发布;opaque 项也发布,使引用它时报"仍为 opaque"(而不是"未登记")。 +# 同时把节点的 packed 标记(NamedTypeNode.IsPacked,设置点唯一: +# module_set_named_type_packed)与类型一并登记,供渲染侧按名字回查。 +# ============================================================ +def _MlirPublishNamedTypes(mod: rawptr[LLVMModule], pool: memhub.MemBuddy*): + """按 NamedTypeHead 重建命名结构体登记表(打印期查表用,含 packed 标记) + + 先数一遍真实链表长度(不依赖 NamedTypeCount 字段),再按该数量精确分配槽位、 + 逐项登记 —— TPV 真实模块的具名类型可达数千个(见 README §5.9),故不做固定 + 容量假设;无具名类型时不分配。 + """ + if mod is None or pool is None: + return + cnt: sizet = 0 + nt: rawptr[NamedTypeNode] = mod.NamedTypeHead + nt_steps: sizet = 0 + while nt is not None: + nt_steps += 1 + if nt_steps > IR_DUMP_CHAIN_LIMIT: + stdio.printf("[IR-DUMP] 链表成环: module named types count(NamedTypeCount=%zu,计数终止)\n", + mod.NamedTypeCount) + stdlib.exit(1) + cnt += 1 + nt = nt.Next + MlirNamedTypesBegin(pool, cnt) + nt = mod.NamedTypeHead + nt_steps = 0 + while nt is not None: + nt_steps += 1 + if nt_steps > IR_DUMP_CHAIN_LIMIT: + stdio.printf("[IR-DUMP] 链表成环: module named types publish(NamedTypeCount=%zu,发布终止)\n", + mod.NamedTypeCount) + stdlib.exit(1) + if nt.Ty is not None: + # packed 标记只挂在 NamedTypeNode 上(LLVMType.Struct 无该字段), + # 设置点唯一:module_set_named_type_packed。发布时与类型一并登记。 + MlirNamedTypeRegister(nt.Ty, nt.IsPacked) + nt = nt.Next # ============================================================ @@ -401,11 +462,14 @@ def module_add_named_type(mod: rawptr[LLVMModule], pool: memhub.MemBuddy*, def module_set_named_type_packed(mod: rawptr[LLVMModule], pool: memhub.MemBuddy*, ty: rawptr[LLVMType], is_packed: int): - """按名称设置命名结构体的 packed 标记(输出 %"name" = type <{...}>)。 + """按名称设置命名结构体的 packed 标记(MLIR 输出 !llvm.struct<"name", packed (…)>)。 编译器解析 @c.Attribute(t.attr.packed) 后调用,使 llc 按 packed 布局 - (无字段填充)计算字段偏移/GEP。ty 为命名 Struct 类型(按 get_struct_name - 定位已注册节点);未找到则静默跳过(opaque/跨模块 stub 场景无布局需求)。 + (无字段填充)计算字段偏移/GEP。MLIR 侧由打印期登记表携带该标记 + (_MlirPublishNamedTypes 读 NamedTypeNode.IsPacked),使用处内联形态加 + `packed ` 关键字。ty 为命名 Struct 类型(按 get_struct_name 定位已注册节点); + 未找到则静默跳过(opaque/跨模块 stub 场景无布局需求)。匿名结构体 + (Name 为 None)在此直接返回 —— packed 标记无法也没有位置挂在匿名结构体上。 """ if mod is None or ty is None: return @@ -568,8 +632,13 @@ def LLVMModulePrint(buf: str, size: sizet, # llvm.mlir.global @counter(0 : i32) : i32 # llvm.mlir.global external @x : i32 # linkage/constant/unnamed_addr/section 映射为对应关键字/属性。 -# v1 支持的初值文本形态: 整数字面量、浮点、null、zeroinitializer、cstring、 -# 整型数组;其它形态(尤其 { … } 结构体/vtable 初始化串)**响亮报错**。 +# 初值有两条路径(判定见 _MlirGlobalInitNeedsRegion): +# * 属性路径:整数字面量、浮点、cstring、字符串字面量、纯标量数组; +# * region 路径:根级裸 null/zeroinitializer(属性形态 `(zeroinitializer)` +# 被 mlir-opt 拒绝)、结构体/vtable、含符号引用的聚合、指针型初值 → +# 空值括号 + `: 类型 {` 内的 initializer region +# (叶子 op + undef + insertvalue 链 + return)。 +# 两条路径都覆盖不到的形态一律 MlirFail **响亮报错**,绝不静默输出错误 MLIR。 # ============================================================ def _MlirGlobalLine(buf: str, size: sizet, pool: memhub.MemBuddy*, gv: rawptr[GlobalVariable], @@ -606,25 +675,44 @@ def _MlirGlobalLine(buf: str, size: sizet, _append_cstr(buf, size, "\"") else: _append_cstr(buf, size, gv.Name) - # 初值 - if stub == 0 and gv.Initializer is not None: - _append_cstr(buf, size, "(") - _MlirGlobalInit(buf, size, pool, gv) - _append_cstr(buf, size, ")") - # 属性(section / alignment 按属性字典输出;形态未经 mlir-opt 验证,见 README) - if stub == 0 and (gv.Section is not None or gv.Align > 0): - _append_cstr(buf, size, " {") - if gv.Section is not None: - _append_cstr(buf, size, "section = \"") - _append_cstr(buf, size, gv.Section) - _append_cstr(buf, size, "\"") - if gv.Align > 0: - _append_cstr(buf, size, ", ") - if gv.Align > 0: - ab: str = pool.alloc(48) - if ab is not None: - viperlib.snprintf(ab, 48, "alignment = %d : i64", gv.Align) - _append_cstr(buf, size, ab) + # 初值:聚合/含符号引用/根级裸零值形态改用 initializer region + # (见 _MlirGlobalInitNeedsRegion) + region: int = 0 + if stub == 0 and gv.Initializer is not None: + region = _MlirGlobalInitNeedsRegion(gv.Initializer) + if region == 0: + _append_cstr(buf, size, "(") + _MlirGlobalInit(buf, size, pool, gv) + _append_cstr(buf, size, ")") + else: + # region 形态:值括号为空(初值由 region 内的 insertvalue 链构造) + _append_cstr(buf, size, "()") + else: + # 无初值(stub=1 的 external 声明 / 无初值全局):**值括号必须保留** + # ⚠ mlir-opt 22.1.8 实测:`llvm.mlir.global external @x : i32` 报 + # "expected '('";`llvm.mlir.global external @x() : i32` exit 0。 + _append_cstr(buf, size, "()") + # 属性字典(section / alignment):**必须在类型之前**(值之后)输出。 + # ⚠ mlir-opt 22.1.8 实测确立(违反即解析失败): + # * 正确 = `@"s"("ab\00") {section = "data", alignment = 16 : i64} : !llvm.array<3 x i8>` + # (exit 0); + # * 错误 = 字典放在**类型之后**(`… : !llvm.array<3 x i8> {section = "data"}` + # 被当成新 op,报 "Dialect `' not found for custom op 'section'"); + # * `unnamed_addr` 是**关键字**(见上,在名字之前输出),**不能**放进字典 + # (放进字典报 "attribute 'unnamed_addr' occurs more than once")。 + if stub == 0 and (gv.Section is not None or gv.Align > 0): + _append_cstr(buf, size, " {") + if gv.Section is not None: + _append_cstr(buf, size, "section = \"") + _append_cstr(buf, size, gv.Section) + _append_cstr(buf, size, "\"") + if gv.Align > 0: + _append_cstr(buf, size, ", ") + if gv.Align > 0: + ab: str = pool.alloc(48) + if ab is not None: + viperlib.snprintf(ab, 48, "alignment = %d : i64", gv.Align) + _append_cstr(buf, size, ab) _append_cstr(buf, size, "}") # 类型 _append_cstr(buf, size, " : ") @@ -633,6 +721,11 @@ def _MlirGlobalLine(buf: str, size: sizet, ty_buf[0] = '\0' TypePrint(ty_buf, 512, gv.Ty, pool) _append_cstr(buf, size, ty_buf) + # initializer region(顶层聚合/指针初值:叶子 op + undef + insertvalue 链 + return) + if region != 0: + _append_cstr(buf, size, " {\n") + _MlirGlobalInitRegion(buf, size, pool, gv) + _append_cstr(buf, size, "}") _append_cstr(buf, size, "\n") @@ -660,10 +753,12 @@ def _MlirGlobalInit(buf: str, size: sizet, _MlirArrayDenseAt(buf, size, x, pool) return if string.strcmp(x, "null") == 0 or string.strcmp(x, "zeroinitializer") == 0: + # 根级裸零值现由 region 路径处理(_MlirGlobalInitNeedsRegion 判 1), + # 属性形态 `(zeroinitializer)` 已被 mlir-opt 拒绝,此处仅为兜底保留。 _append_cstr(buf, size, "zeroinitializer") return if x[0] == '@': - # 符号引用初值 + # 符号引用初值(顶层 @name 现由 region 路径处理,这里是属性路径的兜底) _append_cstr(buf, size, x) return c0: int = x[0] @@ -677,8 +772,10 @@ def _MlirGlobalInit(buf: str, size: sizet, TypePrint(ty_buf, 512, gv.Ty, pool) _append_cstr(buf, size, ty_buf) return - # { … } 结构体/vtable 初始化串等:响亮报错,绝不静默输出错误 MLIR - MlirFail("MLIR 渲染: 全局初值形态 v1 不支持(结构体/vtable 初始化串需改用结构化初始化)") + # 属性路径未覆盖且未走 region 的残留形态:响亮报错,绝不静默输出错误 MLIR + # (结构体/vtable、含符号引用的聚合、指针型初值已由 _MlirGlobalInitRegion + # 在 region 路径渲染,不再落到这里) + MlirFail("MLIR 渲染: 全局初值形态 v1 不支持(属性路径与 initializer region 均未覆盖)") def _MlirCStringToMlir(buf: str, size: sizet, src: str, pool: memhub.MemBuddy*): @@ -748,6 +845,526 @@ def _MlirArrayDenseAt(buf: str, size: sizet, src: str, return p +# ============================================================ +# 全局初值的 initializer region 渲染(结构体/vtable 等聚合与指针初值) +# +# 实测 mlir-opt 22.1.8 通过形态(internal 与 internal constant 均可): +# llvm.mlir.global constant @vt() : !llvm.struct<(!llvm.ptr, !llvm.ptr)> { +# %v0 = llvm.mlir.addressof @"sha1.m" : !llvm.ptr +# %undef = llvm.mlir.undef : !llvm.struct<(!llvm.ptr, !llvm.ptr)> +# %s1 = llvm.insertvalue %v0, %undef[0] : !llvm.struct<(!llvm.ptr, !llvm.ptr)> +# %v1 = llvm.mlir.zero : !llvm.ptr +# %s2 = llvm.insertvalue %v1, %s1[1] : !llvm.struct<(!llvm.ptr, !llvm.ptr)> +# llvm.return %s2 : !llvm.struct<(!llvm.ptr, !llvm.ptr)> +# } +# +# 实现要点: +# * 叶子 op(addressof / zero / constant)**先集中发射**,随后 undef 与 +# insertvalue 链;叶子 op 与插入 op 分写两个缓冲,最后按此顺序拼装; +# * 每个 insertvalue 的**类型后缀始终是顶层聚合类型**,下标是"初值树 × +# 类型树"并行遍历得到的下标路径(嵌套聚合用多下标,如 [0, 1]); +# * 元素 MLIR 类型来自**全局类型树**(Struct 字段链 / Array 元素类型)经 +# TypePrint 取文本;初值文本里的类型记号只用于推进游标,不作类型依据; +# * bitcast/getelementptr 常量表达式整体视为"符号引用"——不透明指针下 +# bitcast 恒等、GEP 取首元素地址等价于 addressof,故不还原 bitcast; +# * 符号名一律经 _MlirSymRefAppend(先剥 LLVM 口径引号,再按 MLIR 口径重加); +# * 不支持的叶子形态一律 MlirFail(带原始文本),绝不静默输出错误 MLIR。 +# ============================================================ +GV_REGION_BUF_SIZE: t.CDefine = 32768 + +# region 渲染期状态(单线程递归渲染,逐全局重置) +_MlirGvLeafBuf: str = None # 叶子 op 缓冲 +_MlirGvInsBuf: str = None # insertvalue 链缓冲 +_MlirGvTopType: str = None # 顶层聚合 MLIR 类型文本 +_MlirGvLast: str = None # 当前节点的 SSA 名 +_MlirGvPrev: str = None # insertvalue 链前一结果 SSA 名 +_MlirGvPool: memhub.MemBuddy* = None +_MlirGvLeafSeq: int = 0 +_MlirGvInsSeq: int = 0 + + +def _MlirGlobalInitNeedsRegion(x: str) -> int: + """判定全局初值是否改走 initializer region(1=region / 0=属性路径) + + 属性路径(已实测通过,不得回退)仅覆盖: c 字符串、字符串字面量、 + 标量字面量、**纯标量**数组。 + 其余形态一律 region: **根级裸 `null` / `zeroinitializer`**、结构体 `{…}`、 + 含符号引用(@)或结构体的数组、顶层裸符号引用 `@name`、指针/聚合类型前缀 + 开头的初值(如 `i8* null`、`i8* bitcast (…)`、`i8* getelementptr …`)。 + """ + if x is None or x[0] == '\0': + return 0 + if x[0] == 'c' and x[1] == '"': + return 0 + if x[0] == '"': + return 0 + if x[0] == '[': + return _MlirGvHasSymOrStruct(x) + if x[0] == '{': + return 1 + if string.strcmp(x, "null") == 0 or string.strcmp(x, "zeroinitializer") == 0: + # 根级裸零值改走 region:属性形态 `(zeroinitializer)` 被 mlir-opt 拒绝 + # (实测 22.1.8: "expected attribute value");region 输出 + # `llvm.mlir.zero : <顶层类型>` + 直接 `llvm.return`。 + return 1 + if x[0] == '@': + return 1 + c0: int = x[0] + if c0 == '-' or (c0 >= '0' and c0 <= '9'): + return 0 + return 1 + + +def _MlirGvHasSymOrStruct(x: str) -> int: + """扫描初值文本(跳过字符串字面量): 出现符号引用 '@' 或结构体 '{' 返回 1""" + p: str = x + while p[0] != '\0': + ch: int = p[0] + if ch == '"': + p = p + 1 + while p[0] != '\0' and p[0] != '"': + # LLVM 字符串用 \xx 十六进制转义;'\' 跳过其后一字节 + if p[0] == 0x5C and p[1] != '\0': + p = p + 1 + p = p + 1 + if p[0] == '"': + p = p + 1 + continue + if ch == '@' or ch == '{': + return 1 + p = p + 1 + return 0 + + +def _MlirGlobalInitRegion(buf: str, size: sizet, + pool: memhub.MemBuddy*, gv: rawptr[GlobalVariable]): + """渲染 initializer region 的体(2 空格缩进的行序列,不含最外层大括号) + + 每行都带 '\\n';调用方在末尾补 '}',模块层再统一缩进 2 空格。 + """ + global _MlirGvLeafBuf, _MlirGvInsBuf, _MlirGvTopType, _MlirGvLast + global _MlirGvPrev, _MlirGvPool, _MlirGvLeafSeq, _MlirGvInsSeq + x: str = gv.Initializer + if x is None: + return + topbuf: str = pool.alloc(1024) + leafbuf: str = pool.alloc(GV_REGION_BUF_SIZE) + insbuf: str = pool.alloc(GV_REGION_BUF_SIZE) + lastbuf: str = pool.alloc(32) + prevbuf: str = pool.alloc(32) + if topbuf is None or leafbuf is None or insbuf is None \ + or lastbuf is None or prevbuf is None: + MlirFail("MLIR 渲染: 全局 region 缓冲分配失败") + topbuf[0] = '\0' + leafbuf[0] = '\0' + insbuf[0] = '\0' + lastbuf[0] = '\0' + prevbuf[0] = '\0' + TypePrint(topbuf, 1024, gv.Ty, pool) + + _MlirGvLeafBuf = leafbuf + _MlirGvInsBuf = insbuf + _MlirGvTopType = topbuf + _MlirGvLast = lastbuf + _MlirGvPrev = prevbuf + _MlirGvPool = pool + _MlirGvLeafSeq = 0 + _MlirGvInsSeq = 0 + + is_agg: int = 0 + match gv.Ty: + case LLVMType.Struct(sf, sfc, sn): + is_agg = 1 + case LLVMType.Array(ae, ac): + is_agg = 1 + case _: + is_agg = 0 + if is_agg != 0: + # insertvalue 链的起点: 顶层 undef + string.strcpy(prevbuf, "%undef") + + _MlirGvWalk(x, gv.Ty, "", 0) + + # 叶子 op 集中发射,再 undef + insertvalue 链 + _append_cstr(buf, size, leafbuf) + if is_agg != 0: + _append_cstr(buf, size, " %undef = llvm.mlir.undef : ") + _append_cstr(buf, size, topbuf) + _append_cstr(buf, size, "\n") + _append_cstr(buf, size, insbuf) + _append_cstr(buf, size, " llvm.return ") + if lastbuf[0] != '\0': + _append_cstr(buf, size, lastbuf) + else: + _append_cstr(buf, size, "%undef") + _append_cstr(buf, size, " : ") + _append_cstr(buf, size, topbuf) + _append_cstr(buf, size, "\n") + + +def _MlirGvWalk(p: str, ty: rawptr[LLVMType], path: str, depth: int) -> str: + """并行遍历"初值树 × 类型树"的一个节点,返回新游标 + + 结果 SSA 名写入 _MlirGvLast(叶子=物化名;聚合=最后一个 insertvalue 名; + 空聚合= %undef)。下标路径 path 形如 "" / "0" / "0, 1"。 + """ + if depth > 64: + MlirFail("MLIR 渲染: 全局初值嵌套过深(> 64 层)") + if ty is None: + MlirFail("MLIR 渲染: 全局初值类型树缺失(无法确定元素 MLIR 类型)") + p = _mlir_ws(p) + if p[0] == '\0': + MlirFail("MLIR 渲染: 全局初值文本意外结束") + # 显式数组类型前缀(如字段写成 "[3 x i8] [i8 1, i8 2]")先整体读掉 + if p[0] == '[' and _MlirGvIsArrayType(p) != 0: + tb: str = _MlirGvPool.alloc(512) + if tb is None: + MlirFail("MLIR 渲染: 数组类型前缀缓冲分配失败") + p = _mlir_read_type(p, tb, 512) + p = _mlir_ws(p) + if p[0] == '{': + return _MlirGvWalkStruct(p, ty, path, depth) + if p[0] == '[': + return _MlirGvWalkArray(p, ty, path, depth) + return _MlirGvWalkLeaf(p, ty, path, depth) + + +def _MlirGvIsArrayType(p: str) -> int: + """判断游标处是否为 LLVM 数组类型前缀 "[N x TY]"(用于与数组字面量区分)""" + if p is None or p[0] != '[': + return 0 + q: str = p + 1 + nd: int = 0 + while q[0] >= '0' and q[0] <= '9': + q = q + 1 + nd += 1 + if nd == 0 or q[0] != ' ': + return 0 + q = q + 1 + if q[0] != 'x' or q[1] != ' ': + return 0 + return 1 + + +def _MlirGvPathChild(path: str, idx: int) -> str: + """由父下标路径与子序号拼出子下标路径(顶层 path="" → "0")""" + nb: str = _MlirGvPool.alloc(string.strlen(path) + 16) + if nb is None: + MlirFail("MLIR 渲染: 下标路径缓冲分配失败") + if path[0] == '\0': + viperlib.snprintf(nb, string.strlen(path) + 16, "%d", idx) + else: + viperlib.snprintf(nb, string.strlen(path) + 16, "%s, %d", path, idx) + return nb + + +def _MlirGvWalkStruct(p: str, ty: rawptr[LLVMType], path: str, depth: int) -> str: + """结构体初值 { 元素, 元素, … }: 字段类型取类型树的 Struct 字段链""" + fields: rawptr[ParamNode] = None + match ty: + case LLVMType.Struct(sf, sfc, sn): + fields = sf + case _: + MlirFail("MLIR 渲染: 初值为结构体,但全局类型不是结构体") + cur: rawptr[ParamNode] = fields + p = p + 1 + idx: int = 0 + done: int = 0 + guard: int = 0 + while done == 0: + guard += 1 + if guard > 4096: + MlirFail("MLIR 渲染: 结构体初值元素过多") + p = _mlir_ws(p) + if p[0] == '\0': + MlirFail("MLIR 渲染: 结构体初值缺少 '}'") + if p[0] == '}': + p = p + 1 + done = 1 + elif p[0] == ',': + p = p + 1 + else: + fty: rawptr[LLVMType] = None + if cur is not None: + fty = param_get_ty(cur) + p = _MlirGvWalk(p, fty, _MlirGvPathChild(path, idx), depth + 1) + if cur is not None: + cur = param_get_next(cur) + idx += 1 + if idx == 0: + string.strcpy(_MlirGvLast, "%undef") + return p + + +def _MlirGvWalkArray(p: str, ty: rawptr[LLVMType], path: str, depth: int) -> str: + """数组初值 [ 元素, 元素, … ]: 元素类型取类型树的 Array 元素类型""" + elem_ty: rawptr[LLVMType] = None + match ty: + case LLVMType.Array(ae, ac): + elem_ty = ae + case _: + MlirFail("MLIR 渲染: 初值为数组,但全局类型不是数组") + p = p + 1 + idx: int = 0 + done: int = 0 + guard: int = 0 + while done == 0: + guard += 1 + if guard > 65536: + MlirFail("MLIR 渲染: 数组初值元素过多") + p = _mlir_ws(p) + if p[0] == '\0': + MlirFail("MLIR 渲染: 数组初值缺少 ']'") + if p[0] == ']': + p = p + 1 + done = 1 + elif p[0] == ',': + p = p + 1 + else: + p = _MlirGvWalk(p, elem_ty, _MlirGvPathChild(path, idx), depth + 1) + idx += 1 + if idx == 0: + string.strcpy(_MlirGvLast, "%undef") + return p + + +def _MlirGvIsBareValue(p: str) -> int: + """游标处是否**直接**是值(无 LLVM/MLIR 类型前缀) + + TPV 的根初值可能是无类型前缀的裸形态(如字符串指针全局的 + `getelementptr inbounds ([N x i8], … @.str.x …)`、对象 storage 引用 `@name`); + 聚合元素则通常是 `<类型> <值>`。类型记号从不以 `@`/数字/`-`/`.` 开头, + 也不会有 `bitcast`/`getelementptr`/`null`/`zeroinitializer` 这些名字, + 故该判据无歧义。 + """ + if p is None or p[0] == '\0': + return 0 + if p[0] == '@': + return 1 + if string.strncmp(p, "bitcast", 7) == 0: + return 1 + if string.strncmp(p, "getelementptr", 13) == 0: + return 1 + if string.strncmp(p, "null", 4) == 0: + return 1 + if string.strncmp(p, "zeroinitializer", 15) == 0: + return 1 + c0: int = p[0] + if c0 == '-' or (c0 >= '0' and c0 <= '9') or c0 == '.': + return 1 + return 0 + + +def _MlirGvWalkLeaf(p: str, ty: rawptr[LLVMType], path: str, depth: int) -> str: + """叶子元素 ` <值>`: 值解析为 符号引用 / null / 标量字面量 + + 元素 MLIR 类型不取初值文本里的类型记号,而取类型树(ty)。 + 顶层叶子(depth == 0,整棵初值即该叶子,如指针型全局)不走 insertvalue + 链;只有位于聚合内部的叶子才需要插入。 + """ + p = _mlir_ws(p) + if _MlirGvIsBareValue(p) == 0: + # 元素自带类型记号(`i8* null` / `!llvm.ptr null` 等):只用于推进游标 + tb: str = _MlirGvPool.alloc(512) + if tb is None: + MlirFail("MLIR 渲染: 叶子类型记号缓冲分配失败") + p = _mlir_read_type(p, tb, 512) + p = _mlir_ws(p) + if p[0] == '\0': + MlirFail("MLIR 渲染: 叶子初值只有类型、缺少值") + if p[0] == '@': + # 裸符号引用(剥 LLVM 引号 → 经 _MlirSymRefAppend 按 MLIR 口径重加) + nb: str = _MlirGvPool.alloc(512) + if nb is None: + MlirFail("MLIR 渲染: 符号名缓冲分配失败") + p = _MlirGvReadSymName(p, nb, 512) + _MlirGvEmitAddressOf(nb) + elif string.strncmp(p, "bitcast", 7) == 0 \ + or string.strncmp(p, "getelementptr", 13) == 0: + # 常量表达式整体视为符号引用(bitcast 不透明指针下恒等;GEP 取首元素地址) + nb2: str = _MlirGvPool.alloc(512) + if nb2 is None: + MlirFail("MLIR 渲染: 常量表达式符号名缓冲分配失败") + p = _MlirGvScanConstExprSym(p, nb2, 512) + _MlirGvEmitAddressOf(nb2) + elif string.strncmp(p, "null", 4) == 0: + _MlirGvEmitZero(ty) + p = p + 4 + elif string.strncmp(p, "zeroinitializer", 15) == 0: + _MlirGvEmitZero(ty) + p = p + 15 + else: + c0: int = p[0] + if c0 == '-' or (c0 >= '0' and c0 <= '9') or c0 == '.': + vb: str = _MlirGvPool.alloc(512) + if vb is None: + MlirFail("MLIR 渲染: 标量初值缓冲分配失败") + p = _mlir_read_value(p, vb, 512) + _MlirGvEmitConst(vb, ty) + else: + # 不支持的叶子形态: 带原始文本响亮报错,绝不静默产错 + emb: str = _MlirGvPool.alloc(string.strlen(p) + 128) + if emb is not None: + viperlib.snprintf(emb, string.strlen(p) + 128, + "MLIR 渲染: 全局初值叶子形态 v1 不支持:'%s'", p) + MlirFail(emb) + MlirFail("MLIR 渲染: 全局初值叶子形态 v1 不支持") + if depth > 0: + _MlirGvInsertLeaf(path) + return p + + +def _MlirGvScanConstExprSym(p: str, out: str, outsz: sizet) -> str: + """扫描 `bitcast (…)` / `getelementptr … (…)`: 在括号组内提取首个 '@' 符号名 + (剥引号写入 out),返回括号组之后的游标""" + q: str = p + while q[0] != '\0' and q[0] != '(': + q = q + 1 + if q[0] != '(': + MlirFail("MLIR 渲染: 常量表达式缺少 '('(bitcast/getelementptr)") + dep: int = 0 + at: str = None + guard: int = 0 + while q[0] != '\0': + guard += 1 + if guard > 4096: + MlirFail("MLIR 渲染: 常量表达式过长") + ch: int = q[0] + if ch == '(': + dep += 1 + elif ch == ')': + dep -= 1 + if dep == 0: + q = q + 1 + break + elif ch == '@' and at is None: + at = q + q = q + 1 + if at is None: + MlirFail("MLIR 渲染: 常量表达式中未找到符号引用 '@'") + _MlirGvReadSymName(at, out, outsz) + return q + + +def _MlirGvReadSymName(at: str, out: str, outsz: sizet) -> str: + """从 '@' 处读符号名,剥掉 LLVM 口径引号("…"),返回名字之后的游标""" + if at is None or at[0] != '@': + MlirFail("MLIR 渲染: 期望符号引用('@')") + q: str = at + 1 + n: sizet = 0 + if q[0] == '"': + q = q + 1 + while q[0] != '\0' and q[0] != '"': + if n + 1 < outsz: + out[n] = q[0] + n += 1 + q = q + 1 + if q[0] == '"': + q = q + 1 + else: + while q[0] != '\0' and _MlirGvSymNameEnd(q[0]) == 0: + if n + 1 < outsz: + out[n] = q[0] + n += 1 + q = q + 1 + out[n] = '\0' + if n == 0: + MlirFail("MLIR 渲染: 空符号名") + return q + + +def _MlirGvSymNameEnd(ch: int) -> int: + """裸符号名的终止字符(空白 / ',' / ')' / ']' / '}')""" + if ch == 0 or ch == 32 or ch == 9 or ch == 10 or ch == 13: + return 1 + if ch == ',' or ch == ')' or ch == ']' or ch == '}': + return 1 + return 0 + + +def _MlirGvNextLeafName() -> str: + """分配下一个叶子 SSA 名(%v0, %v1, …)""" + global _MlirGvLeafSeq + ln: str = _MlirGvPool.alloc(32) + if ln is None: + MlirFail("MLIR 渲染: 叶子 SSA 名缓冲分配失败") + viperlib.snprintf(ln, 32, "%%v%d", _MlirGvLeafSeq) + _MlirGvLeafSeq += 1 + return ln + + +def _MlirGvEmitAddressOf(name: str): + """发射叶子 op: %vN = llvm.mlir.addressof @"name" : !llvm.ptr""" + ln: str = _MlirGvNextLeafName() + _append_cstr(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, " ") + _append_cstr(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, ln) + _append_cstr(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, " = llvm.mlir.addressof ") + _MlirSymRefAppend(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, name) + _append_cstr(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, " : !llvm.ptr\n") + string.strcpy(_MlirGvLast, ln) + + +def _MlirGvEmitZero(ty: rawptr[LLVMType]): + """发射叶子 op: %vN = llvm.mlir.zero : <元素 MLIR 类型>""" + mt: str = _MlirGvPool.alloc(512) + if mt is None: + MlirFail("MLIR 渲染: 零初值类型缓冲分配失败") + mt[0] = '\0' + TypePrint(mt, 512, ty, _MlirGvPool) + ln: str = _MlirGvNextLeafName() + _append_cstr(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, " ") + _append_cstr(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, ln) + _append_cstr(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, " = llvm.mlir.zero : ") + _append_cstr(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, mt) + _append_cstr(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, "\n") + string.strcpy(_MlirGvLast, ln) + + +def _MlirGvEmitConst(val: str, ty: rawptr[LLVMType]): + """发射叶子 op: %vN = llvm.mlir.constant( : <元素 MLIR 类型>) : <同类型>""" + mt: str = _MlirGvPool.alloc(512) + if mt is None: + MlirFail("MLIR 渲染: 标量元素类型缓冲分配失败") + mt[0] = '\0' + TypePrint(mt, 512, ty, _MlirGvPool) + if mt[0] != 'i' and mt[0] != 'f': + MlirFail("MLIR 渲染: 全局初值标量元素类型既非整数也非浮点") + ln: str = _MlirGvNextLeafName() + _append_cstr(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, " ") + _append_cstr(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, ln) + _append_cstr(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, " = llvm.mlir.constant(") + _append_cstr(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, val) + _append_cstr(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, " : ") + _append_cstr(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, mt) + _append_cstr(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, ") : ") + _append_cstr(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, mt) + _append_cstr(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, "\n") + string.strcpy(_MlirGvLast, ln) + + +def _MlirGvInsertLeaf(path: str): + """发射 insertvalue(多下标路径;类型后缀始终是顶层聚合类型)""" + global _MlirGvInsSeq + _MlirGvInsSeq += 1 + sn: str = _MlirGvPool.alloc(32) + if sn is None: + MlirFail("MLIR 渲染: insertvalue SSA 名缓冲分配失败") + viperlib.snprintf(sn, 32, "%%s%d", _MlirGvInsSeq) + _append_cstr(_MlirGvInsBuf, GV_REGION_BUF_SIZE, " ") + _append_cstr(_MlirGvInsBuf, GV_REGION_BUF_SIZE, sn) + _append_cstr(_MlirGvInsBuf, GV_REGION_BUF_SIZE, " = llvm.insertvalue ") + _append_cstr(_MlirGvInsBuf, GV_REGION_BUF_SIZE, _MlirGvLast) + _append_cstr(_MlirGvInsBuf, GV_REGION_BUF_SIZE, ", ") + _append_cstr(_MlirGvInsBuf, GV_REGION_BUF_SIZE, _MlirGvPrev) + _append_cstr(_MlirGvInsBuf, GV_REGION_BUF_SIZE, "[") + _append_cstr(_MlirGvInsBuf, GV_REGION_BUF_SIZE, path) + _append_cstr(_MlirGvInsBuf, GV_REGION_BUF_SIZE, "] : ") + _append_cstr(_MlirGvInsBuf, GV_REGION_BUF_SIZE, _MlirGvTopType) + _append_cstr(_MlirGvInsBuf, GV_REGION_BUF_SIZE, "\n") + string.strcpy(_MlirGvPrev, sn) + string.strcpy(_MlirGvLast, sn) + + def _MlirAppendIndented(dst: str, dst_size: sizet, src: str, indent: int, pool: memhub.MemBuddy*): """把 src 的每一行前加 indent 个空格后追加到 dst(模块成员缩进)""" diff --git a/mlirlite/__types.vp b/mlirlite/__types.vp index f8332fc..30a5c61 100644 --- a/mlirlite/__types.vp +++ b/mlirlite/__types.vp @@ -74,11 +74,26 @@ class LLVMType(t.REnum): # ============================================================ # 内部分配辅助 # ============================================================ +def _TypeSizeGuard() -> sizet: + """LLVMType 的字节尺寸;为 0 时响亮失败。 + + __sizeof__() 在布局未就绪(结构体未注册 / opaque / 字段链未填充)时被 + 算成 0,此时 alloc(0) 只会静默返回 NULL,类型对象随之丢失且无从追查。 + """ + sz: sizet = LLVMType.__sizeof__() + if sz == 0: + stdio.printf("FATAL: mlirlite.LLVMType.__sizeof__() == 0" + "(布局未注册或 opaque),拒绝 alloc(0)\n") + stdlib.exit(1) + return sz + + def _new_type(pool: memhub.MemBuddy*) -> rawptr[LLVMType]: """从 mpool 分配一个零初始化的 LLVMType""" - ptr: rawptr[LLVMType] = pool.alloc(LLVMType.__sizeof__()) + sz_nt: sizet = _TypeSizeGuard() + ptr: rawptr[LLVMType] = pool.alloc(sz_nt) if ptr: - string.memset(ptr, 0, LLVMType.__sizeof__()) + string.memset(ptr, 0, sz_nt) return ptr @@ -380,13 +395,260 @@ def MlirFail(msg: str): # ============================================================ -# 说明:本库**不需要**命名结构体类型登记表。 +# 渲染上下文行:类型文本转换失败时随报文一并打印 # -# builders 未改,其内嵌类型一律经 _type_str → TypePrint 生成,因此中间指令 -# 文本里出现的结构体类型已经是完整的 MLIR 内联形态(!llvm.struct<(…)>), -# 不含 %"name" 这种"只有名字"的形态;指针也统一收敛为 !llvm.ptr。 -# 若出现 %"name"(手工构造的中间文本),MlirTypeText 会响亮报错。 +# MlirFail 是即时终止,拿不到调用栈。故由渲染侧(__function.vp 的 +# _MlirRenderLine 入口)把"当前正在渲染的中间指令文本行"寄存到本模块, +# 类型转换失败报文再把它追加出来,一次定位到出错的那一行中间文本。 +# 依赖方向保持单向:本模块只存/读,不反向 import __function.vp。 # ============================================================ +_MlirCtxLine: str = None + + +def MlirSetContextLine(line: str = None): + """寄存当前正在渲染的中间文本行(无行时传 None)""" + global _MlirCtxLine + _MlirCtxLine = line + + +# ============================================================ +# 命名结构体(%"name" = type {…})在 MLIR 的表示 +# +# MLIR(llvm 方言) **没有独立类型定义语法**(故 llvmlite 的 +# `%"name" = type {…}` 定义行在 MLIR 侧**整体丢弃**,见 __module.vp 的 +# PrintModule:NamedTypeHead 只作模型保留、不产生输出),也**不允许无体 +# 前向声明**(实测 mlir-opt 22.1.8:`!llvm.struct<"x">` 报 +# "struct without a body only allowed in a recursive struct")。 +# 故唯一可表达形态是**使用处的内联具名体**: +# +# !llvm.struct<"名字", (字段类型…)> +# +# 四种形态均由 mlir-opt / mlir-translate 22.1.8 实测 exit 0(逐字照此): +# 按值 : !llvm.struct<"a.b", (i32, !llvm.ptr)> +# 特殊字符 : !llvm.struct<"01c70390f01048df.Lock[0]", (i32)> +# (名字含数字/`.`/`[`/`]` 直接作字符串字面量,**不额外转义/加引号**) +# 嵌套具名 : !llvm.struct<"A.Lock", (!llvm.struct<"A.CRIT", (i32)>)> +# 空体 : !llvm.struct<"x.Empty", ()> +# 降级证据:mlir-translate --mlir-to-llvmir 会由使用处的内联形态**自动**生成 +# 定义行(实测得 `%aa11bb22cc33dd44.Cls = type { i32, ptr }` 等,见 README §2.10)。 +# 指向命名结构体的指针仍是 !llvm.ptr(不透明指针,既有路径不变)。 +# +# 边界(一律响亮报错,绝不静默输出错误类型): +# * fcount < 0(opaque 哨兵,跨模块前向声明)⇒ MLIR 无法表达无体结构体; +# * 名字含 `"` / `\` / 控制字符 ⇒ MLIR 字符串字面量非法字符; +# * 名字为空、或具名而字段链缺失(FCount 非 0 但 Fields == None)⇒ 模型不自洽。 +# ============================================================ + + +# ============================================================ +# 命名结构体登记表(模块打印期由 __module.vp 发布;供"只有名字"的类型文本回查) +# +# 中间文本里的 `%"name"`(只有名字的结构体引用:手工构造 / TPV 注入)本身不含 +# 字段,而 MLIR 又不允许无体前向声明 ⇒ 只有回查**模块里已登记的完整定义**才能 +# 展开为使用处的内联具名形态。__module.vp 的 PrintModule 在打印开始时按 +# NamedTypeHead 重建本表(module_add_named_type 已保证同名节点至多一项、且 +# opaque 会被后到的完整定义就地替换),打印结束时清空 —— 表内类型指针只在 +# 当次打印期间有效,清空后不再解引用(避免 pool reset 后悬空)。 +# 表按当次模块的具名类型**实际数量**精确分配(无具名类型则零开销)。 +# ============================================================ +_MlirNamedTys: sizet* = None # 槽位数组:每槽 16 字节 = { LLVMType*, IsPacked } +_MlirNamedCount: int = 0 # 已登记项数 +_MlirNamedCap: sizet = 0 # 槽位容量(= 当次模块的具名类型数) + +# 槽位布局: [0] = LLVMType*(命名结构体类型);[1] = IsPacked(1=packed)。 +# packed 标记只存在于 NamedTypeNode 上(LLVMType.Struct 变体没有该字段, +# 匿名结构体结构上无法携带),设置点唯一:module_set_named_type_packed。 +# 故登记时由 __module.vp 的 _MlirPublishNamedTypes 从节点读出后一并存入本表, +# 渲染侧(TypePrint 具名分支 / 文本→文本具名路径)只从本表取标记。 + + +def _MlirNamedSlot(idx: int) -> sizet*: + """登记表槽位地址(数组基址 + idx*16)""" + return (sizet*)(t.CPtr(uint64(_MlirNamedTys) + uint64(idx * 16))) + + +def MlirNamedTypesBegin(pool: memhub.MemBuddy*, capacity: sizet): + """按模块的具名结构体**实际数量**分配登记表并清空计数(模块打印开始时调用) + + 按需精确分配(TPV 真实模块里具名类型可达数千个,见 README §5.9), + capacity == 0 时不分配(无具名类型的模块零开销)。 + """ + global _MlirNamedTys, _MlirNamedCount, _MlirNamedCap + _MlirNamedTys = None + _MlirNamedCount = 0 + _MlirNamedCap = 0 + if capacity <= 0: + return + _MlirNamedTys = pool.alloc(capacity * 16) + if _MlirNamedTys is None: + MlirFail("MLIR 渲染: 命名结构体登记表分配失败") + _MlirNamedCap = capacity + + +def MlirNamedTypesClear(): + """清空登记表(模块打印结束时调用;表内类型指针此后不再解引用)""" + global _MlirNamedTys, _MlirNamedCount, _MlirNamedCap + _MlirNamedTys = None + _MlirNamedCount = 0 + _MlirNamedCap = 0 + + +def MlirNamedTypeRegister(ty: rawptr[LLVMType], is_packed: int): + """登记一个命名结构体类型及其 packed 标记(同名:完整定义覆盖 opaque 声明) + + 与 module_add_named_type 的去重口径一致:opaque 声明不覆盖已有的完整定义。 + 非命名结构体(Name 为 None)忽略;超出容量(理论不可达,容量取自链表长度) + 则停止登记 —— 查不到 ⇒ 响亮报错,不静默产出错误类型。 + is_packed 取自 NamedTypeNode.IsPacked(packed 标记只在具名节点上存在, + 匿名结构体结构上无法携带,见 README §2.10)。 + """ + global _MlirNamedCount + if ty is None or _MlirNamedTys is None: + return + name: str = get_struct_name(ty) + if name is None: + return + fcount: int = get_struct_fcount(ty) + i: int = 0 + while i < _MlirNamedCount: + slot: sizet* = _MlirNamedSlot(i) + ex: rawptr[LLVMType] = (rawptr[LLVMType])(slot[0]) + ex_name: str = get_struct_name(ex) + if ex_name is not None and string.strcmp(ex_name, name) == 0: + if fcount < 0 and get_struct_fcount(ex) >= 0: + return + slot[0] = sizet(ty) + slot[1] = sizet(is_packed) + return + i += 1 + if _MlirNamedCount >= int(_MlirNamedCap): + return + slot2: sizet* = _MlirNamedSlot(_MlirNamedCount) + slot2[0] = sizet(ty) + slot2[1] = sizet(is_packed) + _MlirNamedCount += 1 + + +def _MlirNamedFindSlot(name: str) -> int: + """按名字回查已登记槽位下标(未登记 / 表未发布 ⇒ -1)""" + if name is None or _MlirNamedTys is None: + return -1 + i: int = 0 + while i < _MlirNamedCount: + slot: sizet* = _MlirNamedSlot(i) + ty: rawptr[LLVMType] = (rawptr[LLVMType])(slot[0]) + nm: str = get_struct_name(ty) + if nm is not None and string.strcmp(nm, name) == 0: + return i + i += 1 + return -1 + + +def _MlirNamedPackedOf(name: str) -> int: + """按名字回查登记表里的 packed 标记(未登记 / 表未发布 ⇒ 0) + + TypePrint 的具名分支只有内部模型的 Name(LLVMType.Struct 无 packed 字段), + 标记经此从登记表取,与文本→文本路径同源;故 packed 只在模块打印期 + (登记表已发布)可见,独立 TypePrint 调用无表即视为非 packed。 + """ + si: int = _MlirNamedFindSlot(name) + if si < 0: + return 0 + slot: sizet* = _MlirNamedSlot(si) + return int(slot[1]) + + +def _MlirFailNamed(pool: memhub.MemBuddy*, reason: str, name: str): + """带类型名 + 当前中间文本行的响亮报错 + + MlirFail 是即时终止、拿不到调用栈(见 README §5.8),故把定位信息备齐: + 类型名 + 当前正在渲染的中间文本行(_MlirCtxLine,由渲染侧寄存)。 + """ + if name is None or name[0] == '\0': + MlirFail(reason) + nlen: sizet = string.strlen(name) + rlen: sizet = string.strlen(reason) + clen: sizet = 0 + if _MlirCtxLine is not None: + clen = string.strlen(_MlirCtxLine) + bsize: sizet = nlen + rlen + clen + 64 + buf: str = pool.alloc(bsize) + if buf is None: + MlirFail(reason) + if _MlirCtxLine is not None: + viperlib.snprintf(buf, bsize, "%s(类型名 '%s') [行: %s]", + reason, name, _MlirCtxLine) + else: + viperlib.snprintf(buf, bsize, "%s(类型名 '%s')", reason, name) + MlirFail(buf) + + +def _MlirNamedCheckName(pool: memhub.MemBuddy*, name: str): + """校验名字可作 MLIR 字符串字面量内容(`"` / `\\` / 控制字符 ⇒ 响亮报错)""" + if name is None or name[0] == '\0': + MlirFail("MLIR 渲染: 命名结构体名字为空") + i: sizet = 0 + while name[i] != '\0': + ch: int = name[i] + if ch == '"' or ch == 0x5C or (ch > 0 and ch < 32): + _MlirFailNamed(pool, + "MLIR 渲染: 命名结构体名字含 MLIR 字符串字面量非法字符(\" / \\ / 控制字符)", + name) + i += 1 + + +def _MlirNamedStructAppend(dst: str, dstsz: sizet, name: str, + fields: rawptr[ParamNode], fcount: int, packed: int, + pool: memhub.MemBuddy*, depth: int): + """追加 MLIR 具名结构体类型文本(追加写 dst) + + 非 packed: `!llvm.struct<"name", (字段…)>`; + packed: `!llvm.struct<"name", packed (字段…)>` —— 实测 mlir-opt / + mlir-translate 22.1.8 exit 0,降级为 `%"name" = type <{ … }>`(见 README §2.10)。 + MLIR 只在**具名**形态下支持该关键字(匿名 `{…}` 对应 `!llvm.struct<(...)>`, + 没有关键字位);而内部模型的 packed 标记只挂在 NamedTypeNode 上 + (LLVMType.Struct 变体无该字段),匿名结构体结构上无法携带 packed。 + + opaque(fcount < 0)与"具名但字段链缺失"一律响亮报错:MLIR 无法表达无体 + 结构体,静默输出空体(`!llvm.struct<"x", ()>`)会得到**错误布局**。 + """ + if dst is None or dstsz == 0: + return + _MlirNamedCheckName(pool, name) + if fcount < 0: + _MlirFailNamed(pool, + "MLIR 渲染: 命名结构体在打印时仍为 opaque(无字段体)——MLIR 不允许无体前向声明", + name) + if fields is None and fcount != 0: + _MlirFailNamed(pool, + "MLIR 渲染: 命名结构体字段链缺失(FCount 非 0 但无字段)", + name) + _strappend(dst, dstsz, "!llvm.struct<\"") + _strappend(dst, dstsz, name) + _strappend(dst, dstsz, "\", ") + if packed != 0: + _strappend(dst, dstsz, "packed ") + _strappend(dst, dstsz, "(") + cur: rawptr[ParamNode] = fields + first: int = 1 + fidx: int = 0 + tmp: str = pool.alloc(512) + while cur is not None: + # 悬空保护:字段链成环/内容复用时限制节点数并**响亮报错**——截断(llvmlite + # 匿名路径的 "...")会得到静默错误的布局,故具名路径不做截断。 + if fidx >= 4096: + _MlirFailNamed(pool, + "MLIR 渲染: 命名结构体字段过多或字段链成环(> 4096)", + name) + if first == 0: + _strappend(dst, dstsz, ", ") + if tmp is not None and cur.Ty is not None: + _MlirTypePrintDepth(tmp, 512, cur.Ty, pool, depth + 1) + _strappend(dst, dstsz, tmp) + cur = cur.Next + first = 0 + fidx += 1 + _strappend(dst, dstsz, ")>") # ============================================================ @@ -561,15 +823,25 @@ def _MlirTypeAppend(dst: str, dstsz: sizet, src: str, pool: memhub.MemBuddy*, if fp != 0: _MlirFuncText(dst, dstsz, p, lt, fp, pool, depth) return - # 报文带上原始类型文本(截断到 lt),便于一次定位: - # 若谓词/记号被误当类型,会直接看到 'sgt' 这类内容。 + # 报文带上原始类型文本(截断到 lt)+ 当前正在渲染的中间文本行,便于一次定位: + # 若谓词/记号被误当类型,会直接看到 'sgt' 这类内容;行上下文由 + # MlirSetContextLine 寄存(见本文件顶部的 _MlirCtxLine)。 emb: str = pool.alloc(lt + 1) if emb is not None: string.strncpy(emb, p, lt) emb[lt] = '\0' - emsg: str = pool.alloc(lt + 128) + clen: sizet = 0 + if _MlirCtxLine is not None: + clen = string.strlen(_MlirCtxLine) + emsg: str = pool.alloc(lt + clen + 128) if emsg is not None: - viperlib.snprintf(emsg, lt + 128, "无法转换为 MLIR 类型文本(v1 未覆盖该形态):'%s'", emb) + if _MlirCtxLine is not None: + viperlib.snprintf(emsg, lt + clen + 128, + "无法转换为 MLIR 类型文本(v1 未覆盖该形态):'%s' [行: %s]", + emb, _MlirCtxLine) + else: + viperlib.snprintf(emsg, lt + clen + 128, + "无法转换为 MLIR 类型文本(v1 未覆盖该形态):'%s'", emb) MlirFail(emsg) MlirFail("无法转换为 MLIR 类型文本(v1 未覆盖该形态)") @@ -653,12 +925,61 @@ def _MlirStructText(dst: str, dstsz: sizet, p: str, lt: sizet, def _MlirNamedStructText(dst: str, dstsz: sizet, p: str, lt: sizet, pool: memhub.MemBuddy*, depth: int): - """命名结构体类型文本(形如 %name):v1 不支持 → 响亮报错 + """命名结构体类型文本(形如 `%"name"` / `%name`)→ 使用处的内联具名形态 - builders 内嵌类型由 TypePrint 内联渲染为 !llvm.struct<(…)>, - 中间指令文本不应再出现"只有名字"的结构体类型。 + MLIR 无独立类型定义语法、不允许无体前向声明(实测 mlir-opt 22.1.8: + `!llvm.struct<"x">` 报 "struct without a body only allowed in a recursive + struct"),故名字必须回查**模块里已登记的完整定义**(登记表由 __module.vp + 的 PrintModule 按 NamedTypeHead 发布)才能展开为 + `!llvm.struct<"name", (字段…)>`;packed 标记与类型同槽登记,一并取出, + 故 packed 具名结构体在此路径输出 `!llvm.struct<"name", packed (…)>`。 + 文本里的名字可能带 LLVM 口径引号(`%"a.b"`)——剥掉 `%` 与引号后,名字 + 原样作 MLIR 字符串字面量内容(实测 `[`/`]`/数字开头无需额外转义)。 + 查不到 / 仍是 opaque / 名字非法 ⇒ 响亮报错(绝不静默输出空体错误布局)。 """ - MlirFail("MLIR 渲染: 命名结构体类型文本 v1 不支持(应使用内联 !llvm.struct<(…)>)") + if dst is None or dstsz == 0 or p is None: + return + nm: str = pool.alloc(lt + 2) + if nm is None: + MlirFail("MLIR 渲染: 命名结构体名字缓冲分配失败") + n: sizet = 0 + q: str = p + 1 + if q[0] == '"': + q = q + 1 + while q[0] != '\0' and q[0] != '"' and n < lt: + nm[n] = q[0] + n += 1 + q = q + 1 + else: + while q[0] != '\0' and _MlirNamedNameEnd(q[0]) == 0 and n < lt: + nm[n] = q[0] + n += 1 + q = q + 1 + nm[n] = '\0' + si: int = _MlirNamedFindSlot(nm) + if si < 0: + _MlirFailNamed(pool, + "MLIR 渲染: 命名结构体类型文本未在模块中登记完整定义(MLIR 不允许无体前向声明,须以 !llvm.struct<\"name\", (…)> 内联形态使用)", + nm) + slot: sizet* = _MlirNamedSlot(si) + ty: rawptr[LLVMType] = (rawptr[LLVMType])(slot[0]) + packed: int = int(slot[1]) + match ty: + case LLVMType.Struct(fields, fcount, _): + _MlirNamedStructAppend(dst, dstsz, nm, fields, fcount, packed, pool, depth) + case _: + _MlirFailNamed(pool, + "MLIR 渲染: 登记表中该名字不是结构体类型", + nm) + + +def _MlirNamedNameEnd(ch: int) -> int: + """裸命名结构体名(`%name`)的终止字符(空白 / 顶层分隔符)""" + if ch == 0 or ch == 32 or ch == 9 or ch == 10 or ch == 13: + return 1 + if ch == ',' or ch == ')' or ch == ']' or ch == '}' or ch == '(' or ch == '{' or ch == '[': + return 1 + return 0 def _MlirFindFuncParen(p: str, lt: sizet) -> sizet: @@ -766,9 +1087,18 @@ def _MlirTypePrintDepth(buf: str, size: sizet, _strappend(buf, size, tmp) _strappend(buf, size, ">") case LLVMType.Struct(fields, fcount, name): - # MLIR 侧不区分匿名/命名:一律渲染字段列表 - # (指针场景已由 Ptr → !llvm.ptr 收敛,名字不参与渲染) - _MlirStructFields(buf, size, fields, pool, depth) + # 命名结构体(Name 非 None):内联具名形态 !llvm.struct<"name", (…)>, + # 名字参与渲染(MLIR 无定义行,名字只在使用处出现)。 + # packed 标记不在 LLVMType 上(Struct 变体无该字段),而在 NamedTypeNode + # 上 ⇒ 由登记表按名字回查(模块打印期已发布;独立 TypePrint 无表 ⇒ 非 packed)。 + # 匿名结构体(Name 为 None):沿用 !llvm.struct<(…)>(无关键字位)。 + # 指针场景已由 Ptr → !llvm.ptr 收敛(不透明指针),不在此分支。 + if name is not None: + buf[0] = '\0' + _MlirNamedStructAppend(buf, size, name, fields, fcount, + _MlirNamedPackedOf(name), pool, depth) + else: + _MlirStructFields(buf, size, fields, pool, depth) case LLVMType.Func(ret, params, count): # MLIR 函数类型:!llvm.func # 与文本→文本的 _MlirFuncText 保持同形外壳。 @@ -949,10 +1279,11 @@ def _DeepCopyDepth(pool: memhub.MemBuddy*, """ if ty is None: return None - new_ty: rawptr[LLVMType] = (rawptr[LLVMType])(pool.alloc(LLVMType.__sizeof__())) + sz_dc: sizet = _TypeSizeGuard() + new_ty: rawptr[LLVMType] = (rawptr[LLVMType])(pool.alloc(sz_dc)) if new_ty is None: return None - string.memset(new_ty, 0, LLVMType.__sizeof__()) + string.memset(new_ty, 0, sz_dc) if depth >= DEEPCOPY_MAX_DEPTH: # 深度超限:浅拷贝 tag 后停止递归(复制当前变体但不深拷子类型) match ty: diff --git a/os/__init__.vp b/os/__init__.vp index b822948..a59972a 100644 --- a/os/__init__.vp +++ b/os/__init__.vp @@ -110,7 +110,7 @@ def chmod(path: str, mode: int) -> int: # Table of Contents Listing # ============================================================================= -def listdir(path: str, out_names: str, max_count: ULONG) -> ULONG: +def listdir(path: str, out_names: str*, max_count: ULONG) -> ULONG: """列举目录下的文件/子目录名。 out_names 为调用者提供的字符串指针数组(char**),每个元素指向一个文件名。 文件名通过 _mbuddy 分配内存。 diff --git a/os/_posix.vp b/os/_posix.vp index 8456627..3ce8887 100644 --- a/os/_posix.vp +++ b/os/_posix.vp @@ -137,7 +137,7 @@ def chmod_impl(path: str, mode: int) -> int: # 目录列举(opendir / readdir / closedir) # ============================================================================= -def listdir(path: str, out_names: str, max_count: ULONG) -> ULONG: +def listdir(path: str, out_names: str*, max_count: ULONG) -> ULONG: """列举目录下的文件/子目录名。 out_names 为调用者提供的字符串指针数组(char**),每个元素指向一个文件名。 文件名通过 _mbuddy 分配内存。 diff --git a/os/_win32.vp b/os/_win32.vp index f78f5bc..20f368a 100644 --- a/os/_win32.vp +++ b/os/_win32.vp @@ -174,7 +174,7 @@ def chmod(path: str, mode: int) -> int: # 目录列举(FindFirstFile / FindNextFile) # ============================================================================= -def listdir(path: str, out_names: str, max_count: ULONG) -> ULONG: +def listdir(path: str, out_names: str*, max_count: ULONG) -> ULONG: """列举目录下的文件/子目录名。 out_names 为调用者提供的字符串指针数组(char**),每个元素指向一个文件名。 文件名通过 _mbuddy 分配内存。 diff --git a/subprocess.vp b/subprocess.vp index b29c9ce..c1cbaa7 100644 --- a/subprocess.vp +++ b/subprocess.vp @@ -77,17 +77,17 @@ def _run_win32(args: str, capture_output: bool, text: bool) -> CompletedProcess # Security attributes (allow handle inheritance) sa: w32.win32base.SECURITY_ATTRIBUTES = w32.win32base.SECURITY_ATTRIBUTES() sa.nLength = w32.win32base.SECURITY_ATTRIBUTES.__sizeof__() - sa.lpSecurityDescriptor = t.CPtr(0) + sa.lpSecurityDescriptor = None sa.bInheritHandle = 1 # TRUE # Create stdout pipe - stdout_read: w32.win32base.HANDLE = t.CPtr(0) - stdout_write: w32.win32base.HANDLE = t.CPtr(0) + stdout_read: w32.win32base.HANDLE = None + stdout_write: w32.win32base.HANDLE = None if capture_output: pipe_result: BOOL = w32.win32file.CreatePipe( - t.CPtr(c.Addr(stdout_read)), - t.CPtr(c.Addr(stdout_write)), - t.CPtr(c.Addr(sa)), + &stdout_read, + &stdout_write, + &sa, 0 ) if pipe_result == 0: @@ -110,16 +110,16 @@ def _run_win32(args: str, capture_output: bool, text: bool) -> CompletedProcess # use 0 when capture_output=False (inherit parent process console, let stdout directly display) create_flags: ULONG = w32.win32process.CREATE_NO_WINDOW if capture_output else 0 create_result: BOOL = w32.win32process.CreateProcessA( - t.CPtr(0), # lpApplicationName (NULL, parse from command line) + None, # lpApplicationName (NULL, parse from command line) cmd_line, # lpCommandLine (modifiable) - t.CPtr(0), # lpProcessAttributes - t.CPtr(0), # lpThreadAttributes + None, # lpProcessAttributes + None, # lpThreadAttributes 1, # bInheritHandles = TRUE create_flags, # dwCreationFlags - t.CPtr(0), # lpEnvironment (inherit from parent process) - t.CPtr(0), # lpCurrentDirectory (inherit from parent process) - t.CPtr(c.Addr(si)), - t.CPtr(c.Addr(pi)) + None, # lpEnvironment (inherit from parent process) + None, # lpCurrentDirectory (inherit from parent process) + &si, + &pi ) if create_result == 0: @@ -148,8 +148,8 @@ def _run_win32(args: str, capture_output: bool, text: bool) -> CompletedProcess stdout_read, buf + total, buf_size - 1 - total, - t.CPtr(c.Addr(bytes_read)), - t.CPtr(0) + &bytes_read, + None ) if read_result == 0 or bytes_read == 0: break diff --git a/sys.vp b/sys.vp index 035c006..12c868d 100644 --- a/sys.vp +++ b/sys.vp @@ -59,16 +59,24 @@ def _init_argv(): """Initialize global argv once on first access""" global _argc, _argv if _argc != 0: return - _init_argv_win32() if platmacro.IS_WINDOWS else _init_argv_posix() + # 必须用 if/else 派发:编译器只翻译"调用表达式语句",三元表达式语句会被丢弃(argv 恒空) + if platmacro.IS_WINDOWS: + _init_argv_win32() + else: + _init_argv_posix() def _init_argv_win32(): """Windows: GetCommandLineA + manual parsing (handle spaces/double quotes/backslashes)""" global _argc, _argv cmdline: str = w32.win32base.GetCommandLineA() - if cmdline == None: return + if cmdline == None: + stdio.printf("ERROR: GetCommandLineA 返回 NULL,无法获取命令行,argv 将为空\n") + return cmdlen: sizet = len(cmdline) - if cmdlen == 0: return + if cmdlen == 0: + stdio.printf("ERROR: 命令行字符串为空,无法获取参数,argv 将为空\n") + return # Allocate buffer for parameter strings (worst case: entire command line + argc \0s) buf: bytes = _mbuddy.alloc(cmdlen + 1) if buf == None: return diff --git a/traceback.vp b/traceback.vp new file mode 100644 index 0000000..829e8dc --- /dev/null +++ b/traceback.vp @@ -0,0 +1,1854 @@ +import t, c +from stdint import * +from w32.win32base import * +import string +import stdio +import stdlib +import w32.win32file as wf +import w32.win32console as wc +import w32.win32process as wp +import w32.win32memory as wm +import w32.win32dbghelp as dbg + +# 转储重入保护(见 DumpMainThread)**不使用 InterlockedCompareExchange**:该名字在 x64 上是 +# winnt.h 的编译器内部函数(映射到 lock cmpxchg 指令),**不是可链接符号**,直接引用会在链接期 +# 报 undefined symbol(实测:ld.lld: undefined symbol: InterlockedCompareExchange)。 +# 本模块改用普通 32 位标志做「检查并占用」:并发回调最多导致一次重复转储(诊断工具可接受), +# 且不引入任何链接期依赖。 + +# ============================================================================= +# traceback — 运行时「按键取栈」模块(Ctrl+C / Ctrl+Break 时转储主线程调用栈) +# +# 解决的问题:编译器(或任何长任务)卡死时,用户按一个键就能拿到「现在卡在哪一行」 +# 的完整调用栈——从 CRT 入口(mainCRTStartup / __tmainCRTStartup)到当前帧。 +# +# ----------------------------------------------------------------------------- +# 一、触发语义(SetConsoleCtrlHandler 回调) +# ----------------------------------------------------------------------------- +# Ctrl+C (CTRL_C_EVENT) → 打印调用栈后**返回 FALSE**(交还系统默认处理 ⇒ 进程终止) +# Ctrl+Break (CTRL_BREAK_EVENT)→ 打印调用栈后**返回 TRUE**(已处理 ⇒ 进程继续运行) +# 其它控制事件(关窗/注销/关机,CTRL_CLOSE_EVENT 等)→ 一律返回 FALSE,不转储、不干预。 +# 回调由系统在**新建的线程**上执行,不是主线程——所以「当前执行到哪」必须靠 +# SuspendThread(主线程) + GetThreadContext 取得主线程寄存器,再回溯主线程的栈。 +# 同一时刻**只允许一次转储**:连续按键会由系统在多个新线程上并发回调,而它们共用 +# 同一个 LineBuf 与输出句柄 ⇒ 输出互相撕裂(实测出现过"降级:"行重复且交织)。 +# 故 DumpMainThread 入口用内联汇编 `lock cmpxchg`(AtomicTryEnterDump)对 _DumpLock +# 做原子"检查并占用":抢占失败的调用直接返回(不挂起主线程、不输出任何内容)。 +# 每次转储还会打印自增 Seq 与回调线程 id:若两条转储记录的 Seq 不同 ⇒ 顺序两次; +# 同一 Seq 的行却互相截断 ⇒ 说明仍存在共享缓冲的并发写(据此定位,不再靠猜)。 +# +# ----------------------------------------------------------------------------- +# 二、安装时机与总开关(本模块的开关落地点) +# ----------------------------------------------------------------------------- +# 约定:**旁文件存在** ⟺ 编译时用了 TPC 的 `--traceback`。 +# Install() 先求 <当前 exe 完整路径> + ".stackmap";该文件不存在(CreateFileA 报 +# ERROR_FILE_NOT_FOUND)则**立刻返回 0,零副作用**(不分配、不注册 handler); +# 存在才注册 SetConsoleCtrlHandler。故 TPC 侧无需生成任何其它开关代码,只需在 +# 启用该选项时让产物链接 dbghelp(见第五节)并调用一次 Install()(见第六节)。 +# 文件存在但其头部/记录区非法时:打印可定位的警告并把映射标记为不可用——调用栈仍 +# 照常转储(只有地址),不会静默退出(诊断工具取不到信息时给降级信息,不掩盖错误)。 +# +# ----------------------------------------------------------------------------- +# 三、旁文件(.stackmap)格式 —— 小端 +# ----------------------------------------------------------------------------- +# 头部 20 字节:magic="TPCS"(4B) + u32 version=1 + u32 entry_count + u32 str_count +# + u32 str_bytes +# 记录区 entry_count × 20B(**按 rva 升序**):u64 rva; u32 file_off; u32 line; u32 func_off +# 字符串池 str_bytes 字节:\0 结尾 UTF-8 串;file_off/func_off 是池内偏移 +# (func_off = 0 表示无函数名) +# 运行时:GetModuleHandleA(NULL) 取映像基址,帧地址 - 基址 = rva,再二分查找记录区。 +# 命中 → 帧行 " File \"<文件>\", line <行>, in <函数名>",其下再打一行 4 空格缩进的源码 +# (运行期重开该文件读第 <行> 行;读不到就整行不打,见第十三节); +# 未命中 → " File \"<0x<地址> +0x no source line mapping>\", line 0, in "; +# 地址落在主模块之外(更低,典型为 DLL/系统帧)→ " File \"<0x<地址> external>\", line 0, in "。 +# 三种形态都把地址/RVA 保留在同一行(见第十三节)。 +# 装载时做一次全表校验(rva 严格升序、字符串偏移落在池内),不合法即禁用映射—— +# 二分查找依赖升序,破损的表会给出**错行号**,比"只打地址"更坏。 +# 校验失败一律**逐项打印挂在哪一项**(含头部实读的 magic/version/count/str_bytes +# 与文件实际大小),失败原因不外泄为一句"旁文件非法"。 +# ⚠ 读字节必须逐个 & 0xFF:Viper 的 Buf[i] 是**有符号** char,>= 0x80 的字节若直接 +# 转 u32 会符号扩展(例:记录数 1000=0x3E8 的首字节 0xE8 → 0xFFFFFFE8),使头部的 +# entry_count/str_bytes 变成天文数字、`20+count*20+str_bytes > 文件大小` 必然成立 +# ⇒ 映射永远装载失败(这正是"源行映射=无"的真因,详见 ReadU32)。 +# +# ----------------------------------------------------------------------------- +# 四、回溯两条路径与降级条件(**本轮已主副互换,见第十一节 B**) +# ----------------------------------------------------------------------------- +# ⚠ 先读第十一节 B:本轮起**主路径 = RBP 链**(零 dbghelp 依赖),dbghelp 降为 +# "可选加强"。下面这段 dbghelp 说明仍有效,只是它描述的是**加强路径**的历史细节。 +# 加强路径(可选):dbghelp!StackWalk64(MachineType=IMAGE_FILE_MACHINE_AMD64)。 +# SymSetOptions(SYMOPT_DEFERRED_LOADS|SYMOPT_UNDNAME|SYMOPT_NO_PROMPTS) + +# SymInitialize(GetCurrentProcess(), NULL, **TRUE**) —— TRUE 是硬要求:它让 dbghelp +# 枚举已加载模块填进自己的模块表;置 FALSE 时模块表为空,SymFunctionTableAccess64 +# 取不到 RUNTIME_FUNCTION、SymGetModuleBase64 返回 0,StackWalk64 **首次调用即失败** +# (表现为"符号回溯=可用但 0 帧")。配合 DEFERRED_LOADS 不会真的去加载 PDB。 +# 三个回调地址里 FunctionTableAccess/GetModuleBase 仍在 Install 时用 GetProcAddress +# 取(本仓既有的动态解析写法,先例 VET/App/lib/VGuard.vp:852-867):避免依赖 +# 「跨模块导入的函数名作为函数指针实参」这一未经本仓验证的用法,取不到就传 NULL。 +# ReadProcessMemory 回调改用**本模块内定义**的 ReadMemCb(同模块内"函数名作 +# 函数指针实参"已由 CtrlHandler 先例验证):直接传 kernel32!ReadProcessMemory +# 本体时拿不到失败原因,而回调返回 0 会让 StackWalk64 立即终止(恰好是"0 帧")。 +# 主路径(**RBP 链手工回溯**):无条件先走它。它拿不到更多帧(<=1)时**先走第二兜底 +# = 栈扫描(ScanStack,不依赖帧指针)**;再之后才轮到 dbghelp 加强(编译期开关为 1、 +# SymInitialize 成功、两个回调地址与帧缓冲齐备、且**未探测到 ASan 运行时**);任一 +# 前提不满足都会打印明确原因(`[diag] symwalk=disabled reason=…`),**不静默 0 帧**。 +# 本轮新增:每步无条件打 `[diag] rbp:` 原始读数 + 判定、以及 `walk: fallback=stack_scan` +# 扫描推断帧(`~` 前缀)——详见第十二节。 +# 主路径实现:打印当前 RIP,然后沿主线程 RBP 链读 [rbp](调用者 rbp)与 [rbp+8] +# (返回地址)逐帧回溯;要求 rbp 8 字节对齐、严格向高地址增长(`[rbp] > rbp`)、 +# 单步跨度 <= TRACEBACK_RBP_MAX_STEP、返回地址落在主模块基址区间内,否则立即停止 +# (防野指针把回溯带飞);**读内存一律经 ReadProcessMemory,绝不直接解引用野 rbp**。 +# RBP 链只覆盖保持帧指针的函数;帧指针一旦未保留([rbp+8] 不是返回地址)链即在第 1 +# 步断——此时由 ScanStack 兜底,另叠一层 dbghelp 加强(可用时)。 +# --traceback 时 TPC 给 llc 加 `-frame-pointer=all`(Phase2Translator.py:2187-2193); +# 该 flag 是否真正生效见第十二节 C 的调查结论与修法落点。 +# 【取证:先取证,再决策】每次转储在 banner 之后打印一组前置诊断(**TRACEBACK_DIAG=1 时**)—— +# Seq / 回调线程 id / 主线程 id / Dumping 进入前值 / MapOk / SymReady / ModInited / +# SymGetModuleBase64(主模块) 的**实际返回值** / 主模块基址 / 帧缓冲与两个回调地址 +# 是否就位;进入 dbghelp 主路径后,**首帧**再打印 StackWalk64 的返回码、传入的 +# machine/RIP/RSP/RBP、返回的 AddrPC,以及 SymFunctionTableAccess64(proc, AddrPC) 的 +# 返回值。这些行**先于任何降级判断**产出,使"0 帧"的根因可直接判定(见第九节)。 +# +# ----------------------------------------------------------------------------- +# 五、链接期依赖(TPC 侧必须知道) +# ----------------------------------------------------------------------------- +# dbghelp(-ldbghelp)+ kernel32(既有)。SymInitialize / StackWalk64 / SymSetOptions / +# SymGetModuleBase64 / SymFunctionTableAccess64 是**静态**引用(后两个用于首帧取证诊断), +# 产物必须能解析 dbghelp 的导入库,否则链接失败。若不便新增 -ldbghelp, +# 把 TRACEBACK_USE_DBGHELP 置 0:该常量比较会在编译期把唯一含 dbghelp 调用的分支 +# 整体去掉(见 TRACEBACK_USE_DBGHELP 的注释),只保留 RBP 链回溯。 +# +# ----------------------------------------------------------------------------- +# 六、TPC 侧调用入口(符号与签名) +# ----------------------------------------------------------------------------- +# import traceback +# traceback.Install() # 返回 int:1=handler 已注册;0=未安装(无旁文件/失败) +# 调用时机:进程启动期一次即可。两种等价做法—— +# (a) 在入口 main 的第一条语句调用; +# (b) 注入一个 @llvm.global_ctors(.CRT$XCU 语义)构造器调用该符号(本仓先例: +# TransPyC/lib/core/LLVMCG/BaseGen.py:2061-2097 用 llvm.global_ctors 注册 +# Ctrl+C 处理器)。构造器在 CRT 初始化期执行,能覆盖「main 之前就卡住」的场景。 +# 本模块自身**没有**模块级构造器(状态对象由 Install 懒分配),故与 TPC 注入的 +# 构造器不存在初始化顺序依赖,重复调用是幂等的。 +# ⚠ 本模块把「主线程」定义为**调用 Install() 的那个线程**(Install 里 GetCurrentThreadId +# 记录其 id,回调时挂起/回溯的就是它)。故必须在真正的入口线程调用(main 或 CRT +# 构造器所在线程);若在启动期的工作线程里调用,转储的将是那个线程的栈。 +# +# ----------------------------------------------------------------------------- +# 七、为什么直接 WriteFile 而不是 printf +# ----------------------------------------------------------------------------- +# 转储发生在「主线程已被挂起」的时刻:若主线程恰好停在 CRT 的 stdout 锁内,回调线程 +# 再调用 printf 会**永久等待同一把锁**,把"卡死"变成"按了键彻底不动"。 +# 故输出走 kernel32!WriteFile(GetStdHandle(STD_ERROR_HANDLE))(无用户态 CRT 流锁), +# 格式化走 CRT snprintf(只写内存缓冲,不碰 FILE 锁)。stderr 的另一好处:stdout 常被 +# 管道占用(如被父工具读取),stderr 一般直达终端。句柄与全部缓冲都在 Install 时备好, +# 回调线程**零分配**(MemBuddy 非线程安全,回调里绝不触碰池)。 +# +# ----------------------------------------------------------------------------- +# 七之二、转储**同时落文件** + 控制台纯 ASCII(本轮的两次修正) +# ----------------------------------------------------------------------------- +# (a) 落文件(解决"没有 Break 键"与"控制台代码页吞字"):Install 成功时以 +# ` + ".stackdump.txt"` 打开一个**追加写**句柄(OPEN_ALWAYS + +# FILE_APPEND_DATA,不存在则创建),EmitLine 把**同一份内容**既写控制台又追加落盘。 +# 文件是 UTF-8 字节原样,不受控制台代码页影响;每次转储在段前追加一小段分隔头 +# (本地时间戳/pid/触发键/主线程 id/Seq,中文标签,仅文件)。文件写失败**不阻断** +# 控制台,且只提示一次(见 EmitToFile)。Install 成功时打印一行 +# `[traceback] stack dump also written to: <路径>` 便于确认去向。 +# (b) 控制台纯 ASCII:原先的运行时文本是中文(UTF-8 字节),而 cmd 的控制台输出代码页 +# 默认为 936(GBK)——WriteFile 到控制台会按 936 解释 UTF-8 字节,一段多字节在多次 +# WriteFile 之间被拆开/重组,表现为**整行消失**(最长的"首帧"行首当其冲)或相邻行错位、 +# 看似"重复且残缺"(含 '�')。故**运行时输出到控制台的文本一律改英文/ASCII** +# (banner / [diag] / Trigger= / exe base= / walk: primary=rbp_chain / frames=N 等), +# 任何代码页下都可读、可 grep;` File "<文件>", line <行>, in <函数名>` 帧行保留源路径 +# 原文(分隔符/新增说明文字一律 ASCII)。 +# 中文仅保留在:本文件的注释/文档串、以及**只写文件**的分隔头。 +# +# ----------------------------------------------------------------------------- +# 八、依赖的既有声明(未改动)与本模块新增声明 +# ----------------------------------------------------------------------------- +# 既有(`from w32.win32base import *` 取裸名):GetModuleHandleA / GetProcAddress / +# CloseHandle / GetLastError / INVALID_HANDLE_VALUE / ERROR_FILE_NOT_FOUND / +# LARGE_INTEGER / SYSTEMTIME / GetLocalTime / HANDLE / BOOL / ULONG / VOIDPTR; +# wf.CreateFileA / wf.ReadFile / wf.WriteFile / wf.GetFileSizeEx / wf.GetStdHandle / +# wf.GENERIC_READ / wf.FILE_SHARE_READ / wf.FILE_SHARE_WRITE / wf.OPEN_EXISTING / +# wf.OPEN_ALWAYS / wf.FILE_ATTRIBUTE_NORMAL / +# wf.STD_ERROR_HANDLE / wf.STD_OUTPUT_HANDLE; +# wp.OpenThread / wp.SuspendThread / wp.ResumeThread / wp.GetCurrentThreadId / +# wp.GetCurrentProcess / wp.GetCurrentProcessId / wp.THREAD_SUSPEND_RESUME / +# wp.THREAD_GET_CONTEXT; +# string.memset / string.strcpy / stdlib.malloc / free / c.Addr。 +# 本轮新增:wc.SetConsoleCtrlHandler / wc.CTRL_C_EVENT / wc.CTRL_BREAK_EVENT; +# wp.CONTEXT(x64 完整布局)/ wp.CONTEXT_FULL / wp.GetThreadContext; +# wm.ReadProcessMemory(由本模块 ReadMemCb 包装后作 StackWalk64 回调); +# w32/win32dbghelp.vp(STACKFRAME64/ADDRESS64/KDHELP64 + SymSetOptions/ +# SYMOPT_* + SymInitialize/SymFunctionTableAccess64/SymGetModuleBase64/ +# StackWalk64); +# 转储文件:wf.WriteFile 追加写 .stackdump.txt + 自备常量 +# TRACEBACK_FILE_APPEND_DATA(FILE_APPEND_DATA,wf 未定义); +# 转储重入保护用内联汇编的一条 `lock cmpxchg`(AtomicTryEnterDump)对模块级 +# 32 位 _DumpLock 做原子"检查并占用"(不用 InterlockedCompareExchange:x64 上是 +# 编译器内部函数、无导出符号、不可链接)。 +# 本轮新增(真 Python traceback 输出):kernel32!GetEnvironmentVariableA 的**本模块自声明** +# (win32base 未声明,仓库仅 VET/App/lib/VDirs.vp:45 有;kernel32 本就链接,TPC/TPV 均 +# 可解析),用于 TRACEBACK_DIAG 开关;其余仅用既有 API(见第十三节)。 +# +# ----------------------------------------------------------------------------- +# 九、"0 帧"的可执行结论(哪种情形 → 看哪个现场字段) +# ----------------------------------------------------------------------------- +# ⚠ 本节所有取证行都是 `[diag]` 行,故**只在 TRACEBACK_DIAG=1 时产出**(见第十三节 C); +# 排查"0 帧"时请先用 `set TRACEBACK_DIAG=1` 启动目标进程。 +# 首帧取证两行(" [diag] StackWalk64 first: ret=… addrpc=… fta=…" 与紧随其后的 +# " [diag] StackWalk64 in: machine=… rip=… rsp=… rbp=…"——原为单条 143 字符长行, +# 拆成两条短行以消除窄终端折行造成的"显示残缺"假象,字段一个不减) +# 与前置诊断行("[diag] Seq=… … modinited=…" / "SymGetModuleBase64(main)=…")合起来直接给出结论: +# (1) modinited == 0 且 SymGetModuleBase64(main) == 0 ⇒ dbghelp 的模块表不含主模块。 +# 根因是 SymInitialize 的 fInvadeProcess 参数(必须 TRUE)或调用时机(要在进程启动期、 +# 模块枚举可用时调用,本模块放在 Install 里)。修法:确认 Install 里 SymInitialize 的 +# 第三个实参为 1,且 Install 在入口线程、模块全部加载后调用;必要时改用 +# SymRefreshModuleList 重新枚举。 +# (2) 首帧 ret == 0,且紧邻有 "[diag] readmem failed addr=… size=… GetLastError=…" +# ⇒ 读栈内存失败。`ReadProcessMemory` 在回调里返回 0 会让 StackWalk64 立刻终止(=0 帧)。 +# 修法:核对 ReadMemCb 的宽度口径(dbghelp 侧是 DWORD nSize + LPDWORD,kernel32 侧是 +# SIZE_T,本模块已用本地 sizet 承接再写回 DWORD)与进程句柄/权限。 +# (3) 首帧 ret != 0 但返回 addrpc == 0,或 fta(SymFunctionTableAccess64(addrpc))== 0 +# ⇒ 上下文/模块基址口径问题(传给 StackWalk64 的 RIP/RSP/RBP 或模块基址不对)。 +# 修法:核对 CONTEXT 的 Rip/Rsp/Rbp 取值与 GetModuleHandleA(NULL) 基址。 +# 补充:首帧行里 `ret=not-called` 表示 SymInitialize/回调地址/帧缓冲前提不全、根本没调用 +# StackWalk64(此时该行仍会产出,保证"转 RBP 链"之前永远有首帧证据)。 +# 三者互斥且可判:只要把上面两组打印字段与判据对照,就能**当场**确定属于哪一类。 +# +# ----------------------------------------------------------------------------- +# 十、本轮新增的两组定位打印(应答"walk 到底跑到没有"与"重复残缺真因") +# ----------------------------------------------------------------------------- +# A. walk 是否被调用、走了哪条出口(全部走 DumpMainThread / WalkViaDbgHelp 的既有输出通道; +# 这些行**只在 TRACEBACK_DIAG=1 时输出**): +# * DumpMainThread 在**调用前**打 " [diag] walk: enter use_dbghelp=… symready=… +# modinited=… frame_buf=… mapok=… asan=…",**调用后**打 " [diag] walk: leave +# frames=… mapok=…"。两行都在 ⇒ walk 一定进了 RBP 主路径(dbghelp 加强可选)。 +# * 若 **连 walk: enter 都不出现**(且 TRACEBACK_DIAG=1)⇒ DumpMainThread 在本行之前就返回: +# 只可能是三处 fallback(OpenThread / SuspendThread / GetThreadContext 失败,各自都有 +# "fallback: … cannot dump stack" 行)——据该行即可定位被跳过的分支。 +# * WalkViaDbgHelp 的**每一条 return** 都先打 " [diag] walk: return reason=<…> frames=…", +# reason ∈ {null_state, disabled, null_frame_buf, null_context, stackwalk_first_failed, +# stackwalk_failed, addrpc_zero, frame_limit}(含义见函数内注释),故 walk 的走向 +# 必可判定、不再靠猜。本轮起主路径是 RBP 链,DumpMainThread 另打 +# `walk: primary=rbp_chain` / `walk: rbp_chain frames=…` / `walk: dbghelp enhancement engaged`; +# dbghelp 不可用时另打 `[diag] symwalk=disabled reason=…`(见第十一节 B)。 +# B. "重复残缺"真因的判别凭据(唯一凭据 = 行号是否重复): +# * EmitLine 在 **TRACEBACK_DIAG=1** 时给**每一行**加全局递增前缀 " [dump#] " +# (k 由 _DumpLineNo 每次 +1);关闭(默认)时**不加前缀**,使帧行保持纯 Python +# traceback 形态(见第十三节)。 +# * 同一段文本出现多份且**序号重复**(同一 k 出现两次)⇒ 该文本被写了两次,即**并发写 +# 同一 LineBuf**;此时才需要"输出互斥"。同一序号不可能出自单线程的一次 EmitLine。 +# * 文本被**截断/换行错位**但**序号严格递增、无重复** ⇒ 内容只写了一次(乱不了号), +# 只可能是**终端长行折行/回显**层的显示效果;修法是缩短长行(本轮已把三条最长行拆短), +# **不动锁**。 +# * 本轮补打:`[dump#skip]` 仅在使用输出互斥时才需要;本模块判定为"显示层", +# 故未加互斥、未打印该标记(判据与结论见交付说明)。 +# +# ----------------------------------------------------------------------------- +# 十一、本轮两根因修复(崩溃 = 属性访问被降级;0 帧 = dbghelp 与环境互斥) +# ----------------------------------------------------------------------------- +# A. **崩溃根因不是分配失败,而是 TPC 把 `_State.<字段>` 静默降级成常量 null。** +# 实测证据(构建产物 IR,模块前缀 = 本文件 sha1 前 16 位 9b4ee486e08837c5): +# * 源码 `string.memset(_State.FrameBuf, 0, …)` 被生成为 +# `call i8* @memset(i8* null, i32 0, i64 248)` —— 参数就是字面 null; +# 同一函数里 `_State.LineBuf`(snprintf)、`_State.FuncTableAddr / ModuleBaseAddr` +# (StackWalk64 的两个回调槽)同样全被折成 null。 +# * `Install` 里的 `calloc(1,248)` 与其"分配失败即 return 0"的判空**都在且正确** +# (IR 里 `@calloc` 返回值被正常 store 到字段 20),故**不是** calloc 返回 NULL。 +# 机理(TPC 侧,只报不改):`TransPyC/lib/core/Handles/HandlesExprAttr.py` +# `_resolve_var_classname`(约 L285-293)对 `rawptr[X]` 形态的模块级全局只看 +# `GVar.type.pointee`(= `X*`,仍是指针)⇒ 认不出 `X`,返回 None;兜底的 +# `_recover_struct_class`(约 L723-740)虽能多解一层指针,但要求 `X` 已进入 +# **函数级**的 `Gen.variables/global_vars`(`HandlesFunctions.py:1848` 每函数清空), +# 而该登记只发生在"裸名 `X` 被求值一次"时(`HandlesExpr.py:479/487`)。 +# ⇒ 凡**一上手就直接读写 `_State.<字段>`、且函数内从未单独求值过裸名 `_State` +# 的函数**,`_handle_attr_name` 在 L507-508 返回 None: +# - 作为调用实参 ⇒ 被静默替换为 null(→ memset(null) 崩溃); +# - 作为 `if X == None:` 的条件 ⇒ 整个 `if` 被 `HandlesIf` L170-171 丢弃 +# (→ 我们原来写的 `if _State.FrameBuf == None: …` **根本没被生成**)。 +# 受影响函数(IR 佐证:这些函数体内 `@_State` 的 load 次数为 0): +# `EntryRva` / `LookupRva` / `EmitFrame` / `WalkViaDbgHelp`。 +# 其余函数(`EmitLine` / `DumpMainThread` / `ReadMemCb` / `Install` …)入口都有 +# `_State == None` 判空 ⇒ 裸名已登记 ⇒ 字段访问正常(这就是为什么 +# `DumpMainThread` 打出的 `frame_buf=1`、`modinited=1` 都是真的)。 +# 本模块的修法(**不去改 TPC**):凡直接使用 `_State.<字段>` 的函数,入口 +# **第一句就写 `if _State == None: …`**(即使是冗余判空)——它既是真正的兜底, +# 也把裸名 `_State` 登记进 `Gen.variables`,使其后的字段访问被正常解析。 +# 崩溃点再加一道局部量兜底:`FBuf: VOIDPTR = _State.FrameBuf` → 判 `FBuf == None` +# → `memset(FBuf, …)`,把"局部变量"这条最可靠的通道用在最危险的一行上。 +# B. **0 帧/符号化互斥**:ASan 运行时会自己初始化 DbgHelp 做符号化,同进程不允许两种 +# 不兼容用法(实测 ASan 侧打印 "Failed to initialize DbgHelp!")。故本模块**不再把 +# dbghelp 当主路径**,改为: +# * 主路径 = `WalkRbpChain`(纯 RBP 链,**零 dbghelp 依赖**); +# * **第二兜底 = `ScanStack` 栈扫描**(零 dbghelp、且**不依赖 RBP / 帧指针**): +# RBP 链只拿到当前 RIP(<=1 帧)时启用(`walk: fallback=stack_scan`),见第十二节; +# * dbghelp 仅作**可选加强**:只有 RBP 链与栈扫描都拿不到更多帧(<=1)且 dbghelp +# 确实可用时才补一次 `WalkViaDbgHelp`。 +# * 降级**必须给原因**(不静默 0 帧):`[diag] symwalk=disabled reason=…`, +# reason ∈ {compile_time_switch, dbghelp_conflict_with_asan, syminitialize_failed, +# callback_missing, null_frame_buf};ASan 探测用 GetModuleHandleA 两个常见 +# ASan 运行时名(clang_rt / libclang_rt 前缀)。 +# `WalkRbpChain` 同步加固(升级为主路径后它必须"绝不把卡死变成崩溃"): +# * 读 [rbp]/[rbp+8] 一律走 `wm.ReadProcessMemory`(**不直接解引用**野 rbp); +# * **每一步无条件打 `[diag] rbp:` 原始读数 + 判定**(先取证后决策,含被拒步;见第十二节); +# * 自洽校验:非 0 / 8 字节对齐 / `[rbp] > rbp` / 单步跨度 <= RBP_MAX_STEP / +# 返回地址落在主模块基址区间 [Base, Base+TRACEBACK_MODULE_MAX) 内才**接受**该帧。 +# +# ----------------------------------------------------------------------------- +# 十二、本轮新增:RBP 链逐步原始读数 + 栈扫描兜底(应答"栈走不长") +# ----------------------------------------------------------------------------- +# 现场(用户实测,`#0` 已正确解析:RIP 在主模块、映射/查表/帧 #0 全对):`#1 = 0x121fa` +# ——远小于主模块基址、也不是栈地址 ⇒ 链在第 1 步即断。⇒ 先把"断在哪一步、为什么断" +# 变成可读数据,再补一条**不依赖 RBP**的路径。 +# A. **逐步原始读数**(`WalkRbpChain` 内,前缀 `[diag] rbp:`,**先于任何 reject 早退**, +# TRACEBACK_DIAG=1 时才输出): +# frame= rbp=0x… [rbp]=0x… [rbp+8]=0x… ← 读到的原始 8 字节值 +# frame= rpm_ok=<0/1> rpm_err= accept=<0/1> reject_reason=<…> +# reject_reason ∈ {rbp_zero, not_aligned, rpm_failed, ret_zero, ret_out_of_module, +# not_growing, step_too_large};被接受时 reason=`accept`、accept=1。 +# 仅被接受的步才 FramePush 入帧缓冲(旧实现在校验前就输出,会把 `0x121fa` 这类垃圾值当成 +# 一帧 external 打出来,误导读者)。三种断链根因据此一眼可分:读失败 / 值本身不是返回 +# 地址(帧指针未保留)/ 被自洽校验档掉。 +# B. **栈扫描兜底**(`ScanStack`):RBP 链 <=1 帧时启用(`walk: fallback=stack_scan`)。 +# 在 [RSP, RSP+2MiB) 内按 8 字节步长遍历,**4 KiB/块整块读**(失败即停并打地址/err); +# 候选 = 落在主模块区间、且**相邻 8 字节字(A-8 或 A+8)像栈地址**的字;按值去重、 +# 上限 64(诊断打 `scan_candidates=`);推断帧入缓冲时带 Scanned=1,打印时在帧行末尾加 +# ` [inferred]`(` File "…", line 0, in [inferred]`)明确标注**扫描推断** +# (可含误报,不与权威帧混淆,见第十三节)。全部读取只经 ReadProcessMemory, +# 绝不直接解引用主线程内存(诊断设施不许把"卡死"变成"崩溃")。 +# C. **帧指针是否保留**(调查结论与 TPC 侧修法落点,本模块只报不改): +# * 单一 llc 调用点 = `TransPyC/lib/Projectrans/Phase2Translator.py:2160 _llc_compile`, +# 它在 `--traceback` 打开时把 `-frame-pointer=all` 追加到命令(2187-2193); +# App 模块(如 `TransPyV/App/lib/core/Gen/Expr/ExprGenOps.vp`,模块 sha1 +# `8a96a2885bae09ca`)走的是**同一个**调用点(2608 的 _dso.ll / 2319 的内联合并), +# 故 flag 的落点本身没问题。 +# * 缓存失效:App 模块 obj = `output/{sha1}.obj`,`_compile_ll_files` 见其存在即跳过 +# (2621),但 `--traceback` 时 `hotspot_on`(1144)为真 ⇒ 1196-1204 会删除旧 obj +# 强制重编;include 模块另有隔离目录 `includes.binary_traceback`(3097-3099)+ +# 旧 obj 删除(3181-3190)。⇒ App/include 都不会静默复用"无帧指针的旧 obj"。 +# * 交付结论:若 `[diag] rbp:` 显示 `[rbp+8]` 仍不是返回地址(`ret_out_of_module`), +# 则说明 `-frame-pointer=all`(llc 默认值级别的开关)没有生效,最稳的改法是在**IR 里 +# 按函数显式标注** `"frame-pointer"="all"` —— 落点 +# `TransPyV/App/lib/core/Gen/Func/FuncGenBase.vp`(属性拼装 `_append_llvm_attr`,154-181) +# 与 `TransPyC/lib/core/Handles/HandlesFunctions.py:1735`(`func.attributes.add('noredzone')` +# 旁)—— 让帧指针成为**函数属性**而非 llc 命令行默认值,从根上免疫"llc 是否采纳默认值"。 +# +# ----------------------------------------------------------------------------- +# 十三、本轮:真 Python traceback 风格输出 + TRACEBACK_DIAG 开关 +# ----------------------------------------------------------------------------- +# A. 输出形状(默认,TRACEBACK_DIAG 未开)——整段 traceback 形如: +# Traceback (most recent call last): +# File "D:\...\App\main.vp", line 238, in main +# w_upper: str = w_str.upper() +# File "<0x7ff6... +0x1a2b no source line mapping>", line 0, in +# File "<0x7ffb... external>", line 0, in [inferred] +# * **帧序反过来了**:回溯从最内层(当前 RIP)向调用者走,收集顺序 = 从内到外,打印时 +# 逆序 ⇒ **最外层(CRT/调用者)在前、最内层最后**(与 Python traceback 一致)。故必须 +# 先把帧收集进缓冲(FramePush),走栈结束后再逆序打印(DumpMainThread)。 +# * 命中源位置的帧打两行:第一行 2 空格缩进 `File "路径", line N, in 函数名`(函数名取不到 +# 写 ``);第二行 4 空格缩进 + 该行源码——**运行期重开该文件**顺序读第 N 行 +# (去行首尾空白、超长按 TRACEBACK_LINE_CAP 截断);**打不开 / 行号越界 / 读失败则整行 +# 不打**(无占位、无报错,静默跳过),实现见 EmitSourceText(栈上缓冲,回调线程零堆分配)。 +# * ScanStack 推断帧在帧行末尾加 ` [inferred]`(保留"可能是误报"的信息,不再用 `~` 前缀)。 +# * 未命中映射的帧 → `File "<0x<地址> +0x no source line>", line 0, in ` +# (地址+RVA 保留在同一行);落在主模块之外的帧 → `File "<0x<地址> external>", line 0, in `。 +# B. 帧缓冲:TracebackState.FrameAddr / FrameScan(`t.CArray[uint64/int32, TRACEBACK_FRAME_BUF]`, +# `TRACEBACK_FRAME_BUF = TRACEBACK_MAX_FRAMES + TRACEBACK_SCAN_MAX_HITS = 128`), +# **内嵌于状态对象** ⇒ 由 Install 的 `TracebackState.__sizeof__()` 分配覆盖,无需独立 malloc、 +# 回调线程零分配。缓冲满时新帧直接丢弃(不越界),最终 `frames=` 以实际收集数为准。 +# C. **TRACEBACK_DIAG 开关**:读环境变量 `TRACEBACK_DIAG`,值 == "1" 才开;**每进程只读一次 +# 并缓存**(_DiagFlag -1/0/1,见 DiagEnabled)。关闭(默认)时不输出任何 `[diag] ...` / +# `srcline map: ...` 行,也不加 ` [dump#] ` 行号前缀(见第十节 B),使 dump 成为干净的 +# traceback;打开时诊断信息量与既有一致(含逐帧 `[diag] rbp:` / `[diag] scan:` / symwalk 行)。 +# 取值 API = kernel32!GetEnvironmentVariableA(本模块顶部自声明,见第八节)。 +# D. 转储序号与落文件提示移到整段 traceback **之后**(` [traceback] dump seq=… also appended +# to .stackdump.txt` 行),不再插在帧中间;`Ctrl+C` 转储后返回 FALSE(进程退出)、 +# `Ctrl+Break` 转储后返回 TRUE(继续运行)语义不变。 +# ============================================================================= +# This module needs kernel32!GetEnvironmentVariableA for the TRACEBACK_DIAG switch, but +# includes/w32/win32base.vp does not declare it (the repository declares it only in +# VET/App/lib/VDirs.vp:45). Declared here with the same style; kernel32 is linked anyway, +# so both TPC and TPV resolve it. +# DWORD GetEnvironmentVariableA(LPCSTR lpName, LPSTR lpBuffer, DWORD nSize); +extern def GetEnvironmentVariableA(lpName: LPCSTR, lpBuffer: CHARPTR, nSize: DWORD) -> DWORD + + +# ---- 编译期开关 ------------------------------------------------------------- + +# 1 = 允许在**可选加强**路径上使用 dbghelp!StackWalk64(**产物必须链接 -ldbghelp**); +# 注意:本轮起主回溯路径是 WalkRbpChain(零 dbghelp 依赖),dbghelp 只在 RBP 链 +# 拿不到更多帧且进程环境允许(无 ASan 冲突)时才补一次(见模块头第十一节 B)。 +# 0 = 完全不用 dbghelp。切换机制:常量比较在 HandleIf 处被常量折叠,凡出现在 +# `if TRACEBACK_USE_DBGHELP != 0:` 分支里的语句(Install 的 SymInitialize/GetProcAddress +# 段、WalkViaDbgHelp 的整个函数体、DumpMainThread 的可选加强调用)在置 0 时整体不生成代码 +# ——范式与依据同 includes/memhub.vp 的 MEMBUDDY_THREAD_SAFE 注释("未取分支整体不生成 +# 代码 ⇒ 函数体为空")。故置 0 后本模块不留任何 dbghelp 链接期符号。 +TRACEBACK_USE_DBGHELP: t.CDefine = 1 + +# Trigger event values: see includes/w32/win32console.vp (CTRL_C_EVENT=0 / CTRL_BREAK_EVENT=1). + +# ---- Sidecar (.stackmap) format ---- +# 20-byte header (magic "TPCS" read little-endian as u32); 20-byte records. +TRACEBACK_MAP_MAGIC: t.CDefine = 0x53435054 # "TPCS" ('T','P','C','S' little-endian) +TRACEBACK_MAP_VERSION: t.CDefine = 1 +TRACEBACK_MAP_HEADER: t.CDefine = 20 # header size in bytes +TRACEBACK_ENTRY_SIZE: t.CDefine = 20 # bytes per record +TRACEBACK_ENTRY_FILEOFF: t.CDefine = 8 # file_off offset within a record +TRACEBACK_ENTRY_LINE: t.CDefine = 12 # line offset within a record +TRACEBACK_ENTRY_FUNCOFF: t.CDefine = 16 # func_off offset within a record +# Sidecar size cap (defence only: a corrupt entry_count/str_bytes must not demand a huge alloc). +TRACEBACK_MAP_LIMIT: t.CDefine = 67108864 # 64 MiB + +# ---- Runtime buffers and limits ---- +TRACEBACK_PATH_CAP: t.CDefine = 4096 # exe path + ".stackmap" buffer +TRACEBACK_LINE_CAP: t.CDefine = 1024 # single output line buffer +TRACEBACK_MAX_FRAMES: t.CDefine = 64 # max frames printed +TRACEBACK_RBP_MAX_STEP: t.CDefine = 0x100000 # max gap between adjacent rbp values (1 MiB) +# RBP chain sanity check: a return address is accepted only within [Base, Base+this). +# 128 MiB is far above any real PE image, a coarse "obviously out of range" filter. +TRACEBACK_MODULE_MAX: t.CDefine = 0x8000000 + +# ---- Collected-frame buffer (Python-traceback output) ---- +# Frames are collected innermost-first by the walk paths and printed in reverse (outermost +# first), so the buffer must hold every frame the walk paths can produce: the RBP chain +# (TRACEBACK_MAX_FRAMES) plus the ScanStack candidates (TRACEBACK_SCAN_MAX_HITS). It is an +# inline array inside TracebackState, so Install's TracebackState.__sizeof__() allocation +# covers it (the callback thread allocates nothing). Frames beyond the limit are dropped. +TRACEBACK_FRAME_BUF: t.CDefine = 128 # = TRACEBACK_MAX_FRAMES + TRACEBACK_SCAN_MAX_HITS +# A hit frame re-reads its source line from the .vp file at dump time, sequentially in fixed +# stack chunks (no heap allocation from the callback thread). +TRACEBACK_SRC_CHUNK: t.CDefine = 4096 # bytes per ReadFile while scanning for a source line + +# ---- Stack scan fallback (ScanStack) ---- +# Scan window [RSP, RSP + this) towards higher addresses (caller direction). +TRACEBACK_SCAN_MAX_BYTES: t.CDefine = 0x200000 # 2 MiB +# Chunked reads: one ReadProcessMemory per chunk instead of one per 8 bytes. +TRACEBACK_SCAN_CHUNK_BYTES: t.CDefine = 4096 # 4 KiB per chunk +TRACEBACK_SCAN_CHUNK_WORDS: t.CDefine = 512 # 4096 / 8 u64 words per chunk +TRACEBACK_SCAN_MAX_HITS: t.CDefine = 64 # max candidate return addresses (scan_candidates=) +TRACEBACK_SUSPEND_FAILED: t.CDefine = 0xFFFFFFFF # SuspendThread failure return value +# The suffixes ".stackmap" (10) and ".stackdump.txt" (16, incl. NUL) share the same 16-byte +# slack check in Install; no extra constant is needed. +# FILE_APPEND_DATA(0x0004) is not defined by wf; open the dump file with it so every WriteFile +# appends to the end (combined with OPEN_ALWAYS to create the file when missing). +TRACEBACK_FILE_APPEND_DATA: t.CDefine = 0x00000004 + + +class TracebackState(): + """The module's single global state object (allocated once by Install; read-only after). + + All buffers are allocated during Install (the callback thread allocates nothing) and are + never freed: this is a process-lifetime diagnostic facility (there is no uninstall path). + """ + + Installed: int # 1 = SetConsoleCtrlHandler registered + MapOk: int # 1 = sidecar loaded and fully validated (address -> source line) + SymReady: int # 1 = SymInitialize succeeded (dbghelp usable) + ModInited: int # 1 = dbghelp module table contains the main module + Dumping: int # reentrancy flag (readable mirror of the _DumpLock atomic lock) + Seq: int # dump sequence number, +1 per dump entered (concurrency evidence) + ReadMemFailed: int # 1 = the read-memory callback has failed at least once + MissReported: int # 1 = first "source line not hit" diagnostic already printed + ReadMemAddr: uint64 # address of the first failed read-memory call + ReadMemSize: uint32 # byte count requested by the first failed read-memory call + ReadMemErr: uint32 # GetLastError of the first failed read-memory call + MainThreadId: ULONG # thread that ran Install (= the main thread) + OutHandle: HANDLE # dump output target (stderr, falling back to stdout) + EntryCount: uint32 # number of records + StrBytes: uint32 # string pool byte count + MapBase: bytes # sidecar file base (stdlib.malloc) + StrBase: bytes # string pool base = MapBase + 20 + EntryCount*20 + FuncTableAddr: VOIDPTR # dbghelp!SymFunctionTableAccess64 (StackWalk64 callback 2) + ModuleBaseAddr: VOIDPTR # dbghelp!SymGetModuleBase64 (StackWalk64 callback 3) + ContextBuf: VOIDPTR # CONTEXT buffer (calloc, 16-byte aligned) + FrameBuf: VOIDPTR # STACKFRAME64 buffer (calloc) + LineBuf: bytes # output line buffer + FrameAddr: t.CArray[uint64, TRACEBACK_FRAME_BUF] # collected frames, innermost first + FrameScan: t.CArray[int32, TRACEBACK_FRAME_BUF] # 0 = authoritative, 1 = inferred (ScanStack) + FrameCount: int # frames collected so far (also the printed frame count) + + +# Sole global state: None = not installed (zero side effects while the sidecar is absent). +# Pointer + lazy allocation instead of a module-level object constant, which would emit a +# constructor that may run after Install and wipe the state it wrote. +_State: rawptr[TracebackState] = None + + +# ---- Dump atomic lock / locked output handle ---- +# _DumpLock: dump reentrancy lock, 0 = free / 1 = held. AtomicTryEnterDump uses one inline +# `lock cmpxchg` to test-and-set it, closing the read-modify-write race of a plain flag. +# _State.Dumping is its readable mirror. +_DumpLock: uint32 = 0 + +# _DumpOut: output handle locked for the whole dump. Chosen once at DumpMainThread entry so a +# single dump never splits across stdout/stderr. EmitLine prefers it; Install sets it too. +_DumpOut: HANDLE = None + +# _HandlerReg: "register once" guard for SetConsoleCtrlHandler. +_HandlerReg: int = 0 + +# _DumpFile: append handle of the dump file (OpenAlways + FILE_APPEND_DATA). When None/invalid +# only the file write is skipped; console output is unaffected. +_DumpFile: HANDLE = None + +# _DumpFileFailWarned: report a dump-file write failure only once. +_DumpFileFailWarned: int = 0 + +# _DumpLineNo: global output line counter, incremented by EmitLine and written as the +# " [dump#] " prefix of every line (only while TRACEBACK_DIAG is enabled). Repeated +# numbers prove concurrent writes to the shared LineBuf; strictly increasing numbers mean any +# truncation is only terminal wrapping. +_DumpLineNo: int = 0 + +# _DiagFlag: cached TRACEBACK_DIAG state; -1 = not read yet, 0 = off, 1 = on (read once/process). +_DiagFlag: int = -1 + + +def DiagEnabled() -> int: + """Return 1 when the environment variable TRACEBACK_DIAG equals "1" (read once, then cached). + + With the switch off (default) every "[diag] ..." / "srcline map: ..." line and the per-line + " [dump#] " counter prefix are suppressed, so a dump is a plain Python-style traceback; + with it on the previous diagnostic detail is kept. The single cached global keeps both the + console and the file output consistent within one process. + """ + global _DiagFlag + if _DiagFlag >= 0: + return _DiagFlag + Val: t.CArray[char, 8] + Val[0] = char(0) + R: DWORD = GetEnvironmentVariableA("TRACEBACK_DIAG", &Val, DWORD(8)) + _DiagFlag = 0 + if R == 1: + if Val[0] == char(49): # '1' + _DiagFlag = 1 + return _DiagFlag + + +# ============================================================ +# Output (WriteFile, bypassing CRT stream locks; see header section 7) +# Defined before the sidecar reader so load-time diagnostics share this single path. +# Console text is always ASCII (code-page independent, greppable); the same bytes are also +# appended to .stackdump.txt. +# ============================================================ + +def WriteConsoleOnly(N: int): + """Write the first N bytes of LineBuf to the console only (no file). + + Split out of EmitLine so the "dump file write failed" notice can reach the console without + re-entering EmitLine (and thus the file write), avoiding recursion. Handle rules match. + """ + if _State == None or N <= 0: + return + H: HANDLE = _DumpOut + if H == None or H == INVALID_HANDLE_VALUE: + H = _State.OutHandle + if H == None or H == INVALID_HANDLE_VALUE: + return + Wr: ULONG = 0 + wf.WriteFile(H, _State.LineBuf, ULONG(N), &Wr, None) + + +def EmitToFile(N: int): + """Append the first N bytes of LineBuf to the dump file (raw UTF-8, code-page independent). + + The file handle is opened at Install (FILE_APPEND_DATA + OPEN_ALWAYS) and kept open; the + callback thread opens nothing and allocates nothing. A write failure never affects the + console (EmitLine already wrote it); it is reported once, written straight to the console + handle via a local stack buffer (not through EmitLine) to avoid recursion. + """ + global _DumpFileFailWarned + if _State == None or N <= 0: + return + if _DumpFile == None or _DumpFile == INVALID_HANDLE_VALUE: + return + Wr: ULONG = 0 + if wf.WriteFile(_DumpFile, _State.LineBuf, ULONG(N), &Wr, None) != 0: + return + if _DumpFileFailWarned != 0: + return + _DumpFileFailWarned = 1 + LB: t.CArray[char, 160] + M: int = stdio.snprintf(&LB, t.CSizeT(160), + "[traceback] WARN: stack dump file write failed (further file-write failures not reported)\n") + if M <= 0: + return + H: HANDLE = _DumpOut + if H == None or H == INVALID_HANDLE_VALUE: + H = wf.GetStdHandle(wf.STD_ERROR_HANDLE) + if H == None or H == INVALID_HANDLE_VALUE: + return + W2: ULONG = 0 + wf.WriteFile(H, &LB, ULONG(M), &W2, None) + + +def EmitLine(N: int): + """Write the first N bytes of LineBuf to console + dump file (N is snprintf's return value). + + The single output choke point for EmitText / EmitFrame / every diagnostic: + 0) line-number prefix " [dump#] " (see _DumpLineNo), only while TRACEBACK_DIAG is on: + inserted in place via string.memmove so callers are unchanged and no extra buffer is + used; a repeated k proves concurrent writes to LineBuf, a strictly increasing k means + display wrapping. With the switch off the bytes are written verbatim, keeping the + Python-traceback frame lines clean. + 1) console (WriteFile, bypassing CRT stream locks): prefers the dump-locked _DumpOut, + falling back to _State.OutHandle during load (both resolved once at Install). + 2) dump file (EmitToFile): appends the same bytes (UTF-8, code-page independent). + A file write failure never blocks the console (see EmitToFile); handles are never + re-resolved mid-dump. + """ + global _DumpLineNo + if _State == None or N <= 0: + return + if N > TRACEBACK_LINE_CAP - 1: + N = TRACEBACK_LINE_CAP - 1 + # Line-number prefix: shift the buffer right by the prefix length, then write the prefix. + if DiagEnabled() != 0: + _DumpLineNo = _DumpLineNo + 1 + Pfx: t.CArray[char, 32] + Pl: int = stdio.snprintf(&Pfx, t.CSizeT(32), " [dump#%d] ", _DumpLineNo) + if Pl > 0: + # Keep room for the trailing '\0' after prefixing. + if N + Pl > TRACEBACK_LINE_CAP - 1: + N = TRACEBACK_LINE_CAP - 1 - Pl + # Move the trailing '\0' too (N+1) so the buffer stays NUL-terminated. + string.memmove(t.CPtr(_State.LineBuf + int(Pl)), t.CPtr(_State.LineBuf), sizet(N + 1)) + string.memmove(t.CPtr(_State.LineBuf), &Pfx, sizet(Pl)) + N = N + Pl + WriteConsoleOnly(N) + EmitToFile(N) + + +def EmitText(Text: str): + """Write one constant line with no format specifiers (same buffer / WriteFile path).""" + if _State == None or _State.LineBuf == None: + return + N: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), "%s", Text) + EmitLine(N) + + +def EmitEarly(Text: str): + """Install-stage early diagnostic (_State / LineBuf not ready yet) on the same output path. + + With _State and LineBuf ready it forwards to EmitText (same buffer, same handle); otherwise + it writes via WriteFile(GetStdHandle(STD_ERROR_HANDLE)) directly (the dump's own sink), so + stdout and stderr never interleave. Install is not a callback thread, so resolving a handle + once here is harmless. + """ + if _State != None and _State.LineBuf != None: + EmitText(Text) + return + H: HANDLE = wf.GetStdHandle(wf.STD_ERROR_HANDLE) + if H == None or H == INVALID_HANDLE_VALUE: + H = wf.GetStdHandle(wf.STD_OUTPUT_HANDLE) + if H == None or H == INVALID_HANDLE_VALUE: + return + Len: sizet = string.strlen(Text) + if Len == 0: + return + Wr: ULONG = 0 + wf.WriteFile(H, Text, ULONG(Len), &Wr, None) + + +def EmitEarlyCode(Text: str, Code: ULONG): + """EmitEarly variant that appends a GetLastError code (same output path).""" + if _State != None and _State.LineBuf != None: + N: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), "%s(GetLastError=%u)\n", Text, Code) + EmitLine(N) + return + LB: t.CArray[char, 256] + N2: int = stdio.snprintf(&LB, t.CSizeT(256), "%s(GetLastError=%u)\n", Text, Code) + if N2 <= 0: + return + H: HANDLE = wf.GetStdHandle(wf.STD_ERROR_HANDLE) + if H == None or H == INVALID_HANDLE_VALUE: + H = wf.GetStdHandle(wf.STD_OUTPUT_HANDLE) + if H == None or H == INVALID_HANDLE_VALUE: + return + Wr: ULONG = 0 + wf.WriteFile(H, &LB, ULONG(N2), &Wr, None) + + +def AtomicTryEnterDump() -> int: + """Atomically test-and-set _DumpLock (0→1); returns the previous value (0 = acquired). + + One inline `lock cmpxchg`: eax = expected old value 0, ecx = new value 1. + Only this module-level lock and registers are touched; nothing is allocated. + """ + Old: uint32 = 0 + __asm__(f"""xor eax, eax + mov ecx, 1 + lock cmpxchg [{asminp(&_DumpLock, ASM_DESCR.REG_ANY)}], ecx + mov {asmout(Old, ASM_DESCR.OUTPUT_REG)}, eax""", + op=[ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RCX, ASM_DESCR.CLOBBER_MEMORY, ASM_DESCR.CLOBBER_CC]) + return int(Old) + + +def AtomicLeaveDump(): + """Release the dump lock: _DumpLock -> 0 (and mirror _State.Dumping -> 0). + + Only the holder reaches here; another callback thread seeing 1 returns immediately, so a + plain write of 0 is safe. + """ + global _DumpLock + _DumpLock = 0 + if _State != None: + _State.Dumping = 0 + + +# ============================================================ +# Sidecar reader (little-endian, byte by byte, avoids unaligned access) +# ============================================================ + +def ReadU32(Buf: bytes, Off: int) -> uint32: + """Read a little-endian u32 from Buf[Off..Off+3] (Buf is an unsigned byte base). + + Each byte must be masked with 0xFF: Viper's `Buf[i]` is a signed char, so a byte >= 0x80 + would sign-extend through the u32 cast (0xE8 -> 0xFFFFFFE8) and corrupt the high bits, + blowing up `20+count*20+str_bytes` and making the sidecar unloadable. Masking after the + cast yields 0..255 whether the cast sign- or zero-extends. + """ + Val: uint32 = uint32(Buf[Off]) & uint32(0xFF) + Val = Val | ((uint32(Buf[Off + 1]) & uint32(0xFF)) << 8) + Val = Val | ((uint32(Buf[Off + 2]) & uint32(0xFF)) << 16) + Val = Val | ((uint32(Buf[Off + 3]) & uint32(0xFF)) << 24) + return Val + + +def ReadU64(Buf: bytes, Off: int) -> uint64: + """Read a little-endian u64 as two u32 halves (avoids an 8-byte unaligned read).""" + Lo: uint64 = uint64(ReadU32(Buf, Off)) + Hi: uint64 = uint64(ReadU32(Buf, Off + 4)) + return Lo | (Hi << 32) + + +# ---- Validation-failure diagnostics (each prints and returns 0) ---- + +def MapFailPlain(Size: int64, Reason: str) -> int: + """Validation failed before the header could be read: print reason + actual file size.""" + if _State == None or _State.LineBuf == None: + return 0 + N: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + "[traceback] WARN: .stackmap invalid -- %s (file %lld bytes), addresses only this run\n", + Reason, Size) + EmitLine(N) + return 0 + + +def MapFailHdr(Size: int64, Reason: str, Head: bytes) -> int: + """Validation failed with the header read: also print the actual header fields.""" + if _State == None or _State.LineBuf == None: + return 0 + N: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + "[traceback] WARN: .stackmap invalid -- %s" + " (read magic=0x%x version=%u entry_count=%u str_count=%u str_bytes=%u; file %lld bytes)," + " addresses only this run\n", + Reason, ReadU32(Head, 0), ReadU32(Head, 4), ReadU32(Head, 8), ReadU32(Head, 12), + ReadU32(Head, 16), Size) + EmitLine(N) + return 0 + + +def MapFailEntry(Size: int64, Index: int, Reason: str, Got: uint64) -> int: + """Record-table validation failed: print the offending index and value.""" + if _State == None or _State.LineBuf == None: + return 0 + N: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + "[traceback] WARN: .stackmap invalid -- %s (entry %d, value=0x%llx; file %lld bytes)," + " addresses only this run\n", + Reason, Index, Got, Size) + EmitLine(N) + return 0 + + +def EntryRva(Index: int) -> uint64: + """RVA of record Index (record area starts at MapBase + 20). + + The bare `_State` null-check below is REQUIRED: the attribute layer registers `_State` per + function before it can resolve this module-level `rawptr[TracebackState]`; without it + `_State.MapBase` is silently folded to null (see header section 11 A). It also guards + against an uninstalled state (returns 0). + """ + if _State == None: + return 0 + Off: int = TRACEBACK_MAP_HEADER + Index * TRACEBACK_ENTRY_SIZE + return ReadU64(_State.MapBase, Off) + + +def LookupRva(Rva: uint64) -> int: + """Look up Rva in the ascending table: return the largest index with rva <= Rva, else -1. + + Granularity matches the writing side (TPC `_WriteStackMap`): the table stores DWARF + line-start addresses, and a stack-frame return address almost never lands exactly on one, + so a floor search is required; otherwise every frame degrades to +0x...(unknown). + Hit = some rva <= target (the largest such); miss = target below the smallest rva. + + The bare `_State` null-check below is REQUIRED for the same reason as EntryRva: without it + `_State.EntryCount` folds to null (Hi = -1, the search never runs). + """ + if _State == None: + return -1 + Lo: int = 0 + Hi: int = int(_State.EntryCount) - 1 + Best: int = -1 + while Lo <= Hi: + Mid: int = Lo + (Hi - Lo) / 2 + E: uint64 = EntryRva(int(Mid)) + if E <= Rva: + Best = Mid + Lo = Mid + 1 + else: + Hi = Mid - 1 + return Best + + +def LoadMapFile(hFile: HANDLE, Size: int64) -> int: + """Read and validate the sidecar (header / ascending records / string offsets). 1 = MapOk. + + Any validation failure disables the map (returns 0): a broken table yields wrong line + numbers, worse than addresses only. This function prints exactly where it failed itself + (MapFailPlain/MapFailHdr/MapFailEntry, with the actual header values and file size). + + Field-by-field checks, aligned with the writing side (TPC `_WriteStackMap`): + magic little-endian u32 == 0x53435054 ("TPCS") + version u32 == 1 + entry_count part of `20+count*20+str_bytes <= file size` + str_bytes same (str_count is diagnostic only, not checked) + record rva strictly ascending + file_off < str_bytes and >= 1 + func_off == 0 means no name; otherwise < str_bytes + """ + if Size < int64(TRACEBACK_MAP_HEADER): + return MapFailPlain(Size, "file smaller than the 20-byte header") + if Size > int64(TRACEBACK_MAP_LIMIT): + return MapFailPlain(Size, "file exceeds the 64 MiB limit (header likely corrupt)") + Buf: bytes = stdlib.malloc(sizet(Size)) + if Buf == None: + return MapFailPlain(Size, "load buffer allocation failed (file too large or out of memory)") + Got: ULONG = 0 + if wf.ReadFile(hFile, Buf, ULONG(Size), &Got, None) == 0: + Err: ULONG = GetLastError() + stdlib.free(t.CPtr(Buf)) + if _State != None and _State.LineBuf != None: + RN: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + "[traceback] WARN: read .stackmap failed (GetLastError=%u)," + " addresses only this run\n", Err) + EmitLine(RN) + return 0 + if t.CSizeT(Got) != t.CSizeT(Size): + stdlib.free(t.CPtr(Buf)) + return MapFailPlain(Size, "read fewer bytes than the file size") + # Header: magic / version / entry_count / str_count / str_bytes + if ReadU32(Buf, 0) != uint32(TRACEBACK_MAP_MAGIC): + stdlib.free(t.CPtr(Buf)) + return MapFailHdr(Size, "magic is not \"TPCS\" (writer/reader endianness or offset mismatch)", Buf) + if ReadU32(Buf, 4) != uint32(TRACEBACK_MAP_VERSION): + stdlib.free(t.CPtr(Buf)) + return MapFailHdr(Size, "version is not 1 (stackmap written by another TPC version)", Buf) + Cnt: uint32 = ReadU32(Buf, 8) + StrCnt: uint32 = ReadU32(Buf, 12) + SBytes: uint32 = ReadU32(Buf, 16) + # Records + string pool must fit inside the file (u64 math to avoid u32 wrap). + Need: uint64 = uint64(TRACEBACK_MAP_HEADER) \ + + uint64(Cnt) * uint64(TRACEBACK_ENTRY_SIZE) \ + + uint64(SBytes) + if Need > uint64(Size): + stdlib.free(t.CPtr(Buf)) + return MapFailHdr(Size, "20+entry_count*20+str_bytes exceeds end of file " + "(header corrupt or file truncated)", Buf) + if SBytes == 0: + stdlib.free(t.CPtr(Buf)) + return MapFailHdr(Size, "str_bytes is 0 (string pool missing)", Buf) + # The last pool byte must be '\0' so any later %s print stays inside the pool. + PoolEnd: int = TRACEBACK_MAP_HEADER + int(Cnt) * TRACEBACK_ENTRY_SIZE + int(SBytes) - 1 + if Buf[PoolEnd] != 0: + stdlib.free(t.CPtr(Buf)) + return MapFailHdr(Size, "string pool does not end with '\\0' (pool corrupt)", Buf) + # Full-table check: strictly ascending rva + string offsets inside the pool. + Prev: uint64 = 0 + i: int = 0 + while i < int(Cnt): + Off: int = TRACEBACK_MAP_HEADER + int(i) * TRACEBACK_ENTRY_SIZE + Cur: uint64 = ReadU64(Buf, Off) + if i > 0 and Cur <= Prev: + stdlib.free(t.CPtr(Buf)) + return MapFailEntry(Size, int(i), + "record rva not strictly ascending (breaks binary search)", Cur) + Prev = Cur + FO: uint32 = ReadU32(Buf, Off + TRACEBACK_ENTRY_FILEOFF) + FN: uint32 = ReadU32(Buf, Off + TRACEBACK_ENTRY_FUNCOFF) + if uint64(FO) >= uint64(SBytes): + stdlib.free(t.CPtr(Buf)) + return MapFailEntry(Size, int(i), "file_off outside the string pool", uint64(FO)) + if FN != 0 and uint64(FN) >= uint64(SBytes): + stdlib.free(t.CPtr(Buf)) + return MapFailEntry(Size, int(i), + "func_off is neither 0 (no name) nor inside the string pool", + uint64(FN)) + i += 1 + _State.MapBase = Buf + _State.EntryCount = Cnt + _State.StrBytes = SBytes + _State.StrBase = Buf + (TRACEBACK_MAP_HEADER + int(Cnt) * TRACEBACK_ENTRY_SIZE) + _State.MapOk = 1 + return 1 + + +# ============================================================ +# Frame collection and single-frame printing (Python traceback style) +# ============================================================ + +def FramePush(Addr: uint64, Scanned: int) -> int: + """Append one frame (address + inferred flag) to the dump buffer; 1 = stored, 0 = full. + + The walk paths call this innermost-first (the order they discover frames); the caller prints + the buffer in reverse afterwards, giving the outermost-first Python traceback order. When the + buffer is full the new frame is dropped (no overflow). The bare `_State` null-check is + REQUIRED (it touches _State.FrameAddr). + """ + if _State == None: + return 0 + if _State.FrameCount >= TRACEBACK_FRAME_BUF: + return 0 + _State.FrameAddr[_State.FrameCount] = Addr + _State.FrameScan[_State.FrameCount] = int32(Scanned) + _State.FrameCount = _State.FrameCount + 1 + return 1 + + +def EmitSourceText(Path: str, LineNo: int): + """Print the 4-space-indented text of line LineNo from Path (console + dump file). + + Best effort: the file is read sequentially in fixed stack chunks (no heap allocation -- the + callback thread must not take the CRT heap lock the suspended main thread may hold). Leading + whitespace is skipped while collecting and trailing whitespace is trimmed afterwards, so the + printed text is the source line with its leading/trailing whitespace removed. Any failure -- + the file cannot be opened, a read fails, or LineNo is past EOF -- prints nothing at all (no + placeholder line, no error). + """ + if _State == None or _State.LineBuf == None or LineNo <= 0: + return + h: HANDLE = wf.CreateFileA(Path, wf.GENERIC_READ, wf.FILE_SHARE_READ | wf.FILE_SHARE_WRITE, + t.CPtr(0), wf.OPEN_EXISTING, wf.FILE_ATTRIBUTE_NORMAL, t.CPtr(0)) + if h == INVALID_HANDLE_VALUE: + return + Chunk: t.CArray[char, TRACEBACK_SRC_CHUNK] + Src: t.CArray[char, TRACEBACK_LINE_CAP] + Line: int = 1 + Out: int = 0 # bytes collected for the target line + Done: int = 0 + while Done == 0: + Got: ULONG = 0 + if wf.ReadFile(h, &Chunk, ULONG(TRACEBACK_SRC_CHUNK), &Got, None) == 0: + CloseHandle(h) + return + if Got == 0: + break # EOF: the requested line does not exist + k: int = 0 + while k < int(Got): + Ch: int = int(Chunk[int(k)]) & 0xFF + if Ch == 10: # '\n' ends the current line + if Line == LineNo: + Done = 1 + break + Line += 1 + k += 1 + continue + if Line == LineNo and Ch != 13: + if Out > 0 or (Ch != 32 and Ch != 9): + if Out < TRACEBACK_LINE_CAP - 8: + Src[Out] = char(Ch) + Out += 1 + k += 1 + CloseHandle(h) + if Out == 0: + return # out of range or blank line: print nothing + while Out > 0: + E: int = int(Src[Out - 1]) & 0xFF + if E != 32 and E != 9: + break + Out -= 1 + if Out == 0: + return + Src[Out] = char(0) + N: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), " %s\n", Src) + EmitLine(N) + + +def EmitFrame(Addr: uint64, Base: uint64, Scanned: int): + """Print one collected frame in Python traceback style. + + Hit frame : ` File "", line , in ` and, when the source line can be re-read + at dump time, a second ` ` line (see EmitSourceText). + Unmapped : ` File "<0xaddr +0xrva no source line mapping>", line 0, in `. + Outside : ` File "<0xaddr external>", line 0, in `. + Scanned (inferred, from ScanStack) frames carry a trailing " [inferred]": they may be false + positives and stay distinguishable from authoritative frames. + + The bare `_State` null-check below is REQUIRED: this function uses `_State.LineBuf` (snprintf + target) and `_State.MapOk`, both of which would otherwise fold to null (see header section 11 A). + """ + if _State == None: + return + Tail: str = "" + if Scanned != 0: + Tail = " [inferred]" + N: int = 0 + if Base == 0 or Addr < Base: + # Outside the main image (DLL / system / corrupt frame): address only. + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " File \"<0x%llx external>\", line 0, in %s\n", Addr, Tail) + EmitLine(N) + return + Rva: uint64 = Addr - Base + if _State.MapOk == 0: + # No map (sidecar missing/corrupt): keep the address + RVA in the same File line. + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " File \"<0x%llx +0x%llx no source line mapping>\", line 0, in %s\n", + Addr, Rva, Tail) + EmitLine(N) + return + Hit: int = LookupRva(Rva) + if Hit < 0: + # Table loaded but not hit: report why (empty table / rva below the first entry), once. + if DiagEnabled() != 0: + if _State.MissReported == 0: + _State.MissReported = 1 + if _State.EntryCount == 0: + EmitText(" srcline map: rva not hit -- map is empty (0 entries)\n") + else: + First: uint64 = EntryRva(0) + Last: uint64 = EntryRva(int(_State.EntryCount) - 1) + M: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " srcline map: rva 0x%llx not hit (%u entries: first rva=0x%llx last rva=0x%llx;" + " target < first => writer RVA base mismatch)\n", + Rva, _State.EntryCount, First, Last) + EmitLine(M) + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " File \"<0x%llx +0x%llx no source line mapping>\", line 0, in %s\n", + Addr, Rva, Tail) + EmitLine(N) + return + Off: int = TRACEBACK_MAP_HEADER + Hit * TRACEBACK_ENTRY_SIZE + FileOff: uint32 = ReadU32(_State.MapBase, Off + TRACEBACK_ENTRY_FILEOFF) + Line: uint32 = ReadU32(_State.MapBase, Off + TRACEBACK_ENTRY_LINE) + FuncOff: uint32 = ReadU32(_State.MapBase, Off + TRACEBACK_ENTRY_FUNCOFF) + FileName: str = str(_State.StrBase + int(FileOff)) + FuncName: str = "unknown" + if FuncOff != 0: + FuncName = str(_State.StrBase + int(FuncOff)) + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " File \"%s\", line %u, in %s%s\n", FileName, Line, FuncName, Tail) + EmitLine(N) + EmitSourceText(FileName, int(Line)) + + +# ============================================================ +# The two walk paths +# ============================================================ + +def ReadMemCb(hProcess: HANDLE, Addr: ULONGLONG, Buf: VOIDPTR, Size: DWORD, + pRead: t.CPtr) -> BOOL: + """This module's stand-in for the StackWalk64 memory callback (PREAD_PROCESS_MEMORY_ROUTINE64). + + Not kernel32!ReadProcessMemory directly: + (1) it returns only BOOL, and a callback returning 0 stops StackWalk64 immediately (the + "0 frames" symptom) while swallowing GetLastError; the first failure's address/size/ + error is recorded here and printed next to the first-frame diagnostic (once); + (2) dbghelp's prototype is `DWORD nSize` + `LPDWORD`, while kernel32's ReadProcessMemory is + `SIZE_T nSize` + `SIZE_T*` (both 8 bytes on x64); handing dbghelp's 4-byte LPDWORD to + kernel32 would write through 4 bytes on success. A local `sizet` receives the count, + written back to dbghelp's buffer as DWORD. + Passing this function by name as a t.CPtr mirrors SetConsoleCtrlHandler(CtrlHandler). + """ + Read: sizet = 0 + R: BOOL = wm.ReadProcessMemory(hProcess, t.CPtr(Addr), Buf, sizet(Size), &Read) + if pRead != None: + Rd: rawptr[uint32] = (rawptr[uint32])(pRead) + Rd[0] = uint32(Read) + if R == 0 and _State != None and _State.ReadMemFailed == 0: + # Record only the first failure (address/size/error); WalkViaDbgHelp prints it next to the + # first-frame return code so "callback returned 0 -> 0 frames" is visible at once. + _State.ReadMemFailed = 1 + _State.ReadMemAddr = uint64(Addr) + _State.ReadMemSize = uint32(Size) + _State.ReadMemErr = GetLastError() + return R + + +def WalkViaDbgHelp(hProcess: HANDLE, hThread: HANDLE, CtxPtr: VOIDPTR, + Base: uint64) -> int: + """dbghelp!StackWalk64 frame walk; returns the number of frames printed (0 = caller degrades). + + Optional enhancement path: the primary path is WalkRbpChain (no dbghelp). Called only when + the RBP chain found no more frames and the caller judged dbghelp usable (no ASan conflict, + see header section 11 B); on conflict the caller never enters. + + STACKFRAME64 fields are accessed through explicit sub-struct pointers (rawptr[dbg.ADDRESS64]): + the ADDRESS64 structures sit at offsets 0 / 16 / 32 / 48 (= AddrPC / AddrReturn / AddrFrame / + AddrStack, see includes/w32/win32dbghelp.vp). After zeroing, AddrPC/AddrFrame/AddrStack are + pre-filled with the context's Rip/Rbp/Rsp and Mode = AddrModeFlat(3); MachineType is passed on + every call. STACKFRAME64 has no size/field-count member (that is StackWalkEx's STACKFRAME_EX); + the repository's sizeof must simply match the C side (0xF8). First-frame evidence (return code, + returned AddrPC, SymFunctionTableAccess64 result) is produced unconditionally, and every return + prints a "[diag] walk: return reason=... frames=..." line. The whole body is under + `if TRACEBACK_USE_DBGHELP != 0`, so with the switch at 0 no dbghelp symbol is emitted. + """ + # A short diagnostic precedes every return so "was walk called, and which exit" is always + # visible. reason values: + # null_state global _State missing + # disabled compile-time switch TRACEBACK_USE_DBGHELP == 0 + # null_frame_buf frame buffer missing (Install-time calloc failed) + # null_context CONTEXT pointer null (bad caller argument) + # stackwalk_first_failed first StackWalk64 returned 0 (cause in the first-frame lines) + # stackwalk_failed a later StackWalk64 returned 0 (normal top of stack) + # addrpc_zero StackWalk64 succeeded but AddrPC == 0 + # frame_limit reached TRACEBACK_MAX_FRAMES + # + # The bare `_State` null-check below MUST be the first statement: the attribute layer + # registers `_State` per function before it can resolve its struct type; otherwise every + # `_State.` folds to null (header section 11 A). + if _State == None: + if DiagEnabled() != 0: + EmitText(" [diag] walk: return reason=null_state frames=0\n") + return 0 + if TRACEBACK_USE_DBGHELP == 0: + if DiagEnabled() != 0: + EmitText(" [diag] walk: return reason=disabled (TRACEBACK_USE_DBGHELP=0) frames=0\n") + return 0 + # The frame buffer goes through a local, the most reliable channel, pinning the riskiest + # memset to an already-checked value. + FBuf: VOIDPTR = _State.FrameBuf + if FBuf == None: + if DiagEnabled() != 0: + EmitText(" [diag] walk: return reason=null_frame_buf frames=0\n") + return 0 + if CtxPtr == None: + if DiagEnabled() != 0: + EmitText(" [diag] walk: return reason=null_context frames=0\n") + return 0 + Frames: int = 0 + Reason: str = "frame_limit" + if TRACEBACK_USE_DBGHELP != 0: + string.memset(FBuf, 0, dbg.STACKFRAME64.__sizeof__()) + ctx: rawptr[wp.CONTEXT] = (rawptr[wp.CONTEXT])(CtxPtr) + Pc: rawptr[dbg.ADDRESS64] = (rawptr[dbg.ADDRESS64])(FBuf) + Fm: rawptr[dbg.ADDRESS64] = (rawptr[dbg.ADDRESS64])(FBuf + 32) + St: rawptr[dbg.ADDRESS64] = (rawptr[dbg.ADDRESS64])(FBuf + 48) + Pc.Offset = ctx.Rip + Pc.Mode = uint32(dbg.ADDR_MODE_FLAT) + Fm.Offset = ctx.Rbp + Fm.Mode = uint32(dbg.ADDR_MODE_FLAT) + St.Offset = ctx.Rsp + St.Mode = uint32(dbg.ADDR_MODE_FLAT) + i: int = 0 + while i < TRACEBACK_MAX_FRAMES: + # Read before the call: StackWalk64 rewrites STACKFRAME64 in place. + InRip: uint64 = uint64(Pc.Offset) + InRsp: uint64 = uint64(St.Offset) + InRbp: uint64 = uint64(Fm.Offset) + Ok: BOOL = dbg.StackWalk64(uint32(dbg.IMAGE_FILE_MACHINE_AMD64), hProcess, hThread, t.CPtr(FBuf), + CtxPtr, ReadMemCb, _State.FuncTableAddr, _State.ModuleBaseAddr, None) + Cur: uint64 = uint64(Pc.Offset) + if i == 0 and DiagEnabled() != 0: + # First-frame evidence (before any failure test): return code + returned AddrPC + + # the SymFunctionTableAccess64(AddrPC) result; together they classify the "0 + # frames" cause (modinited / read-memory failure / context). + Fta: uint64 = 0 + if Cur != 0: + Fta = uint64(dbg.SymFunctionTableAccess64(hProcess, Cur)) + D0: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] StackWalk64 first: ret=%d addrpc=0x%llx fta=0x%llx\n", int(Ok), Cur, Fta) + EmitLine(D0) + D1: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] StackWalk64 in: machine=0x%x rip=0x%llx rsp=0x%llx rbp=0x%llx\n", + uint32(dbg.IMAGE_FILE_MACHINE_AMD64), InRip, InRsp, InRbp) + EmitLine(D1) + if Ok == 0: + if i == 0: + Reason = "stackwalk_first_failed" + else: + Reason = "stackwalk_failed" + if DiagEnabled() != 0: + D2: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] StackWalk64 returned 0 at frame %d (addrpc=0x%llx), stop\n", i, Cur) + EmitLine(D2) + if _State.ReadMemFailed != 0 and DiagEnabled() != 0: + # Right after the first-frame code, print the read-memory failure (one "0 + # frames" cause: the callback returned 0 and StackWalk64 stopped). + DR: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] readmem failed addr=0x%llx size=%u GetLastError=%u\n", + _State.ReadMemAddr, _State.ReadMemSize, _State.ReadMemErr) + EmitLine(DR) + break + if Cur == 0: + Reason = "addrpc_zero" + if DiagEnabled() != 0: + D3: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] StackWalk64 frame %d addrpc=0, stop\n", i) + EmitLine(D3) + break + FramePush(Cur, 0) + i += 1 + Frames = i + # Unified "leave walk" diagnostic covering every break and the normal end; Reason is set. + if DiagEnabled() != 0: + N: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] walk: return reason=%s frames=%d\n", Reason, Frames) + EmitLine(N) + return Frames + + +def WalkRbpChain(Rip: uint64, Rbp: uint64, Base: uint64) -> int: + """Primary walk path: collect RIP, then follow the RBP chain reading [rbp] (caller rbp) and + [rbp+8] (return address) frame by frame. + + While TRACEBACK_DIAG is enabled every iteration — including a rejected step — first emits two + ASCII `[diag] rbp:` lines: + line 1: `frame= rbp=0x... [rbp]=0x... [rbp+8]=0x...` (the raw 8-byte values) + line 2: `frame= rpm_ok=<0/1> rpm_err= accept=<0/1> reject_reason=...` + This makes "why the chain broke at step 1" directly readable (read failure / value not a + return address / rejected). `reject_reason` is one of: rbp_zero / not_aligned / rpm_failed / + ret_zero / ret_out_of_module / not_growing / step_too_large; an accepted step prints accept=1. + Only an accepted step calls FramePush (authoritative frame); rejected values go to diagnostics + only, never into the stack. + + * Zero dbghelp dependency (immune to ASan owning DbgHelp). + * All memory reads go through ReadProcessMemory, never dereferencing a wild rbp: a diagnostic + must not turn a hang into a crash. + * A step is accepted only if rbp is non-zero, 8-byte aligned, the read succeeds, [rbp+8] is a + non-zero address inside the main module [Base, Base+TRACEBACK_MODULE_MAX), the caller rbp + grows strictly ([rbp] > rbp), the step size <= TRACEBACK_RBP_MAX_STEP, and the frame limit is + not reached. (Base == 0 skips the range check.) + + The bare `_State` null-check below is REQUIRED: this function builds diagnostics into + `_State.LineBuf` (header section 11 A). + """ + if _State == None: + return 0 + FramePush(Rip, 0) + Cur: uint64 = Rbp + i: int = 1 + HProc: HANDLE = wp.GetCurrentProcess() + Two: t.CArray[uint64, 2] # [0] = [rbp] (caller rbp), [1] = [rbp+8] (return address) + while i < TRACEBACK_MAX_FRAMES: + # Raw readings + decision for this step, produced before any early reject. + Rd: sizet = 0 + RpmOk: int = 0 + RpmErr: ULONG = 0 + V0: uint64 = 0 # [rbp] caller rbp + V1: uint64 = 0 # [rbp+8] return address + Reason: str = "accept" + Reject: int = 0 # 0 = accept this step, 1 = reject (reason in Reason) + if Cur == 0: + Reason = "rbp_zero" + Reject = 1 + elif (Cur & uint64(7)) != 0: + Reason = "not_aligned" + Reject = 1 + else: + # Read 16 bytes via ReadProcessMemory (invalid/unreadable addresses return 0 without + # faulting); dereferencing a wild rbp directly would crash, which the primary path + # must never do. + OkRead: BOOL = wm.ReadProcessMemory(HProc, t.CPtr(Cur), &Two, sizet(16), &Rd) + if OkRead == 0 or t.CSizeT(Rd) < t.CSizeT(16): + # GetLastError must be read immediately after ReadProcessMemory. + RpmErr = GetLastError() + Reason = "rpm_failed" + Reject = 1 + else: + RpmOk = 1 + V0 = Two[0] + V1 = Two[1] + if V1 == 0: + Reason = "ret_zero" + Reject = 1 + elif Base != 0: + if V1 < Base or V1 >= Base + uint64(TRACEBACK_MODULE_MAX): + # Not a return address (below the base / outside the image) => this rbp is + # not a frame pointer, so the chain breaks here. + Reason = "ret_out_of_module" + Reject = 1 + if Reject == 0: + if V0 <= Cur: + Reason = "not_growing" + Reject = 1 + elif V0 - Cur > uint64(TRACEBACK_RBP_MAX_STEP): + Reason = "step_too_large" + Reject = 1 + if DiagEnabled() != 0: + D0: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] rbp: frame=%d rbp=0x%llx [rbp]=0x%llx [rbp+8]=0x%llx\n", i, Cur, V0, V1) + EmitLine(D0) + Acc: int = 1 + if Reject != 0: + Acc = 0 + if DiagEnabled() != 0: + D1: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] rbp: frame=%d rpm_ok=%d rpm_err=%u accept=%d reject_reason=%s\n", + i, RpmOk, RpmErr, Acc, Reason) + EmitLine(D1) + if Reject != 0: + break + # Accepted: collect the authoritative frame, then keep walking up. + FramePush(V1, 0) + i += 1 + Cur = V0 + return i + + +def ScanStack(StartIndex: int, Rsp: uint64, Base: uint64) -> int: + """Second fallback: stack scan (after the RBP chain; no RBP / frame-pointer dependency). + + Needed when the build kept no frame pointer, so [rbp+8] is not a return address and the RBP + chain breaks at step 1; then the only way on is scanning the used stack directly. + + Read-only, never dereferencing main-thread memory: + 1. walk [Rsp, Rsp+TRACEBACK_SCAN_MAX_BYTES) in 8-byte steps towards higher addresses (caller + direction), so candidates come out in ascending stack order; + 2. one ReadProcessMemory per chunk (TRACEBACK_SCAN_CHUNK_BYTES = 4 KiB, a local array, the + callback thread allocates nothing) instead of one per 8 bytes; a failed chunk stops the + scan and prints address/GetLastError (only with TRACEBACK_DIAG on); + 3. a candidate word W lies in [Base, Base+TRACEBACK_MODULE_MAX) and a neighbouring 8-byte word + ([A-8], the saved rbp when frame pointers are kept, or [A+8]) looks like a stack address + in [Rsp, upper); either is accepted, covering both frame layouts; + 4. deduplicate by candidate value, capped at TRACEBACK_SCAN_MAX_HITS (scan_candidates=); + 5. collect via FramePush(..., Scanned=1); EmitFrame later marks them with " [inferred]". + Returns the number of inferred frames collected (0 = none). + + The bare `_State` null-check below is REQUIRED (this function uses _State.LineBuf). + """ + if _State == None: + return 0 + if Rsp == 0 or Base == 0: + # No RSP or no module base: skip to avoid a range of false positives; give a greppable + # reason when diagnostics are on. + if DiagEnabled() != 0: + if Rsp == 0: + EmitText(" [diag] scan: skipped reason=no_rsp\n") + else: + EmitText(" [diag] scan: skipped reason=no_base\n") + return 0 + HProc: HANDLE = wp.GetCurrentProcess() + Upper: uint64 = Rsp + uint64(TRACEBACK_SCAN_MAX_BYTES) + ModEnd: uint64 = Base + uint64(TRACEBACK_MODULE_MAX) + Words: t.CArray[uint64, TRACEBACK_SCAN_CHUNK_WORDS] # 4 KiB chunk buffer (local) + Found: t.CArray[uint64, TRACEBACK_SCAN_MAX_HITS] # deduplicated candidate return addresses + Hits: int = 0 + Prev: uint64 = 0 # [A-8]: previous word unknown at chunk start, 0 (one less confirmation) + Addr: uint64 = Rsp + Stop: str = "reached_scan_limit" + if DiagEnabled() != 0: + D0: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] scan: rsp=0x%llx upper=0x%llx chunk=%d hits_max=%d\n", + Rsp, Upper, TRACEBACK_SCAN_CHUNK_BYTES, TRACEBACK_SCAN_MAX_HITS) + EmitLine(D0) + while Addr < Upper: + Rd: sizet = 0 + OkRead: BOOL = wm.ReadProcessMemory(HProc, t.CPtr(Addr), &Words, sizet(TRACEBACK_SCAN_CHUNK_BYTES), &Rd) + N: int = int(t.CSizeT(Rd) / t.CSizeT(8)) # u64 words actually read in this chunk + if OkRead == 0 or N <= 0: + Er: ULONG = GetLastError() + if DiagEnabled() != 0: + D1: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] scan: block read failed addr=0x%llx err=%u, stop\n", Addr, Er) + EmitLine(D1) + Stop = "block_read_failed" + break + k: int = 0 + while k < N: + W: uint64 = Words[int(k)] + if W >= Base and W < ModEnd: + # A neighbouring 8-byte word (A-8 or A+8) looks like a stack address: accept W. + NearStack: int = 0 + if Prev >= Rsp and Prev < Upper: + NearStack = 1 + if k + 1 < N: + Q: uint64 = Words[int(k + 1)] + if Q >= Rsp and Q < Upper: + NearStack = 1 + if NearStack != 0 and Hits < TRACEBACK_SCAN_MAX_HITS: + # Deduplicate by candidate value (discovered in ascending stack order, so the + # order is kept). + Dup: int = 0 + t_i: int = 0 + while t_i < Hits: + if Found[t_i] == W: + Dup = 1 + break + t_i += 1 + if Dup == 0: + Found[Hits] = W + Hits += 1 + Prev = W + k += 1 + Addr = Addr + uint64(TRACEBACK_SCAN_CHUNK_BYTES) + if DiagEnabled() != 0: + D2: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] scan: scan_candidates=%d stop=%s\n", Hits, Stop) + EmitLine(D2) + # Collect the inferred frames, capped by the global frame limit (authoritative frames already + # used StartIndex slots). + Lim: int = TRACEBACK_MAX_FRAMES - StartIndex + j: int = 0 + while j < Hits: + if j >= Lim: + break + FramePush(Found[j], 1) + j += 1 + return j + + +# ============================================================ +# Dump the main thread +# ============================================================ + +def WriteDumpHeaderFile(IsBreak: int): + """Append a separator header to the dump file (file only, not the console). + + The file accumulates many dumps, so each section gets a small greppable header: local + timestamp / pid / trigger key / main thread id / Seq. The labels are Chinese on purpose (the + file is UTF-8, unaffected by the console code page, unlike the always-ASCII console text). + """ + if _State == None or _DumpFile == None or _DumpFile == INVALID_HANDLE_VALUE: + return + St: SYSTEMTIME = SYSTEMTIME() + GetLocalTime(&St) + Trig: str = "Ctrl+C" + if IsBreak != 0: + Trig = "Ctrl+Break" + N: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + "\n===== 栈转储 %04u-%02u-%02u %02u:%02u:%02u.%03u pid=%u 触发=%s 主线程id=%u Seq=%d =====\n", + uint32(St.wYear), uint32(St.wMonth), uint32(St.wDay), + uint32(St.wHour), uint32(St.wMinute), uint32(St.wSecond), + uint32(St.wMilliseconds), wp.GetCurrentProcessId(), + Trig, _State.MainThreadId, _State.Seq) + if N <= 0: + return + if N > TRACEBACK_LINE_CAP - 1: + N = TRACEBACK_LINE_CAP - 1 + Wr: ULONG = 0 + wf.WriteFile(_DumpFile, _State.LineBuf, ULONG(N), &Wr, None) + + +def DumpMainThread(IsBreak: int) -> int: + """Suspend the main thread -> read CONTEXT -> collect frames -> print -> resume. Returns frames. + + Invariants: + * after a successful SuspendThread, every return path must ResumeThread + CloseHandle, or the + main thread freezes forever (worse than a hang); + * reentrancy: AtomicTryEnterDump (inline `lock cmpxchg`) test-and-sets _DumpLock; every + return path calls AtomicLeaveDump; a caller that fails to acquire returns immediately + printing nothing (it does not suspend the main thread or touch LineBuf); + * evidence: the "[diag] ..." lines are printed only while TRACEBACK_DIAG is enabled; when it + is off the dump is a plain Python-style traceback, which is why the diagnostic lines must + never be interleaved between the frames; + * frame order: the walk paths collect frames innermost-first into _State.FrameAddr; this + function prints that buffer in reverse (outermost/CRT first, innermost last). + """ + global _DumpOut + if _State == None or _State.LineBuf == None: + return 0 + # Reentrancy guard: the SetConsoleCtrlHandler callbacks run on new threads and share the same + # LineBuf/out handle, so an atomic test-and-set avoids interleaved output from a race. + EnterOld: int = AtomicTryEnterDump() + if EnterOld != 0: + return 0 + _State.Dumping = 1 + _State.Seq = _State.Seq + 1 + # The output handle is fixed and locked at dump start; only _DumpOut is written afterwards. + _DumpOut = _State.OutHandle + # Reset the per-dump flags: anti-spam flags plus the collected-frame count. + _State.ReadMemFailed = 0 + _State.MissReported = 0 + _State.FrameCount = 0 + EmitText("=== TPC stack dump (Ctrl+Break: keep running / Ctrl+C: exit) ===\n") + # File-side separator header (file only: timestamp / pid / trigger / main thread id). + WriteDumpHeaderFile(int(IsBreak)) + if _DumpOut == None or _DumpOut == INVALID_HANDLE_VALUE: + EmitText(" WARN: no usable console handle (stderr/stdout invalid), console output discarded\n") + # ---- Pre-diagnostics (evidence before decisions), obtained without suspending ---- + # Emitted only while TRACEBACK_DIAG is enabled; all fields are ASCII. + MBase: uint64 = uint64(GetModuleHandleA(None)) + ModBaseRet: uint64 = 0 + if TRACEBACK_USE_DBGHELP != 0: + if _State.SymReady != 0: + ModBaseRet = uint64(dbg.SymGetModuleBase64(wp.GetCurrentProcess(), MBase)) + FB: int = 1 if _State.FrameBuf != None else 0 + FT: int = 1 if _State.FuncTableAddr != None else 0 + MB: int = 1 if _State.ModuleBaseAddr != None else 0 + N: int = 0 + if DiagEnabled() != 0: + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] Seq=%d cbthread=%u mainthread=%u dump_enter_prev=%d " + "mapok=%d symready=%d modinited=%d\n", + _State.Seq, wp.GetCurrentThreadId(), _State.MainThreadId, EnterOld, + _State.MapOk, _State.SymReady, _State.ModInited) + EmitLine(N) + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] modbase=0x%llx SymGetModuleBase64(main)=0x%llx\n", MBase, ModBaseRet) + EmitLine(N) + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] frame_buf=%d cb_fta=%d cb_mba=%d\n", FB, FT, MB) + EmitLine(N) + Trigger: str = "Ctrl+C" + AfterTag: str = "exit" + if IsBreak != 0: + Trigger = "Ctrl+Break" + AfterTag = "continue" + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " Trigger=%s after_dump=%s mainthread=%u cbthread=%u\n", + Trigger, AfterTag, _State.MainThreadId, wp.GetCurrentThreadId()) + EmitLine(N) + hThread: HANDLE = wp.OpenThread(ULONG(wp.THREAD_SUSPEND_RESUME | wp.THREAD_GET_CONTEXT), 0, _State.MainThreadId) + if hThread == None or hThread == INVALID_HANDLE_VALUE: + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " fallback: OpenThread(main) failed (GetLastError=%u), cannot dump stack\n", + GetLastError()) + EmitLine(N) + AtomicLeaveDump() + return 0 + if wp.SuspendThread(hThread) == ULONG(TRACEBACK_SUSPEND_FAILED): + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " fallback: SuspendThread(main) failed (GetLastError=%u), cannot dump stack\n", + GetLastError()) + EmitLine(N) + CloseHandle(hThread) + AtomicLeaveDump() + return 0 + # CONTEXT: zero it first, then set ContextFlags (x64 needs the CONTEXT_AMD64 bit). + string.memset(_State.ContextBuf, 0, wp.CONTEXT.__sizeof__()) + ctx: rawptr[wp.CONTEXT] = (rawptr[wp.CONTEXT])(_State.ContextBuf) + ctx.ContextFlags = uint32(wp.CONTEXT_FULL) + if wp.GetThreadContext(hThread, t.CPtr(_State.ContextBuf)) == 0: + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " fallback: GetThreadContext failed (GetLastError=%u), cannot dump stack\n", + GetLastError()) + EmitLine(N) + wp.ResumeThread(hThread) + CloseHandle(hThread) + AtomicLeaveDump() + return 0 + Base: uint64 = uint64(GetModuleHandleA(None)) + Rip: uint64 = uint64(ctx.Rip) + Rsp: uint64 = uint64(ctx.Rsp) + Rbp: uint64 = uint64(ctx.Rbp) + # ---- ASan vs dbghelp mutual-exclusion probe + dbghelp usability ---- + # ASan initializes DbgHelp itself; the process cannot host both incompatible uses. If an ASan + # runtime is present, dbghelp is not used as the primary path and the reason is printed (never + # silent 0 frames). Both common runtime DLL names are probed. + AsanPresent: int = 0 + if GetModuleHandleA("clang_rt.asan_dynamic-x86_64.dll") != None: + AsanPresent = 1 + if GetModuleHandleA("libclang_rt.asan_dynamic-x86_64.dll") != None: + AsanPresent = 1 + DbgUsable: int = 0 + if TRACEBACK_USE_DBGHELP != 0 and _State.SymReady != 0 \ + and _State.FuncTableAddr != None and _State.ModuleBaseAddr != None \ + and _State.FrameBuf != None: + DbgUsable = 1 + if AsanPresent != 0: + # ASan present: dbghelp is treated as conflicting and disabled; the RBP chain is primary. + DbgUsable = 0 + if DiagEnabled() != 0: + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] symwalk: use_dbghelp=%d symready=%d asan=%d dbg_usable=%d\n", + TRACEBACK_USE_DBGHELP, _State.SymReady, AsanPresent, DbgUsable) + EmitLine(N) + if DbgUsable == 0: + # Each degradation gives an explicit reason (never silent 0 frames), most specific first. + if TRACEBACK_USE_DBGHELP == 0: + EmitText(" [diag] symwalk=disabled reason=compile_time_switch" + " (TRACEBACK_USE_DBGHELP=0)\n") + elif AsanPresent != 0: + EmitText(" [diag] symwalk=disabled reason=dbghelp_conflict_with_asan\n") + elif _State.SymReady == 0: + EmitText(" [diag] symwalk=disabled reason=syminitialize_failed\n") + elif _State.FuncTableAddr == None or _State.ModuleBaseAddr == None: + EmitText(" [diag] symwalk=disabled reason=callback_missing\n") + else: + EmitText(" [diag] symwalk=disabled reason=null_frame_buf\n") + MapTag: str = "none (see [traceback] warnings above; addresses only)" + if _State.MapOk != 0: + MapTag = "loaded" + # Primary path is the RBP chain (no dbghelp); dbghelp is optional enhancement only. + SymTag: str = "rbp-chain primary (dbghelp not required)" + if DbgUsable != 0: + SymTag = "rbp-chain primary, dbghelp optional" + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " exe base=0x%llx RIP=0x%llx RSP=0x%llx RBP=0x%llx\n", + Base, Rip, Rsp, Rbp) + EmitLine(N) + if DiagEnabled() != 0: + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " srcline_map=%s symwalk=%s\n", MapTag, SymTag) + EmitLine(N) + Frames: int = 0 + # "enter walk" evidence (only with diagnostics on); if it is missing after a key press while + # TRACEBACK_DIAG=1, DumpMainThread returned earlier at one of the three fallbacks above. + if DiagEnabled() != 0: + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] walk: enter use_dbghelp=%d symready=%d modinited=%d " + "frame_buf=%d mapok=%d asan=%d\n", + TRACEBACK_USE_DBGHELP, _State.SymReady, _State.ModInited, FB, _State.MapOk, + AsanPresent) + EmitLine(N) + if DbgUsable == 0: + # Keep a "[diag] StackWalk64 first:" line before the RBP chain (here ret=not-called, with + # the prerequisites); a real call would emit it from inside WalkViaDbgHelp. + DNC: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] StackWalk64 first: ret=not-called symready=%d cb_fta=%d cb_mba=%d frame_buf=%d\n", + _State.SymReady, FT, MB, FB) + EmitLine(DNC) + EmitText(" [diag] walk: primary=rbp_chain\n") + # ---- Primary path: RBP chain (no dbghelp) ---- + Frames = WalkRbpChain(Rip, Rbp, Base) + if DiagEnabled() != 0: + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] walk: rbp_chain frames=%d\n", Frames) + EmitLine(N) + # ---- Second fallback: stack scan (no RBP / frame pointer) ---- + # Enabled only when the RBP chain got just the current RIP (<=1 frame); scanned frames are + # marked " [inferred]" and are not mixed with authoritative frames. + if Frames <= 1: + if DiagEnabled() != 0: + EmitText(" [diag] walk: fallback=stack_scan\n") + Sc: int = ScanStack(Frames, Rsp, Base) + Frames = Frames + Sc + if DiagEnabled() != 0: + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] walk: scan frames=%d total=%d\n", Sc, Frames) + EmitLine(N) + # ---- Optional enhancement: only when the RBP chain got <=1 frame and dbghelp is usable ---- + # The outer constant condition is a separate if so the switch folds this block away entirely, + # leaving no dbghelp link-time symbol. + if TRACEBACK_USE_DBGHELP != 0: + if Frames <= 1 and DbgUsable != 0: + if DiagEnabled() != 0: + EmitText(" [diag] walk: dbghelp enhancement engaged\n") + # dbghelp re-walks from the same CONTEXT and its first frame is the same RIP already + # collected: save that single frame, let dbghelp collect the authoritative list, and + # restore it only if dbghelp produced nothing (never lose or duplicate the RIP frame). + HadFrame: int = 0 + SaveAddr: uint64 = 0 + SaveScan: int = 0 + if _State.FrameCount > 0: + HadFrame = 1 + SaveAddr = _State.FrameAddr[0] + SaveScan = int(_State.FrameScan[0]) + _State.FrameCount = 0 + F2: int = WalkViaDbgHelp(wp.GetCurrentProcess(), hThread, _State.ContextBuf, Base) + if F2 == 0 and HadFrame != 0: + FramePush(SaveAddr, SaveScan) + # The count that is actually buffered is authoritative. + Frames = _State.FrameCount + if DiagEnabled() != 0: + # "leave walk" evidence, paired with walk: enter. + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [diag] walk: leave frames=%d mapok=%d\n", Frames, _State.MapOk) + EmitLine(N) + if Frames == 0: + # Neither the RBP chain, the stack scan, nor any dbghelp enhancement produced a frame. + EmitText(" [diag] walk: no frames from any path (rbp_chain/stack_scan/dbghelp)\n") + # ---- Print the collected frames outermost-first (Python traceback order) ---- + EmitText("Traceback (most recent call last):\n") + j: int = Frames - 1 + while j >= 0: + EmitFrame(_State.FrameAddr[j], Base, int(_State.FrameScan[j])) + j -= 1 + # Summary after the whole traceback: frame count, dump sequence, dump-file notice. + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " frames=%d\n", Frames) + EmitLine(N) + if _DumpFile != None and _DumpFile != INVALID_HANDLE_VALUE: + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [traceback] dump seq=%d also appended to .stackdump.txt\n", _State.Seq) + else: + N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + " [traceback] dump seq=%d console only (dump file unavailable)\n", _State.Seq) + EmitLine(N) + EmitText("=== stack dump end ===\n") + wp.ResumeThread(hThread) + CloseHandle(hThread) + AtomicLeaveDump() + return Frames + + +# ============================================================ +# Control handler / install +# ============================================================ + +def CtrlHandler(CtrlType: ULONG) -> BOOL: + """SetConsoleCtrlHandler callback (invoked by the system on a new thread). + + Ctrl+C -> dump then return FALSE (default handling resumes => process exits); + Ctrl+Break -> dump then return TRUE (handled => process continues); + any other control event -> no dump, return FALSE (default behaviour untouched). + Thread-safe: only reads global state and writes reserved buffers, allocates nothing. + """ + if _State == None or _State.Installed == 0: + return 0 + IsBreak: int = 0 + if CtrlType == ULONG(wc.CTRL_BREAK_EVENT): + IsBreak = 1 + elif CtrlType != ULONG(wc.CTRL_C_EVENT): + return 0 + DumpMainThread(int(IsBreak)) + if IsBreak != 0: + return 1 + return 0 + + +def NoteError(Text: str, Code: ULONG): + """Install/degradation diagnostic: one '(GetLastError=%u)' line to the dump sink.""" + if _State == None or _State.LineBuf == None: + return + N: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + "%s(GetLastError=%u)\n", Text, Code) + EmitLine(N) + + +def Install() -> int: + """Register the console control handler. 1 = registered (possibly degraded), 0 = not installed. + + 0 has two cases, both diagnosed (a missing sidecar is the expected "off" state, returned + silently): .stackmap absent (--traceback not used) -> silent 0, zero side effects; or + the sidecar exists but open/read/parse fails, or a key buffer cannot be allocated -> warning + then 0. Idempotent: once attempted (success or not) it returns the previous result without + re-allocating, so a retry cannot leak the state object or buffers. + """ + global _State, _DumpOut, _HandlerReg, _DumpFile + if _State != None: + return int(_State.Installed) + # (1) + ".stackmap"; _State does not exist yet, so failures go through + # EmitEarly (the dump's own sink), never stdio.printf, to avoid stdout/stderr interleaving. + Pb: bytes = stdlib.malloc(TRACEBACK_PATH_CAP) + if Pb == None: + EmitEarly("[traceback] WARN: path buffer alloc failed, ctrl handler not installed\n") + return 0 + Plen: ULONG = wf.GetModuleFileNameA(None, Pb, ULONG(TRACEBACK_PATH_CAP)) + if Plen == 0 or t.CSizeT(Plen) >= t.CSizeT(TRACEBACK_PATH_CAP): + stdlib.free(t.CPtr(Pb)) + EmitEarly("[traceback] WARN: GetModuleFileNameA failed, ctrl handler not installed\n") + return 0 + if t.CSizeT(Plen) + 16 > t.CSizeT(TRACEBACK_PATH_CAP): + # 16 bytes of slack covers both ".stackmap" (10) and ".stackdump.txt" (16). + stdlib.free(t.CPtr(Pb)) + EmitEarly("[traceback] WARN: exe path too long to append suffix safely, ctrl handler not installed\n") + return 0 + string.strcpy(str(Pb + int(Plen)), ".stackmap") + # (2) sidecar existence = master switch (absent -> return immediately, no side effects). + hFile: HANDLE = wf.CreateFileA(Pb, wf.GENERIC_READ, wf.FILE_SHARE_READ, t.CPtr(0), wf.OPEN_EXISTING, wf.FILE_ATTRIBUTE_NORMAL, t.CPtr(0)) + if hFile == INVALID_HANDLE_VALUE: + Err0: ULONG = GetLastError() + stdlib.free(t.CPtr(Pb)) + if Err0 != ULONG(ERROR_FILE_NOT_FOUND): + EmitEarlyCode("[traceback] WARN: cannot open .stackmap, ctrl handler not installed ", Err0) + return 0 + # (3) state object + all buffers (the callback thread allocates nothing). The collected-frame + # arrays (FrameAddr/FrameScan) are inline fields of TracebackState, so the single + # __sizeof__() allocation below covers them and the memset zeroes FrameCount. + _State = stdlib.malloc(TracebackState.__sizeof__()) + if _State == None: + stdlib.free(t.CPtr(Pb)) + CloseHandle(hFile) + EmitEarly("[traceback] WARN: state alloc failed, ctrl handler not installed\n") + return 0 + string.memset(_State, 0, TracebackState.__sizeof__()) + _State.LineBuf = stdlib.malloc(TRACEBACK_LINE_CAP) + _State.ContextBuf = t.CPtr(stdlib.calloc(1, wp.CONTEXT.__sizeof__())) + _State.FrameBuf = t.CPtr(stdlib.calloc(1, dbg.STACKFRAME64.__sizeof__())) + _State.OutHandle = wf.GetStdHandle(wf.STD_ERROR_HANDLE) + if _State.OutHandle == None or _State.OutHandle == INVALID_HANDLE_VALUE: + _State.OutHandle = wf.GetStdHandle(wf.STD_OUTPUT_HANDLE) + # The output handle is fixed and locked into _DumpOut here; dumps only use it afterwards. + _DumpOut = _State.OutHandle + _State.MainThreadId = wp.GetCurrentThreadId() + if _State.LineBuf == None or _State.ContextBuf == None or _State.FrameBuf == None: + # Name the missing buffer now (not later in the callback thread); any missing buffer makes + # Install return 0. _State exists here, so EmitEarly forwards to EmitText; if LineBuf is + # the missing one, EmitEarly falls back to a direct WriteFile and the notice is not lost. + stdlib.free(t.CPtr(Pb)) + CloseHandle(hFile) + if _State.LineBuf == None: + EmitEarly("[traceback] WARN: LineBuf alloc failed, ctrl handler not installed\n") + elif _State.ContextBuf == None: + EmitEarly("[traceback] WARN: ContextBuf alloc failed, ctrl handler not installed\n") + elif _State.FrameBuf == None: + EmitEarly("[traceback] WARN: FrameBuf alloc failed, ctrl handler not installed\n") + else: + EmitEarly("[traceback] WARN: runtime buffer alloc failed, ctrl handler not installed\n") + return 0 + # (3b) open the dump file .stackdump.txt (append: OPEN_ALWAYS creates it, FILE_APPEND_DATA + # appends every WriteFile). Opened before loading the sidecar so load-time diagnostics are + # written too. Raw UTF-8 bytes, unaffected by the console code page; kept open for the + # whole process lifetime. + string.strcpy(str(Pb + int(Plen)), ".stackdump.txt") + _DumpFile = wf.CreateFileA(Pb, TRACEBACK_FILE_APPEND_DATA, wf.FILE_SHARE_READ | wf.FILE_SHARE_WRITE, t.CPtr(0), wf.OPEN_ALWAYS, wf.FILE_ATTRIBUTE_NORMAL, t.CPtr(0)) + if _DumpFile == INVALID_HANDLE_VALUE: + _DumpFile = None + EmitText("[traceback] WARN: cannot open stack dump file, console-only output\n") + else: + DN: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP), + "[traceback] stack dump also written to: %s\n", str(Pb)) + EmitLine(DN) + # (4) load the sidecar (a failure only disables the map; the handler is still installed so at + # least addresses are available). LoadMapFile prints the exact validation failure itself. + Sz: LARGE_INTEGER = LARGE_INTEGER() + if wf.GetFileSizeEx(hFile, &Sz) == 0: + NoteError("[traceback] WARN: GetFileSizeEx(.stackmap) failed, addresses only ", GetLastError()) + else: + LoadMapFile(hFile, int64(Sz.QuadPart)) + CloseHandle(hFile) + stdlib.free(t.CPtr(Pb)) + # (5) dbghelp initialization (must happen here: Install runs at startup on the main thread, + # while the Ctrl+C/Break callbacks run on new threads where dbghelp's module enumeration + # may be unavailable) + the two cross-module callback addresses, resolved dynamically. + if TRACEBACK_USE_DBGHELP != 0: + # SymSetOptions must precede SymInitialize: DEFERRED_LOADS avoids loading symbols/PDBs and + # NO_PROMPTS avoids an interactive dialog blocking startup. + dbg.SymSetOptions(uint32(dbg.SYMOPT_DEFERRED_LOADS | dbg.SYMOPT_UNDNAME | dbg.SYMOPT_NO_PROMPTS)) + # fInvadeProcess MUST be 1 (TRUE) so dbghelp enumerates loaded modules into its table; with + # 0 the table stays empty, SymFunctionTableAccess64 gets no RUNTIME_FUNCTION and + # SymGetModuleBase64 returns 0, so StackWalk64 fails on the first call and walks no frames. + if dbg.SymInitialize(wp.GetCurrentProcess(), None, 1) != 0: + _State.SymReady = 1 + else: + NoteError("[traceback] WARN: SymInitialize failed (ASan also uses DbgHelp" + " and conflicts with us; RBP chain is the primary path anyway) ", GetLastError()) + # ASan initializes DbgHelp itself, conflicting with this module's SymInitialize; probe once + # at install and warn explicitly (the dump then prints + # symwalk=disabled reason=dbghelp_conflict_with_asan and uses the RBP chain). + HAsan: HANDLE = GetModuleHandleA("clang_rt.asan_dynamic-x86_64.dll") + if HAsan == None: + HAsan = GetModuleHandleA("libclang_rt.asan_dynamic-x86_64.dll") + if HAsan != None: + EmitText("[traceback] WARN: ASan runtime detected, it also initializes DbgHelp;" + " dbghelp symwalk disabled, RBP chain will be the primary path\n") + Hdbg: HANDLE = GetModuleHandleA("dbghelp.dll") + if Hdbg != None and Hdbg != INVALID_HANDLE_VALUE: + _State.FuncTableAddr = GetProcAddress(Hdbg, "SymFunctionTableAccess64") + _State.ModuleBaseAddr = GetProcAddress(Hdbg, "SymGetModuleBase64") + if _State.FuncTableAddr == None or _State.ModuleBaseAddr == None: + EmitText("[traceback] WARN: StackWalk64 callback addresses incomplete," + " dbghelp enhancement unavailable (RBP chain is the primary path)\n") + # Self-check: does dbghelp's module table really contain the main module? The result feeds + # the dump's "[diag] StackWalk64 first:" / symwalk lines and the dbg_usable decision. + if _State.SymReady != 0: + if dbg.SymGetModuleBase64(wp.GetCurrentProcess(), uint64(GetModuleHandleA(None))) != 0: + _State.ModInited = 1 + else: + EmitText("[traceback] WARN: dbghelp module table lacks main module (SymGetModuleBase64=0)," + " StackWalk64 will produce 0 frames\n") + # (6) register last, once all state is ready. _HandlerReg guards "register once" so a repeated + # callback cannot re-register (and thus cannot repeat the same dump). + if _HandlerReg == 0: + if wc.SetConsoleCtrlHandler(CtrlHandler, 1) == 0: + NoteError("[traceback] WARN: SetConsoleCtrlHandler failed ", GetLastError()) + return 0 + _HandlerReg = 1 + _State.Installed = 1 + EmitText("[traceback] ready: Ctrl+C prints stack then exits, Ctrl+Break prints then continues\n") + return 1 diff --git a/vector.vp b/vector.vp index 0af980b..62da1c4 100644 --- a/vector.vp +++ b/vector.vp @@ -23,7 +23,7 @@ class Vector[T](): self._grow() if self.length >= self.capacity: return offset: uint64 = uint64(self.data) + self.length * self.elem_size - dst: rawptr = rawptr(offset) + dst: T* = (T*)offset # typed pointer: store whole T, not its first byte *dst = value self.length += 1 @@ -47,7 +47,7 @@ class Vector[T](): if index >= self.length: return if self.data == None: return offset: uint64 = uint64(self.data) + index * self.elem_size - dst: rawptr = rawptr(offset) + dst: T* = (T*)offset # typed pointer: store whole T, not its first byte *dst = value def len(self) -> sizet: diff --git a/viperlib.vp.txt b/viperlib.vp.txt new file mode 100644 index 0000000..36b7a9c --- /dev/null +++ b/viperlib.vp.txt @@ -0,0 +1,1396 @@ +import t, c +import viperio +from stdarg import arg, va_arg + +define TEMP_BUF_SIZE = 68 + +def get_digit_count(num: uint, base: int) -> int: + count: int = 0 + if num == 0: return 1 + while num > 0: + count += 1 + num //= base + return count + +# 返回类型必须与 CRT `sprintf` 以及 includes/stdio.vp 的 +# 语义上直接返回 vsnprintf 的写入长度(与 CRT 一致)。 +export def sprintf(buf: str, rule: str, *args) -> int: + if buf is None or rule is None: return 0 + return vsnprintf(buf, 0x7FFFFFFF, rule, args) + +def vsprintf(buf: str, rule: str, *args) -> int: + if buf is None or rule is None: return 0 + return vsnprintf(buf, 0x7FFFFFFF, rule, args) + +export def snprintf(buf: str, size: sizet, fmt: str, *args) -> int: + if fmt is None: return 0 + if buf is None or size == 0: return vsnprintf(None, 0, fmt, args) + ret: int = vsnprintf(buf, size, fmt, args) + return ret + +# 检查浮点特殊值(IEEE 754) +# 返回:0=正常, 1=+inf, 2=-inf, 3=nan +# 注意:不可用位双关 `*&fbits = *&fv` 读取 IEEE 位模式——该赋值会被编译器静默 +# 丢弃(IR 中 fbits 恒为 0,见 TransPyV/output/db514c1af999c5f5.text.ll),会使 +# 本函数恒返回 0、特殊值检查彻底失效,进而让 inf 进入浮点格式化循环而死循环。 +# 这里改用纯浮点语义判定,不依赖任何位操作: +# NaN :fv != fv 为真 +# ±inf:fv - fv 得到 NaN(有限值得到 0.0) +def _check_special_float(fv: double) -> int: + if fv != fv: + return 3 + diff: double = fv - fv + if diff != diff: + if fv < 0.0: + return 2 + return 1 + return 0 + +export def vsnprintf(buf: bytes, size: sizet, fmt: str, ap: str) -> int: + if fmt is None: return 0 + # Length detection mode: when buf is None, only calculate the formatted length, without actually writing + # _step=0 makes ptr not advance, repeatedly writing the same dummy byte (harmless), write_count accumulates correctly + count_mode: int = 0 + _step: int = 1 + _dummy: char = '\0' + if buf is None or size == 0: + count_mode = 1 + _step = 0 + buf = &_dummy + size = 0x7FFFFFFF + ptr: str = buf + write_count: int = 0 + temp: t.CArray[char, 68] + temp[0] = '\t' + while *fmt != 0 and write_count < size - 1: + if *fmt != 37: + ptr[0] = *fmt + write_count += 1 + ptr += _step + fmt += 1 + else: + fmt += 1 + if *fmt == 0: break + width: int = 0 + pad_zero: int = 0 + left_align: int = 0 + show_sign: int = 0 + space_sign: int = 0 + alt_form: int = 0 + precision: int = -1 + is_long: int = 0 + is_long_long: int = 0 + is_size: int = 0 + is_short: int = 0 + is_char: int = 0 + while True: + if *fmt == 45: + left_align = 1 + fmt += 1 + elif *fmt == 48: + pad_zero = 1 + fmt += 1 + elif *fmt == 43: + show_sign = 1 + fmt += 1 + elif *fmt == 32: + space_sign = 1 + fmt += 1 + elif *fmt == 35: + alt_form = 1 + fmt += 1 + else: break + if show_sign: space_sign = 0 + while *fmt >= 48 and *fmt <= 57: + width = width * 10 + (*fmt - 48) + fmt += 1 + if *fmt == 46: + fmt += 1 + precision = 0 + while *fmt >= 48 and *fmt <= 57: + precision = precision * 10 + (*fmt - 48) + fmt += 1 + if left_align: pad_zero = 0 + if *fmt == 108: + is_long = 1 + fmt += 1 + if *fmt == 108: + is_long_long = 1 + is_long = 0 + fmt += 1 + elif *fmt == 104: + is_short = 1 + fmt += 1 + if *fmt == 104: + is_char = 1 + is_short = 0 + fmt += 1 + elif *fmt == 122: + is_size = 1 + fmt += 1 + elif *fmt == 106: + is_long_long = 1 + fmt += 1 + elif *fmt == 116: + is_long = 1 + fmt += 1 + if *fmt == 99: + if write_count < size - 1: + ptr_i: int = va_arg(ap) + ptr[0] = char(ptr_i) + write_count += 1 + ptr += _step + else: + ptr_i: int = va_arg(ap) + elif *fmt == 115: + s: str = va_arg(ap, t.CPtr) + if s is None: s = "(None)" + slen: int = 0 + sc: str = s + while c.Deref(sc) != 0: + slen += 1 + sc += 1 + if not left_align and width > slen: + pad_count: int = width - slen + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + while c.Deref(s) != 0 and write_count < size - 1: + ptr[0] = c.Deref(s) + write_count += 1 + ptr += _step + s += 1 + if left_align and width > slen: + pad_count = width - slen + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 100 or *fmt == 105: + if is_long_long or is_size or is_long: + num_l: int64 = va_arg(ap, int64) + else: + num_l: int64 = int64(va_arg(ap, int)) + if is_char: + num_l = int64(char(num_l)) + elif is_short: + num_l = int64(short(num_l)) + sign: int = 0 + if num_l < 0: + sign = 1 + if num_l == (-9223372036854775807 - 1): + u_num: uint64 = uint64(0x8000000000000000) + else: + u_num: uint64 = uint64(-num_l) + else: + u_num: uint64 = uint64(num_l) + if show_sign: sign = 2 + elif space_sign: sign = 3 + i: int = 0 + while True: + if i >= TEMP_BUF_SIZE - 1: break + temp[i] = char(u_num % 10 + 48) + i += 1 + u_num //= 10 + if u_num <= 0: break + if sign == 1 and i < TEMP_BUF_SIZE - 1: + temp[i] = 45 + i += 1 + elif sign == 2 and i < TEMP_BUF_SIZE - 1: + temp[i] = 43 + i += 1 + elif sign == 3 and i < TEMP_BUF_SIZE - 1: + temp[i] = 32 + i += 1 + if not left_align and not pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + while i > 0 and write_count < size - 1: + i -= 1 + ptr[0] = temp[i] + write_count += 1 + ptr += _step + if left_align and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 117: + if is_long or is_long_long or is_size: + u_num_l: uint64 = va_arg(ap, uint64) + else: + u_num_l: uint64 = uint64(uint(va_arg(ap))) + i = 0 + while True: + if i >= TEMP_BUF_SIZE - 1: break + temp[i] = char(u_num_l % 10 + 48) + i += 1 + u_num_l //= 10 + if u_num_l <= 0: break + if not left_align and not pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + while i > 0 and write_count < size - 1: + i -= 1 + ptr[0] = temp[i] + write_count += 1 + ptr += _step + if left_align and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 120: + if is_long or is_long_long or is_size: + u_num_l: uint64 = va_arg(ap, uint64) + else: + u_num_l: uint64 = uint64(uint(va_arg(ap))) + is_nonzero: int = 0 + if u_num_l != 0: + is_nonzero = 1 + i = 0 + while True: + if i >= TEMP_BUF_SIZE - 1: break + digit: int = int(u_num_l % 16) + if digit < 10: + temp[i] = char(digit + 48) + else: + temp[i] = char(digit - 10 + 97) + u_num_l //= 16 + i += 1 + if u_num_l <= 0: break + digit_count: int = i + prefix_len: int = 0 + if alt_form and is_nonzero: + prefix_len = 2 + effective_len: int = digit_count + prefix_len + if not left_align and not pad_zero and width > effective_len: + pad_count = width - effective_len + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if prefix_len > 0: + if write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + if write_count < size - 1: + ptr[0] = 120 + write_count += 1 + ptr += _step + if pad_zero and width > effective_len: + pad_count = width - effective_len + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + while i > 0 and write_count < size - 1: + i -= 1 + ptr[0] = temp[i] + write_count += 1 + ptr += _step + if left_align and width > effective_len: + pad_count = width - effective_len + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 88: + if is_long or is_long_long or is_size: + u_num_l: uint64 = va_arg(ap) + else: + u_num_l: uint64 = uint64(uint(va_arg(ap))) + is_nonzero = 0 + if u_num_l != 0: + is_nonzero = 1 + i = 0 + while True: + if i >= TEMP_BUF_SIZE - 1: break + digit = int(u_num_l % 16) + if digit < 10: + temp[i] = char(digit + 48) + else: + temp[i] = char(digit - 10 + 65) + u_num_l //= 16 + i += 1 + if u_num_l <= 0: break + digit_count = i + prefix_len = 0 + if alt_form and is_nonzero: + prefix_len = 2 + effective_len = digit_count + prefix_len + if not left_align and not pad_zero and width > effective_len: + pad_count = width - effective_len + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if prefix_len > 0: + if write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + if write_count < size - 1: + ptr[0] = 88 + write_count += 1 + ptr += _step + if pad_zero and width > effective_len: + pad_count = width - effective_len + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + while i > 0 and write_count < size - 1: + i -= 1 + ptr[0] = temp[i] + write_count += 1 + ptr += _step + if left_align and width > effective_len: + pad_count = width - effective_len + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 111: + if is_long or is_long_long or is_size: + u_num_l: uint64 = va_arg(ap, uint64) + else: + u_num_l: uint64 = uint64(uint(va_arg(ap))) + if is_char: + u_num_l = uint64(char(u_num_l)) + elif is_short: + u_num_l = uint64(short(u_num_l)) + i = 0 + if u_num_l == 0 and not alt_form: + temp[0] = 48 + i = 1 + else: + while True: + if i >= TEMP_BUF_SIZE - 1: break + temp[i] = char(u_num_l % 8 + 48) + i += 1 + u_num_l //= 8 + if u_num_l <= 0: break + if alt_form and i < TEMP_BUF_SIZE - 1: + temp[i] = 48 + i += 1 + if not left_align and not pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + while i > 0 and write_count < size - 1: + i -= 1 + ptr[0] = temp[i] + write_count += 1 + ptr += _step + if left_align and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 98: + if is_long or is_long_long or is_size: + u_num_l: uint64 = va_arg(ap, uint64) + else: + u_num_l: uint64 = uint64(uint(va_arg(ap))) + if is_char: + u_num_l = uint64(char(u_num_l)) + elif is_short: + u_num_l = uint64(short(u_num_l)) + i = 0 + if u_num_l == 0: + temp[0] = 48 + i = 1 + else: + while True: + if i >= TEMP_BUF_SIZE - 1: break + temp[i] = char(u_num_l % 2 + 48) + i += 1 + u_num_l //= 2 + if u_num_l <= 0: break + if alt_form and i < TEMP_BUF_SIZE - 1: + temp[i] = 98 + i += 1 + if not left_align and not pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + while i > 0 and write_count < size - 1: + i -= 1 + ptr[0] = temp[i] + write_count += 1 + ptr += _step + if left_align and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 112: + ptr_val: uint64 = uint64(va_arg(ap, t.CPtr)) + if ptr_val == 0: + temp[0] = 40 + temp[1] = 110 + temp[2] = 105 + temp[3] = 108 + temp[4] = 41 + i = 5 + else: + u_num_l = ptr_val + i = 0 + while True: + if i >= TEMP_BUF_SIZE - 1: break + digit = int(u_num_l % 16) + if digit < 10: + temp[i] = char(digit + 48) + else: + temp[i] = char(digit - 10 + 97) + u_num_l //= 16 + i += 1 + if u_num_l <= 0: break + if i < TEMP_BUF_SIZE - 1: + temp[i] = 120 + i += 1 + if i < TEMP_BUF_SIZE - 1: + temp[i] = 48 + i += 1 + if not left_align and not pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + while i > 0 and write_count < size - 1: + i -= 1 + ptr[0] = temp[i] + write_count += 1 + ptr += _step + if left_align and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 102 or *fmt == 70: + f_upper: int = 1 if *fmt == 70 else 0 + if precision < 0: precision = 6 + fv: double = va_arg(ap, double) + f_sp: int = _check_special_float(fv) + if f_sp != 0: + f_sgn_len: int = 0 + if f_sp == 2: + f_sgn_len = 1 + elif f_sp == 1 and show_sign: + f_sgn_len = 1 + elif f_sp == 1 and space_sign: + f_sgn_len = 1 + f_total_len: int = f_sgn_len + 3 + if not left_align and width > f_total_len: + f_pad: int = width - f_total_len + while f_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + f_pad -= 1 + if f_sp == 2: + if write_count < size - 1: + ptr[0] = 45 + write_count += 1 + ptr += _step + elif f_sp == 1 and show_sign: + if write_count < size - 1: + ptr[0] = 43 + write_count += 1 + ptr += _step + elif f_sp == 1 and space_sign: + if write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + fi_c: int = 0 + while fi_c < 3 and write_count < size - 1: + fc: int = 0 + if f_sp == 3: + # nan + if fi_c == 0: + fc = 110 + elif fi_c == 1: + fc = 97 + else: + fc = 110 + else: + # inf (f_sp==1 +inf 或 f_sp==2 -inf) + if fi_c == 0: + fc = 105 + elif fi_c == 1: + fc = 110 + else: + fc = 102 + if f_upper and fc >= 97 and fc <= 122: + fc -= 32 + ptr[0] = char(fc) + write_count += 1 + ptr += _step + fi_c += 1 + if left_align and width > f_total_len: + f_pad = width - f_total_len + while f_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + f_pad -= 1 + fmt += 1 + continue + if fv < 0.0: + if write_count < size - 1: + ptr[0] = 45 + write_count += 1 + ptr += _step + fv = 0.0 - fv + elif show_sign: + if write_count < size - 1: + ptr[0] = 43 + write_count += 1 + ptr += _step + elif space_sign: + if write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + di: int = 0 + work: double = fv + if work < 1.0: + temp[0] = 48 + di = 1 + else: + scale: double = 1.0 + # 上界防护:inf 会让 `work >= scale*10.0` 永真,正常有限值最多 308 次 + scale_guard: int = 0 + while work >= scale * 10.0 and scale_guard < 400: + scale *= 10.0 + scale_guard += 1 + while scale >= 1.0 and di < TEMP_BUF_SIZE - 1: + d: int = int(work / scale) + if d > 9: d = 9 + temp[di] = char(d + 48) + di += 1 + work = work - double(d) * scale + scale /= 10.0 + frac_val: double = work + p: int = 0 + while p < precision: + frac_val *= 10.0 + p += 1 + frac_part: uint64 = uint64(frac_val + 0.5) + f_total: int = di + (precision + 1 if precision > 0 else 0) + if not left_align and not pad_zero and width > f_total: + pad_count = width - f_total + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if pad_zero and width > f_total: + pad_count = width - f_total + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + while di > 0 and write_count < size - 1: + di -= 1 + ptr[0] = temp[di] + write_count += 1 + ptr += _step + if precision > 0: + if write_count < size - 1: + ptr[0] = 46 + write_count += 1 + ptr += _step + fi: int = 0 + while fi < precision and fi < TEMP_BUF_SIZE - 1: + temp[fi] = char(frac_part % 10 + 48) + fi += 1 + frac_part //= 10 + while fi < precision and fi < TEMP_BUF_SIZE - 1: + temp[fi] = 48 + fi += 1 + while fi > 0 and write_count < size - 1: + fi -= 1 + ptr[0] = temp[fi] + write_count += 1 + ptr += _step + elif alt_form: + if write_count < size - 1: + ptr[0] = 46 + write_count += 1 + ptr += _step + if left_align and width > f_total: + pad_count = width - f_total + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 101 or *fmt == 69: + if precision < 0: precision = 6 + fv: double = va_arg(ap, double) + e_upper: int = 1 if *fmt == 69 else 0 + e_sp: int = _check_special_float(fv) + if e_sp != 0: + e_sgn_len: int = 0 + if e_sp == 2: + e_sgn_len = 1 + elif e_sp == 1 and show_sign: + e_sgn_len = 1 + elif e_sp == 1 and space_sign: + e_sgn_len = 1 + e_total_len: int = e_sgn_len + 3 + if not left_align and width > e_total_len: + e_pad: int = width - e_total_len + while e_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + e_pad -= 1 + if e_sp == 2: + if write_count < size - 1: + ptr[0] = 45 + write_count += 1 + ptr += _step + elif e_sp == 1 and show_sign: + if write_count < size - 1: + ptr[0] = 43 + write_count += 1 + ptr += _step + elif e_sp == 1 and space_sign: + if write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + ei_c: int = 0 + while ei_c < 3 and write_count < size - 1: + ec: int = 0 + if e_sp == 3: + # nan + if ei_c == 0: + ec = 110 + elif ei_c == 1: + ec = 97 + else: + ec = 110 + else: + # inf (e_sp==1 +inf 或 e_sp==2 -inf) + if ei_c == 0: + ec = 105 + elif ei_c == 1: + ec = 110 + else: + ec = 102 + if e_upper and ec >= 97 and ec <= 122: + ec -= 32 + ptr[0] = char(ec) + write_count += 1 + ptr += _step + ei_c += 1 + if left_align and width > e_total_len: + e_pad = width - e_total_len + while e_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + e_pad -= 1 + fmt += 1 + continue + if fv < 0.0: + if write_count < size - 1: + ptr[0] = 45 + write_count += 1 + ptr += _step + fv = 0.0 - fv + elif show_sign: + if write_count < size - 1: + ptr[0] = 43 + write_count += 1 + ptr += _step + elif space_sign: + if write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + exp10: int = 0 + if fv != 0.0: + # 上界防护:inf 会让下面两个循环永真(inf/10==inf、inf*10==inf) + exp_guard: int = 0 + while fv >= 10.0 and exp_guard < 400: + fv /= 10.0 + exp10 += 1 + exp_guard += 1 + exp_guard = 0 + while fv < 1.0 and exp_guard < 400: + fv *= 10.0 + exp10 -= 1 + exp_guard += 1 + int_digit: int = int(fv) + if int_digit > 9: int_digit = 9 + if int_digit < 0: int_digit = 0 + frac_v: double = fv - double(int_digit) + p: int = 0 + while p < precision: + frac_v *= 10.0 + p += 1 + frac_p: uint64 = uint64(frac_v + 0.5) + carry: uint64 = 1 + p = 0 + while p < precision: + carry *= 10 + p += 1 + if frac_p >= carry and carry > 0: + frac_p = 0 + int_digit += 1 + if int_digit > 9: + int_digit = 1 + exp10 += 1 + e_total: int = 1 + (precision + 1 if precision > 0 else 0) + 4 + if not left_align and not pad_zero and width > e_total: + pad_count = width - e_total + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if pad_zero and width > e_total: + pad_count = width - e_total + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + if write_count < size - 1: + ptr[0] = char(int_digit + 48) + write_count += 1 + ptr += _step + if precision > 0: + if write_count < size - 1: + ptr[0] = 46 + write_count += 1 + ptr += _step + fi: int = 0 + while fi < precision and fi < TEMP_BUF_SIZE - 1: + temp[fi] = char(frac_p % 10 + 48) + fi += 1 + frac_p //= 10 + while fi < precision and fi < TEMP_BUF_SIZE - 1: + temp[fi] = 48 + fi += 1 + while fi > 0 and write_count < size - 1: + fi -= 1 + ptr[0] = temp[fi] + write_count += 1 + ptr += _step + elif alt_form: + if write_count < size - 1: + ptr[0] = 46 + write_count += 1 + ptr += _step + if write_count < size - 1: + ptr[0] = 101 if e_upper == 0 else 69 + write_count += 1 + ptr += _step + if write_count < size - 1: + ptr[0] = 45 if exp10 < 0 else 43 + write_count += 1 + ptr += _step + exp_abs: uint64 = uint64(exp10) if exp10 >= 0 else uint64(0 - exp10) + ei: int = 0 + if exp_abs == 0: + temp[0] = 48 + temp[1] = 48 + ei = 2 + else: + while exp_abs > 0 and ei < TEMP_BUF_SIZE - 1: + temp[ei] = char(exp_abs % 10 + 48) + ei += 1 + exp_abs //= 10 + while ei < 2: + temp[ei] = 48 + ei += 1 + while ei > 0 and write_count < size - 1: + ei -= 1 + ptr[0] = temp[ei] + write_count += 1 + ptr += _step + if left_align and width > e_total: + pad_count = width - e_total + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 103 or *fmt == 71: + if precision < 0: precision = 6 + if precision == 0: precision = 1 + fv: double = va_arg(ap, double) + g_upper: int = 1 if *fmt == 71 else 0 + g_sp: int = _check_special_float(fv) + if g_sp != 0: + g_sgn_len: int = 0 + if g_sp == 2: + g_sgn_len = 1 + elif g_sp == 1 and show_sign: + g_sgn_len = 1 + elif g_sp == 1 and space_sign: + g_sgn_len = 1 + g_total_len: int = g_sgn_len + 3 + if not left_align and width > g_total_len: + g_pad: int = width - g_total_len + while g_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + g_pad -= 1 + if g_sp == 2: + if write_count < size - 1: + ptr[0] = 45 + write_count += 1 + ptr += _step + elif g_sp == 1 and show_sign: + if write_count < size - 1: + ptr[0] = 43 + write_count += 1 + ptr += _step + elif g_sp == 1 and space_sign: + if write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + gi_c: int = 0 + while gi_c < 3 and write_count < size - 1: + gc: int = 0 + if g_sp == 3: + # nan + if gi_c == 0: + gc = 110 + elif gi_c == 1: + gc = 97 + else: + gc = 110 + else: + # inf (g_sp==1 +inf 或 g_sp==2 -inf) + if gi_c == 0: + gc = 105 + elif gi_c == 1: + gc = 110 + else: + gc = 102 + if g_upper and gc >= 97 and gc <= 122: + gc -= 32 + ptr[0] = char(gc) + write_count += 1 + ptr += _step + gi_c += 1 + if left_align and width > g_total_len: + g_pad = width - g_total_len + while g_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + g_pad -= 1 + fmt += 1 + continue + if fv < 0.0: + if write_count < size - 1: + ptr[0] = 45 + write_count += 1 + ptr += _step + fv = 0.0 - fv + elif show_sign: + if write_count < size - 1: + ptr[0] = 43 + write_count += 1 + ptr += _step + elif space_sign: + if write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + fv_orig: double = fv + exp10: int = 0 + if fv != 0.0: + # 上界防护:卡死现场即此循环(inf 会让 fv>=10.0 永真,inf/10==inf) + exp_guard: int = 0 + while fv >= 10.0 and exp_guard < 400: + fv /= 10.0 + exp10 += 1 + exp_guard += 1 + exp_guard = 0 + while fv < 1.0 and exp_guard < 400: + fv *= 10.0 + exp10 -= 1 + exp_guard += 1 + g_use_e: int = 0 + if exp10 < -4 or exp10 >= precision: + g_use_e = 1 + g_len: int = 0 + if g_use_e: + int_digit: int = int(fv) + if int_digit > 9: int_digit = 9 + if int_digit < 0: int_digit = 0 + frac_v: double = fv - double(int_digit) + prec_e: int = precision - 1 + p: int = 0 + while p < prec_e: + frac_v *= 10.0 + p += 1 + frac_p: uint64 = uint64(frac_v + 0.5) + carry: uint64 = 1 + p = 0 + while p < prec_e: + carry *= 10 + p += 1 + if frac_p >= carry and carry > 0: + frac_p = 0 + int_digit += 1 + if int_digit > 9: + int_digit = 1 + exp10 += 1 + temp[0] = char(int_digit + 48) + g_len = 1 + if prec_e > 0: + temp[1] = 46 + g_len = 2 + # 小数位逆序存入(个位放 g_len+prec_e-1),temp 直接为正序。 + # 原交换反转循环在 TPV 编译产物中失效(小数位未反转,如 456->654), + # 改逆序存储,与 %f 的倒序输出一致,绕开三变量交换。 + fi: int = 0 + fp_inv: uint64 = frac_p + fp_idx: int = g_len + prec_e - 1 + while fi < prec_e and fp_idx < TEMP_BUF_SIZE - 1: + temp[fp_idx] = char(fp_inv % 10 + 48) + fp_idx -= 1 + fp_inv //= 10 + fi += 1 + g_len += prec_e + else: + prec_f: int = precision - 1 - exp10 + if prec_f < 0: prec_f = 0 + work: double = fv_orig + if work < 1.0: + temp[0] = 48 + g_len = 1 + else: + scale: double = 1.0 + # 上界防护:inf 会让 `work >= scale*10.0` 永真,正常有限值最多 308 次 + scale_guard: int = 0 + while work >= scale * 10.0 and scale_guard < 400: + scale *= 10.0 + scale_guard += 1 + while scale >= 1.0 and g_len < TEMP_BUF_SIZE - 1: + d: int = int(work / scale) + if d > 9: d = 9 + temp[g_len] = char(d + 48) + g_len += 1 + work = work - double(d) * scale + scale /= 10.0 + if prec_f > 0 and g_len < TEMP_BUF_SIZE - 1: + temp[g_len] = 46 + g_len += 1 + frac_val: double = work + p = 0 + while p < prec_f: + frac_val *= 10.0 + p += 1 + frac_part: uint64 = uint64(frac_val + 0.5) + # Store decimal digits in reverse order (ones place goes to g_len prec_f-1), temp is directly in forward order. + # The original swap-reverse loop doesn't work in TPV compiled output (decimal digits aren't reversed, e.g., 456->654), + # Change to reverse storage, consistent with %f's reverse output, avoiding three-variable swapping. + fi = 0 + fp_inv: uint64 = frac_part + fp_idx: int = g_len + prec_f - 1 + while fi < prec_f and fp_idx < TEMP_BUF_SIZE - 1: + temp[fp_idx] = char(fp_inv % 10 + 48) + fp_idx -= 1 + fp_inv //= 10 + fi += 1 + g_len += prec_f + if not alt_form and g_len > 0: + while g_len > 0: + if temp[g_len - 1] == 48: + g_len -= 1 + else: + break + if g_len > 0 and temp[g_len - 1] == 46: + g_len -= 1 + g_total: int = g_len + if g_use_e: g_total = g_len + 4 + if not left_align and not pad_zero and width > g_total: + pad_count = width - g_total + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if pad_zero and width > g_total: + pad_count = width - g_total + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + gi: int = 0 + while gi < g_len and write_count < size - 1: + ptr[0] = temp[gi] + write_count += 1 + ptr += _step + gi += 1 + if g_use_e: + if write_count < size - 1: + ptr[0] = 101 if g_upper == 0 else 69 + write_count += 1 + ptr += _step + if write_count < size - 1: + ptr[0] = 45 if exp10 < 0 else 43 + write_count += 1 + ptr += _step + exp_abs: uint64 = uint64(exp10) if exp10 >= 0 else uint64(0 - exp10) + ei: int = 0 + if exp_abs == 0: + temp[0] = 48 + temp[1] = 48 + ei = 2 + else: + while exp_abs > 0 and ei < TEMP_BUF_SIZE - 1: + temp[ei] = char(exp_abs % 10 + 48) + ei += 1 + exp_abs //= 10 + while ei < 2: + temp[ei] = 48 + ei += 1 + while ei > 0 and write_count < size - 1: + ei -= 1 + ptr[0] = temp[ei] + write_count += 1 + ptr += _step + if left_align and width > g_total: + pad_count = width - g_total + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 97 or *fmt == 65: + a_upper: int = 1 if *fmt == 65 else 0 + fv: double = va_arg(ap, double) + a_sp: int = _check_special_float(fv) + if a_sp != 0: + a_sgn_len: int = 0 + if a_sp == 2: + a_sgn_len = 1 + elif a_sp == 1 and show_sign: + a_sgn_len = 1 + elif a_sp == 1 and space_sign: + a_sgn_len = 1 + a_total_len: int = a_sgn_len + 3 + if not left_align and width > a_total_len: + a_pad: int = width - a_total_len + while a_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + a_pad -= 1 + if a_sp == 2: + if write_count < size - 1: + ptr[0] = 45 + write_count += 1 + ptr += _step + elif a_sp == 1 and show_sign: + if write_count < size - 1: + ptr[0] = 43 + write_count += 1 + ptr += _step + elif a_sp == 1 and space_sign: + if write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + ai_c: int = 0 + while ai_c < 3 and write_count < size - 1: + ac: int = 0 + if a_sp == 3: + # nan + if ai_c == 0: + ac = 110 + elif ai_c == 1: + ac = 97 + else: + ac = 110 + else: + # inf (a_sp==1 +inf 或 a_sp==2 -inf) + if ai_c == 0: + ac = 105 + elif ai_c == 1: + ac = 110 + else: + ac = 102 + if a_upper and ac >= 97 and ac <= 122: + ac -= 32 + ptr[0] = char(ac) + write_count += 1 + ptr += _step + ai_c += 1 + if left_align and width > a_total_len: + a_pad = width - a_total_len + while a_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + a_pad -= 1 + fmt += 1 + continue + fbits: uint64 = 0 + c.Load(c.Addr(fbits), c.Addr(fv)) + a_neg: int = 1 if (fbits >> 63) != 0 else 0 + a_exp: int = int((fbits >> 52) & 0x7FF) + a_frac: uint64 = fbits & 0x000FFFFFFFFFFFFF + a_leading: int = 0 + a_binexp: int = 0 + if a_exp == 0: + a_leading = 0 + if a_frac == 0: + a_binexp = 0 + else: + a_binexp = -1022 + else: + a_leading = 1 + a_binexp = a_exp - 1023 + a_prec: int = precision + if a_prec < 0: + a_prec = 13 + if a_prec > 50: + a_prec = 50 + a_hi: int = 0 + while a_hi < 13 and a_hi < a_prec: + a_digit: int = int((a_frac >> (48 - 4 * a_hi)) & 0xF) + if a_digit < 10: + temp[a_hi] = char(a_digit + 48) + else: + if a_upper: + temp[a_hi] = char(a_digit - 10 + 65) + else: + temp[a_hi] = char(a_digit - 10 + 97) + a_hi += 1 + while a_hi < a_prec: + temp[a_hi] = 48 + a_hi += 1 + a_hd_count: int = a_prec + if precision < 0: + while a_hd_count > 0 and temp[a_hd_count - 1] == 48: + a_hd_count -= 1 + a_has_dot: int = 0 + if a_hd_count > 0: + a_has_dot = 1 + elif alt_form: + a_has_dot = 1 + a_eb: int = a_binexp + a_esign: int = 0 + if a_binexp < 0: + a_esign = 1 + a_eb = -a_binexp + a_ei: int = 0 + if a_eb == 0: + temp[60] = 48 + a_ei = 1 + else: + while a_eb > 0 and a_ei < 7: + temp[60 + a_ei] = char(a_eb % 10 + 48) + a_ei += 1 + a_eb //= 10 + a_ecount: int = a_ei + a_sign_len: int = 0 + if a_neg: + a_sign_len = 1 + elif show_sign: + a_sign_len = 1 + elif space_sign: + a_sign_len = 1 + a_content_len: int = a_sign_len + 2 + 1 + a_has_dot + a_hd_count + 1 + 1 + a_ecount + if not left_align and not pad_zero and width > a_content_len: + a_pad = width - a_content_len + while a_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + a_pad -= 1 + if a_neg: + if write_count < size - 1: + ptr[0] = 45 + write_count += 1 + ptr += _step + elif show_sign: + if write_count < size - 1: + ptr[0] = 43 + write_count += 1 + ptr += _step + elif space_sign: + if write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + if pad_zero and width > a_content_len: + a_pad = width - a_content_len + while a_pad > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + a_pad -= 1 + if write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + if write_count < size - 1: + if a_upper: + ptr[0] = 88 + else: + ptr[0] = 120 + write_count += 1 + ptr += _step + if write_count < size - 1: + ptr[0] = char(a_leading + 48) + write_count += 1 + ptr += _step + if a_has_dot: + if write_count < size - 1: + ptr[0] = 46 + write_count += 1 + ptr += _step + a_hj: int = 0 + while a_hj < a_hd_count and write_count < size - 1: + ptr[0] = temp[a_hj] + write_count += 1 + ptr += _step + a_hj += 1 + if write_count < size - 1: + if a_upper: + ptr[0] = 80 + else: + ptr[0] = 112 + write_count += 1 + ptr += _step + if write_count < size - 1: + if a_esign: + ptr[0] = 45 + else: + ptr[0] = 43 + write_count += 1 + ptr += _step + a_ek: int = a_ecount + while a_ek > 0 and write_count < size - 1: + a_ek -= 1 + ptr[0] = temp[60 + a_ek] + write_count += 1 + ptr += _step + if left_align and width > a_content_len: + a_pad = width - a_content_len + while a_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + a_pad -= 1 + elif *fmt == 110: + if is_long_long or is_long: + np: int64* = va_arg(ap, int64*) + if np != None: + np[0] = int64(write_count) + elif is_short: + sp: short* = va_arg(ap, short*) + if sp != None: + sp[0] = short(write_count) + elif is_char: + cp: str = va_arg(ap, str) + if cp != None: + cp[0] = char(write_count) + else: + ip: int* = va_arg(ap, int*) + if ip != None: + ip[0] = write_count + elif *fmt == 37: + if write_count < size - 1: + ptr[0] = 37 + write_count += 1 + ptr += _step + else: + if write_count < size - 1: + ptr[0] = *fmt + write_count += 1 + ptr += _step + fmt += 1 + ptr[0] = 0 + return write_count diff --git a/w32/win32dbghelp.vp b/w32/win32dbghelp.vp index 3dc525a..0d9f631 100644 --- a/w32/win32dbghelp.vp +++ b/w32/win32dbghelp.vp @@ -3,7 +3,7 @@ from stdint import * from w32.win32base import * # ============================================================================= -# dbghelp / StackWalk64 声明(供 includes/dbgstack.vp 的 Ctrl+C 栈转储使用) +# dbghelp / StackWalk64 声明(供 includes/traceback.vp 的 Ctrl+C 栈转储使用) # # 声明本仓用到的 dbghelp 栈回溯 API 与三个栈帧结构体: # SymInitialize 初始化 dbghelp 的进程上下文(fInvadeProcess=TRUE 只枚举模块、 @@ -13,10 +13,10 @@ from w32.win32base import * # SymGetModuleBase64 按 RIP 取模块基址 # StackWalk64 逐帧回溯(MachineType=IMAGE_FILE_MACHINE_AMD64) # -# 其中 SymInitialize / SymSetOptions / StackWalk64 由 dbgstack.vp **直接调用**(静态引用); +# 其中 SymInitialize / SymSetOptions / StackWalk64 由 traceback.vp **直接调用**(静态引用); # SymFunctionTableAccess64 / SymGetModuleBase64 既作为 StackWalk64 的回调原型在此登记,也被 -# dbgstack.vp **直接调用**做首帧取证诊断(首帧返回码/AddrPC/SymFunctionTableAccess64 返回值, -# 用于判定 StackWalk64 "0 帧"的根因)——同样属静态引用。dbgstack.vp 仍用 GetProcAddress 取它们 +# traceback.vp **直接调用**做首帧取证诊断(首帧返回码/AddrPC/SymFunctionTableAccess64 返回值, +# 用于判定 StackWalk64 "0 帧"的根因)——同样属静态引用。traceback.vp 仍用 GetProcAddress 取它们 # 的地址以填入 StackWalk64 的回调槽,为的是避开「跨模块函数名 → 函数指针」的隐式转换 # (该动态解析写法见 VET/App/lib/VGuard.vp:852-867)。 # diff --git a/w32/win32memory.vp b/w32/win32memory.vp index ca8ce2f..ad9f9a9 100644 --- a/w32/win32memory.vp +++ b/w32/win32memory.vp @@ -67,7 +67,7 @@ extern def LocalFree(hMem: VOIDPTR) -> VOIDPTR # BOOL ReadProcessMemory(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, # SIZE_T nSize, SIZE_T *lpNumberOfBytesRead); # 该导出的签名恰为 dbghelp 的 PREAD_PROCESS_MEMORY_ROUTINE64:StackWalk64 的回调槽 -# 要求填这个函数(includes/dbgstack.vp 在 Install 时用 GetProcAddress 取其地址传入, +# 要求填这个函数(includes/traceback.vp 在 Install 时用 GetProcAddress 取其地址传入, # 见 includes/w32/win32dbghelp.vp 的 StackWalk64)。 # 返回 0 = 失败(GetLastError 取原因)。 extern def ReadProcessMemory(hProcess: HANDLE, lpBaseAddress: t.CConst | VOIDPTR, lpBuffer: VOIDPTR, nSize: sizet, lpNumberOfBytesRead: sizet | t.CPtr) -> BOOL