Skip to Content

反汇编 / Hexview

本页专讲 Shadow CE 客户端的两个核心阅读视图 —— Hex view 与 Disasm view —— 以及承载它们的 Memory View window。重点是 widget 的渲染实现、交互、与 cache 的协作。


1. 定位与关系

Shadow CE 的 Memory View 仿照 Cheat Engine,把”看代码”和”看字节”做成 上下分栏的两个独立 widget

视图文件主要用途粒度可写
DisasmViewdisasm_view.pyARM64 反汇编阅读、跳转追踪、分支可视化4 字节一行否(当前只读)
HexViewhex_view.py查看/修改字节、fade 高亮最近变化、选中范围下断点16 字节一行是(hex 与 ASCII 双模式)

两者 不共享父类。它们分别直接继承 QWidget,只在外部(MemoryViewWindow) 被组合。共用的只有:

  • 同一份 MemoryCachemem_cache.py,页粒度 4096 字节)
  • 同一份字体 Fonts.mono()fonts.py
  • 同一份颜色表 MEMVIEW_COLORSstyle.py:216

也就是说,memory_view.py 不是抽象基类,而是 宿主窗口:它实例化 disasm 和 hex 两个 widget,把它们塞进 QSplitter,再建立两条信号(地址联动、断点 请求)把它们串起来。

两条 TCP 连接(_refresh_client 走 500ms 定时重扫、_fetch_client 走按需 补页)是 Memory View 不卡顿的关键,见 memory_view.py:58-75


2. Hex view 实现

hex_view.py(593 行)整体是一个 自绘 QWidget(不是 QTextEdit), 渲染走 offscreen QPixmap 双缓冲,同时有一整套”零每帧分配”的预计算表。

2.1 行布局

每行固定 16 字节,分三段:

012ABCDEF000 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF | ASCII.... └── 12位hex地址 ─┘└──── 8字节 ─────┘ └──── 8字节 ─────┘ └─16 ASCII─┘

几何参数在 _calc_geometry()hex_view.py:105)里按 QFontMetrics 算出:

  • _cw = 单字符宽度('0' 的 horizontalAdvance)
  • _lh = 行高 = fm.height() + 4
  • _addr_w = _cw * 13 + 8
  • _hex_x 起点 = _addr_w
  • 8 字节处插一条竖线分隔符(_pen_sep
  • hex 与 ASCII 之间隔 _cw * 2

16 字节的 x 位置在进入渲染循环前 一次性预计算好_hex_col_x[]), 循环里只做查表。

2.2 预计算表

模块级单例,启动时就算好:

常量位置用途
_chex_view.py:11MEMVIEW_COLORS dict 翻译成 QColor 对象
_HEXhex_view.py:12[f"{i:02X}" for i in range(256)] — 256 个 2 字符字符串
_ASCIIhex_view.py:13256 个 ASCII 单字符(不可打印→.
_FADE_COLORShex_view.py:15-2121 步”最近改动”→“正常”的渐隐颜色

实例级预缓存(__init__ 里):

  • _pen_normal / _pen_zero / _pen_addr / _pen_ascii / _pen_dot / _pen_hdr / _pen_sel / _pen_edit / _pen_cursor / _pen_sep / _pen_code

目的就一个:paintEvent 里不 new QColor、不 new QPen、不 new QFont

2.3 自绘 paintEvent — offscreen buffer 双缓冲

def paintEvent(self, event): p = QPainter(self) p.setFont(self._font) self._render(p) p.end()

实际工作在 _render()hex_view.py:128)里。它支持两种模式:

  • 如果传入了 QPainter(正常路径),直接画到 widget。
  • 如果 p is None,创建 _buf: QPixmap 离屏画布,画完可复用。 (当前 paintEvent 每帧都传 painter,离屏 buffer 保留给 mark_dirty() 优化。)

渲染循环里的”绘色最小化”: cur_pen_id 追踪上一次 setPen 的类别,只有类别变了才真的 setPen。对 16 × N 行的密集绘图,这省掉绝大多数 pen 切换。

2.4 Fade(最近改动高亮)

MemoryCache.get_change_alphas(base, size)mem_cache.py:111)一次性返回 [alpha_0, alpha_1, ...]_render 拿到这个列表后,每字节查一次,映射到 _FADE_COLORS 的 21 级渐隐 pen。

  • alpha == 0.0 → 正常颜色(白色或执行段绿)
  • alpha > 0.0 → pen_id = 'fade',从 _FADE_COLORS[idx] 取色

FADE 生命期 = MemoryCache.FADE_MS = 1000msmem_cache.py:11)。注意 fade 计算只调一次 time.time() —— 这是刻意的,见 mem_cache.py:112 注释。

2.5 选中、光标、编辑状态机

  • _cursor 是当前字节的 offset(相对 _base_addr),-1 表示无。
  • _sel_start / _sel_end 构成选中范围,鼠标按下时跟 _cursor 一致,拖动 时 _sel_end 跟着走。
  • _edit_nibble ∈ {0, 1} 跟踪 hex 模式下当前在改字节的高位还是低位半字节。
  • _edit_region ∈ {'hex', 'ascii'} 区分光标在哪一侧 —— 按 Tab 切换。

渲染时:

  • 光标所在 byte → 填 edit_bg,pen 切到 edit;ascii 那列也一起染。
  • 选中范围内 byte → 填 sel_bg,pen 切到 sel
  • 执行段(set_executable(True))→ 正常 byte 改用绿色 pen(_pen_code), ASCII 也绿。

光标竖线由 _pen_cursor(红色 2px)画在相应列的左边缘。

2.6 编辑写入(write-through)

HexView 自己不连 TCP,它持有 write_fn(由宿主传入,memory_view.py:180 给的是 lambda a, d: self._own_client.write_memory(self.handle, a, d))。 两条编辑路径:

Hex 键入_edit_hexhex_view.py:472):

  1. 读当前字节(blocking=True —— 必须实时)
  2. 替换对应 nibble
  3. write_fn(addr, bytes([b]))
  4. cache.invalidate_page(addr) 让下次 refresh 真的 re-read
  5. self._dirty = Trueself.update()
  6. 第二个 nibble 写完后 _cursor += 1 自动前进

ASCII 键入_edit_ascii):单字符 UTF-8 → 直接写一字节 → 前进。

双击对话框mouseDoubleClickEventhex_view.py:375): 弹 QInputDialog.getText,允许一次改多字节的 hex string(空格允许但会剥掉)。

注意 不缓存写入 —— 写完马上 invalidate 再靠 refresh timer 拉新数据, 保证”屏幕上看到的等于游戏进程里的”。

2.7 键盘与右键菜单

keyPressEventhex_view.py:393)支持的组合:

按键行为
↑↓←→移动光标(自动翻页)
PageUp / PageDown_visible_lines - 1 翻页
Tab切换 hex / ASCII 编辑区
Esc清掉光标与选区
SpaceFollow pointer(把光标处 8 字节当小端地址跳过去)
Ctrl+GGoto 对话框
Ctrl+C拷贝选区(hex mode 输出空格分隔 hex,ASCII mode 输出可见字符)
0-9 a-f编辑 hex
可打印 ASCII编辑 ASCII
Ctrl+Shift+F打印字体调试信息(开发用)

右键菜单(contextMenuEventhex_view.py:337):

  • Edit(等同双击)
  • Goto Address (Ctrl+G)
  • Copy to clipboard (Ctrl+C)
  • Data Breakpoint 子菜单:
    • Find what writes → 发 bp_requested(addr, size, 'w')
    • Find what accesses → 'rw'
    • Break and trace → 'x'

bp_requested 由宿主 MemoryViewWindow._on_bp_requestedmemory_view.py:358)接住并打开 HwbpWindow

2.8 滚动

自带 QScrollBar(right side,宽 16px),range 0-100,value 固定在 50: 每次滚一格就改 _base_addr,然后把 value 重置回 50(避免”拖到底”的概念)。 wheelEvent 同理:一次 angleDelta/40 行 × 16 字节。

这是一种典型的 infinite scroll 模型 —— 地址空间太大,没法直接映射到 scrollbar 刻度,所以只借 scrollbar 当”连续滚动输入”。


3. Disasm view 实现

disasm_view.py(731 行)同样是自绘 QWidget,比 hex 多了: Capstone 反汇编、跳转箭头、列头可拖拽、中英文指令提示

3.1 反汇编引擎 = Capstone

from capstone import Cs, CS_ARCH_ARM64, CS_MODE_ARM self._cs = Cs(CS_ARCH_ARM64, CS_MODE_ARM) self._cs.detail = True

位置:disasm_view.py:181-186。带 try/except ImportError 兜底 —— capstone 没装时 _instructions = [],整个 view 空白但不崩。不支持模式切换(没有 Thumb、没有 x86),因为 Shadow CE 的靶机就是 ARM64 Android。

detail = True 用来拿 insn.groupsinsn.operands,判断分支/调用和抽 branch target。

3.2 逐条 decode(而不是一次流式 disasm)

关键细节在 _decode()disasm_view.py:247):

for i in range(0, min(len(raw), n * 4), 4): chunk = raw[i:i+4] decoded = list(self._cs.disasm(chunk, addr)) if decoded: insn = decoded[0] ... else: val = int.from_bytes(chunk, 'little') self._instructions.append(DisasmLine( addr=addr, mnemonic="db", op_str=f"0x{val:08X}", ...))

Capstone 遇到非法指令会整体停下来。如果一段 ROM 里混了跳转表、字符串、 导入表,流式 disasm 会在第一条非法 insn 截断,后面全看不见。所以这里 强制一次只喂 4 字节,非法就标 db 0x....(CE 风格),保证每行都有东西。

代价是循环多了点,但因为只 decode 可见行 + 4 条 padding,体感无差。

3.3 分支检测与箭头

DisasmLine 的 dataclass(disasm_view.py:15)记录:

is_branch: bool # CS_GRP_JUMP ∈ groups is_call: bool # CS_GRP_CALL ∈ groups is_conditional: bool # is_branch and mn not in (b, br, ret) branch_target: int # 首个 ARM64_OP_IMM 操作数 comment: str # resolve_fn(target) → "UE4+0x1234"

_build_arrows()disasm_view.py:303)再扫一遍 _instructions,构造 JumpArrow,dst_idx 语义:

  • >= 0:可见范围内的行索引
  • -1:目标在可见上方
  • -2:目标在可见下方

颜色按类别映射:arrow_call='#ccaa44'arrow_cond='#cc4444'arrow_uncond='#008000'style.py:226-228)。条件跳转用虚线 (Qt.PenStyle.DashLine)。

_draw_arrows()disasm_view.py:463)画成 L 形:水平 → 垂直 → 水平, 带三角箭头。slot(slot = counter % 5)让多条重叠的跳转在 x 方向错开。

3.4 三列布局(百分比 + 拖拽)

与 hex 的固定几何不同,disasm 的列宽是 百分比 + 用户可拖拽

字段默认占比范围
Address_col_addr_pct25%10%-50%
Bytes_col_bytes_pct15%5%-40%
Opcode剩余--

拖拽触发点:_hit_col_sepdisasm_view.py:641)—— 鼠标在列分隔线附近 5px 时,光标切 SplitHCursor,按下后进入 _drag_col 模式,移动时按实时 x 重算 pct。

Header 还有 hover / pressed 高亮(_hover_col / _pressed_col),用 QLinearGradient 画”上深下浅”的按钮感,见 disasm_view.py:354-372

3.5 选中行

单选(不是多选):_selected 是行索引。

  • 鼠标点击非 header 区域 → 设 _selected、emit insn_selected(desc)disasm_view.py:673
  • ↑↓ 键移动 _selected,到边界时滚 4 字节

被选中的行画蓝底(_sel_bg = #2A82DA)+ 虚线黄框(#e0c030)。

insn_selected 信号把助记符的人类可读解释(中英文各一份表)送到 MemoryViewWindowlbl_insn_hint 那条横条上显示,见 memory_view.py:182。中英文切换由 i18n.get_lang() 决定 (disasm_view.py:128-135)。

3.6 语法着色

操作数着色走正则 tokenize:

r'([xwqbhsdv]\d{1,2}|sp|lr|xzr|wzr|fp|pc|nzcv)|' # register r'(#-?0x[0-9a-fA-F]+|#-?\d+)|' # immediate r'([\[\]{}!,\s]+)|' # punctuation r'([^\s,\[\]{}!#]+)' # other (labels)

每种 token 绑一种 pen color:register 红、immediate 蓝、符号/标签绿、标点 白。实现在 _draw_operands()disasm_view.py:441)。

助记符本身用 bold 字体 画(_bold_font),分支类用 branch_mn='#ffdd66'(暖黄),其余白色。

3.7 符号解析

resolve_fn: addr -> str 由宿主注入(memory_view.py:178MemoryViewWindow.resolve_modulememory_view.py:256)。

实现是 二分 module 列表(按 base 排序),命中返回 libUE4+0x1234 这样的短名 + 文件内偏移。Module 列表在窗口打开时 一次性从 server 拉下来(module_listmemory_view.py:112)。

两处会用到:

  1. Address 列:优先显示 module+offset,fallback 原始十六进制
  2. 分支 comment; 后边的跳转目标名,仅当目标落在某 module 内

3.8 跳转 / 历史栈

_back_stack: list[int] 记录每次 follow 前的 _base_addr

操作行为
双击带 branch_target 的行压栈,跳过去
Space同上
Backspace弹栈,回到上一处
Ctrl+G弹对话框输入 hex 地址,压栈跳
G(无 modifier)等同 Ctrl+G

历史栈是 disasm 自己的。MemoryViewWindow._history 不是同一个 —— 后者是整个窗口的地址历史,被 toolbar 的 < > 按钮用。双轨历史在 宿主里靠 _on_disasm_nav 桥接(memory_view.py:349)。

3.9 不支持编辑

Disasm view 当前只读。右键菜单没实现,下断点要走 hex view 的 Data Breakpoint 菜单。原因:汇编级 patch 需要 assembler(keystone 或 手写 encoder),整个项目目前不做 instrumentation,所有断点是 HW-BP 或 内核 PTE-BP —— 不改目标字节。


4. 宿主 MemoryViewWindow 与视图联动

memory_view.pyMemoryViewWindow(只 420 行)基本只干三件事:

  1. 开两条独立 TCP 连接(refresh / fetch 不互相阻塞)
  2. 把 disasm + hint + hex 塞进 QSplitter(垂直)
  3. 串信号

4.1 双连接策略

# Connection 1: background refresh (500ms timer) self._refresh_client = CEClient() self._refresh_handle = self._refresh_client.open_process(pid) # Connection 2: on-demand fetch (click / paint cache miss) self._fetch_client = CEClient() self._fetch_handle = self._fetch_client.open_process(pid)

MemoryCache 构造时传两个读函数:

self.cache = MemoryCache(_fetch_read, refresh_read_fn=_refresh_read)

两条连接各自带断线自动重连(memory_view.py:83-89 / 97-103)。

4.2 三大信号

self.disasm.address_changed.connect(self._on_disasm_nav) self.hex_view.address_changed.connect(self._on_hex_nav) self.hex_view.bp_requested.connect(self._on_bp_requested) self.disasm.insn_selected.connect(lambda desc: self.lbl_insn_hint.setText(desc))
  • disasm.address_changed → 同步 hex view 地址、更新 toolbar、压历史栈
  • hex_view.address_changed → 更新底部 DataInspector
  • hex_view.bp_requested(addr, size, type) → 打开 HwbpWindow
  • disasm.insn_selected(desc) → 填中间 hint bar

4.3 DataInspector

memory_view.py:15 的小 QLabel,显示当前光标 8 字节解读成 byte / word / int32 / int64 / float / double 的所有值,CE 经典功能。

选区有多字节时显示范围 XXXX - YYYY (N bytes)

4.4 刷新循环

_start_timermemory_view.py:214)起一个 500ms QTimer + 一个长驻 reader thread。每 tick:

  1. 取两 view 的 visible_range()(hex 按 _visible_lines+2、 disasm 按 _visible_lines+4
  2. queue.put_nowait(ranges)(队列大小 1,drop if full —— 防抖)
  3. reader thread 调 cache.refresh(ranges)(TCP 往返、fade 检测)
  4. 主线程 disasm.mark_dirty() + hex.mark_dirty() 触发重绘
  5. 刷新 DataInspector

这种 “定时 refresh + 画面查 cache” 的分离让 paint 永远不走 TCP, rendering 延迟只受本地 CPU 影响。

4.5 Goto 与 module 解析

Toolbar 的 goto 支持三种输入(_on_gotomemory_view.py:320):

输入行为
libUE4+0x1234找 module,跳到 base+offset
libUE4找 module,跳 base
0x1234ABCD / 1234ABCD纯十六进制地址

Module 查找(_find_module)支持精确、partial、stem 三档 fallback。


5. 数据访问路径

视图本身 从不调 CEClient.read_memory,全走 cache。

关键字段(ce_client.py:104-136):

  • read_memory(handle, addr, size) — 单次读(命令 0x09
  • read_memory_batch(handle, requests) — 批量读(命令 0xCB,一次 round-trip N 对 (addr,size)
  • write_memory(handle, addr, data) — 单次写(命令 0x0A

HexView 的 blocking=True_edit_hex 读当前字节)是唯一一个 走 blocking 路径的地方 —— 因为要保证 nibble 编辑是 read-modify-write 原子。 除此之外 所有读都是非阻塞 + 返回零 placeholder,靠 cache fetch thread 兜底。

Page 粒度与大小

MemoryCache.PAGE_SIZE = 4096MAX_PAGES = 128mem_cache.py:9-10)—— 整个 cache 占用上限约 512KB,LRU 淘汰。

_merge_runs() 会把连续页合并成一次 TCP 请求,减少 syscall 和 round-trip。


6. 性能与渲染策略

目标实现
零每帧分配模块级 _HEX / _ASCII / _FADE_COLORS;实例级 pen 全部在 __init__ 缓存
最小 setPen 切换cur_pen_id 追踪,类别相同就不切
预计算列 x_hex_col_x 在循环前一次 build
一次 time.time()get_change_alphas 批量算 fade
Offscreen bufferQPixmap _buf,仅当 _dirty 或尺寸变化时重画
TCP 解耦Paint 只查 cache;refresh 在独立 thread + queue
Queue drop-if-fullmaxsize=1,防刷新累积
LRU page cache128 × 4KB,连续页合并成一次 round-trip
Refresh 只覆盖可见范围visible_range() 返回 viewport + 小量 overscan

字体是 Fonts.mono()fonts.py)—— 统一用一个 monospace family,跨视图 对齐到同一 baseline。Hex view 用 NoAntialias style strategy(hex_view.py:45), 让像素网格更清晰;disasm 用默认策略,bold 的助记符更软。


7. 快捷键与右键菜单总表

Hex view

按键动作
←↑→↓移动光标(跨页自动翻)
PageUp/Down翻可见高度 - 1 行
Tab切 hex / ASCII 编辑区
Esc清光标 + 选区
SpaceFollow pointer(小端 8 字节)
Ctrl+GGoto address 对话框
Ctrl+C复制选区(hex 或 ASCII 格式随当前 region)
0-9 a-f编辑当前 nibble
32-126 ASCII编辑 ASCII
Ctrl+Shift+FDump 字体诊断到 stdout
双击弹对话框编辑 4 字节 hex
右键Edit / Goto / Copy / Data Breakpoint (w/rw/x)

Disasm view

按键动作
↑↓选中上/下一行(跨边界滚 4 字节)
PageUp/Down翻 visible-1 行
Space 或双击Follow 分支目标(压栈)
Backspace回到上一个 follow 点
Ctrl+G / GGoto 对话框
Ctrl+C复制选中行(含地址 + 助记符 + 操作数)
Ctrl+Shift+FDump 字体诊断到 stdout
拖 header 分隔线调整列宽

8. 与其它功能的挂钩

  • Address list → Memory View:主窗口地址行右键 “Browse memory region” (main_window.py:1380 / 1607)调 _open_memory_view(addr)main_window.py:2156)。
  • 扫描结果 → Memory View:扫描结果表右键 “Browse this address” (main_window.py:1379-1380)同样 _open_memory_view(addr)
  • Hex view → HW/PTE 断点:Data Breakpoint 菜单发 bp_requested, 被 MemoryViewWindow._on_bp_requested 接住打开 HwbpWindow,同时支持 w / rw / x 三种类型。
  • Disasm view → 指令解释insn_selected 在 hint bar 显示 helper 文本,中英文根据 i18n.get_lang() 切换。
  • 符号解析resolve_fn 依赖主窗口 module_listmemory_view.py:112), 所以 Memory View 打开时会先拉一次 module 清单。

相关交叉阅读:

Last updated on