REA Hopper 桥接层:完整解析冷启动与缓存失效
【免费下载链接】reaReverse engineer anything with agents, from app behavior down to native binaries.项目地址: https://gitcode.com/GitHub_Trending/rea2/rea
第一条请求发出后,3 秒内没有任何窗口弹出——但 REA 的 Hopper 桥接其实已在后台把分析器悄悄拉起来了,串行队列也已就位,等着接收下一个操作。
你以为启动是隐藏的,其实不是 🪤
你以为open --hide --background就能让 Hopper 完全隐藏启动,实际上做不到。Hopper 官方的hopper命令行助手内部会发一条 AppleScriptactivate,所以窗口仍可能被拉回前台。REA 在类注释里把这事挑明了:不替换官方 launcher 就无法可靠地抑制这次激活,调用方应把它当作上游约束,而不是分析失败(BridgeLauncher.ts)。
这里还有两道硬边界。平台只放行darwin与linux,其他系统直接返回unsupported_host(HopperProvider.ts)。macOS 上保存的 Hopper database 目标被直接拒绝——launcher 打开.db时没法同时跑桥接脚本(HopperProvider.ts);DOS MZ/COM 则被指去 Ghidra 的 16 位适配器(HopperProvider.ts)。
从进程拉起走到健康握手要几步 🤝
你观察到的是一段静默等待。代码实际做的分四步(BridgeLauncher.ts):
- 用
ps -ax -o command=探测 Hopper 可执行文件是否已在运行,是则跳过冷启动(BridgeLauncher.ts); - 冷启动时执行
/usr/bin/open --hide --background -a <Hopper.app>; - 固定等待常量
HOPPER_BACKGROUND_STARTUP_MS = 5_000,给应用留足加载时间(BridgeLauncher.ts); - 进入握手:HopperClient 创建
/tmp/rea-*私有目录,生成randomBytes(32)的十六进制令牌。
为什么这么设计?冷启动昂贵且有状态,先探测再拉起能避免重复唤醒同一个应用。握手本身是"认证注入":launcher 把令牌写进0o600权限的bootstrap.py(BridgeLauncher.ts),Hopper 以-Y bootstrap.py加载目标,脚本 exec 注入到 Hopper 进程内的hopper_bridge.py,后者监听同目录下的bridge.sockUnix 套接字。
第一条业务请求是health,超时取整个启动截止时间的剩余毫秒数(HopperClient.ts)。收到正常回应,会话才算建立。
请求在 FIFO 队列里到底怎么排 🔄
底层限制是:Hopper 内嵌的 Python 脚本 API 单线程串行,同一时刻只能处理一个请求。REA 不去并发它,而是把它显式建模成队列,类注释写得很直白:FIFO that keeps the wire serialized until Hopper actually replies(HopperRequestQueue.ts)。
类比来说,这就像只有一个柜台的银行:对应到代码里就是#drain()只有在#active为空时才从队列取下一条(HopperRequestQueue.ts)。
排队时的反馈是可感知的:后到的请求会收到queued behind N Hopper request(s)(HopperRequestQueue.ts);进入执行后每 1 秒播报一次is still running in Hopper (X ms elapsed)的心跳(HopperRequestQueue.ts)。线路上每一行 JSON 形如{ id, token, method, params },id自增,响应按 id 精确关联(HopperClient.ts)。
取消只取消自己:排队中的条目被摘除,进行中的请求仅在调用侧标记cancelled,不打断 Hopper 正在执行的操作。上层可调用activity()拿实时快照——当前 id、操作名、已耗时、排队数(HopperRequestQueue.ts)。
昂贵的 decompile 为什么只算一次 ⚡
贵在哪:procedure.decompile()是 Hopper 中最昂贵的操作之一,而上层对同一函数常做无数次有界投影(取签名、取局部变量、取调用者)。桥接脚本用两张缓存摊平它(hopper_bridge.py):
- 伪代码缓存
_pseudocode_cache,键为(id(document), procedure.getEntryPoint()),同一函数无论查多少次,decompile()只执行一次; - 搜索清单缓存
_search_inventory_cache,键为(id(document), kind),存的是不可变、按地址排序的元组快照(hopper_bridge.py)。
缓存键为什么带文档 id:同一个进程里可能并存多个文档,(id, kind)才能保证互不串键。
失效策略是文档粒度的:任何分析标注变更都触发_invalidate_pseudocode与_invalidate_search_inventory,按id(document)前缀批量删除(hopper_bridge.py)。一个容易被忽略的点:set_address_name等写操作把失效放在finally里,即使写入半途失败,旧缓存也一定会被清掉(hopper_bridge.py)。失效为什么是可信底线:若不失效,重命名之后搜到的还是旧名字,反编译出的伪代码也不反映新标注——对一个以证据可追溯为卖点的平台,过期缓存比没有缓存更糟。
谁拥有哪个进程,关什么不关什么 🛡️
所有权划得很清:REA 只拥有短命 launcher 的进程组,不声称拥有 Hopper GUI 主进程,关闭时也只终止明确标记为 owned 的启动进程(HopperClient.ts)。归属凭据写在ownership.json里,记录run_id、进程组 id、父 pid,权限0o600(HopperClient.ts)。
前三节的东西在这里汇合:握手阶段的私有目录隔离了套接字与 bootstrap,排队阶段每条请求强制携带的令牌在桥接端用hmac.compare_digest比对,缺令牌直接拒收(hopper_bridge.py)。并发冲突由目标租约兜底:acquireHopperTargetLease 防止两个会话同时打开同一个二进制,先失败的一方拿到"去关闭那个会话"的明确指引。
收尾走cleanupHopperSession(HopperCleanup.ts):先落定在途请求,再删除会话产物,全程受HOPPER_SHUTDOWN_TIMEOUT_MS = 30_000约束(HopperSessionValues.ts)。关什么:launcher 进程组、私有目录、在途请求。不关什么:macOS LaunchServices 管理的 Hopper 应用本体。
这套机制把一个交互式 GUI 反汇编器,驯化成了可被 Agent 稳定驱动的分析后端。详见docs/native-investigation.md,实现集中在src/hopper/。
【免费下载链接】reaReverse engineer anything with agents, from app behavior down to native binaries.项目地址: https://gitcode.com/GitHub_Trending/rea2/rea
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考