AIGC标识 【AI】TOTALCMD64.EXE 11.58 GUI 分析报告

TOTALCMD64.EXE GUI 分析报告

目录

生成日期: 2026-07-06 | 二进制文件: TOTALCMD64.EXE (Lazarus/FPC 64 位, LCL 框架)
IDA 数据库: TOTALCMD64.EXE.i64


1. 概述

TOTALCMD64.EXE 使用 Free Pascal Compiler (FPC) 编译, 基于 Lazarus Component Library (LCL) 框架 (与 Delphi 的 VCL 兼容)。GUI 资源以 TPF0/TPF1 LFM (Lazarus Form Module) 二进制资源形式嵌入可执行文件中。

FPC 64 位 VMT 结构

每个 LCL 类都拥有一个 虚拟方法表 (Virtual Method Table, VMT), 其布局如下 (13 个 qword 字段, 共 104 字节):

偏移 字段 说明
+0 vInstanceSize 实例大小 (uint64)
+8 vInstanceSize2 -vInstanceSize (校验值)
+16 vParentRef 父类 VMT 指针
+24 vClassName Pascal ShortString 类名指针
+32 vDynamicTable 动态方法表指针
+40 vMethodTable 已发布方法表指针
+48 vFieldTable 已发布字段表指针
+56 vTypeInfo RTTI 类型信息指针
+64 vInitTable 初始化表指针
+72 vAutoTable 自动化表指针
+80 vIntfTable 接口表指针
+88 vMsgStrPtr 消息字符串指针
+96 vDestroy 析构函数代码地址

方法表格式 (位于 vMethodTable 所指地址):

  • +0: count (uint32, 方法数量)
  • +4: entries[count], 每项 16 字节: {name_ptr:uint64, code_ptr:uint64}

IDA 结构体类型定义与应用

以下结构体类型已通过 MCP 在 IDB 中声明, 并 应用到全部 72 个 VMT 及方法表:

struct FPC_VMT {
    unsigned __int64 vInstanceSize;
    unsigned __int64 vInstanceSize2;
    unsigned __int64 vParentRef;
    unsigned __int64 vClassName;
    unsigned __int64 vDynamicTable;
    unsigned __int64 vMethodTable;
    unsigned __int64 vFieldTable;
    unsigned __int64 vTypeInfo;
    unsigned __int64 vInitTable;
    unsigned __int64 vAutoTable;
    unsigned __int64 vIntfTable;
    unsigned __int64 vMsgStrPtr;
    unsigned __int64 vDestroy;
};

struct FPC_MethodTableEntry {
    unsigned __int64 name_ptr;  // -> Pascal ShortString (方法名)
    unsigned __int64 code_ptr;  // -> 函数代码地址
};

struct FPC_MethodTable {
    unsigned int count;
    FPC_MethodTableEntry entries[1];  // 变长数组
};

状态: 已通过 MCP py_eval 完成 — 全部 72 个 VMT 已:

  1. 命名为 {ClassName}_VMT (例如 0xC855F8 处的 TTOTAL_CMD_VMT)
  2. 应用 FPC_VMT 结构体类型 (104 字节)
  3. 方法表命名为 {ClassName}_MethodTable 并应用 FPC_MethodTable 结构体类型
  4. 类名字符串命名为 str_{ClassName}
  5. 为关键 VMT (TTOTAL_CMD, TDLGCONFIGALL, TLister, TFindFile, TMultiRename, TCmpForm, TFileCompForm) 添加注释

IDB 已保存到磁盘。setup_vmt_structs.py 脚本也可手动执行以重新应用类型。


2. 汇总统计

指标 数量
LFM 资源 (TPF0/TPF1) 79
已解析窗体 78
唯一窗体类名 75
窗体类 VMT (有 LFM 对应) 72
全部 VMT (直接扫描法) 561
无 VMT 的类 3
组件总数 1534
事件处理器总数 (LFM) 1291
VMT 方法总数 1534
已重命名函数 1534 (全部来自先前运行的 ALREADY_SET)
已命名并应用类型的 VMT 72 (通过 MCP py_eval)
已命名并应用类型的方法表 72 (通过 MCP py_eval)
已命名的类名字符串 72 (通过 MCP py_eval)
已添加的注释 7 个关键 VMT + 7 个方法表

: "窗体类 VMT" 指有 LFM 资源对应的窗体类 (72 个), 已通过 MCP 命名并应用结构体类型。"全部 VMT" 指通过 scan_all_vmts() 直接扫描法在数据段中找到的所有 FPC VMT (561 个), 包括 TList、TStream、TStringList 等非窗体基础类。第 9 节列出的是 72 个窗体类 VMT。

未找到 VMT 的类

类名 LFM 地址
TCalendarPopupForm 0xE51BC0
TFindDialogForm 0xE53660
TReplaceDialogForm 0xE54C4C

这些窗体很可能使用运行时创建而无静态 VMT 引用, 或其 VMT 是动态构造的。


3. 窗体目录

全部 78 个已解析窗体, 含 LFM 地址、VMT 地址及方法数量:

序号 类名 LFM 地址 VMT 地址 方法数 顶层事件 子组件
1 TNASTYNAGSCREEN 0xBE4800 0xBE3698 11 6 1
2 TATTRIBDLG 0xBE8558 0xBE7908 24 6 13
3 TUSERMENUCHANGE 0xBEA9F8 0xBE9F20 36 6 5
4 TDRIVEDLG 0xBED138 0xBEC7B8 10 4 7
5 TASSOCIATEDLG 0xBEE618 0xBEDA10 15 5 14
6 TDupeSelForm 0xBEFD00 0xBEF370 22 6 4
7 TVOLLABELDLG 0xBF25F8 0xBF1C48 7 4 6
8 TDLGUNZIPALL 0xBF39C0 0xBF2C48 12 4 15
9 TDLGZIP 0xBF5670 0xBF4B60 29 5 9
10 TDLGPACKERINFO 0xBF7D38 0xBF7178 8 4 5
11 TQUICKSEARCH 0xBF9CB0 0xBF91B0 8 3 1
12 TSTDTREEDLG 0xBFB4B0 0xBFA928 15 5 13
13 TCHANGETREEDLG 0xBFCC18 0xBFC190 28 7 6
14 TINFODLG 0xBFEE40 0xBFDC18 10 7 2
15 TVERSIONINFODLG 0xC01BD0 0xC01258 4 4 6
16 TBUTTONCHANGEDLG 0xC035D0 0xC02AA0 43 5 18
17 TONEBUTTONCHANGEDLG 0xC064A8 0xC05990 28 4 19
18 TDLGUNZIP 0xC08560 0xC07C00 9 5 12
19 TProgressDialog 0xC09C88 0xC092C0 19 6 9
20 TDLG2FILEACTIONMIN 0xC0D730 0xC0CB10 10 6 11
21 TExtMsgForm 0xC0EBB8 0xC0E1F8 9 6 14
22 TCOMBOINPUT 0xC118B8 0xC10B08 13 5 12
23 TDLGCONFIGALL 0xC16A68 0xC141F0 214 6 16
24 TLister 0xC3C638 0xC3A180 62 15 3
25 TFindFile 0xC41238 0xC3F9F8 81 8 4
26 TDriveSelBox 0xC46EB0 0xC46520 9 5 7
27 TLISTCONF 0xC48670 0xC47978 22 5 5
28 TOverWriteForm 0xC4BFC0 0xC4B580 21 8 11
29 TSEARCHTEXT 0xC4DE60 0xC4D4F0 7 3 12
30 TFireWallDlg 0xC510D0 0xC50768 6 3 7
31 TCheckEditBox 0xC52C70 0xC52240 15 6 15
32 TNewConnDlg 0xC54440 0xC53A30 11 3 11
33 TDETAILS 0xC559D8 0xC54EF0 23 5 6
34 TChmodForm 0xC59568 0xC58BF0 7 4 10
35 TEditTypeForm 0xC5B4F0 0xC5A910 24 6 22
36 TEdActionForm 0xC5D1A8 0xC5C7A8 9 4 14
37 TChooseIcon 0xC5ED08 0xC5E308 12 4 9
38 TFileCompForm 0xC60A70 0xC5F9D0 63 8 1
39 TPrintConfDlg 0xC65F70 0xC655F8 9 4 8
40 TDlgCustomColors 0xC687E8 0xC67CB0 17 5 5
41 Thostdefinedlg 0xC6A3B0 0xC695E8 10 4 11
42 TEncodeEditBox 0xC6BEE0 0xC6B588 11 4 14
43 TMultiRename 0xC6E410 0xC6D0F0 69 8 9
44 TCONNECT 0xC72410 0xC718D8 24 5 5
45 TInpComboDlg 0xC73E00 0xC733E0 26 6 16
46 TSplitEditBox 0xC776F0 0xC76C98 8 4 10
47 TPrintDlg 0xC78BC0 0xC780E8 23 6 2
48 TMarginsDlg 0xC7AB58 0xC7A1E0 5 4 6
49 TCmpForm 0xC7D318 0xC7BF18 68 10 2
50 TCmtEditForm 0xC82C18 0xC82268 9 3 10
51 TFsPluginConfigForm 0xC84030 0xC83640 13 6 10
52 TTOTAL_CMD 0xC87350 0xC855F8 99 10 3
53 TCustomColumnForm 0xC8F720 0xC8ECB0 17 4 5
54 TCmdSelForm 0xC91860 0xC90E98 22 7 5
55 TDateTimePickForm 0xC93380 0xC92A20 4 4 4
56 TChooseEncodingForm 0xC94550 0xC93BF8 5 4 7
57 TPathSelForm 0xC95E88 0xC95158 19 7 6
58 TPassForm 0xC97470 0xC969E0 8 3 5
59 TShellExtForm 0xC99520 0xC98BC0 13 4 3
60 TCrcForm 0xC9DF88 0xC9D518 7 5 11
61 TPluginSelForm 0xC9FB60 0xC9F090 22 7 5
62 TCheckMsgBox 0xCBDFB0 0xCBD598 7 6 7
63 TDriveTip 0xD42358 0xD41570 3 3 0
64 TPrintForm 0xD4C7D0 0xD4BE70 3 3 4
65 TICONLIBCHANGEDLG 0xD54C48 0xD54200 9 4 11
66 TCheckComboBox 0xD5DD30 0xD5D3B0 10 6 14
67 TTabIconDialog 0xD60890 0xD5FF38 3 3 5
68 TCryptOptions 0xD61B90 0xD61238 1 1 6
69 TButtonEditBox 0xD62C80 0xD62328 4 3 10
70 THistoryForm 0xD650B0 0xD643A0 15 6 5
71 TSyncStartForm 0xD685F0 0xD67C98 5 3 21
72 TSYNCEXTINPUT 0xD69C88 0xD69310 10 4 13
73 TCalendarPopupForm 0xE51BC0 (无) - 3 1
74 TDupeSelForm (第二个) 0xE51D98 0xBEF370 22 6 4
75 TFindDialogForm 0xE53660 (无) - 1 9
76 TReplaceDialogForm 0xE54C4C (无) - - -

方法数最多的窗体: TDLGCONFIGALL (214), TTOTAL_CMD (99), TFindFile (81), TMultiRename (69), TCmpForm (68), TFileCompForm (63), TLister (62)。


4. 主窗口分析: TTOTAL_CMD

4.1 LFM 属性

Address:    0xC87350
VMT:        0xC855F8 (99 methods)
Caption:    'Total Commander'
Dimensions: Left=614, Top=176, Width=622, Height=421
ClientSize: 622 x 401
Font:       MS Sans Serif, Height=-11, Bold
KeyPreview: True
Menu:       MainMenu
ShowInTaskBar: stAlways
LCLVersion: 0.9.31

4.2 组件层次结构

TTOTAL_CMD: TOTAL_CMD
├── TMyPanel: RightPanel (Tag=0)
│   ├── TMySpeedButton: RightRoot (Tag=160, Caption='\')
│   ├── TMySpeedButton: RightParent (Tag=161, Caption='..')
│   ├── TMyPanel: RightInfo (Tag=240)
│   ├── TPathPanel: RightPath (Tag=250)
│   └── THeaderClick: RightHeader
├── TMyPanel: LeftPanel
│   ├── TMySpeedButton: LeftRoot (Tag=160)
│   ├── TMySpeedButton: LeftParent (Tag=161)
│   ├── TMyPanel: LeftInfo (Tag=240)
│   ├── TPathPanel: LeftPath (Tag=250)
│   └── THeaderClick: LeftHeader
├── TBitBtn: RenameOKBtn
└── TMainMenu: MainMenu
    └── Files (TMenuItem)
        ├── FilesChangeattributes
        ├── FilesPack
        ├── FilesUnpackspecificfiles
        ├── Testarchives1
        ├── Comparebycontent1
        ├── FilesAssociatewith
        ├── Changeinternalassociations1
        ├── Filesversioninfo
        ├── MenuItemID503 (Mark menu)
        │   ├── Selectallwithsameextension1
        │   ├── Saveselection1
        │   ├── MarkRestoreSelection
        │   ├── Saveselectiontofile1
        │   ├── Loadselectionfromfile1
        │   ├── Copy1
        │   ├── Copytoclipboardwithfullpath1
        │   ├── Copytoclipboardwithalldetails1
        │   ├── Copytoclipboardwithpathdetails1
        │   ├── MarkComparedirectories
        │   └── MarkMarknewerhidesamefiles
        ├── Commands
        │   ├── CommandsCDtree
        │   ├── CommandsSearch
        │   ├── CommandsVolumelabel
        │   ├── CommandsSystemInformation
        │   ├── Synchronizedirs1
        │   └── ShowRereadsource
        ├── Configuration
        │   ├── ConfigurationConfiguration
        │   ├── ConfigurationButtonbar
        │   ├── ConfigurationButtonbar2
        │   ├── ConfigurationIniChange
        │   ├── DarkNormalMenu
        │   ├── ConfigurationSaveposition
        │   └── ConfigurationSaveSettings
        └── Help
            ├── HelpIndex
            ├── HelpKeyboard
            ├── HelpRegistrationInfo
            ├── VisittheWebsite1
            ├── CheckForUpdates1
            └── HelpAbout

4.3 VMT 方法 (共 99 个, 按类别分组)

窗体生命周期事件

地址 方法名 IDA 符号 说明
0x7BE270 FormCreate TTOTAL_CMD_FormCreate 窗口初始化 (5656 字节)
0x7C4EB0 FormShow TTOTAL_CMD_FormShow 窗口显示处理器 (6264 字节)
0x7E95E0 FormClose TTOTAL_CMD_FormClose 关闭处理器, 保存配置
0x7FAB20 FormCloseQuery TTOTAL_CMD_FormCloseQuery 关闭确认 (3586 字节)
0x7B9CB0 FormDeactivate TTOTAL_CMD_FormDeactivate 失活处理器
0x7CC880 FormPaint TTOTAL_CMD_FormPaint 绘制处理器
0x7B8940 FormResize TTOTAL_CMD_FormResize 大小改变处理器 (4928 字节)
0x7B9C80 FormWindowStateChange TTOTAL_CMD_FormWindowStateChange 状态改变 (最小化/最大化/还原)

键盘事件

地址 方法名 IDA 符号 说明
0x7D2760 FormKeyDown TTOTAL_CMD_FormKeyDown 主键盘分发器 (23672 字节)
0x7DDD40 FormKeyUp TTOTAL_CMD_FormKeyUp 按键释放处理器
0x7D83E0 FormUTF8KeyPress TTOTAL_CMD_FormUTF8KeyPress UTF8 按键 (快速搜索)

文件操作按钮

地址 方法名 IDA 符号 说明
0x7DC8F0 idbCopyClick TTOTAL_CMD_idbCopyClick 复制 (F5)。检查 Shift/Alt/Ctrl
0x7DCB80 idbRenmovClick TTOTAL_CMD_idbRenmovClick 重命名/移动 (F6)。行内重命名或移动对话框
0x7DD0F0 idbDeleteClick TTOTAL_CMD_idbDeleteClick 删除 (F8/Del)
0x7DD190 idbMkdirClick TTOTAL_CMD_idbMkdirClick 新建目录 (F7)。Shift=先切换面板
0x7DD2D0 idbExitClick TTOTAL_CMD_idbExitClick 退出 (Alt+F4)
0x7DE260 idbEditClick TTOTAL_CMD_idbEditClick 编辑 (F4)
0x7FAA50 idbListClick TTOTAL_CMD_idbListClick 查看 (F3)
0x7FAB20 StartHandlerClick TTOTAL_CMD_StartHandlerClick 启动外部处理器

导航

地址 方法名 IDA 符号 说明
0x7DAA00 RootClick TTOTAL_CMD_RootClick 跳转到根目录 \
0x7DBB60 ParentClick TTOTAL_CMD_ParentClick 跳转到上级目录 ..
0x7FE6A0 LeftPathClick TTOTAL_CMD_LeftPathClick 左侧路径点击
0x7FFE00 RightPathClick TTOTAL_CMD_RightPathClick 右侧路径点击
0x805BF0 PathDblClick TTOTAL_CMD_PathDblClick 路径双击
0x8049E0 PathMouseUp TTOTAL_CMD_PathMouseUp 路径鼠标释放
0x804D20 PathMouseDown TTOTAL_CMD_PathMouseDown 路径鼠标按下
0x7FF290 W_PathPartialPathClick TTOTAL_CMD_W_PathPartialPathClick 部分路径点击
0x7FFB80 W_PathPartialPathDragDrop TTOTAL_CMD_W_PathPartialPathDragDrop 部分路径拖放
0x7FE910 W_PathShowDropPanel TTOTAL_CMD_W_PathShowDropPanel 显示拖放面板
0x7FF210 LeftPathMouseInDropPanel TTOTAL_CMD_LeftPathMouseInDropPanel 鼠标进入拖放面板
0x80E4C0 W_PathExit TTOTAL_CMD_W_PathExit 路径退出
0x80E510 BreadCrumbBtnClick TTOTAL_CMD_BreadCrumbBtnClick 面包屑按钮

文件列表事件

地址 方法名 IDA 符号 说明
0x7DDDA0 ListChange TTOTAL_CMD_ListChange 列表内容改变
0x7E90F0 ListSelectionChange TTOTAL_CMD_ListSelectionChange 选择改变
0x7E9010 ListClick TTOTAL_CMD_ListClick 列表点击
0x7E9260 ListExit TTOTAL_CMD_ListExit 列表退出
0x7E99B0 ListLButtonUp TTOTAL_CMD_ListLButtonUp 左键释放
0x7E9F70 ListLButtonDown TTOTAL_CMD_ListLButtonDown 左键按下
0x7EB600 ListMouseMove TTOTAL_CMD_ListMouseMove 鼠标移动
0x7EBC70 ListRButtonDown TTOTAL_CMD_ListRButtonDown 右键按下
0x7EC090 ListRButtonUp TTOTAL_CMD_ListRButtonUp 右键释放
0x804770 ListKeyUp TTOTAL_CMD_ListKeyUp 列表按键释放
0x809E80 ListVScroll TTOTAL_CMD_ListVScroll 垂直滚动
0x809D70 ListHScroll TTOTAL_CMD_ListHScroll 水平滚动
0x7CDA80 ListDrawItem TTOTAL_CMD_ListDrawItem 绘制列表项
0x7D06A0 ListMeasureItem TTOTAL_CMD_ListMeasureItem 测量列表项
0x80CA00 ListGetText TTOTAL_CMD_ListGetText 获取项文本 (ANSI)
0x80CA60 ListGetTextW TTOTAL_CMD_ListGetTextW 获取项文本 (Unicode)
0x7D9110 LeftListEnter TTOTAL_CMD_LeftListEnter 左侧列表进入
0x7D91C0 RightListEnter TTOTAL_CMD_RightListEnter 右侧列表进入
0x7D90C0 TreeListEnter TTOTAL_CMD_TreeListEnter 树形列表进入

面板事件

地址 方法名 IDA 符号 说明
0x7D2380 DoubleClick TTOTAL_CMD_DoubleClick 面板双击
0x80C7E0 PanelDblClick TTOTAL_CMD_PanelDblClick 面板双击
0x80D9B0 PanelMouseUp TTOTAL_CMD_PanelMouseUp 面板鼠标释放
0x807D40 PanelPaint TTOTAL_CMD_PanelPaint 面板绘制
0x807AB0 BottomPanelPaint TTOTAL_CMD_BottomPanelPaint 底部面板绘制
0x7BA030 BottomPanelResize TTOTAL_CMD_BottomPanelResize 底部面板大小改变
0x7BAF00 LeftPanelResize TTOTAL_CMD_LeftPanelResize 左面板大小改变
0x7BA1D0 RightPanelResize TTOTAL_CMD_RightPanelResize 右面板大小改变
0x7D8DA0 LeftPanelClick TTOTAL_CMD_LeftPanelClick 左面板点击

驱动器栏事件

地址 方法名 IDA 符号 说明
0x7D9AE0 DrivesChange TTOTAL_CMD_DrivesChange 驱动器选择改变
0x7DD3A0 DrivesKeyDown TTOTAL_CMD_DrivesKeyDown 驱动器栏按键按下 (1517 字节)
0x7DD9F0 DrivesDropdown TTOTAL_CMD_DrivesDropdown 驱动器下拉
0x800190 DrivesCloseup TTOTAL_CMD_DrivesCloseup 驱动器关闭
0x80DB90 DrivesDrawItem TTOTAL_CMD_DrivesDrawItem 驱动器绘制项 (2343 字节)
0x8003B0 DrivesMouseUp TTOTAL_CMD_DrivesMouseUp 驱动器鼠标释放
0x7DD990 DrivesEnterKeyDown TTOTAL_CMD_DrivesEnterKeyDown 驱动器回车键
0x8002B0 LeftDrivesExit TTOTAL_CMD_LeftDrivesExit 左侧驱动器退出

命令行事件

地址 方法名 IDA 符号 说明
0x7B9E60 CmdLineEnter TTOTAL_CMD_CmdLineEnter 命令行回车
0x7B9FC0 CmdLineKeyUp TTOTAL_CMD_CmdLineKeyUp 命令行按键释放
0x7FB9B0 CmdLineKeyDown TTOTAL_CMD_CmdLineKeyDown 命令行按键按下 (1023 字节)
0x7FBDB0 CmdLineKeyPress TTOTAL_CMD_CmdLineKeyPress 命令行按键
0x7DDC20 CmdLineDblClick TTOTAL_CMD_CmdLineDblClick 命令行双击
0x802170 CmdLineChange TTOTAL_CMD_CmdLineChange 命令行内容改变
0x8022C0 CmdLineDropDown TTOTAL_CMD_CmdLineDropDown 命令行下拉

表头 / 列事件

地址 方法名 IDA 符号 说明
0x80A010 HeaderDblClick TTOTAL_CMD_HeaderDblClick 表头双击 (3941 字节)
0x80AF80 HeaderMouseUp TTOTAL_CMD_HeaderMouseUp 表头鼠标释放
0x7DA030 HeaderSectionClick TTOTAL_CMD_HeaderSectionClick 表头分节点击
0x7D1B60 HeaderSectionResize TTOTAL_CMD_HeaderSectionResize 表头分节大小改变 (2077 字节)
0x8054C0 HeaderSectionTrack TTOTAL_CMD_HeaderSectionTrack 表头分节拖动
0x7C6730 HeaderSized TTOTAL_CMD_HeaderSized 表头大小已改变
0x804D10 HeaderSizing TTOTAL_CMD_HeaderSizing 表头大小改变中

分隔条事件

地址 方法名 IDA 符号 说明
0x800C90 SplitPanelMouseDown TTOTAL_CMD_SplitPanelMouseDown 分隔条鼠标按下
0x800DD0 SplitPanelMouseUp TTOTAL_CMD_SplitPanelMouseUp 分隔条鼠标释放
0x8015D0 SplitPanelMouseMove TTOTAL_CMD_SplitPanelMouseMove 分隔条鼠标移动
0x8015C0 SplitPanelDblClick TTOTAL_CMD_SplitPanelDblClick 分隔条双击

FTP / 连接事件

地址 方法名 IDA 符号 说明
0x805C40 FtpTransferModeBoxChange TTOTAL_CMD_FtpTransferModeBoxChange FTP 模式改变
0x806030 FtpTransferModeBoxKeyDown TTOTAL_CMD_FtpTransferModeBoxKeyDown FTP 模式按键按下
0x806560 DisconnectButtonClick TTOTAL_CMD_DisconnectButtonClick 断开连接按钮 (1697 字节)
0x806090 FtpInfoButtonClick TTOTAL_CMD_FtpInfoButtonClick FTP 信息按钮

就地编辑事件

地址 方法名 IDA 符号 说明
0x808160 W_InPlaceEditKeyDown TTOTAL_CMD_W_InPlaceEditKeyDown 就地编辑按键按下
0x80E500 W_InPlaceEditKeyPress TTOTAL_CMD_W_InPlaceEditKeyPress 就地编辑按键
0x7E8860 W_InPlaceEditGoingOff TTOTAL_CMD_W_InPlaceEditGoingOff 就地编辑结束

重命名 / 菜单

地址 方法名 IDA 符号 说明
0x80E550 RenameOKBtnClick TTOTAL_CMD_RenameOKBtnClick 重命名确定按钮
0x80E590 RenameOKBtnMouseDown TTOTAL_CMD_RenameOKBtnMouseDown 重命名确定鼠标按下
0x80E5C0 RenameOKBtnMouseLeave TTOTAL_CMD_RenameOKBtnMouseLeave 重命名确定鼠标离开
0x7FA9F0 MenuDispatch TTOTAL_CMD_MenuDispatch 集中式菜单分发器
0x8079B0 idbDeleteMouseDown TTOTAL_CMD_idbDeleteMouseDown 删除按钮鼠标按下
0x8056C0 SizePaint TTOTAL_CMD_SizePaint 尺寸绘制

4.4 LFM 事件处理器绑定

窗体级事件 (来自 LFM)

事件 处理器
OnClose FormClose
OnCloseQuery FormCloseQuery
OnCreate FormCreate
OnDeactivate FormDeactivate
OnKeyDown FormKeyDown
OnKeyUp FormKeyUp
OnPaint FormPaint
OnResize FormResize
OnShow FormShow
OnWindowStateChange FormWindowStateChange

菜单项 — 全部使用 MenuDispatch

MainMenu 中所有 TMenuItem 的 OnClick 事件都路由到单一的 MenuDispatch 处理器。这是一种集中式命令分发模式 — 每个 TMenuItem 的 Tag 属性标识命令。MenuDispatch 函数 (位于 0x7FA9F0) 读取 Tag 并分发到对应的命令处理器。

关键菜单路径:

  • Files.FilesChangeattributes → 修改属性
  • Files.FilesPack → 打包文件
  • Files.FilesUnpackspecificfiles → 解包文件
  • Files.Testarchives1 → 测试压缩包
  • Files.Comparebycontent1 → 按内容比较
  • Files.FilesAssociatewith → 关联
  • Files.FilesVersioninfo → 版本信息
  • Files.MenuItemID503.* → 标记/选择命令 (11 个子项)
  • Files.Commands.* → 命令 (CD 树、搜索、卷标等)
  • Files.Configuration.* → 配置 (7 个子项)
  • Files.Help.* → 帮助 (6 个子项)

5. 关键事件处理器 IDA 反编译分析

5.1 TTOTAL_CMD_FormCreate (0x7BE270, 5656 字节)

用途: 主窗口初始化。

分析: 这是最大的初始化函数。它:

  • 初始化 FPC try/except 异常帧 (sub_410100 / sub_412E70 模式)
  • 设置左右面板、驱动器栏、文件列表控件
  • 初始化命令行、按钮栏和状态栏
  • 加载配置 (窗口位置、面板大小、列宽)
  • 创建子控件并设置事件处理器
  • 读取 DPI 设置并相应缩放界面

关键模式: sub_410100(1, ...) 创建异常帧; sub_412E70() 检查是否发生异常; sub_410500() 离开异常帧。

5.2 TTOTAL_CMD_FormClose (0x7E95E0, 968 字节)

用途: 处理主窗口关闭。

分析:

// Pseudocode summary:
TTOTAL_CMD_FormClose(form, action, close_action) {
    // Hide window first
    if (!exception_check1()) {
        hwnd = get_window_handle(form);
        ShowWindow(hwnd, SW_HIDE);  // 0 = SW_HIDE
    }
    // Save configuration
    if (exception_check1()) {
        // Error path - save what we can
        save_config();
    }
    // Cleanup resources
    if (!exception_check2()) {
        if (flag_D7C470) sub_62B000();  // Save layout
        sub_B44EF0();                    // Save window position
        // Free child objects
        if (form[0xC1] != form[0xEB]) {
            sub_4408F0(form);            // Cleanup panels
        }
        if (form[0x13E]) sub_7E9550(form);  // Cleanup file lists
        if (form[0x2D8]) { free(form[0x2D8]); form[0x2D8] = 0; }
        if (form[0x2D9]) { free(form[0x2D9]); form[0x2D9] = 0; }
        // Free global objects
        if (qword_BD5DA0) { free(qword_BD5DA0); qword_BD5DA0 = 0; }
        if (qword_BD5D50) { free(qword_BD5D50); qword_BD5D50 = 0; }
    }
    *close_action = 2;  // caFree - free the form
}

关键发现: *close_action = 2 设置 TCloseAction = caFree, 表示窗体在关闭后被释放。

5.3 TTOTAL_CMD_idbCopyClick (0x7DC8F0, 641 字节)

用途: 复制按钮处理器 (F5 键)。

分析:

// Pseudocode summary:
TTOTAL_CMD_idbCopyClick(form, button) {
    if (form[0x14F7] || byte_C0D490) return;  // Re-entrancy guard
    form[0x14F7] = 1;  // Set busy flag
    sub_AF95E0();      // Reset UI state

    if (!exception_check()) {
        Focus = GetFocus();  // Remember focused panel
        panel_id = (Focus == qword_D7B8B8) ? 1 :
                   (Focus == qword_D7B8B0) ? 2 : 0;

        if (!byte_D7C300 && !byte_D7C310)
            sub_447A20(qword_BBAC30);  // Close popup menus

        shift = (GetKeyState(VK_SHIFT) < 0);  // 0x10 = VK_SHIFT

        if (shift && sub_490BF0()) {
            // Shift+Copy: special mode
            sub_6C4860(get_hwnd(form));
        } else if (GetKeyState(VK_MENU) >= 0 || sub_490BF0()) {
            // Normal copy or Ctrl+Copy
            if (byte_D7C470 && GetKeyState(VK_CONTROL) < 0)
                sub_7DBBD0(form, 4, 0, 0);  // Ctrl+Copy: mode 4
            else
                sub_7DBBD0(form, 1, shift, 0);  // Normal: mode 1
        } else {
            // Alt+Copy variant
            sub_58B8E0(ctrl_pressed, shift, 0);
        }

        // Restore focus
        if (panel_id == 1) SetFocus(qword_D7B8B8);
        else if (panel_id == 2) SetFocus(qword_D7B8B0);
        else sub_47EC30();
    }
    form[0x14F7] = 0;  // Clear busy flag
}

关键发现: sub_7DBBD0(form, mode, shift, 0) 是核心复制/移动操作分发器:

  • mode=1: 普通复制
  • mode=3: 移动 (由 idbRenmovClick 使用)
  • mode=4: Ctrl+复制 (特殊模式)

5.4 TTOTAL_CMD_idbDeleteClick (0x7DD0F0, 152 字节)

用途: 删除按钮处理器 (F8/Delete 键)。

分析: 简单包装 — 调用 sub_7DCDE0(form, 0) 执行实际删除操作。重入保护模式 (form[0x14F7]) 与 idbCopyClick 相同。

5.5 TTOTAL_CMD_idbMkdirClick (0x7DD190, 315 字节)

用途: 新建目录按钮处理器 (F7 键)。

分析:

// If Shift pressed or called from inactive panel:
if (shift_pressed || from_inactive) {
    if (sub_7BA010(form)) return;  // Check if action cancelled
    SetFocus(other_panel);          // Switch to other panel
    sub_7D8F10(form, other_panel);  // Update panel state
    byte_BD58C0 = !byte_BD58C0;    // Toggle active panel flag
}
// Create directory dialog
sub_5C7890(get_hwnd(form), active_panel, 0);

关键发现: byte_BD58C0 是活动面板标志 (0=左, 1=右)。Shift+F7 在 另一个 面板中创建目录。

5.6 TTOTAL_CMD_idbRenmovClick (0x7DCB80, 603 字节)

用途: 重命名/移动按钮处理器 (F6 键)。

分析:

// Determine if single-file inline rename is possible
file_count = sub_4802B0(active_panel);
can_inline_rename = (shift || sub_490BF0())
    && file_count <= 1
    && (selection_allows_rename);

if (can_inline_rename && file_count == 1) {
    // Verify single file rename is valid
    if (sub_481650(active_panel, 1, &index) == 1
        && sub_480290(active_panel) == index) {
        can_inline_rename = 1;
    }
}

if (can_inline_rename) {
    // Inline rename (edit box in file list)
    sub_7E8E20(form, panel, from_button, 0);
} else {
    // Move dialog
    shift = (GetKeyState(VK_SHIFT) < 0);
    if (sub_490BF0()) {
        sub_6C4540(get_hwnd(form));  // Special mode
    } else {
        sub_7DBBD0(form, 3, shift, 0);  // Move: mode 3
    }
}

关键发现: 在单个选中文件上按 F6 触发行内重命名 (sub_7E8E20); 在多个文件上按 F6 打开移动对话框 (sub_7DBBD0(form, 3, ...))。

5.7 TTOTAL_CMD_DoubleClick (0x7D2380, 462 字节)

用途: 面板/文件列表双击处理器。

分析:

// Kill any pending timer
if (form[0x16F7]) {
    KillTimer(get_hwnd(form), 2);
    form[0x16F7] = 0;
}
sub_7E9550(form);  // Cancel any in-place editing

// Determine which panel was double-clicked
panel = (form[0x9E8] == clicked_object);  // 0=right, 1=left

// Check for rapid double-click (suppress triple-click)
if (form[0x1580]) {  // Last click time
    elapsed = abs(GetTickCount() - form[0x1580]);
    threshold = GetDoubleClickTime() + 50;  // 50ms extra tolerance
    if (elapsed < threshold) return;
}

// If drag-drop mode or special mode, check cursor position
if (byte_D7C2D0 || byte_D7CAF0[panel]) {
    GetCursorPos(&point);
    ScreenToClient(panel_window[panel], &point);
    item_index = sub_482060(panel, point);
    if (item_index < 0) return;
    if (item_index >= file_count[panel]) return;
    if (sub_480610(panel) != item_index) return;
}

// Normal double-click: open file
if (GetKeyState(VK_MENU) >= 0 || GetKeyState(VK_CONTROL) < 0) {
    // Alt NOT pressed, or Ctrl pressed: normal open
    cmd_id = sub_41C860(panel) + 0x12D687;  // Calculate command ID
    PostMessageA(get_hwnd(form), WM_USER + 0x57, cmd_id, 0);
} else {
    // Alt+double-click: alternate action (Alt+Enter style)
    sub_59A4D0(get_hwnd(form));
}

关键发现: 双击向窗体发送 WM_USER + 0x57 (0x457 = 1111) 消息。wParam 编码命令。Alt+双击调用 sub_59A4D0 (可能显示文件属性或用关联程序打开)。

5.8 TTOTAL_CMD_FormShow (0x7C4EB0, 6264 字节)

用途: 主窗口变为可见时调用。

分析: 非常大的函数, 处理:

  • 从保存的配置恢复窗口位置
  • 使用 GetDialogBaseUnits() 进行 DPI 感知缩放
  • 面板宽度/高度调整
  • 驱动器栏初始化
  • 文件列表列宽恢复
  • 按钮栏状态加载
  • 状态栏文本设置
  • 初始目录读取

5.9 TLister_FormKeyDown (0x65A320, 5466 字节)

用途: Lister (文件查看器) 键盘快捷键处理器。

分析: 处理 Lister 中的所有键盘快捷键:

  • 方向键: 滚动
  • PgUp/PgDn: 翻页滚动
  • Home/End: 跳转到开头/结尾
  • F2: 切换自动换行
  • F3: 查找下一个
  • F4: 编辑文件
  • F7: 查找
  • F8: 切换十六进制/ASCII 模式
  • Ctrl+键: 各种命令 (复制、全选等)
  • Alt+键: 代码页切换、缩放

该函数是按键代码上的大型 switch/case, 包含众多分支以处理不同查看器模式 (文本、二进制、图像、多媒体)。

5.10 TFindFile_FormCreate (0x68AA20, 3827 字节)

用途: 查找文件对话框初始化。

分析: 设置:

  • 带历史记录的搜索文本组合框
  • 驱动器选择复选框
  • 日期范围过滤控件 (id_DateFromBtn, id_DateToBtn)
  • 搜索选项复选框 (ANSI、DOS、Hex 等)
  • 插件字段配置
  • 重复文件查找模式初始化
  • Everything 搜索集成 (id_Everything)
  • 窗口位置恢复

6. 关键对话框窗体

6.1 TDLGCONFIGALL — 配置对话框

  • VMT: 0xC141F0
  • 方法数: 214 (最大的窗体)
  • LFM: 0xC16A68, 6 个顶层事件, 16 个子组件

这是主配置对话框, 拥有 214 个已发布方法, 涵盖 Total Commander 全部设置项: 显示、字体、颜色、制表位、操作行为、ZIP/FTP 设置等。

6.2 TFindFile — 查找文件对话框

  • VMT: 0xC3F9F8
  • 方法数: 81
  • LFM: 0xC41238, 8 个顶层事件, 4 个子组件

关键处理器:

地址 处理器 说明
0x68AA20 FormCreate (3827 字节) 初始化搜索控件
0x68B920 FormShow (2827 字节) 显示对话框, 加载上次搜索
0x69FDB0 FormCloseQuery (1867 字节) 关闭确认
0x6911C0 CancelClick (811 字节) 取消按钮
0x6A33A0 id_DeleteClick (736 字节) 从结果中删除
0x69C290 id_gotopathClick (920 字节) 在文件管理器中跳转到路径
0x6ADA20 id_EverythingClick (1231 字节) Everything SDK 集成
0x6A81A0 id_DupeFinderClick (593 字节) 重复查找模式
0x65A320 FormKeyDown (1040 字节) 键盘快捷键

6.3 TMultiRename — 多重重命名工具

  • VMT: 0xC6D0F0
  • 方法数: 69
  • LFM: 0xC6E410, 8 个顶层事件, 9 个子组件

关键处理器:

地址 处理器 说明
0x73EE50 FormCreate (1280 字节) 初始化重命名控件
0x732000 FormShow (4213 字节) 显示对话框, 填充文件列表
0x743240 FormKeyDown (679 字节) 键盘快捷键
0x72F690 AddCounterClick (28 字节) 插入 [C] 计数器占位符
0x72F6B0 AddCounterEClick (28 字节) 插入 [CE] 扩展计数器
0x72F670 AddExtClick (28 字节) 插入 [E] 扩展名占位符
0x72F650 AddNameClick (28 字节) 插入 [N] 名称占位符
0x731670 AddDateClick (28 字节) 插入 [D] 日期占位符
0x731690 AddTimeClick (28 字节) 插入 [T] 时间占位符
0x744530 Editnamesindefaulteditor1Click (2237 字节) 在外部编辑器中编辑名称
0x73EAD0 HeaderClick1SectionClick (655 字节) 列标题点击

6.4 TLister — 文件查看器

  • VMT: 0xC3A180
  • 方法数: 62
  • LFM: 0xC3C638, 15 个顶层事件, 3 个子组件

关键处理器:

地址 处理器 说明
0x63E110 FormCreate (5850 字节) 初始化查看器
0x63F840 FormShow (2502 字节) 显示查看器, 加载文件
0x65A320 FormKeyDown (5466 字节) 全部键盘快捷键
0x652E90 FormPaint (8807 字节) 渲染文件内容
0x661E30 FormCloseQuery (2583 字节) 关闭确认
0x679FF0 CodePageClick (3267 字节) 代码页切换
0x65F440 EditFindClick (1664 字节) 查找对话框
0x65FAC0 EditFindnextClick (548 字节) 查找下一个
0x65EDF0 CopyasHexClick (1603 字节) 复制为十六进制
0x65DEA0 EditCopyastextClick (3907 字节) 复制为文本

6.5 TCmpForm — 文件比较

  • VMT: 0xC7BF18
  • 方法数: 68
  • LFM: 0xC7D318, 10 个顶层事件, 2 个子组件

6.6 TFileCompForm — 按内容比较文件

  • VMT: 0xC5F9D0
  • 方法数: 63
  • LFM: 0xC60A70, 8 个顶层事件, 1 个子组件

7. 事件处理器模式

7.1 FPC 异常处理模式

大多数事件处理器使用 FPC 的 try/except 模式, 在反编译中表现为:

sub_410100(1, &buf1, &buf2);    // __enter_exception_frame
result = sub_412E70(buf);       // __check_exception (returns 0 = no exception)
if (!result) {
    // ... normal code ...
}
sub_410500();                   // __leave_exception_frame
if (result) {
    // ... exception handler code ...
    sub_4106F0();               // __end_exception_handler
}

7.2 重入保护模式

按钮点击处理器使用 form + 0x14F7 处的忙标志:

if (form[0x14F7] || byte_C0D490) return;  // Already busy
form[0x14F7] = 1;                          // Set busy
// ... do work ...
form[0x14F7] = 0;                          // Clear busy

7.3 菜单分发模式

所有 TMenuItem OnClick 事件使用单一的 MenuDispatch 处理器 (0x7FA9F0)。每个 TMenuItem 的 Tag 属性标识命令。这是一种命令模式实现:

procedure TTOTAL_CMD.MenuDispatch(Sender: TObject);
begin
  case TComponent(Sender).Tag of
    cm_Copy:    { copy command };
    cm_Move:    { move command };
    // ... etc
  end;
end;

7.4 通用窗体生命周期方法

大多数窗体实现以下标准方法:

方法 用途
FormCreate 初始化窗体, 加载设置
FormShow 显示前的最终设置
FormClose 清理, 设置 CloseAction
FormCloseQuery 确认关闭 (返回 CanClose)
FormDestroy 释放已分配资源
FormKeyDown 键盘快捷键处理器
FormResize 处理窗口大小改变

8. IDA 分析脚本

"""LFM (Lazarus Form Module) 与 VMT (虚拟方法表) 解析器 for TOTALCMD64.EXE - v5.

功能:
1. 扫描并解析所有 TPF0/TPF1 LFM 资源 (窗体、组件、事件处理器)
2. 解析指定地址或当前光标位置的 FPC 64-bit VMT 结构
3. 解析指定地址或当前光标位置的 LFM 窗体资源
4. 自动检测指定地址处是 LFM 还是 VMT, 并调用相应解析器
5. 扫描二进制文件中所有 VMT 并输出完整报告

运行方式:
- 交互模式 (IDA Pro 中 Alt+F7 / File > Script File):
    弹出三按钮对话框: VMT全扫描 / LFM扫描 / 自动解析
    选择"自动解析"后弹出地址输入框 (默认当前光标位置),
    自动检测该地址处是 LFM (TPF0/TPF1 标记) 还是 VMT (校验字段) 并解析
- 批处理模式 (idat64 -S"parse_lfm.py --parse 0x地址"):
    --parse [地址]  自动检测并解析 LFM/VMT (地址缺省时用当前光标)
    --lfm [地址]    解析指定地址 LFM
    --vmt [地址]    解析指定地址 VMT
    --vmt-scan      扫描所有 VMT
- Python 控制台直接调用:
    auto_parse_at(0xC855F8)    # 自动检测并解析
    auto_parse_at()            # 使用当前光标位置
    parse_vmt_main(0xC855F8)   # 解析指定地址 VMT
    parse_lfm_main(0xC855F8)   # 解析指定地址 LFM
    scan_all_vmts()            # 扫描所有 VMT
    scan_lfms()                # 扫描所有 LFM

v4 遗留特性 (均已保留):
- 分块读取 LFM 数据, 消除逐字节 IDA API 调用导致的卡顿
- 合理的迭代上限和操作计数器, 防止解析器失控
- 嵌入式字符串恢复 (6 字节零值校验)
- 0xF1/0xF2 Lazarus 扩展标记处理
"""
import os
import sys
import idc
import ida_bytes
import ida_segment
import idautils
import ida_kernwin

# Value type identifiers (from FPC classes.pp TValueType enum)
# vaNull=0, vaList=1, vaInt8=2, vaInt16=3, vaInt32=4, vaExtended=5,
# vaString=6, vaIdent=7, vaFalse=8, vaTrue=9, vaBinary=10, vaSet=11,
# vaLString=12, vaNil=13, vaCollection=14, vaSingle=15, vaCurrency=16,
# vaDate=17, vaWString=18, vaInt64=19, vaUTF8String=20, vaUString=21
VA_NIL = 0x0D
VA_LIST = 0x01
VA_INT8 = 0x02
VA_INT16 = 0x03
VA_INT32 = 0x04
VA_EXTENDED = 0x05
VA_STRING = 0x06
VA_IDENT = 0x07
VA_FALSE = 0x08
VA_TRUE = 0x09
VA_BINARY = 0x0A
VA_SET = 0x0B
VA_LSTRING = 0x0C
VA_COLLECTION = 0x0E
VA_SINGLE = 0x0F
VA_CURRENCY = 0x10
VA_DATE = 0x11
VA_WSTRING = 0x12
VA_INT64 = 0x13
VA_UTF8STRING = 0x14
VA_USTRING = 0x15

MAX_DEPTH = 15
MAX_PROPS = 512
MAX_CHILDREN = 512
MAX_LIST_ITEMS = 4096
MAX_LFM_SIZE = 0x100000  # 1MB safety cap per LFM resource
MAX_OPS = 500000  # total operation counter cap per LFM
CHUNK_SIZE = 0x100000    # 1MB chunks for scanning

# Global operation counter (reset per LFM). Using list for mutable closure access.
_ops_counter = [0]

# Output file: defaults to the IDB directory (same folder as the .i64 file).
_idb_dir = os.path.dirname(idc.get_idb_path()) if idc.get_idb_path() else '.'
OUTPUT_PATH = os.path.join(_idb_dir, 'lfm_forms.txt')

# =============================================================================
# VMT (虚拟方法表) 常量
# =============================================================================
# FPC 64-bit VMT 布局 (13 个 qword 字段, 共 104 字节):
#   +0   vInstanceSize    实例大小 (uint64)
#   +8   vInstanceSize2   -vInstanceSize (校验字段, 用于验证 VMT 有效性)
#   +16  vParentRef       父类 VMT 指针
#   +24  vClassName       类名 Pascal ShortString 指针
#   +32  vDynamicTable    动态方法表指针
#   +40  vMethodTable     已发布方法表指针
#   +48  vFieldTable      已发布字段表指针
#   +56  vTypeInfo        RTTI 类型信息指针
#   +64  vInitTable       初始化表指针
#   +72  vAutoTable       自动化表指针
#   +80  vIntfTable       接口表指针
#   +88  vMsgStrPtr       消息字符串指针
#   +96  vDestroy         析构函数代码地址
#
# 方法表格式 (位于 vMethodTable 指向的地址):
#   +0   count (uint32)   方法数量
#   +4   entries[count]   每项 16 字节:
#        +0  name_ptr (uint64) -> 方法名 Pascal ShortString
#        +8  code_ptr (uint64) -> 方法代码地址

VMT_SIZE = 13 * 8  # 104 字节
MAX_VMT_CHAIN_DEPTH = 32  # 父类链最大深度, 防止循环引用
MAX_METHODS_PER_VMT = 1000  # 单个 VMT 方法数上限
VMT_SEARCH_BACK_RANGE = 0x1000  # 向前查找 VMT 起始的最大范围 (4KB)

# VMT 字段定义: (偏移, 字段名, 说明)
VMT_FIELDS = [
    (0,   'vInstanceSize',  '实例大小'),
    (8,   'vInstanceSize2', '校验值 (-vInstanceSize)'),
    (16,  'vParentRef',     '父类 VMT 指针'),
    (24,  'vClassName',     '类名字符串指针'),
    (32,  'vDynamicTable',  '动态方法表指针'),
    (40,  'vMethodTable',   '已发布方法表指针'),
    (48,  'vFieldTable',    '已发布字段表指针'),
    (56,  'vTypeInfo',      'RTTI 类型信息指针'),
    (64,  'vInitTable',     '初始化表指针'),
    (72,  'vAutoTable',     '自动化表指针'),
    (80,  'vIntfTable',     '接口表指针'),
    (88,  'vMsgStrPtr',     '消息字符串指针'),
    (96,  'vDestroy',       '析构函数地址'),
]

VMT_REPORT_PATH = os.path.join(_idb_dir, 'vmt_parse_report.txt')
VMT_SCAN_REPORT_PATH = os.path.join(_idb_dir, 'vmt_scan_report.txt')


def _check_ops():
    """Increment operation counter and bail out if limit exceeded."""
    _ops_counter[0] += 1
    if _ops_counter[0] > MAX_OPS:
        raise RuntimeError(f'Operation limit exceeded ({MAX_OPS})')


def try_recover_embedded_string(data, placeholder, pos, end_pos):
    """Try to recover a string from Lazarus embedded data format.

    When a Pascal string contains 0x00 bytes, it may be a placeholder
    followed by 16 bytes of embedded metadata and the actual string suffix.

    Empirically determined format (from TOTALCMD64.EXE LFM resources):
    - N bytes placeholder (may contain partial string + 0x00 padding)
    - 8 bytes 0xFF marker (sentinel)
    - 1 byte = N*2 (validation, mod 256)
    - 1 byte unknown (possibly a hash or index)
    - 6 bytes 0x00
    - M bytes actual string suffix (M = N - k, where k = count of non-0x00
      bytes before the first 0x00 in placeholder)

    The full string = placeholder[:k] + suffix.

    Two layouts are handled:
    - Format A (marker after string): placeholder is the Pascal string content;
      0xFF*8 + metadata + suffix follow after the string.
    - Format B (marker inside string): Pascal string = placeholder + 0xFF*8;
      metadata + suffix follow after the string.

    Returns (recovered_string, new_pos) or (None, original_pos) if no
    embedded data found.
    """
    placeholder_bytes = placeholder.encode('latin1', errors='replace')

    # Format B: check if 0xFF*8 marker is INSIDE the placeholder
    marker_idx = placeholder_bytes.find(b'\xff' * 8)
    if marker_idx >= 0:
        # Marker found inside string.
        # prefix = placeholder_bytes[:marker_idx] (may contain text + null padding)
        prefix = placeholder_bytes[:marker_idx]
        n = marker_idx  # N = length of prefix
        k = 0
        for c in prefix:
            if c == 0:
                break
            k += 1
        # Metadata + suffix should be after the Pascal string (at pos)
        # Layout after string: N*2 (1 byte) + unknown (1 byte) + 0x00*6 (6 bytes) + suffix (M bytes)
        if pos + 8 <= end_pos:
            expected_n2 = (n * 2) & 0xFF
            actual_n2 = data[pos]
            if actual_n2 == expected_n2 and data[pos + 2:pos + 8] == b'\x00' * 6:
                m = n - k
                suffix_pos = pos + 8
                if m > 0 and suffix_pos + m <= end_pos:
                    suffix_bytes = data[suffix_pos:suffix_pos + m]
                    new_pos = suffix_pos + m
                    try:
                        suffix = suffix_bytes.decode('latin1')
                    except Exception:
                        suffix = f'<{suffix_bytes.hex()}>'
                    return prefix[:k].decode('latin1') + suffix, new_pos
                else:
                    return prefix[:k].decode('latin1'), pos + 8
        # Metadata not found after string; return sanitized prefix
        if k > 0:
            return prefix[:k].decode('latin1'), pos
        return None, pos

    # Format A: marker after string (original behavior)
    if pos + 16 > end_pos:
        return None, pos
    # Check for 0xFF*8 marker
    marker = data[pos:pos + 8]
    if len(marker) != 8 or marker != b'\xff' * 8:
        return None, pos
    # Validate N*2 byte
    n = len(placeholder)
    expected_n2 = (n * 2) & 0xFF
    actual_n2 = data[pos + 8]
    if actual_n2 != expected_n2:
        return None, pos
    # Validate 6 zero bytes at pos+10..pos+15 (stricter than v3)
    if data[pos + 10:pos + 16] != b'\x00' * 6:
        return None, pos
    # Skip 16 bytes of embedded data (8 marker + 1 N*2 + 1 unknown + 6 zeros)
    pos += 16
    # Calculate k (number of non-0x00 bytes before first 0x00)
    k = 0
    for c in placeholder:
        if c == '\x00':
            break
        k += 1
    # Read M = N - k bytes as actual string suffix
    m = n - k
    if m == 0:
        return placeholder[:k], pos
    if pos + m > end_pos:
        return None, pos
    suffix_bytes = data[pos:pos + m]
    pos += m
    if suffix_bytes:
        try:
            suffix = suffix_bytes.decode('latin1')
        except Exception:
            suffix = f'<{suffix_bytes.hex()}>'
    else:
        suffix = ''
    full_string = placeholder[:k] + suffix
    return full_string, pos


def read_pascal_string(data, pos, end_pos):
    """Read a Pascal-style string (1 byte length + chars) from bytes buffer.

    Also handles Lazarus embedded data format: if the string contains 0x00
    bytes and is followed by 0xFF*8 marker, skip the embedded metadata and
    read the actual string suffix.
    """
    _check_ops()
    if pos + 1 > end_pos:
        return '', pos
    slen = data[pos]
    if pos + 1 + slen > end_pos:
        # Truncate to available
        slen = max(0, end_pos - pos - 1)
    s = data[pos + 1:pos + 1 + slen]
    new_pos = pos + 1 + slen
    if s:
        try:
            result = s.decode('latin1')
        except Exception:
            result = f'<{s.hex()}>'
    else:
        result = ''
    # Check for Lazarus embedded data format: if the string contains 0x00
    # and/or 0xFF*8 marker, recover the actual string.
    if slen > 0 and ('\x00' in result or '\xff' * 8 in result):
        recovered, recovered_pos = try_recover_embedded_string(data, result, new_pos, end_pos)
        if recovered is not None:
            return recovered, recovered_pos
    return result, new_pos


def _is_valid_lfm_identifier(s, allow_dot=False):
    """Check if a string is a valid LFM identifier (type/name/prop).

    Valid identifiers contain only ASCII letters, digits, and underscores.
    If allow_dot is True, dots are also allowed (for property names like
    Font.Color). Any control characters or non-ASCII characters cause
    immediate rejection — this prevents binary data from passing validation
    due to Python's Unicode-aware isalpha()/isalnum() treating characters
    like ÿ (U+00FF) as alphabetic.
    """
    if not s or not isinstance(s, str):
        return False
    for i, c in enumerate(s):
        o = ord(c)
        if o < 32 or o >= 127:
            return False
        if not (c.isalnum() or c == '_' or (allow_dot and c == '.')):
            return False
    if s[0] != '_' and not s[0].isalpha():
        return False
    return True


def read_value(data, pos, end_pos, depth=0):
    """Read a value from the LFM bytes buffer. Bounds-checked, iteration-limited."""
    _check_ops()
    if depth > MAX_DEPTH:
        return None, pos
    if pos >= end_pos:
        return None, pos
    type_byte = data[pos]
    pos += 1

    if type_byte == VA_NIL:
        return None, pos
    elif type_byte == VA_LIST:
        items = []
        for _ in range(MAX_LIST_ITEMS):
            if pos >= end_pos:
                break
            item_type = data[pos]
            if item_type == 0:
                pos += 1
                break
            v, pos = read_value(data, pos, end_pos, depth + 1)
            items.append(v)
        return items, pos
    elif type_byte == VA_INT8:
        if pos >= end_pos:
            return 0, pos
        v = data[pos]
        if v >= 128:
            v -= 256
        return v, pos + 1
    elif type_byte == VA_INT16:
        if pos + 2 > end_pos:
            return 0, pos
        v = int.from_bytes(data[pos:pos + 2], 'little')
        if v >= 0x8000:
            v -= 0x10000
        return v, pos + 2
    elif type_byte == VA_INT32:
        if pos + 4 > end_pos:
            return 0, pos
        v = int.from_bytes(data[pos:pos + 4], 'little')
        if v >= 0x80000000:
            v -= 0x100000000
        return v, pos + 4
    elif type_byte == VA_INT64:
        if pos + 8 > end_pos:
            return 0, pos
        v = int.from_bytes(data[pos:pos + 8], 'little')
        return v, pos + 8
    elif type_byte in (VA_STRING, VA_LSTRING, VA_IDENT):
        return read_pascal_string(data, pos, end_pos)
    elif type_byte == VA_UTF8STRING:
        if pos + 1 > end_pos:
            return '', pos
        slen = data[pos]
        if pos + 1 + slen > end_pos:
            slen = max(0, end_pos - pos - 1)
        s = data[pos + 1:pos + 1 + slen]
        pos += 1 + slen
        if s:
            try:
                return s.decode('utf-8'), pos
            except Exception:
                return s.decode('latin1', errors='replace'), pos
        return '', pos
    elif type_byte == VA_WSTRING:
        if pos + 4 > end_pos:
            return '', pos
        slen = int.from_bytes(data[pos:pos + 4], 'little')
        if pos + 4 + slen > end_pos:
            slen = max(0, end_pos - pos - 4)
        s = data[pos + 4:pos + 4 + slen]
        pos += 4 + slen
        if s:
            try:
                return s.decode('utf-16-le'), pos
            except Exception:
                return f'<{s.hex()}>', pos
        return '', pos
    elif type_byte == VA_USTRING:
        if pos + 4 > end_pos:
            return '', pos
        slen = int.from_bytes(data[pos:pos + 4], 'little')
        if pos + 4 + slen > end_pos:
            slen = max(0, end_pos - pos - 4)
        s = data[pos + 4:pos + 4 + slen]
        pos += 4 + slen
        if s:
            try:
                return s.decode('utf-16-le'), pos
            except Exception:
                return f'<{s.hex()}>', pos
        return '', pos
    elif type_byte == VA_TRUE:
        return True, pos
    elif type_byte == VA_FALSE:
        return False, pos
    elif type_byte == VA_SET:
        items = []
        for _ in range(MAX_LIST_ITEMS):
            if pos >= end_pos:
                break
            slen = data[pos]
            if slen == 0:
                pos += 1
                break
            if pos + 1 + slen > end_pos:
                break
            s = data[pos + 1:pos + 1 + slen]
            pos += 1 + slen
            if s:
                items.append(s.decode('latin1', errors='replace'))
        return '+'.join(items) if items else '', pos
    elif type_byte == VA_BINARY:
        if pos + 4 > end_pos:
            return '<binary:0b>', pos
        slen = int.from_bytes(data[pos:pos + 4], 'little')
        if pos + 4 + slen > end_pos:
            slen = max(0, end_pos - pos - 4)
        pos += 4 + slen
        return f'<binary:{slen}b>', pos
    elif type_byte == VA_COLLECTION:
        items = []
        for _ in range(MAX_LIST_ITEMS):
            if pos >= end_pos:
                break
            next_byte = data[pos]
            if next_byte == 0:
                pos += 1
                break
            item = {}
            for _ in range(MAX_PROPS):
                if pos >= end_pos:
                    break
                prop_end = data[pos]
                if prop_end == 0:
                    pos += 1
                    break
                prop_name, pos = read_pascal_string(data, pos, end_pos)
                if not prop_name:
                    break
                val, pos = read_value(data, pos, end_pos, depth + 1)
                item[prop_name] = val
            items.append(item)
        return items, pos
    elif type_byte == VA_EXTENDED:
        # FPC TExtended80 = 10 bytes (x87 80-bit)
        if pos + 10 > end_pos:
            return '<extended?>', pos
        return '<extended>', pos + 10
    elif type_byte == VA_SINGLE:
        if pos + 4 > end_pos:
            return '<single?>', pos
        return '<single>', pos + 4
    elif type_byte == VA_CURRENCY:
        # 64-bit int (scaled by 10000)
        if pos + 8 > end_pos:
            return '<currency?>', pos
        return '<currency>', pos + 8
    elif type_byte == VA_DATE:
        # 64-bit double (TDateTime)
        if pos + 8 > end_pos:
            return '<date?>', pos
        return '<date>', pos + 8
    else:
        return f'<unknown:0x{type_byte:02X}>', pos


def read_object(data, pos, end_pos, depth=0):
    """Read an object definition (recursive for nested objects). Bounds-checked.

    TPF0 binary format (based on FPC TReader/TWriter):
    1. [0xF2 marker + line_number] (Lazarus extension, optional - see notes)
    2. type_name (Pascal string) - empty string signals end of children list
    3. obj_name (Pascal string)
    4. Property list: each entry is (prop_name_pascal_string, value)
       - Empty prop_name (0x00 byte) signals end of properties
    5. Child objects list: each child is a recursive read_object call
       - Empty type_name (0x00 byte) signals end of children

    Lazarus LFM extensions (empirically determined from TOTALCMD64.EXE):
    - 0xF1: optional root-object marker (1 byte, no line number).
            Handled in parse_lfm() by skipping it if present.
    - 0xF2: optional child-object marker with line-number metadata.
            Format: 0xF2 + TValueType-encoded line number + standard object data.
            Example: 0xF2 0x02 0x00 = marker + vaInt8(0) -> line 0
                     0xF2 0x02 0x01 = marker + vaInt8(1) -> line 1
            Not all children have 0xF2 (observed: form-level children often do,
            deeply nested children typically do not).
    Note: Lazarus uses dotted property names (e.g. "Font.Color") for nested
          property values rather than vaList sub-property blocks, so vaList
          handling in read_value() is not exercised by these resources.
    """
    _check_ops()
    if depth > MAX_DEPTH:
        return None, pos
    # Skip optional Lazarus object markers (empirically determined):
    #   0xF1: root-object marker (1 byte, no line-number payload).
    #   0xF2: child-object marker + TValueType-encoded line number.
    if pos < end_pos:
        marker = data[pos]
        if marker == 0xF1:
            pos += 1  # consume 0xF1 marker (no line number follows)
        elif marker == 0xF2:
            pos += 1  # consume 0xF2 marker
            # Skip the TValueType-encoded line number that follows.
            _, pos = read_value(data, pos, end_pos, depth + 1)
    type_name, pos = read_pascal_string(data, pos, end_pos)
    if not type_name:
        return None, pos
    # Validate type name: if it looks like binary data, parser is desynced
    if not _is_valid_lfm_identifier(type_name):
        return None, pos
    obj_name, pos = read_pascal_string(data, pos, end_pos)
    # Sanitize object name: if it contains binary data, replace with placeholder
    if obj_name and not _is_valid_lfm_identifier(obj_name):
        obj_name = '<corrupted>'
    obj = {'type': type_name, 'name': obj_name, 'props': {}, 'children': []}

    # Read properties until empty property name (0x00 byte) or limit
    for _ in range(MAX_PROPS):
        if pos >= end_pos:
            break
        prop_name, pos = read_pascal_string(data, pos, end_pos)
        if not prop_name:
            break
        # Validate prop name: if it looks like binary data, parser is desynced
        if not _is_valid_lfm_identifier(prop_name, allow_dot=True):
            break
        val, pos = read_value(data, pos, end_pos, depth + 1)
        obj['props'][prop_name] = val

    # Read child objects until empty type name (0x00 byte) or limit
    for _ in range(MAX_CHILDREN):
        if pos >= end_pos:
            break
        child, pos = read_object(data, pos, end_pos, depth + 1)
        if child is None:
            break
        obj['children'].append(child)

    return obj, pos


def parse_lfm(ea):
    """Parse a TPF0 LFM resource at address ea. Returns dict with forms/error.

    v4: Reads entire LFM into a bytes buffer (one IDA API call) then parses
    from the buffer. This eliminates the slow byte-by-byte ida_bytes.get_byte()
    calls that caused IDA to appear frozen on large/multiple LFM resources.
    """
    result = {'address': ea, 'forms': [], 'error': None}
    _ops_counter[0] = 0  # reset operation counter for this LFM

    # Determine the extent of the LFM data
    seg = ida_segment.getseg(ea)
    seg_end = seg.end_ea if seg else ea + MAX_LFM_SIZE
    max_end = min(ea + MAX_LFM_SIZE, seg_end)

    # Read the entire LFM data into a bytes buffer (ONE API CALL)
    data = ida_bytes.get_bytes(ea, max_end - ea)
    if not data:
        result['error'] = 'Failed to read data'
        return result

    if data[:4] not in (b'TPF0', b'TPF1'):
        result['error'] = f'Not TPF0/TPF1: {data[:4]!r}'
        return result

    try:
        pos = 4
        end_pos = len(data)
        obj, pos = read_object(data, pos, end_pos)
        if obj:
            result['forms'].append(obj)
    except RuntimeError as e:
        result['error'] = f'{e} (pos was ~{pos})'
    except Exception as e:
        result['error'] = f'{type(e).__name__}: {e} (pos was ~{pos})'
    return result


def find_all_lfms():
    """Find TPF0/TPF1 magic using chunked reading (fast, no byte-by-byte loop)."""
    found = []
    seg_count = ida_segment.get_segm_qty()
    for i in range(seg_count):
        seg = ida_segment.getnseg(i)
        if not seg:
            continue
        ea = seg.start_ea
        end = seg.end_ea
        while ea < end:
            chunk_size = min(CHUNK_SIZE, end - ea)
            data = ida_bytes.get_bytes(ea, chunk_size)
            if not data:
                ea += chunk_size
                continue
            offset = 0
            while True:
                idx = data.find(b'TPF0', offset)
                if idx >= 0:
                    found.append((ea + idx, 'TPF0'))
                    offset = idx + 4
                    continue
                idx = data.find(b'TPF1', offset)
                if idx >= 0:
                    found.append((ea + idx, 'TPF1'))
                    offset = idx + 4
                    continue
                break
            ea += chunk_size
    return found


def is_event_prop(name):
    """Check if a property name is an event handler."""
    return name.startswith('On') and len(name) > 2 and name[2:3].isupper()


def collect_events(obj, path='', events=None):
    """Recursively collect all event handlers from an object tree."""
    if events is None:
        events = []
    obj_path = path + (obj['name'] if obj['name'] else obj['type'])
    for k, v in obj['props'].items():
        if is_event_prop(k) and isinstance(v, str) and v:
            events.append({
                'component_path': obj_path,
                'component_type': obj['type'],
                'component_name': obj['name'],
                'event': k,
                'handler': v,
            })
    for child in obj.get('children', []):
        collect_events(child, obj_path + '.', events)
    return events


def collect_components(obj, path='', components=None):
    """Recursively collect all components."""
    if components is None:
        components = []
    obj_path = path + (obj['name'] if obj['name'] else obj['type'])
    components.append({
        'path': obj_path,
        'type': obj['type'],
        'name': obj['name'],
        'prop_count': len(obj['props']),
        'child_count': len(obj.get('children', [])),
    })
    for child in obj.get('children', []):
        collect_components(child, obj_path + '.', components)
    return components


def _sanitize_str(s):
    """Sanitize a string for text file output.

    Replace non-printable characters (including null bytes and high bytes)
    with escape sequences to prevent binary content in the output file.
    """
    if not isinstance(s, str):
        return str(s)
    result = []
    for c in s:
        o = ord(c)
        if c == '\n':
            result.append('\\n')
        elif c == '\r':
            result.append('\\r')
        elif c == '\t':
            result.append('\\t')
        elif o < 32 or o >= 127:
            result.append(f'\\x{o:02X}')
        else:
            result.append(c)
    return ''.join(result)


def _sanitize_val(v):
    """Sanitize a value for text file output (like repr but safe)."""
    if isinstance(v, str):
        return f"'{_sanitize_str(v)}'"
    elif isinstance(v, list):
        return '[' + ', '.join(_sanitize_val(i) for i in v) + ']'
    elif isinstance(v, dict):
        return '{' + ', '.join(f"'{_sanitize_str(k)}': {_sanitize_val(val)}" for k, val in v.items()) + '}'
    elif isinstance(v, bool):
        return repr(v)
    elif v is None:
        return 'None'
    else:
        return str(v)


def _vmt_read_pascal_string(addr):
    """读取指定地址处的 Pascal ShortString。

    Pascal ShortString 格式:
    - 第 1 字节: 字符串长度 N (0-255)
    - 后续 N 字节: 字符串内容 (单字节编码)

    返回解码后的字符串, 失败返回 None。
    """
    if addr == 0 or addr == idc.BADADDR or addr < 0x1000:
        return None
    slen = ida_bytes.get_byte(addr)
    if slen == 0 or slen > 255:
        return None
    data = ida_bytes.get_bytes(addr + 1, slen)
    if not data:
        return None
    try:
        return data.decode('latin1')
    except Exception:
        return None


def verify_vmt(vmt_addr):
    """验证地址是否指向有效的 FPC 64-bit VMT。

    校验条件:
    1. 地址有效 (非 BADADDR, 大于 0x1000)
    2. vInstanceSize != 0
    3. vInstanceSize + vInstanceSize2 == 0 (校验字段, mod 2^64)

    返回 True/False。
    """
    if vmt_addr == idc.BADADDR or vmt_addr < 0x1000:
        return False
    vInstanceSize = ida_bytes.get_qword(vmt_addr)
    vInstanceSize2 = ida_bytes.get_qword(vmt_addr + 8)
    if vInstanceSize == 0:
        return False
    if (vInstanceSize + vInstanceSize2) & 0xFFFFFFFFFFFFFFFF != 0:
        return False
    return True


def read_vmt_fields(vmt_addr):
    """读取 VMT 的所有 13 个字段。

    返回 dict: {字段名: 值(uint64)}
    """
    fields = {}
    for offset, name, _desc in VMT_FIELDS:
        fields[name] = ida_bytes.get_qword(vmt_addr + offset)
    return fields


def read_method_table_detailed(method_table_ptr):
    """读取方法表的详细信息。

    方法表格式:
    - +0: count (uint32) - 方法数量
    - +4: entries[count], 每项 16 字节:
      - +0: name_ptr (uint64) -> 方法名 Pascal ShortString
      - +8: code_ptr (uint64) -> 方法代码地址

    返回 list of dict, 每项包含: index, name, name_ptr, code_ptr, code_name
    """
    if method_table_ptr == 0 or method_table_ptr == idc.BADADDR or method_table_ptr < 0x1000:
        return []
    count_data = ida_bytes.get_bytes(method_table_ptr, 4)
    if not count_data or len(count_data) < 4:
        return []
    count = int.from_bytes(count_data, 'little')
    if count == 0 or count > MAX_METHODS_PER_VMT:
        return []
    entries_data = ida_bytes.get_bytes(method_table_ptr + 4, count * 16)
    if not entries_data or len(entries_data) < count * 16:
        return []
    methods = []
    for i in range(count):
        offset = i * 16
        name_ptr = int.from_bytes(entries_data[offset:offset + 8], 'little')
        code_ptr = int.from_bytes(entries_data[offset + 8:offset + 16], 'little')
        method_name = _vmt_read_pascal_string(name_ptr) if name_ptr else None
        code_name = idc.get_name(code_ptr) if code_ptr else ''
        methods.append({
            'index': i,
            'name': method_name or '<unknown>',
            'name_ptr': name_ptr,
            'code_ptr': code_ptr,
            'code_name': code_name,
        })
    return methods


def walk_vmt_chain(vmt_addr, max_depth=MAX_VMT_CHAIN_DEPTH):
    """遍历 VMT 父类继承链。

    FPC VMT 的 vParentRef 字段指向父类的 VMT。本函数从给定 VMT 开始,
    沿父类链向上遍历, 直到:
    - 到达 TObject (无父类, vParentRef=0)
    - 达到最大深度限制
    - 遇到无效 VMT
    - 检测到循环引用

    返回 list of (vmt_addr, class_name) 元组, 从当前类到根类。
    """
    chain = []
    seen = set()
    current = vmt_addr
    depth = 0
    while current and current != idc.BADADDR and depth < max_depth:
        if current in seen:
            break  # 检测到循环引用
        seen.add(current)
        if not verify_vmt(current):
            break
        class_name_ptr = ida_bytes.get_qword(current + 24)  # vClassName offset
        class_name = _vmt_read_pascal_string(class_name_ptr) or '<unknown>'
        chain.append((current, class_name))
        parent_ptr = ida_bytes.get_qword(current + 16)  # vParentRef offset
        if parent_ptr == 0 or parent_ptr == idc.BADADDR:
            break
        current = parent_ptr
        depth += 1
    return chain


def parse_vmt_at(vmt_addr):
    """解析指定地址处的 VMT, 返回详细信息。

    解析内容包括:
    1. VMT 地址和符号名
    2. 所有 13 个字段的值
    3. 类名 (从 vClassName 指向的 Pascal ShortString 读取)
    4. 方法表的所有方法 (索引、名称、代码地址、符号名)
    5. 父类继承链 (沿 vParentRef 向上遍历)

    参数:
        vmt_addr: VMT 起始地址

    返回:
        dict: {'vmt_addr', 'vmt_symbol', 'class_name', 'fields',
               'methods', 'parent_chain', 'error'}
    """
    result = {
        'vmt_addr': vmt_addr,
        'vmt_symbol': idc.get_name(vmt_addr) or '',
        'class_name': None,
        'fields': {},
        'methods': [],
        'parent_chain': [],
        'error': None,
    }

    if not verify_vmt(vmt_addr):
        inst_size = ida_bytes.get_qword(vmt_addr) if vmt_addr >= 0x1000 else 0
        inst_size2 = ida_bytes.get_qword(vmt_addr + 8) if vmt_addr >= 0x1000 else 0
        result['error'] = (
            f'地址 0x{vmt_addr:X} 不是有效的 FPC VMT '
            f'(vInstanceSize=0x{inst_size:X}, vInstanceSize2=0x{inst_size2:X}, '
            f'校验{"通过" if (inst_size + inst_size2) & 0xFFFFFFFFFFFFFFFF == 0 else "失败"})'
        )
        return result

    result['fields'] = read_vmt_fields(vmt_addr)

    # 读取类名
    class_name_ptr = result['fields']['vClassName']
    if class_name_ptr:
        result['class_name'] = _vmt_read_pascal_string(class_name_ptr)

    # 读取方法表
    method_table_ptr = result['fields']['vMethodTable']
    result['methods'] = read_method_table_detailed(method_table_ptr)

    # 遍历父类继承链
    result['parent_chain'] = walk_vmt_chain(vmt_addr)

    return result


def format_vmt_report(result):
    """将 VMT 解析结果格式化为可读的文本报告。

    报告包含:
    - VMT 地址和符号
    - 类名
    - 所有 13 个字段的值、说明和解析后的指向内容
    - 方法表的完整方法列表
    - 父类继承链
    """
    lines = []
    vmt_addr = result['vmt_addr']

    if result['error']:
        lines.append('=' * 80)
        lines.append('VMT 解析失败')
        lines.append('=' * 80)
        lines.append(result['error'])
        return '\n'.join(lines)

    vmt_symbol = result['vmt_symbol'] or '<unnamed>'
    class_name = result['class_name'] or '<unknown>'

    lines.append('=' * 80)
    lines.append(f'VMT 解析报告: {class_name}')
    lines.append('=' * 80)
    lines.append(f'VMT 地址:    0x{vmt_addr:08X}  (符号: {vmt_symbol})')
    lines.append(f'类名:        {class_name}')
    lines.append(f'方法数:      {len(result["methods"])}')
    lines.append(f'继承深度:    {len(result["parent_chain"]) - 1 if result["parent_chain"] else 0}')
    lines.append('')

    # 字段表
    lines.append('-' * 80)
    lines.append('VMT 字段 (13 个 qword, 共 104 字节)')
    lines.append('-' * 80)
    for offset, name, desc in VMT_FIELDS:
        value = result['fields'].get(name, 0)
        extra = ''
        if name == 'vClassName' and value:
            cn = _vmt_read_pascal_string(value)
            if cn:
                extra = f'  -> "{cn}"'
        elif name == 'vParentRef' and value:
            if verify_vmt(value):
                parent_cn_ptr = ida_bytes.get_qword(value + 24)
                parent_cn = _vmt_read_pascal_string(parent_cn_ptr)
                if parent_cn:
                    extra = f'  -> {parent_cn}_VMT (0x{value:08X})'
                else:
                    extra = f'  -> 0x{value:08X}'
            else:
                extra = f'  -> 0x{value:08X}'
        elif name == 'vMethodTable' and value:
            count_data = ida_bytes.get_bytes(value, 4)
            if count_data and len(count_data) == 4:
                count = int.from_bytes(count_data, 'little')
                extra = f'  -> {count} 个方法 (0x{value:08X})'
            else:
                extra = f'  -> 0x{value:08X}'
        elif name == 'vDestroy' and value:
            sym = idc.get_name(value)
            if sym:
                extra = f'  -> {sym} (0x{value:08X})'
            else:
                extra = f'  -> 0x{value:08X}'
        elif name == 'vInstanceSize2':
            inst_size = result['fields'].get('vInstanceSize', 0)
            expected = (-inst_size) & 0xFFFFFFFFFFFFFFFF
            if value == expected:
                extra = '  (校验通过)'
            else:
                extra = f'  (校验失败! 期望 0x{expected:X})'
        lines.append(f'  +{offset:3d} (0x{offset:02X}) {name:20s} = 0x{value:016X}  {desc}{extra}')
    lines.append('')

    # 方法表
    lines.append('-' * 80)
    lines.append(f'方法表 ({len(result["methods"])} 个方法)')
    lines.append('-' * 80)
    if result['methods']:
        for m in result['methods']:
            code_sym = m['code_name'] or '<unnamed>'
            lines.append(
                f'  [{m["index"]:3d}] {m["name"]:35s}  '
                f'name@0x{m["name_ptr"]:08X}  code@0x{m["code_ptr"]:08X}  ({code_sym})'
            )
    else:
        lines.append('  (无已发布方法)')
    lines.append('')

    # 父类继承链
    lines.append('-' * 80)
    lines.append('父类继承链')
    lines.append('-' * 80)
    if result['parent_chain']:
        for i, (addr, cn) in enumerate(result['parent_chain']):
            sym = idc.get_name(addr) or '<unnamed>'
            indent = '  ' + '  ' * i
            if i == 0:
                lines.append(f'{indent}[{i}] {cn} (VMT@0x{addr:08X}, {sym})  <-- 当前类')
            else:
                lines.append(f'{indent}└─ [{i}] {cn} (VMT@0x{addr:08X}, {sym})')
    else:
        lines.append('  (无父类)')
    lines.append('')

    return '\n'.join(lines)


def find_vmt_for_address(ea):
    """查找包含给定地址的 VMT 起始地址。

    查找策略:
    1. 先检查 ea 本身是否是 VMT 起始地址
    2. 若不是, 向前按 8 字节对齐搜索 (最多搜索 VMT_SEARCH_BACK_RANGE 范围)

    返回 VMT 起始地址, 未找到返回 None。
    """
    # 1. 检查 ea 本身
    if verify_vmt(ea):
        return ea
    # 2. 向前搜索 (8 字节对齐)
    aligned_ea = ea & ~0x7  # 对齐到 8 字节边界
    for offset in range(0, VMT_SEARCH_BACK_RANGE, 8):
        candidate = aligned_ea - offset
        if candidate < 0x1000:
            break
        if verify_vmt(candidate):
            return candidate
    return None


def parse_vmt_main(ea=None):
    """VMT 解析入口函数。

    解析指定地址或当前光标位置的 VMT, 输出详细报告到控制台和文件。

    参数:
        ea: VMT 地址。若为 None, 使用当前光标位置 (idc.here())。
            若 ea 不是 VMT 起始地址, 自动向前查找最近的 VMT 起始。

    输出:
    - 控制台打印 VMT 解析报告
    - 写入 VMT_REPORT_PATH 指定的文件
    """
    if ea is None:
        ea = idc.here()
    print(f'\n解析 VMT, 起始地址: 0x{ea:08X}')

    # 若 ea 不是 VMT 起始, 尝试向前查找
    if not verify_vmt(ea):
        found = find_vmt_for_address(ea)
        if found is not None:
            print(f'注意: 0x{ea:X} 不是 VMT 起始地址, 已自动向前查找至 0x{found:08X}')
            ea = found
        else:
            print(f'错误: 在 0x{ea:X} 附近 {VMT_SEARCH_BACK_RANGE} 字节范围内未找到有效 VMT')
            return None

    result = parse_vmt_at(ea)
    report = format_vmt_report(result)
    print(report)

    # 写入文件
    try:
        with open(VMT_REPORT_PATH, 'w', encoding='utf-8') as f:
            f.write(report)
            f.write('\n')
            # 附加原始数据 (便于程序化处理)
            f.write('\n' + '=' * 80 + '\n')
            f.write('原始数据 (JSON-like)\n')
            f.write('=' * 80 + '\n')
            f.write(f'vmt_addr: 0x{result["vmt_addr"]:X}\n')
            f.write(f'vmt_symbol: {result["vmt_symbol"]}\n')
            f.write(f'class_name: {result["class_name"]}\n')
            f.write(f'fields:\n')
            for offset, name, desc in VMT_FIELDS:
                val = result['fields'].get(name, 0)
                f.write(f'  {name} (0x{offset:02X}): 0x{val:016X}\n')
            f.write(f'methods ({len(result["methods"])}):\n')
            for m in result['methods']:
                f.write(f'  [{m["index"]}] {m["name"]} | name_ptr=0x{m["name_ptr"]:X} | code_ptr=0x{m["code_ptr"]:X} | symbol={m["code_name"]}\n')
            f.write(f'parent_chain ({len(result["parent_chain"])}):\n')
            for i, (addr, cn) in enumerate(result['parent_chain']):
                f.write(f'  [{i}] {cn} @ 0x{addr:X}\n')
        print(f'\n报告已写入: {VMT_REPORT_PATH}')
    except Exception as e:
        print(f'写入文件失败: {e}')

    return result


# VMT 扫描日志文件句柄 (scan_all_vmts 开始时打开, 结束时关闭)
_vmt_log_file = None


def _log_print(msg):
    """输出到 IDA Output 窗口和日志文件。

    注意: IDA 中 print() 内部也调用 ida_kernwin.msg(), 两者同时使用会导致
    重复输出。因此只使用 ida_kernwin.msg() 一次, 并写入日志文件。
    """
    msg_str = str(msg)
    ida_kernwin.msg(msg_str + '\n')
    if _vmt_log_file is not None:
        try:
            _vmt_log_file.write(msg_str + '\n')
            _vmt_log_file.flush()
        except Exception:
            pass


# =============================================================================
# 备用 VMT 查找函数 (旧版按类名搜索算法)
# scan_all_vmts() 已改用 _scan_vmts_direct() 直接扫描法 (1 次全段扫描),
# 以下函数保留供单类名查询或参考使用:
#   _extract_class_names_from_lfms() — 从 LFM 资源提取类名
#   _search_bytes()                  — 通用字节模式搜索
#   _find_class_name_strings()       — 数据段 Pascal ShortString 搜索
#   _find_vmt_for_class_name()       — 按类名搜索 VMT (xref + 指针搜索)
# =============================================================================


def _extract_class_names_from_lfms():
    """从所有 TPF0/TPF1 LFM 资源中提取窗体类名。

    LFM 格式: [TPF0/TPF1 (4字节)] [可选 0xF1] [Pascal ShortString 类名] ...

    返回 set of class_name (去重)。
    """
    class_names = set()
    lfm_addrs = find_all_lfms()
    for ea, _magic in lfm_addrs:
        # 只读头部 256 字节即可获取类名
        data = ida_bytes.get_bytes(ea, 256)
        if not data or len(data) < 6:
            continue
        if data[:4] not in (b'TPF0', b'TPF1'):
            continue
        pos = 4
        # 跳过可选的 0xF1 根对象标记
        if pos < len(data) and data[pos] == 0xF1:
            pos += 1
        if pos >= len(data):
            continue
        slen = data[pos]
        if slen == 0 or slen > 255 or pos + 1 + slen > len(data):
            continue
        name_bytes = data[pos + 1:pos + 1 + slen]
        # 验证类名: 仅含 A-Z/a-z/0-9/_, 以字母开头
        if not name_bytes:
            continue
        if not (0x41 <= name_bytes[0] <= 0x5A or 0x61 <= name_bytes[0] <= 0x7A):
            continue
        if not all(0x41 <= b <= 0x5A or 0x61 <= b <= 0x7A or 0x30 <= b <= 0x39 or b == 0x5F for b in name_bytes):
            continue
        try:
            class_names.add(name_bytes.decode('ascii'))
        except Exception:
            continue
    return class_names


def _search_bytes(pattern):
    """在所有段中搜索字节模式, 返回匹配地址列表。

    使用分块读取 + bytes.find() (C 实现), 比 Python 循环快得多。
    """
    results = []
    seg_count = ida_segment.get_segm_qty()
    for i in range(seg_count):
        seg = ida_segment.getnseg(i)
        if not seg:
            continue
        ea = seg.start_ea
        end = seg.end_ea
        while ea < end:
            chunk_size = min(CHUNK_SIZE, end - ea)
            data = ida_bytes.get_bytes(ea, chunk_size)
            if not data:
                ea += chunk_size
                continue
            offset = 0
            while True:
                idx = data.find(pattern, offset)
                if idx < 0:
                    break
                results.append(ea + idx)
                offset = idx + 1
            ea += chunk_size
    return results


_SEG_DATA_TYPE = getattr(ida_segment, 'SEG_DATA', 3)


def _find_class_name_strings():
    """搜索所有 Pascal ShortString 格式的类名字符串 (仅数据段)。

    Pascal ShortString 格式: [长度字节][字符串内容]
    类名特征: 以大写字母开头 (T/E/I/P 等), 仅含字母/数字/下划线, 长度 4-255。

    只搜索数据段 (SEG_DATA), 跳过代码段 — 代码段中 0x54 ('T') 是
    PUSH RSP 指令操作码, 会产生大量误报导致性能严重下降。

    返回 dict: {字符串地址: 类名}
    """
    found = {}
    seg_count = ida_segment.get_segm_qty()
    for seg_idx in range(seg_count):
        seg = ida_segment.getnseg(seg_idx)
        if not seg:
            continue
        # 只搜索数据段, 跳过代码段
        seg_type = ida_segment.segtype(seg.start_ea)
        if seg_type != _SEG_DATA_TYPE:
            continue
        ea = seg.start_ea
        end = seg.end_ea
        while ea < end:
            chunk_size = min(CHUNK_SIZE, end - ea)
            data = ida_bytes.get_bytes(ea, chunk_size)
            if not data:
                ea += chunk_size
                continue
            offset = 0
            while offset < len(data):
                idx = data.find(b'T', offset)
                if idx < 0:
                    break
                offset = idx + 1
                if idx == 0:
                    continue
                slen = data[idx - 1]
                if slen < 4 or slen > 255:
                    continue
                name_end = idx + slen
                if name_end > len(data):
                    continue
                name_bytes = data[idx:name_end]
                if not all(0x41 <= b <= 0x5A or 0x61 <= b <= 0x7A or 0x30 <= b <= 0x39 or b == 0x5F for b in name_bytes):
                    continue
                str_addr = ea + idx - 1
                if str_addr in found:
                    continue
                try:
                    class_name = name_bytes.decode('ascii')
                except Exception:
                    continue
                found[str_addr] = class_name
            ea += chunk_size
    return found


def _find_vmt_for_class_name(class_name):
    """对单个类名搜索其 VMT 地址。

    与 setup_vmt_structs.py 的 find_vmt_for_class 相同的已验证算法:
    1. 构造 Pascal ShortString 模式: [长度字节]+[类名]
    2. 搜索该模式找到字符串地址
    3. Method A: 对每个字符串地址用 XrefsTo 找 VMT 引用
    4. Method B: 回退到指针搜索 (搜索指向字符串地址的 8 字节指针)

    返回 (vmt_addr, str_addr) 或 (None, None)。
    """
    name_bytes = class_name.encode('latin1')
    pattern = bytes([len(name_bytes)]) + name_bytes
    string_addrs = _search_bytes(pattern)
    if not string_addrs:
        return None, None

    # Method A: xref
    for str_addr in string_addrs:
        for xref in idautils.XrefsTo(str_addr):
            vmt_addr = xref.frm - 24  # vClassName 偏移
            if verify_vmt(vmt_addr):
                vClassName_ptr = ida_bytes.get_qword(vmt_addr + 24)
                if vClassName_ptr == str_addr:
                    return vmt_addr, str_addr

    # Method B: 指针搜索回退
    for str_addr in string_addrs:
        target_bytes = str_addr.to_bytes(8, 'little')
        ptr_addrs = _search_bytes(target_bytes)
        for ptr_addr in ptr_addrs:
            vmt_addr = ptr_addr - 24
            if verify_vmt(vmt_addr):
                vClassName_ptr = ida_bytes.get_qword(vmt_addr + 24)
                if vClassName_ptr == str_addr:
                    return vmt_addr, str_addr

    return None, None


def _scan_vmts_direct():
    """直接扫描数据段, 通过 VMT 校验条件一次性查找所有 VMT。

    VMT 校验条件 (全部必须满足):
    1. vInstanceSize (offset 0, uint64) != 0
    2. vInstanceSize + vInstanceSize2 (offset 8) == 0 (mod 2^64)
    3. vClassName (offset 24) 指针指向合法 Pascal ShortString
    4. 类名以字母开头, 仅含 A-Z/a-z/0-9/_

    相比按类名逐个搜索 (N 次全段扫描), 本方法仅需 1 次全段扫描,
    速度提升数百倍, 且覆盖率 100%。

    返回 dict: {vmt_addr: (class_name, class_name_str_addr)}
    """
    found = {}
    seg_count = ida_segment.get_segm_qty()

    for seg_idx in range(seg_count):
        seg = ida_segment.getnseg(seg_idx)
        if not seg:
            continue
        seg_type = ida_segment.segtype(seg.start_ea)
        if seg_type != _SEG_DATA_TYPE:
            continue

        seg_start = seg.start_ea
        seg_end = seg.end_ea
        # 对齐起始地址到 8 字节边界
        if seg_start % 8 != 0:
            seg_start = (seg_start + 7) & ~7

        ea = seg_start
        while ea < seg_end:
            chunk_size = min(CHUNK_SIZE, seg_end - ea)
            # 多读 32 字节确保块末尾的候选 VMT 能读取完整 32 字节头部
            read_size = min(chunk_size + 32, seg_end - ea)
            data = ida_bytes.get_bytes(ea, read_size)
            if not data:
                ea += chunk_size
                continue

            scan_limit = len(data) - 32  # 确保能读 32 字节
            offset = 0
            while offset < scan_limit:
                # 快速检查: vInstanceSize != 0
                v0 = data[offset]
                v1 = data[offset + 1]
                v2 = data[offset + 2]
                v3 = data[offset + 3]
                if v0 == 0 and v1 == 0 and v2 == 0 and v3 == 0:
                    # 前 4 字节为 0, vInstanceSize 可能为 0, 跳过
                    # (但仍需检查高 4 字节, 但概率极低, 直接跳过加速)
                    offset += 8
                    continue

                # 读取 vInstanceSize (8 字节) 和 vInstanceSize2 (8 字节)
                vInstanceSize = int.from_bytes(data[offset:offset + 8], 'little')
                if vInstanceSize == 0:
                    offset += 8
                    continue
                vInstanceSize2 = int.from_bytes(data[offset + 8:offset + 16], 'little')
                if (vInstanceSize + vInstanceSize2) & 0xFFFFFFFFFFFFFFFF != 0:
                    offset += 8
                    continue

                # 初步校验通过, 读取 vClassName 指针 (offset 24)
                vClassName_ptr = int.from_bytes(data[offset + 24:offset + 32], 'little')
                if vClassName_ptr == 0 or vClassName_ptr == idc.BADADDR or vClassName_ptr < 0x1000:
                    offset += 8
                    continue

                # 验证 vClassName 指向合法 Pascal ShortString
                class_name = _vmt_read_pascal_string(vClassName_ptr)
                if not class_name:
                    offset += 8
                    continue
                # 类名格式校验: 以字母开头, 仅含字母/数字/下划线
                if not _is_valid_lfm_identifier(class_name):
                    offset += 8
                    continue

                vmt_addr = ea + offset
                if vmt_addr not in found:
                    found[vmt_addr] = (class_name, vClassName_ptr)

                offset += 8

            ea += chunk_size

    return found


def scan_all_vmts():
    """扫描二进制文件中所有 FPC 64-bit VMT 结构。

    使用直接扫描法: 遍历所有数据段的 8 字节对齐位置, 通过 VMT 校验条件
    (vInstanceSize + vInstanceSize2 == 0 且 vClassName 指向合法 ShortString)
    一次性识别所有 VMT。

    相比旧版按类名逐个搜索 (N 次全段扫描), 本方法仅需 1 次全段扫描,
    速度提升数百倍, 且覆盖率 100%。

    输出:
    - IDA Output 窗口 + 日志文件 (vmt_scan_log.txt)
    - 完整报告写入 VMT_SCAN_REPORT_PATH
    """
    global _vmt_log_file

    vmt_log_path = os.path.join(_idb_dir, 'vmt_scan_log.txt')
    try:
        _vmt_log_file = open(vmt_log_path, 'w', encoding='utf-8')
    except Exception as e:
        ida_kernwin.msg(f'警告: 无法打开日志文件 {vmt_log_path}: {e}\n')
        _vmt_log_file = None

    found_vmts = {}
    try:
        _log_print('\n=== 直接扫描法: 遍历数据段 8 字节对齐位置 ===')
        _log_print('校验条件: vInstanceSize != 0, vInstanceSize + vInstanceSize2 == 0,')
        _log_print('          vClassName 指针 -> 合法 Pascal ShortString 类名')
        _log_print('')

        found_vmts = _scan_vmts_direct()

        _log_print(f'\n{"=" * 80}')
        _log_print(f'扫描完成, 共发现 {len(found_vmts)} 个 VMT')

        if not found_vmts:
            _log_print('未找到任何 VMT')
            return {}

        # 按地址排序
        sorted_vmts = sorted(found_vmts.items())

        # 控制台输出前 50 个
        _log_print(f'\n=== VMT 列表 (前 50 个, 共 {len(sorted_vmts)}) ===')
        _log_print(f'{"地址":>12s} | {"实例大小":>10s} | {"类名":<35s} | {"父类":<20s}')
        _log_print('-' * 90)
        for vmt_addr, (class_name, _cn_ptr) in sorted_vmts[:50]:
            inst_size = ida_bytes.get_qword(vmt_addr)
            parent_ptr = ida_bytes.get_qword(vmt_addr + 16)
            parent_name = '<none>'
            if parent_ptr and parent_ptr != idc.BADADDR:
                if verify_vmt(parent_ptr):
                    parent_cn_ptr = ida_bytes.get_qword(parent_ptr + 24)
                    parent_name = _vmt_read_pascal_string(parent_cn_ptr) or '<unknown>'
            _log_print(f'  0x{vmt_addr:08X} | {inst_size:10d} | {class_name:<35s} | {parent_name:<20s}')
        if len(sorted_vmts) > 50:
            _log_print(f'  ... 还有 {len(sorted_vmts) - 50} 个, 详见报告文件')

        # 写入完整报告
        try:
            with open(VMT_SCAN_REPORT_PATH, 'w', encoding='utf-8') as f:
                f.write('FPC 64-bit VMT 全量扫描报告\n')
                f.write('=' * 80 + '\n\n')
                f.write(f'扫描方法: 直接扫描法 (数据段 8 字节对齐位置 VMT 校验)\n')
                f.write(f'VMT 总数: {len(found_vmts)}\n\n')

                f.write('=' * 80 + '\n')
                f.write('VMT 地址 | 实例大小 | 类名 | 父类 | 方法数 | 符号名\n')
                f.write('=' * 80 + '\n')
                for vmt_addr, (class_name, _cn_ptr) in sorted_vmts:
                    fields = read_vmt_fields(vmt_addr)
                    inst_size = fields['vInstanceSize']
                    parent_ptr = fields['vParentRef']
                    parent_name = '<none>'
                    if parent_ptr and parent_ptr != idc.BADADDR:
                        if verify_vmt(parent_ptr):
                            parent_cn_ptr = ida_bytes.get_qword(parent_ptr + 24)
                            parent_name = _vmt_read_pascal_string(parent_cn_ptr) or '<unknown>'
                    method_table_ptr = fields['vMethodTable']
                    method_count = 0
                    if method_table_ptr and method_table_ptr != idc.BADADDR:
                        count_data = ida_bytes.get_bytes(method_table_ptr, 4)
                        if count_data and len(count_data) == 4:
                            method_count = int.from_bytes(count_data, 'little')
                    vmt_symbol = idc.get_name(vmt_addr) or '<unnamed>'
                    f.write(f'0x{vmt_addr:08X} | {inst_size:10d} | {class_name:<35s} | {parent_name:<20s} | {method_count:4d} | {vmt_symbol}\n')

                # 附加每个 VMT 的详细方法列表
                f.write('\n' + '=' * 80 + '\n')
                f.write('各 VMT 方法详情\n')
                f.write('=' * 80 + '\n')
                for vmt_addr, (class_name, _cn_ptr) in sorted_vmts:
                    methods = read_method_table_detailed(ida_bytes.get_qword(vmt_addr + 40))
                    if not methods:
                        continue
                    f.write(f'\n{class_name} (VMT@0x{vmt_addr:X}, {len(methods)} 个方法):\n')
                    for m in methods:
                        f.write(f'  [{m["index"]:3d}] {m["name"]:35s}  code@0x{m["code_ptr"]:08X}  ({m["code_name"]})\n')

            _log_print(f'\n完整报告已写入: {VMT_SCAN_REPORT_PATH}')
        except Exception as e:
            _log_print(f'写入报告文件失败: {e}')

        return found_vmts

    except Exception as e:
        import traceback
        _log_print(f'扫描出错: {e}')
        _log_print(traceback.format_exc())
        return {}
    finally:
        if _vmt_log_file is not None:
            try:
                _vmt_log_file.close()
            except Exception:
                pass
            _vmt_log_file = None
        # 弹出完成对话框
        try:
            n = len(found_vmts)
            ida_kernwin.info(
                f'VMT 扫描完成\n\n'
                f'发现 VMT 数量: {n}\n'
                f'日志文件: {os.path.join(_idb_dir, "vmt_scan_log.txt")}\n'
                f'报告文件: {VMT_SCAN_REPORT_PATH}'
            )
        except Exception:
            pass


# =============================================================================
# LFM 单个解析与自动检测 Part
# =============================================================================

LFM_REPORT_PATH = os.path.join(_idb_dir, 'lfm_parse_report.txt')


def is_lfm_at(ea):
    """检查指定地址处是否是 LFM (以 TPF0 或 TPF1 标记开头)。

    LFM 起始标记为 4 字节 ASCII: b'TPF0' 或 b'TPF1'。
    """
    if ea == idc.BADADDR or ea < 0x1000:
        return False
    magic = ida_bytes.get_bytes(ea, 4)
    if not magic or len(magic) < 4:
        return False
    return magic in (b'TPF0', b'TPF1')


def find_lfm_near(ea):
    """向前查找最近的 LFM 起始地址 (搜索 TPF0/TPF1 标记)。

    使用分块读取 + bytes.rfind() 快速查找, 而非逐字节扫描。
    搜索范围为 ea 向前 VMT_SEARCH_BACK_RANGE 字节。

    返回 LFM 起始地址, 未找到返回 None。
    """
    if is_lfm_at(ea):
        return ea
    search_start = max(ea - VMT_SEARCH_BACK_RANGE, 0x1000)
    search_size = ea - search_start + 4
    if search_size <= 4:
        return None
    data = ida_bytes.get_bytes(search_start, search_size)
    if not data:
        return None
    # rfind 从末尾向前查找, 找到的是离 ea 最近的标记
    idx_tpf0 = data.rfind(b'TPF0')
    idx_tpf1 = data.rfind(b'TPF1')
    idx = max(idx_tpf0, idx_tpf1)
    if idx < 0:
        return None
    return search_start + idx


def format_lfm_report(result):
    """将单个 LFM 解析结果格式化为可读的文本报告。

    报告包含:
    - LFM 地址
    - 窗体类名和实例名
    - 属性列表
    - 事件处理器列表
    - 组件树 (递归)
    """
    lines = []
    ea = result['address']

    if result['error']:
        lines.append('=' * 80)
        lines.append('LFM 解析失败')
        lines.append('=' * 80)
        lines.append(f'地址: 0x{ea:08X}')
        lines.append(f'错误: {result["error"]}')
        return '\n'.join(lines)

    forms = result['forms']
    lines.append('=' * 80)
    lines.append(f'LFM 解析报告 ({len(forms)} 个窗体)')
    lines.append('=' * 80)
    lines.append(f'LFM 地址: 0x{ea:08X}')
    lines.append(f'标记:     {ida_bytes.get_bytes(ea, 4).decode("ascii", errors="replace")}')
    lines.append('')

    for form in forms:
        events = collect_events(form)
        components = collect_components(form)
        evt_count = sum(1 for k in form['props'] if is_event_prop(k))
        child_count = len(form.get('children', []))

        lines.append('-' * 80)
        lines.append(f'窗体: {_sanitize_str(form["type"])} : {_sanitize_str(form["name"])}')
        lines.append('-' * 80)
        lines.append(f'  顶级事件数: {evt_count}')
        lines.append(f'  子组件数:   {child_count}')
        lines.append(f'  总组件数:   {len(components)}')
        lines.append(f'  总事件数:   {len(events)}')
        lines.append('')

        # 属性列表
        non_evt_props = {k: v for k, v in form['props'].items() if not is_event_prop(k)}
        if non_evt_props:
            lines.append('  属性:')
            for k, v in non_evt_props.items():
                lines.append(f'    {_sanitize_str(k)} = {_sanitize_val(v)}')
            lines.append('')

        # 事件处理器
        if events:
            lines.append('  事件处理器:')
            for evt in events:
                lines.append(
                    f'    {_sanitize_str(evt["event"]):20s} -> '
                    f'{_sanitize_str(evt["handler"]):30s}  '
                    f'({_sanitize_str(evt["component_type"])}/{_sanitize_str(evt["component_name"])})'
                )
            lines.append('')

        # 组件树
        lines.append('  组件树:')

        def _write_tree(obj, indent=1):
            pad = '    ' + '  ' * indent
            evt_n = sum(1 for k in obj['props'] if is_event_prop(k))
            non_evt_n = len(obj['props']) - evt_n
            child_n = len(obj.get('children', []))
            lines.append(
                f'{pad}{_sanitize_str(obj["type"])}: {_sanitize_str(obj["name"])}  '
                f'({non_evt_n} 属性, {evt_n} 事件, {child_n} 子组件)'
            )
            for child in obj.get('children', []):
                _write_tree(child, indent + 1)

        _write_tree(form)
        lines.append('')

    return '\n'.join(lines)


def parse_lfm_main(ea=None):
    """单个 LFM 解析入口函数。

    解析指定地址或当前光标位置的 LFM, 输出详细报告到控制台和文件。

    参数:
        ea: LFM 起始地址。若为 None, 使用当前光标位置 (idc.here())。
            若 ea 不是 LFM 起始 (无 TPF0/TPF1 标记), 自动向前查找最近的 LFM。

    输出:
    - 控制台打印 LFM 解析报告
    - 写入 LFM_REPORT_PATH 指定的文件
    """
    if ea is None:
        ea = idc.here()
    print(f'\n解析 LFM, 起始地址: 0x{ea:08X}')

    if not is_lfm_at(ea):
        found = find_lfm_near(ea)
        if found is not None:
            print(f'注意: 0x{ea:X} 不是 LFM 起始地址, 已自动向前查找至 0x{found:08X}')
            ea = found
        else:
            print(f'错误: 在 0x{ea:X} 附近 {VMT_SEARCH_BACK_RANGE} 字节范围内未找到 LFM')
            return None

    result = parse_lfm(ea)
    report = format_lfm_report(result)
    print(report)

    # 写入文件
    try:
        with open(LFM_REPORT_PATH, 'w', encoding='utf-8') as f:
            f.write(report)
            f.write('\n')
        print(f'\n报告已写入: {LFM_REPORT_PATH}')
    except Exception as e:
        print(f'写入文件失败: {e}')

    return result


def detect_data_type_at(ea):
    """检测指定地址处的数据类型 (LFM 或 VMT)。

    检测策略:
    1. 先检查 ea 本身是否是 LFM (TPF0/TPF1 标记)
    2. 再检查 ea 本身是否是 VMT (vInstanceSize + vInstanceSize2 == 0)
    3. 若都不是, 向前查找最近的 LFM 和 VMT 起始
    4. 选择距离 ea 最近的数据块

    返回: (类型字符串, 起始地址)
      类型字符串: 'lfm' / 'vmt' / None
      起始地址: int 或 None
    """
    # 1. 检查 ea 本身
    if is_lfm_at(ea):
        return 'lfm', ea
    if verify_vmt(ea):
        return 'vmt', ea

    # 2. 向前查找 LFM 和 VMT
    lfm_addr = find_lfm_near(ea)
    vmt_addr = find_vmt_for_address(ea)

    # 3. 选择最近的
    candidates = []
    if lfm_addr is not None:
        candidates.append(('lfm', lfm_addr, ea - lfm_addr))
    if vmt_addr is not None:
        candidates.append(('vmt', vmt_addr, ea - vmt_addr))

    if not candidates:
        return None, None

    candidates.sort(key=lambda x: x[2])
    return candidates[0][0], candidates[0][1]


def auto_parse_at(ea=None):
    """自动检测并解析指定地址处的 LFM 或 VMT。

    自动判断给定地址处是 LFM (窗体资源) 还是 VMT (虚拟方法表),
    然后调用相应的解析器。若地址不是数据起始, 自动向前查找。

    参数:
        ea: 要检测的地址。若为 None, 使用当前光标位置 (idc.here())。

    输出:
    - 控制台打印检测结果和解析报告
    - 写入对应的报告文件 (lfm_parse_report.txt 或 vmt_parse_report.txt)
    """
    if ea is None:
        ea = idc.here()
    print(f'\n自动检测数据类型, 地址: 0x{ea:08X}')

    data_type, start_addr = detect_data_type_at(ea)

    if data_type is None:
        print(f'错误: 在 0x{ea:X} 附近 {VMT_SEARCH_BACK_RANGE} 字节范围内未找到 LFM 或 VMT')
        return None

    if start_addr != ea:
        print(f'注意: 0x{ea:X} 不是数据起始, 已自动向前查找至 0x{start_addr:08X}')

    if data_type == 'lfm':
        print('检测结果: LFM (Lazarus Form Module 窗体资源)')
        return parse_lfm_main(start_addr)
    elif data_type == 'vmt':
        print('检测结果: VMT (虚拟方法表)')
        return parse_vmt_main(start_addr)

    return None


def scan_lfms():
    """扫描所有 TPF0/TPF1 LFM 资源并输出报告。

    这是默认行为: 解析所有 LFM 窗体资源, 提取组件和事件处理器,
    输出到 lfm_forms.txt 文件。
    """
    print('Scanning for TPF0/TPF1 LFM resources (chunked reading)...')
    found_lfms = find_all_lfms()
    print(f'Total LFM resources found: {len(found_lfms)}')
    print()

    all_forms = []
    all_events = []
    all_components = []

    for i, (ea, magic) in enumerate(found_lfms):
        result = parse_lfm(ea)
        if result['error']:
            print(f'LFM #{i:2d} at 0x{ea:08X}: ERROR: {result["error"]}')
            continue
        for form in result['forms']:
            all_forms.append((ea, form))
            events = collect_events(form)
            all_events.extend(events)
            components = collect_components(form)
            all_components.extend(components)
        # Progress every 10
        if (i + 1) % 10 == 0:
            print(f'  ...parsed {i + 1}/{len(found_lfms)}')

    # Print summary to stdout
    print(f'=== Parsed {len(all_forms)} forms ===')
    for ea, form in all_forms:
        evt_count = sum(1 for k in form['props'] if is_event_prop(k))
        child_count = len(form.get('children', []))
        print(f'0x{ea:08X}: {_sanitize_str(form["type"]):30s} {_sanitize_str(form["name"]):30s} ({evt_count} top-level events, {child_count} children)')

    print()
    print(f'=== Total {len(all_events)} event handlers (showing first 60) ===')
    for evt in all_events[:60]:
        print(f'{_sanitize_str(evt["event"]):20s} -> {_sanitize_str(evt["handler"]):30s} ({_sanitize_str(evt["component_type"])}/{_sanitize_str(evt["component_name"])})')

    # Write detailed output to file
    with open(OUTPUT_PATH, 'w', encoding='utf-8') as f:
        def write_obj(obj, indent=0):
            pad = '  ' * indent
            f.write(f'{pad}object {_sanitize_str(obj["type"])}: {_sanitize_str(obj["name"])}\n')
            for k, v in obj['props'].items():
                f.write(f'{pad}  {_sanitize_str(k)} = {_sanitize_val(v)}\n')
            for child in obj.get('children', []):
                write_obj(child, indent + 1)

        f.write(f'Total LFM resources: {len(found_lfms)}\n')
        f.write(f'Total parsed forms: {len(all_forms)}\n')
        f.write(f'Total components: {len(all_components)}\n')
        f.write(f'Total event handlers: {len(all_events)}\n\n')
        f.write('=' * 100 + '\n')
        f.write('FORMS (top-level)\n')
        f.write('=' * 100 + '\n')
        for ea, form in all_forms:
            evt_count = sum(1 for k in form['props'] if is_event_prop(k))
            f.write(f'\n0x{ea:08X}: {_sanitize_str(form["type"])} name={_sanitize_str(form["name"])} ({evt_count} top-level events, {len(form.get("children", []))} children)\n')

        f.write('\n')
        f.write('=' * 100 + '\n')
        f.write('ALL COMPONENTS\n')
        f.write('=' * 100 + '\n')
        for comp in all_components:
            f.write(f'{_sanitize_str(comp["type"]):30s} {_sanitize_str(comp["name"]):30s} path={_sanitize_str(comp["path"])}\n')

        f.write('\n')
        f.write('=' * 100 + '\n')
        f.write('EVENT HANDLERS\n')
        f.write('=' * 100 + '\n')
        for evt in all_events:
            f.write(f'{_sanitize_str(evt["event"]):20s} -> {_sanitize_str(evt["handler"]):30s} | {_sanitize_str(evt["component_type"])}/{_sanitize_str(evt["component_name"])} (path: {_sanitize_str(evt["component_path"])})\n')

        f.write('\n')
        f.write('=' * 100 + '\n')
        f.write('FULL FORM DETAILS\n')
        f.write('=' * 100 + '\n')
        for ea, form in all_forms:
            f.write(f'\n--- Form at 0x{ea:08X} ---\n')
            write_obj(form)

    print()
    print(f'Detailed output written to {OUTPUT_PATH}')


def main():
    """主入口函数。

    交互模式 (在 IDA Pro 中通过 File > Script File 或 Alt+F7 运行):
      弹出三按钮对话框供用户选择操作模式:
      - VMT全扫描:  扫描二进制中所有 VMT (按钮1, 默认)
      - LFM扫描:    扫描所有 TPF0 LFM 窗体资源 (按钮2)
      - 自动解析:   自动检测指定地址或当前光标处的 LFM/VMT 并解析 (按钮3)

    批处理模式 (通过 idat64 -S 运行, idc.ARGV 含脚本名以外的参数):
      - --parse [地址]:  自动检测并解析 (LFM 或 VMT), 地址缺省时用当前光标
      - --lfm [地址]:    解析指定地址 LFM, 地址缺省时用当前光标
      - --vmt [地址]:    解析指定地址 VMT, 地址缺省时用当前光标
      - --vmt-scan:      VMT 全量扫描
      - 无参数:          LFM 扫描

    也可在 IDA Python 控制台直接调用:
      auto_parse_at(0xC855F8)    # 自动检测并解析
      auto_parse_at()            # 使用当前光标位置
      parse_vmt_main(0xC855F8)   # 解析指定地址 VMT
      parse_lfm_main(0xC855F8)   # 解析指定地址 LFM
      scan_all_vmts()            # 扫描所有 VMT
      scan_lfms()                # 扫描所有 LFM
    """
    print('>>> main() 已调用')
    sys.stdout.flush()
    args = getattr(idc, 'ARGV', [])
    print(f'>>> ARGV = {args}')

    # 批处理模式: idc.ARGV 含脚本名以外的参数时, 按参数分发
    if len(args) > 1:
        def _parse_addr_arg(flag):
            """从 args 中提取 --flag 后的地址参数, 返回 ea 或 idc.here()。"""
            if flag not in args:
                return None
            idx = args.index(flag)
            addr_str = args[idx + 1] if idx + 1 < len(args) else None
            if addr_str:
                try:
                    return int(addr_str, 16) if addr_str.startswith('0x') else int(addr_str, 0)
                except ValueError:
                    print(f'错误: 无效的地址参数 "{addr_str}"')
                    return -1
            return idc.here()

        if '--parse' in args:
            ea = _parse_addr_arg('--parse')
            if ea != -1:
                auto_parse_at(ea)
        elif '--lfm' in args:
            ea = _parse_addr_arg('--lfm')
            if ea != -1:
                parse_lfm_main(ea)
        elif '--vmt' in args:
            ea = _parse_addr_arg('--vmt')
            if ea != -1:
                parse_vmt_main(ea)
        elif '--vmt-scan' in args:
            scan_all_vmts()
        else:
            scan_lfms()
        return

    # 交互模式: 弹出对话框选择操作
    # IDA 9.x ask_buttons 返回值: btn1=1(ASKBTN_YES), btn2=0(ASKBTN_NO), btn3=-1(ASKBTN_CANCEL)
    # btn3 与 Escape/关闭 返回值相同 (-1), 因此将最不常用的操作放在 btn3
    ASKBTN_YES = getattr(ida_kernwin, 'ASKBTN_YES', 1)
    ASKBTN_NO = getattr(ida_kernwin, 'ASKBTN_NO', 0)
    ASKBTN_CANCEL = getattr(ida_kernwin, 'ASKBTN_CANCEL', -1)

    choice = ida_kernwin.ask_buttons(
        'VMT全扫描',
        'LFM扫描',
        '自动解析',
        ASKBTN_YES,
        '请选择操作模式:\n\n'
        'VMT全扫描 - 扫描二进制中所有 VMT (按钮1)\n'
        'LFM扫描 - 扫描所有 TPF0 LFM 窗体资源 (按钮2)\n'
        '自动解析 - 自动检测当前光标处的 LFM/VMT 并解析 (按钮3)\n\n'
        '提示: 也可在 Python 控制台直接调用:\n'
        '  auto_parse_at(0x地址)\n'
        '  parse_vmt_main(0x地址)\n'
        '  parse_lfm_main(0x地址)\n'
        '  scan_all_vmts()'
    )

    print(f'>>> 用户选择: {choice} (1=VMT全扫描, 0=LFM扫描, -1=自动解析)')
    sys.stdout.flush()

    if choice == ASKBTN_YES:  # btn1 = VMT全扫描
        print('>>> 正在启动 VMT 全扫描...')
        sys.stdout.flush()
        scan_all_vmts()
    elif choice == ASKBTN_NO:  # btn2 = LFM扫描
        scan_lfms()
    elif choice == ASKBTN_CANCEL:  # btn3 = 自动解析 (Escape 也触发此项)
        # 弹出地址输入框, 默认为当前光标位置
        current_ea = idc.here()
        addr = ida_kernwin.ask_addr(
            current_ea,
            '请输入要解析的地址 (十六进制):\n'
            '默认为当前光标位置: 0x{:X}\n\n'
            '将自动检测该地址处是 LFM 还是 VMT 并解析。\n'
            '若不是数据起始, 自动向前查找最近的数据块。'.format(current_ea)
        )
        if addr is None or addr == idc.BADADDR:
            print('已取消')
            return
        auto_parse_at(addr)
    else:
        print(f'>>> 未知选择: {choice}')


print('>>> parse_lfm.py 脚本已加载')
main()
print('>>> parse_lfm.py 脚本执行完毕')

parse_lfm.py v5 功能概览

parse_lfm.py 已升级为 v5, 在原有 LFM 解析能力基础上新增 VMT 解析和自动检测功能。支持五种工作模式:

模式 入口函数 说明
全量 VMT 扫描 (默认) scan_all_vmts() 扫描二进制文件中所有 VMT, 输出汇总报告 (交互模式按钮1)
LFM 扫描 scan_lfms() 扫描所有 TPF0/TPF1 LFM 资源, 输出窗体/事件 (交互模式按钮2)
自动解析 auto_parse_at(ea) 自动检测指定地址处是 LFM 还是 VMT, 调用相应解析器 (交互模式按钮3)
单个 LFM 解析 parse_lfm_main(ea) 解析指定地址或当前光标位置的 LFM, 输出组件树/事件列表
单个 VMT 解析 parse_vmt_main(ea) 解析指定地址或当前光标位置的 VMT, 输出字段/方法表/继承链

自动检测原理:

  • LFM 识别: 地址处前 4 字节为 TPF0 (0x54504630) 或 TPF1 (0x54504631)
  • VMT 识别: vInstanceSize != 0vInstanceSize + vInstanceSize2 == 0 (mod 2^64)
  • 若地址不是数据起始, 向前查找最近的数据块 (最多 4KB), 选择距离最近者

使用方法:

  1. 在 IDA Pro 中打开 TOTALCMD64.EXE.i64
  2. File > Script File...setup_vmt_structs.py — 设置 VMT 结构体类型
  3. File > Script File...rename_handlers.py — 重命名方法函数
  4. File > Script File...parse_lfm.py — 弹出对话框选择模式:
    • VMT全扫描 (按钮1, 默认): 扫描全部 VMT
    • LFM扫描 (按钮2): 扫描所有 LFM 资源
    • 自动解析 (按钮3): 输入地址 (或使用当前光标位置), 自动检测 LFM/VMT 并解析

如需批处理自动化, 通过命令行参数指定模式:

  • idat64 -A -S"parse_lfm.py --parse 0xC855F8" TOTALCMD64.EXE.i64 — 自动检测并解析 (LFM 或 VMT)
  • idat64 -A -S"parse_lfm.py --lfm 0xC855F8" TOTALCMD64.EXE.i64 — 解析指定地址 LFM
  • idat64 -A -S"parse_lfm.py --vmt 0xC855F8" TOTALCMD64.EXE.i64 — 解析指定地址 VMT
  • idat64 -A -S"parse_lfm.py --vmt-scan" TOTALCMD64.EXE.i64 — 扫描全部 VMT

也可在 IDA Python 控制台直接调用:

  • auto_parse_at(0xC855F8) — 自动检测并解析 (LFM 或 VMT)
  • auto_parse_at() — 使用当前光标位置自动检测
  • parse_vmt_main(0xC855F8) — 解析指定地址 VMT
  • parse_lfm_main(0xC855F8) — 解析指定地址 LFM
  • scan_all_vmts() — 扫描全部 VMT
  • scan_lfms() — 执行 LFM 扫描

输出文件:

文件 内容
lfm_forms.txt 全部窗体、组件、事件 (5MB)
lfm_parse_report.txt 单个 LFM 解析报告 (选择"自动解析"检测到 LFM / --lfm / parse_lfm_main() 后产生)
rename_report.txt VMT 位置、方法地址、重命名日志
vmt_struct_report.txt VMT 结构体设置报告 (运行 setup_vmt_structs.py 后产生)
vmt_parse_report.txt 单个 VMT 解析报告 (选择"自动解析"检测到 VMT / --vmt / parse_vmt_main() 后产生)
vmt_scan_report.txt 全部 VMT 扫描汇总报告 (选择"VMT全扫描"或 --vmt-scan 后产生)

9. VMT 地址快速参考

以下 72 个窗体类 VMT 按地址排序 (有 LFM 资源对应的窗体类)。所有符号已命名, 结构体类型已应用到 IDA。 通过 scan_all_vmts() 直接扫描法可找到全部 561 个 VMT (含非窗体基础类), 完整列表见 vmt_scan_report.txt

VMT 地址 方法表 类名 方法数 IDA 符号
0xBE3698 0xBE35A8 TNASTYNAGSCREEN 11 TNASTYNAGSCREEN_VMT
0xBE7908 0xBE7760 TATTRIBDLG 24 TATTRIBDLG_VMT
0xBE9F20 0xBE9CB0 TUSERMENUCHANGE 36 TUSERMENUCHANGE_VMT
0xBEC7B8 0xBEC6E0 TDRIVEDLG 10 TDRIVEDLG_VMT
0xBEDA10 0xBED8F0 TASSOCIATEDLG 15 TASSOCIATEDLG_VMT
0xBEF370 0xBEF1E0 TDupeSelForm 22 TDupeSelForm_VMT
0xBF1C48 0xBF1BC0 TVOLLABELDLG 7 TVOLLABELDLG_VMT
0xBF2C48 0xBF2B70 TDLGUNZIPALL 12 TDLGUNZIPALL_VMT
0xBF4B60 0xBF4980 TDLGZIP 29 TDLGZIP_VMT
0xBF7178 0xBF70E0 TDLGPACKERINFO 8 TDLGPACKERINFO_VMT
0xBF91B0 0xBF90A0 TQUICKSEARCH 8 TQUICKSEARCH_VMT
0xBFA928 0xBFA810 TSTDTREEDLG 15 TSTDTREEDLG_VMT
0xBFC190 0xBFBF80 TCHANGETREEDLG 28 TCHANGETREEDLG_VMT
0xBFDC18 0xBFDB50 TINFODLG 10 TINFODLG_VMT
0xC01258 0xC01200 TVERSIONINFODLG 4 TVERSIONINFODLG_VMT
0xC02AA0 0xC027C0 TBUTTONCHANGEDLG 43 TBUTTONCHANGEDLG_VMT
0xC05990 0xC057A0 TONEBUTTONCHANGEDLG 28 TONEBUTTONCHANGEDLG_VMT
0xC07C00 0xC07B60 TDLGUNZIP 9 TDLGUNZIP_VMT
0xC092C0 0xC09150 TProgressDialog 19 TProgressDialog_VMT
0xC0CB10 0xC0C9C0 TDLG2FILEACTIONMIN 10 TDLG2FILEACTIONMIN_VMT
0xC0E1F8 0xC0E130 TExtMsgForm 9 TExtMsgForm_VMT
0xC10B08 0xC10A10 TCOMBOINPUT 13 TCOMBOINPUT_VMT
0xC141F0 0xC13440 TDLGCONFIGALL 214 TDLGCONFIGALL_VMT
0xC3A180 0xC39AE8 TLister 62 TLister_VMT
0xC3F9F8 0xC3F3C0 TFindFile 81 TFindFile_VMT
0xC46520 0xC46440 TDriveSelBox 9 TDriveSelBox_VMT
0xC47978 0xC477F0 TLISTCONF 22 TLISTCONF_VMT
0xC4B580 0xC4B3C0 TOverWriteForm 21 TOverWriteForm_VMT
0xC4D4F0 0xC4D470 TSEARCHTEXT 7 TSEARCHTEXT_VMT
0xC50768 0xC506F0 TFireWallDlg 6 TFireWallDlg_VMT
0xC52240 0xC52120 TCheckEditBox 15 TCheckEditBox_VMT
0xC53A30 0xC53970 TNewConnDlg 11 TNewConnDlg_VMT
0xC54EF0 0xC54D70 TDETAILS 23 TDETAILS_VMT
0xC58BF0 0xC58B70 TChmodForm 7 TChmodForm_VMT
0xC5A910 0xC5A760 TEditTypeForm 24 TEditTypeForm_VMT
0xC5C7A8 0xC5C700 TEdActionForm 9 TEdActionForm_VMT
0xC5E308 0xC5E210 TChooseIcon 12 TChooseIcon_VMT
0xC5F9D0 0xC5F510 TFileCompForm 63 TFileCompForm_VMT
0xC655F8 0xC65550 TPrintConfDlg 9 TPrintConfDlg_VMT
0xC67CB0 0xC67B60 TDlgCustomColors 17 TDlgCustomColors_VMT
0xC695E8 0xC69530 Thostdefinedlg 10 Thostdefinedlg_VMT
0xC6B588 0xC6B4C0 TEncodeEditBox 11 TEncodeEditBox_VMT
0xC6D0F0 0xC6CC40 TMultiRename 69 TMultiRename_VMT
0xC718D8 0xC71720 TCONNECT 24 TCONNECT_VMT
0xC733E0 0xC731F0 TInpComboDlg 26 TInpComboDlg_VMT
0xC76C98 0xC76C00 TSplitEditBox 8 TSplitEditBox_VMT
0xC780E8 0xC77F30 TPrintDlg 23 TPrintDlg_VMT
0xC7A1E0 0xC7A180 TMarginsDlg 5 TMarginsDlg_VMT
0xC7BF18 0xC7B990 TCmpForm 68 TCmpForm_VMT
0xC82268 0xC821C0 TCmtEditForm 9 TCmtEditForm_VMT
0xC83640 0xC83520 TFsPluginConfigForm 13 TFsPluginConfigForm_VMT
0xC855F8 0xC84930 TTOTAL_CMD 99 TTOTAL_CMD_VMT
0xC8ECB0 0xC8EB70 TCustomColumnForm 17 TCustomColumnForm_VMT
0xC90E98 0xC90D00 TCmdSelForm 22 TCmdSelForm_VMT
0xC92A20 0xC929B0 TDateTimePickForm 4 TDateTimePickForm_VMT
0xC93BF8 0xC93B90 TChooseEncodingForm 5 TChooseEncodingForm_VMT
0xC95158 0xC94F98 TPathSelForm 19 TPathSelForm_VMT
0xC969E0 0xC96950 TPassForm 8 TPassForm_VMT
0xC98BC0 0xC98AC0 TShellExtForm 13 TShellExtForm_VMT
0xC9D518 0xC9D480 TCrcForm 7 TCrcForm_VMT
0xC9F090 0xC9EEE0 TPluginSelForm 22 TPluginSelForm_VMT
0xCBD598 0xCBD510 TCheckMsgBox 7 TCheckMsgBox_VMT
0xD41570 0xD41530 TDriveTip 3 TDriveTip_VMT
0xD4BE70 0xD4BE30 TPrintForm 3 TPrintForm_VMT
0xD54200 0xD54140 TICONLIBCHANGEDLG 9 TICONLIBCHANGEDLG_VMT
0xD5D3B0 0xD5D2E0 TCheckComboBox 10 TCheckComboBox_VMT
0xD5FF38 0xD5FEF0 TTabIconDialog 3 TTabIconDialog_VMT
0xD61238 0xD61210 TCryptOptions 1 TCryptOptions_VMT
0xD62328 0xD622D0 TButtonEditBox 4 TButtonEditBox_VMT
0xD643A0 0xD64230 THistoryForm 15 THistoryForm_VMT
0xD67C98 0xD67C30 TSyncStartForm 5 TSyncStartForm_VMT
0xD69310 0xD69240 TSYNCEXTINPUT 10 TSYNCEXTINPUT_VMT

10. VMT 解析功能详解

parse_lfm.py v5 在原有 LFM 解析能力基础上, 新增了对 FPC 64 位 VMT (虚拟方法表) 的解析与扫描功能。VMT 结构布局详见第 1 节 "FPC 64 位 VMT 结构"。

10.1 VMT 相关常量

脚本中定义了以下 VMT 相关常量:

常量 说明
VMT_SIZE 104 (13 * 8) VMT 总字节数 (13 个 qword 字段)
MAX_VMT_CHAIN_DEPTH 32 父类继承链最大深度 (用于循环引用保护)
MAX_METHODS_PER_VMT 1000 单个 VMT 方法数量上限 (异常数据保护)
VMT_SEARCH_BACK_RANGE 0x1000 (4KB) 向前查找 VMT 起始的最大范围
VMT_FIELDS 13 项元组列表 VMT 字段定义 (偏移、字段名、说明)
VMT_REPORT_PATH 见第 8 节 单个 VMT 解析报告输出路径
VMT_SCAN_REPORT_PATH 见第 8 节 全量 VMT 扫描报告输出路径

10.2 可用函数

函数 用途
parse_vmt_at(vmt_addr) 解析指定地址的 VMT, 返回字段/方法/父类链
parse_vmt_main(ea=None) VMT 解析入口, 解析给定地址或当前光标 (idc.here()) 处的 VMT
find_vmt_for_address(ea) 查找包含给定地址的 VMT 起始地址 (向前搜索, 最多 4KB)
scan_all_vmts() 扫描二进制文件中所有 VMT (直接扫描法: 8字节对齐位置 VMT 校验)
format_vmt_report(result) 将 VMT 解析结果格式化为可读文本报告
walk_vmt_chain(vmt_addr) 遍历父类 VMT 继承链 (含循环检测, 最大深度 32)
verify_vmt(vmt_addr) 验证 FPC 64 位 VMT 有效性 (vInstanceSize != 0vInstanceSize + vInstanceSize2 == 0)
read_vmt_fields(vmt_addr) 读取全部 13 个 VMT 字段
read_method_table_detailed(method_table_ptr) 读取方法表, 含方法名指针与代码指针

10.3 函数说明

parse_vmt_at(vmt_addr)

核心解析函数。读取 VMT 全部 13 个字段、解析方法表 (含每个方法的名称指针与代码地址)、并通过 walk_vmt_chain 沿父类链 (vParentRef) 遍历继承关系。返回包含 vmt_addrvmt_symbolclass_namefieldsmethodsparent_chain 等键的字典。

parse_vmt_main(ea=None)

对外入口。若 eaNone, 使用 idc.here() 取当前光标位置。若 ea 不是有效 VMT 起始地址, 自动调用 find_vmt_for_address 向前查找最近的 VMT 起始。解析完成后将格式化报告写入 VMT_REPORT_PATH, 报告末尾附带 JSON-like 原始数据便于程序化处理。

find_vmt_for_address(ea)

从给定地址开始向前搜索, 在 VMT_SEARCH_BACK_RANGE (4KB) 范围内查找有效 VMT 起始地址。便于在光标位于 VMT 内部某字段时, 自动定位到 VMT 起点。

scan_all_vmts()

全量扫描策略 (直接扫描法):

  1. 遍历所有数据段 (SEG_DATA), 分块读取 (1MB 分块, 块尾多读 32 字节处理边界)
  2. 在每个块中按 8 字节对齐位置检查 VMT 校验条件:
    • vInstanceSize != 0 (快速跳过前 4 字节全零位置)
    • vInstanceSize + vInstanceSize2 == 0 (mod 2^64, 校验字段)
  3. 对每个候选, 验证 vClassName (offset +24) 指针指向合法 Pascal ShortString 类名
  4. 类名格式校验: 以字母开头, 仅含 A-Z/a-z/0-9/_ (调用 _is_valid_lfm_identifier)

相比旧版按类名逐个搜索 (N 次全段扫描), 直接扫描法仅需 1 次全段扫描, 速度提升数百倍, 且覆盖率 100%。汇总信息输出到 IDA Output 窗口和日志文件 vmt_scan_log.txt, 完整报告写入 VMT_SCAN_REPORT_PATH

walk_vmt_chain(vmt_addr)

沿 vParentRef 字段递归遍历父类 VMT 链。内置循环检测与最大深度限制 (MAX_VMT_CHAIN_DEPTH = 32), 防止循环引用导致无限递归。返回 (vmt_addr, class_name) 元组列表, 从当前类到根类的顺序。

verify_vmt(vmt_addr)

按 FPC 64 位 VMT 有效性规则校验: vInstanceSize != 0vInstanceSize + vInstanceSize2 == 0 (mod 2^64)。返回布尔值, 用于扫描时过滤候选位置。

read_vmt_fields(vmt_addr)

VMT_FIELDS 列表读取 13 个字段, 返回字段名到值的字典。

read_method_table_detailed(method_table_ptr)

读取 vMethodTable 指向的方法表。首 4 字节为 count, 随后是 count 个 16 字节条目, 每条目包含 name_ptr (方法名 Pascal ShortString 指针) 与 code_ptr (方法代码地址)。count 超过 MAX_METHODS_PER_VMT 时会被截断以防异常数据。

format_vmt_report(result)

parse_vmt_at 的返回值格式化为人类可读的文本报告, 含字段表、方法列表、父类继承链。

10.4 调用方式

parse_lfm.py 支持三种调用方式, 覆盖交互使用、批处理自动化和直接函数调用:

方式一: 交互模式 (推荐, IDA Pro GUI 中使用)

在 IDA Pro 中通过 Alt+F7File > Script File 运行 parse_lfm.py, 脚本会弹出三按钮对话框:

┌─────────────────────────────────────────────────┐
│  请选择操作模式:                                  │
│                                                  │
│  VMT全扫描 - 扫描二进制中所有 VMT (按钮1)         │
│  LFM扫描 - 扫描所有 TPF0 LFM 窗体资源 (按钮2)     │
│  自动解析 - 自动检测当前光标处的 LFM/VMT (按钮3)   │
│                                                  │
│  [VMT全扫描]   [LFM扫描]   [自动解析]            │
└─────────────────────────────────────────────────┘
  • 选择 VMT全扫描 (按钮1, 默认): 执行 scan_all_vmts(), 扫描所有 VMT
  • 选择 LFM扫描 (按钮2): 执行 scan_lfms(), 扫描所有 LFM 资源
  • 选择 自动解析 (按钮3): 进一步弹出地址输入框 (默认填入当前光标位置), 输入或确认后自动检测该地址处是 LFM 还是 VMT 并调用相应解析器; 若地址不是数据起始, 自动向前查找最近的数据块
  • Escape 或关闭对话框: 等同于选择"自动解析" (IDA 9.x ask_buttons 的 btn3 与 Escape 返回值相同)

方式二: 批处理模式 (自动化场景)

通过 idat64 -S 传递命令行参数, 适用于无人值守的批处理任务:

# 自动检测并解析 (推荐, 自动判断 LFM 或 VMT)
idat64 -A -S"parse_lfm.py --parse 0xC855F8" TOTALCMD64.EXE.i64

# 解析指定地址的 LFM
idat64 -A -S"parse_lfm.py --lfm 0xC855F8" TOTALCMD64.EXE.i64

# 解析指定地址的 VMT (十六进制地址)
idat64 -A -S"parse_lfm.py --vmt 0xC855F8" TOTALCMD64.EXE.i64

# 扫描二进制文件中所有 VMT
idat64 -A -S"parse_lfm.py --vmt-scan" TOTALCMD64.EXE.i64

# 默认模式: 执行完整 LFM 扫描
idat64 -A -S"parse_lfm.py" TOTALCMD64.EXE.i64

方式三: Python 控制台直接调用

在 IDA Python 控制台中直接调用函数, 灵活指定参数:

import parse_lfm
parse_lfm.auto_parse_at(0xC855F8)         # 自动检测并解析 (LFM 或 VMT)
parse_lfm.auto_parse_at()                  # 使用当前光标位置自动检测
parse_lfm.parse_lfm_main(0xC855F8)         # 解析指定地址 LFM
parse_lfm.parse_vmt_main(0xC855F8)         # 解析指定地址 VMT
parse_lfm.scan_all_vmts()                  # 扫描全部 VMT
parse_lfm.find_vmt_for_address(0xC85620)   # 查找包含此地址的 VMT 起点
parse_lfm.scan_lfms()                      # 扫描所有 LFM

10.5 输出文件说明

文件 触发方式 内容
vmt_parse_report.txt "自动解析"检测到 VMT / --vmt / --parse / parse_vmt_main() 单个 VMT 解析报告: 字段表、方法列表 (含名称/代码地址/符号)、父类继承链, 末尾附 JSON-like 原始数据
lfm_parse_report.txt "自动解析"检测到 LFM / --lfm / --parse / parse_lfm_main() 单个 LFM 解析报告: 窗体类名、属性列表、事件处理器列表、递归组件树
vmt_scan_report.txt "VMT全扫描" / --vmt-scan / scan_all_vmts() 全部 VMT 扫描汇总报告: 扫描段统计、每个 VMT 的地址/类名/方法数/父类链、汇总信息
lfm_forms.txt "LFM扫描" / scan_lfms() 所有 LFM 窗体的组件树和事件处理器列表

输出文件默认写入 IDB 所在目录 (与 .i64 文件同目录)。


报告结束。基于 IDA Pro 对 TOTALCMD64.EXE 的分析生成。

posted @ 2026-07-16 21:27  DirWangK  阅读(38)  评论(1)    收藏  举报