想象你是一位新调任的城市侦探,面前是一座古老又陌生的街区。你想知道:这里的道路怎么走?哪些店铺在偷偷改招牌?有没有人在地下挖通道?顶级的刑侦局(IDA)会动用测绘卫星、无人机和整支工程队,但有时候你只是想在晚饭前先摸个底——这时候,你需要的是一本速写本、一支折叠放大镜,和一个熟悉本地路况的向导。 Hopper 就是这样一个“街区速写侦探”。它是一款面向 macOS 与 Linux 的轻量反汇编工具,能把可执行文件拆成一砖一瓦(汇编指令),画出道路走向(控制流图),再用大白话翻译一遍(伪代码视图)。更妙的是,它对 Objective-C 和 Swift 这两套“本地招牌系统”做过专门优化,能一眼认出连锁店的标志。对于大多数 macOS 应用的分析和漏洞验证任务,它启动更快、上手更轻松,是 IDA 之外恰到好处的第二把钥匙。

岗位关联

掌握 Hopper,意味着你拿到任何 macOS 二进制时都能迅速建立“结构直觉”——从控制流图到伪代码、从 ObjC 消息链到 Swift 符号还原,既能独立完成轻量分析,也能为 IDA 深挖提供精准预判,是安全研究与漏洞验证工作中性价比极高的基础能力。

目录
  1. 可视化演示
  2. 轻量工具定位:口袋里的速写本
  3. 反汇编引擎:读懂砖石的纹理
  4. 控制流图:程序的地铁图
  5. 伪代码视图:导游的大白话
  6. ObjC识别:认出连锁店招牌
  7. Swift优化:新区的门牌系统
  8. 逆向实践:老街区的治安巡逻
  9. 工具对比:速写本与测绘局
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示Hopper控制流图(CFG)基本块与跳转边、汇编到伪代码自动生成与二进制打补丁(jne→jmp)功能,帮助理解macOS逆向分析工具链。

本节配有交互动效(进入视口后自动加载,约 3KB)。

'+captions.cfg+'

轻量工具定位:口袋里的速写本

老侦探手上有两件宝贝:测绘局的全套仪器(IDA),和一本缝在风衣内袋里的速写本(Hopper)。大多数时候,他先掏出速写本,三五分钟就把街区轮廓画完——哪条是主路、哪栋楼可疑,心里立刻有数。只有需要深挖管道、验指纹的时候,才会请测绘局出动。

Hopper Disassembler 就是这样一款面向 macOS 与 Linux 的图形化反汇编工具。它的设计哲学和那本速写本一样:轻、快、够用。启动一个常见应用通常只需几秒,界面清爽,加载一个 Mach-O 文件后立即给出函数列表、字符串引用和导入符号,不需要像 IDA 那样等待漫长的自动分析。

从架构上看,Hopper 的能力可以拆成三层:最底层是反汇编引擎,负责把机器码解码成汇编指令;中间是分析视图,包括控制流图、调用图和伪代码;最上层是语言识别层,专门针对 Objective-C 和 Swift 的运行时特征做符号还原。这三层叠加,构成了一个“够用但不冗余”的分析闭环。

所以当你听说“Hopper 是 IDA 的替代品”时,不妨反过来想:速写本从来不是测绘仪的替代品,它是那个让你先迈出第一步、在 90% 的日常任务里省下时间的伙伴。

Hopper=D⏟反汇编引擎+V⏟分析视图+R⏟语言识别层\displaystyle \text{Hopper} = \underbrace{D}_{\text{反汇编引擎}} + \underbrace{V}_{\text{分析视图}} + \underbrace{R}_{\text{语言识别层}}
这个公式说的是:Hopper 的全部能力可以看作三层堆叠——先有能把字节变成指令的引擎(D),再有让人看懂结构的视图(V),最后有能认出两种苹果系语言“招牌”的识别层(R)。
  1. Hopper(整体能力)指工具呈现给用户的所有功能之和——从加载二进制到给出可读分析结果。Hopper:工具全部能力Hopper: \text{工具全部能力}
  2. D(反汇编引擎)最底层地基,把机器码字节逐条解码成汇编指令,并建立地址与指令的索引,相当于把砖石从墙里抠出来编号。D:bytes→instructionsD: \text{bytes} \rightarrow \text{instructions}
  3. V(分析视图)在指令之上搭建的控制流图、调用图与伪代码视图,让原本线性的指令流变成看得懂的结构。V={CFG,调用图,伪代码}V = \{\text{CFG}, \text{调用图}, \text{伪代码}\}
  4. R(语言识别层)针对 Objective-C 与 Swift 的符号还原,把晦涩的运行时调用重新翻译成方法名,是 Hopper 的差异化优势。R:objc_msgSend→[Class method:]R: objc\_msgSend \rightarrow [Class\ method:]

反汇编引擎:读懂砖石的纹理

速写侦探画的不是街景,而是砖石本身——每一块砖的纹理、每一条裂缝。反汇编引擎要做的,就是把这些“砖”按顺序翻出来,并标出它们的作用。程序在磁盘上只是一堆字节,解释这些字节的钥匙,藏在 macOS 的“城市档案”里。

这个档案叫 Mach-O(Mach Object),是 macOS 与 iOS 可执行文件的标准容器。Hopper 打开一个二进制后,会先解析文件头(header),再遍历加载命令(load commands),定位到存放代码的 __TEXT 段和存放数据的 __DATA 段。这就好比先翻到城市登记册的目录页,找到“住宅区”和“仓库区”各在哪几页。

在 __TEXT 段内部,代码按节(section)组织——你甚至可以把它想成“街道网格”。Hopper 逐条解码指令:把十六进制字节翻译成汇编助记符,同时维护指令的虚拟地址与文件偏移的映射。它还自动识别函数边界:通过入口点、返回指令和跳转目标,把连续的指令流切成一个个逻辑单元,这样你才能看到一个一个“楼栋”,而不是一整面墙。

有了砖石的清单,速写侦探就能开始画地图了——接下来,控制流图会告诉他这些砖石之间如何连接成道路。

addr=seg_base+sec_offset+ins_offset\displaystyle \text{addr} = \text{seg\_base} + \text{sec\_offset} + \text{ins\_offset}
程序运行时,一条指令在内存中的位置等于“段基址 + 节偏移 + 指令偏移”这三段拼起来。Hopper 展示的每个地址,都是这样从文件里一步步算出来的。
  1. addr(指令虚拟地址)程序运行时,一条指令在内存中的最终位置,也是 Hopper 显示在地址栏里的数字。addr:运行时虚拟地址\text{addr}: \text{运行时虚拟地址}
  2. seg_base(段基址)Mach-O 文件加载时,段(如 __TEXT)被映射到的起始内存地址,相当于土地登记册上“某地块的起点坐标”。seg_base=段起始地址\text{seg\_base} = \text{段起始地址}
  3. sec_offset(节偏移)指令所在节(如 __text)相对于段起始位置的距离。sec_offset=节地址−段地址\text{sec\_offset} = \text{节地址} - \text{段地址}
  4. ins_offset(指令偏移)指令在节内部的相对位置,三者相加即为完整地址,缺一不可。ins_offset=指令地址−节地址\text{ins\_offset} = \text{指令地址} - \text{节地址}

控制流图:程序的地铁图

每座城市都有一张地铁图:线路是道路,换乘站是岔路口。控制流图(Control Flow Graph,CFG)就是程序的地铁图——它把函数内部所有可能的走向画出来,让你一眼看出哪条路会绕圈、哪条路是死胡同。

Hopper 先把函数拆成基本块:一段“没有岔路”的连续指令。基本块内部只能从头执行到尾,不存在任何跳转进出的中间点;基本块之间通过跳转指令相连,形成一张有向图。把一条没有岔口的长街当成一个街区,就是这个道理。

图中的节点是基本块,边是跳转关系。条件跳转产生两个后继(条件命中与不命中),无条件跳转产生一个后继,返回指令则把块变成“终点站”。当一条边从较后的块指回较前的块,就构成回边,程序里往往对应一个循环。Hopper 默认把回边标红,就像地铁图上把环线单独着色,让你立刻知道哪里在“绕圈”。

看 CFG 就像看地铁图:你不必背下每一站,只要知道有哪些环线、哪些支线,就能规划出任意一条从入口到出口的路线。这正是漏洞分析与逻辑还原时最需要的“全局视角”。

G=(V,E),V={B0,B1,…,Bn−1},E⊆V×V\displaystyle G = (V, E),\quad V = \{B_0, B_1, \ldots, B_{n-1}\},\quad E \subseteq V \times V
函数的所有路径都浓缩在这张图里:V 是“区块集合”,E 是“区块之间的箭头集合”。看懂这张图,就等于拿到了函数内部的地铁线路图。
  1. G(控制流图)函数内部所有可能执行路径的有向图表达,是 Hopper 最核心的分析视图之一。G:函数的有向图G: \text{函数的有向图}
  2. V(节点集合)每个节点是一个基本块——没有岔路的连续指令段,图中用圆圈表示。V={B0,B1,…,Bn−1}V = \{B_0, B_1, \ldots, B_{n-1}\}
  3. E(边集合)边表示一个块末尾跳转到另一个块开头,它是节点集合的笛卡尔积子集。E⊆V×VE \subseteq V \times V
  4. 回边(back edge)当边从后访问的节点指回先访问的祖先节点时,标记为回边,程序在这里形成循环。回边:Bj→Bi,j 后访问, i 先访问\text{回边}: B_j \rightarrow B_i, \quad j \text{ 后访问}, \ i \text{ 先访问}

伪代码视图:导游的大白话

古老街区里的路牌都是几百年前的文言文。游客看不懂,于是侦探随身带了一位“翻译导游”——他指着路牌,用大白话告诉你:“这块牌子的意思是前方修路、请绕行。”Hopper 的伪代码视图就是这位导游。

Hopper 的伪代码视图(Pseudocode View)由内置的反编译器生成:把汇编指令翻译成类似 C 的高级语言。它重建局部变量、识别循环与分支,把 push/pop 的栈操作折叠成清晰的赋值语句,把 jne 还原成 if。一个原本要读半天的指令序列,瞬间变成能快速扫读的逻辑块。

但要注意,伪代码并不等于原始源码。反编译器根据数据流和类型推断做“合理猜测”:变量名(如 var_8)是机器生成的,某些复杂调用会退化成宏,甚至个别边界情况会翻译错误。因此分析关键路径时,要双击伪代码行跳回对应的汇编指令,逐条对照——导游的大白话能让你听懂故事,但写进案卷的细节,还得靠原文。

有了导游的大白话,再配合原文对照,你就能既快又准地理解一段复杂逻辑——这就是 Hopper 让小白也能上手逆向的原因。

P=Φ(A),sem(P)≡sem(A)\displaystyle P = \Phi(A),\quad \text{sem}(P) \equiv \text{sem}(A)
伪代码是反编译器对汇编做变换(Φ)后的结果,二者语义等价但形式不同。等价性是“可以信”的根据,形式不同是“不能全信”的原因。
  1. A(汇编输入)反汇编得到的指令序列,是“文言文原文”,也是唯一绝对可靠的信息源。A:汇编指令序列A: \text{汇编指令序列}
  2. Φ(反编译变换)Hopper 内置的反编译器,把栈操作、跳转与寄存器使用折叠成表达式与语句。Φ:insns→statements\Phi: \text{insns} \rightarrow \text{statements}
  3. P(伪代码输出)生成的类 C 代码,变量名与类型是推断结果,相当于导游的转述稿。P:可读的高级语言P: \text{可读的高级语言}
  4. 语义等价变换前后程序行为一致,但形式不同——这是伪代码可信度的来源,也是它的边界。sem(P)≡sem(A)\text{sem}(P) \equiv \text{sem}(A)

ObjC识别:认出连锁店招牌

老城区里的大品牌都爱用统一招牌:金底黑字加一个圆标。你只要见过一次,走遍全城都能认出来。Objective-C 的方法调用也是这套“连锁模式”——而 Hopper 专门训练了识别这套招牌的眼力。

Objective-C 的方法调用在编译后并不直接跳转到函数地址,而是统一变成对 objc_msgSend 的调用:把对象、选择器(selector)和参数打包发送,由运行时动态查找实现。对逆向者来说,满屏的 objc_msgSend 就像整条街都是“连锁店”,根本分不清谁在调用谁——这正是 Hopper 要解决的问题。

Hopper 的 ObjC 解析器会扫描 __objc_selrefs、__objc_classrefs 等节,提取类名、方法名与选择器,然后在反汇编视图里把裸的 objc_msgSend 标注成 [ClassName methodName:] 的形式。这使得消息链的追踪变得直观:调用从哪里发起、selector 是什么、目标类是谁,一目了然。你不再需要去翻运行时文档猜“这个参数是干嘛的”。

有了这套“招牌识别系统”,侦探再也不会把便利店错认成银行——每个 objc_msgSend 都露出了自己的品牌名,消息链的追踪也从此变得轻松。

[obj setTitle:x]→编译objc_msgSend(obj, @selector(setTitle:))\displaystyle [obj\ setTitle:x] \xrightarrow{\text{编译}} objc\_msgSend(obj,\ @selector(setTitle:))
源码里写的是 [obj setTitle:x],编译后变成对 objc_msgSend 的统一调用;Hopper 的识别层做的正是反向还原——把连锁店招牌恢复成品牌名。
  1. [obj setTitlex](消息表达式):源码中向 obj 发送 setTitle: 消息、附带参数 x 的写法,是 ObjC 的“店名”。消息: [receiver selector:args]\text{消息: } [receiver\ selector: args]
  2. 编译箭头Objective-C 编译器把这条消息翻译成统一的派发调用,而不是直接跳转到方法地址。编译降级:消息→函数调用\text{编译降级}: \text{消息} \rightarrow \text{函数调用}
  3. objc_msgSend(派发函数)运行时根据对象的 isa 指针与选择器动态查找实现并调用,像总机帮你转接分机。objc_msgSend(obj,sel,args)objc\_msgSend(obj, sel, args)
  4. @selector(setTitle)(选择器):选择器是编译期生成的 SEL 常量,表示消息的名字,本身不含参数。SEL:消息的名字牌\text{SEL}: \text{消息的名字牌}

Swift优化:新区的门牌系统

新区不像老城区那样招牌统一。每栋楼都有自己的门牌系统,而且门牌里藏着楼层、户型、朝向等信息。Swift 的符号修饰(mangling)就是这套门牌系统——Hopper 要做的,是帮你把门牌翻译回“3 栋 2 单元 501”这样的可读地址。

Swift 编译器在生成符号时,会把模块名、类型名、方法名、参数类型、返回类型全部编码进一个“被修饰”的符号里,以避免重载冲突。直接看会是一串像乱码的名字。Hopper 实现了对 Swift mangling 规则的识别,能把 $s1A1AC3fooyyF 这样的符号还原成可读的函数签名。

相比 ObjC,Swift 没有统一的运行时消息枢纽(没有 objc_msgSend),方法调用多是直接跳转或通过 witness table 间接派发。Hopper 的应对策略是:解析 mangled 符号恢复函数名,并结合 Swift 运行时结构(如类型元数据、协议见证表)判断调用关系。需要提醒的是,泛型特化和闭包捕获会在分析中丢失部分信息,这是 Swift 逆向的固有难点——门牌系统虽复杂,但读懂了规则,新区就没有迷路的理由。

$s1A1AC3fooyyF⟶A.foo():()→()\displaystyle \text{\$s}1\text{A}1\text{A}\text{C}3\text{foo}yyF \longrightarrow \text{A.foo()}: () \rightarrow ()
左边是 Swift 编译器生成的门牌号,右边是 Hopper 翻译后的可读地址。中间的每个片段都携带一段信息:模块名、类名、方法名、参数与返回类型。
  1. $s(Swift 前缀)标识这是一个 Swift 修饰符号,让链接器知道它属于 Swift,而不是 C 或 ObjC 符号。前缀 $s\text{前缀 } \$s
  2. 1A(模块名)长度前缀 1 表示名字长度为 1 个字母,模块名是 A。1A:长度前缀 + 名字1A: \text{长度前缀 + 名字}
  3. 1AC3foo(类型与方法)第二个 1A 是类名 A,C 是类型与方法的连接符,3foo 表示方法名为 foo。类型 C 方法\text{类型 } C \text{ 方法}
  4. yyF(参数与返回)y 是 Void 的简写;yyF 表示无参数、无返回值,且这是一个函数。yyF:(Void)→VoidyyF: (\text{Void}) \rightarrow \text{Void}
  5. 还原结果Hopper 综合以上信息得到可读签名——A.foo() -> (),即“类 A 的无参无返回方法 foo”。A.foo():()→()A.foo(): () \rightarrow ()

逆向实践:老街区的治安巡逻

速写侦探的本职不是画地图,而是破案。老街区最近有人报告“地下室漏水”——在逆向世界里,这可能是某个 API 越界、某个校验被绕过。你需要沿管道回溯,找到漏水点。

在 macOS 应用分析中,常见任务包括:还原授权校验逻辑、分析可疑行为、定位崩溃点。Hopper 的 CFG 让你快速锁定“决定性分支”(比如许可证检查的分叉处),伪代码视图让你理解判断条件,交叉引用(Xrefs)帮你找到谁在调用关键函数。这三板斧组合起来,一个陌生应用的核心脉络很快就能摸清。

漏洞验证的基本流程是“输入构造 → 触发执行 → 回溯调用链 → 确认漏洞点”。例如处理一个畸形输入导致崩溃的样本:先用 Hopper 打开目标,翻到崩溃地址所在的函数;再向上回溯调用它的函数,确认输入数据来源是否可控;最后用调试器(如 LLDB)复现,验证漏洞是否真的可以被利用。整个过程里,Hopper 承担“静态地图”的角色,调试器负责“动态巡逻”。

静态地图加动态巡逻,漏水点从“怀疑”变成“实锤”——这正是 Hopper 在漏洞验证场景中的价值所在。

输入→构造触发→回溯漏洞点→验证结论\displaystyle \text{输入} \xrightarrow{\text{构造}} \text{触发} \xrightarrow{\text{回溯}} \text{漏洞点} \xrightarrow{\text{验证}} \text{结论}
这条流水线就是日常漏洞验证的浓缩版:一个可疑输入经过构造、触发、回溯三步,最终得到“是否可利用”的结论。每一步都离不开 CFG 和伪代码视图的指引。
  1. 输入(Input)分析入口,通常是一段畸形数据或特定操作序列,它决定了后续的触发路径。输入:触发条件的源头\text{输入}: \text{触发条件的源头}
  2. 构造箭头与触发通过构造输入执行到目标函数,观察崩溃地址或异常行为,拿到现场证据。crash_addr:崩溃时的 PC\text{crash\_addr}: \text{崩溃时的 PC}
  3. 回溯(Xrefs)利用 Hopper 的交叉引用沿调用链向上找,确认数据从哪里来、是否可控。callers(f)→⋯→输入\text{callers}(f) \rightarrow \cdots \rightarrow \text{输入}
  4. 验证与结论结合调试器复现,确认漏洞影响范围并给出修复建议,形成“可利用/不可利用”的判断。结论:可利用 / 不可利用\text{结论}: \text{可利用 / 不可利用}

工具对比:速写本与测绘局

案子越大,测绘局的价值越明显;案子越日常,速写本的效率越香。选工具不是选“最好”,而是选“最合适”。IDA Pro 是行业标杆:支持几十种架构、插件生态庞大、反编译质量高,但重量级、授权昂贵、学习曲线陡。Hopper 则聚焦 macOS/Linux 生态,界面现代、启动快、授权成本低。

对比维度主要有四个。架构支持广度上,IDA 完胜;易用性上,Hopper 对新手更友好,尤其是 macOS 应用的 ObjC/Swift 识别,有时比 IDA 更顺手;脚本扩展方面,IDA 有 IDAPython,Hopper 也提供 Python 脚本与 SDK,但生态规模小一些;价格上,Hopper 则明显亲民。

实践中常见的做法是“分工协作”:用 Hopper 完成初步分流——快速判断目标是否有分析价值、漏洞点大概在哪;遇到疑难架构、复杂混淆或需要高精度反编译时,再切到 IDA 深度处理。二者可以配合使用:Hopper 导出分析结果,IDA 做深挖。好侦探的背包里既有速写本,也有测绘局的电话——关键时候知道找谁,比什么都重要。

工具∗=arg⁡max⁡t∈{Hopper,IDA}(效率(t)+精度(t)−成本(t))\displaystyle \text{工具}^* = \arg\max_{t \in \{\text{Hopper}, \text{IDA}\}} \big( \text{效率}(t) + \text{精度}(t) - \text{成本}(t) \big)
选工具就是在两个候选中做加法减法:把效率、精度相加,减去成本(价格与学习时间),取分数最高的那个。按场景选,而不是按名气选。
  1. 工具*最终选定的工具,可以是两者中的任一个,且因任务不同而不同。工具∗:最优选择\text{工具}^*: \text{最优选择}
  2. t ∈ {Hopper, IDA}候选工具集合,代表两条不同的分析路线——速写本与测绘局。候选:{Hopper,IDA}\text{候选}: \{\text{Hopper}, \text{IDA}\}
  3. 效率(t) 与 精度(t)完成任务的便捷程度与结果准确度,是分析中的收益项。效率+精度:收益\text{效率} + \text{精度}: \text{收益}
  4. 成本(t)授权价格加学习曲线,是决策中的扣分项。成本:价格 + 学习投入\text{成本}: \text{价格 + 学习投入}
  5. argmax在所有候选中挑出“收益减成本”最大者,即按场景做最优决策。arg⁡max⁡:选择分值最大的工具\arg\max: \text{选择分值最大的工具}

AI 应用场景

macOS 应用授权校验逻辑还原

阶段:静态分析(逻辑还原)

解决的问题:传统方式靠猜测和反复打补丁实验,效率极低且容易误判。用 Hopper 的控制流图快速定位授权校验的“决定性分支”,再用伪代码视图理解判断条件,几分钟就能还原出校验逻辑的全貌。

具体案例:某测试工程师分析自己已购买但授权即将过期的软件。他用 Hopper 打开主二进制,搜索 “license“ 相关字符串,交叉引用到校验函数,在 CFG 中一眼看到”过期即跳失败分支”的分叉点,用伪代码确认了条件表达式,随后定位到可调用的续期入口。

漏洞验证中的崩溃点回溯

阶段:静态定位 + 动态验证

解决的问题:拿到崩溃样本后,盲目用调试器复现耗时且容易迷失。先用 Hopper 在崩溃地址处做静态定位,再向上回溯调用链确认输入可控性,可以大幅缩小排查范围。

具体案例:安全研究员收到一个图像解析器的崩溃样本。他用 Hopper 翻到崩溃地址所在函数,沿交叉引用向上找到读取文件数据的入口函数,确认畸形文件头字段直接影响了分配大小,再结合 LLDB 动态复现,最终确认是一个堆溢出漏洞。

恶意样本行为初筛

阶段:样本分诊(Triage)

解决的问题:应急响应时样本量大、时间紧,不可能每个都上 IDA 精读。用 Hopper 快速检查导入表、字符串和主函数 CFG,可以高效完成“有没有反调试、有没有混淆、连不连 C2”的初判。

具体案例:某安全运营中心收到一个可疑 .app 文件。分析师先用 Hopper 查看其主函数 CFG 和 __DATA 段字符串,发现硬编码的 C2 地址与反调试调用,随即判定为恶意样本并进入深挖流程。整个初筛不到十五分钟。

Python 演示

python106 行
# -*- coding: utf-8 -*-"""mini_hopper.py: 用 Python 模拟 Hopper 的基本块划分与控制流图构建 输入: 带地址、操作码、跳转目标的指令三元组列表输出: 基本块列表、控制流边、以及回边(循环)检测结果"""  def build_basic_blocks(instructions):    """根据跳转目标, 把指令序列切成只有一个入口的基本块"""    targets = set()    for _, op, target in instructions:        if op.startswith('j') and target is not None:            targets.add(target)    blocks = []    cur = []    start = None    for addr, op, target in instructions:        if addr in targets and cur:            blocks.append((start, cur))            cur = []            start = None        if start is None:            start = addr        cur.append((addr, op, target))        if op.startswith('j') or op == 'ret':            blocks.append((start, cur))            cur = []            start = None    if cur:        blocks.append((start, cur))    return blocks  def build_edges(blocks):    """为每个基本块计算后继块, 形成有向边"""    index = {blocks[i][0]: i for i in range(len(blocks))}    edges = set()    for i, (start, body) in enumerate(blocks):        last_op = body[-1][1]        last_tgt = body[-1][2]        if last_op == 'ret':            continue        if last_op == 'jmp':            edges.add((i, index[last_tgt]))        elif last_op.startswith('j'):            edges.add((i, index[last_tgt]))            if i + 1 < len(blocks):                edges.add((i, i + 1))        elif i + 1 < len(blocks):            edges.add((i, i + 1))    return edges  def find_back_edges(edges, num_blocks):    """DFS 找回边: 指向当前 DFS 栈内祖先的边即为循环"""    graph = {}    for u, v in sorted(edges):        graph.setdefault(u, []).append(v)    visited = [False] * num_blocks    in_stack = [False] * num_blocks    back_edges = []     def dfs(u):        visited[u] = True        in_stack[u] = True        for v in graph.get(u, []):            if not visited[v]:                dfs(v)            elif in_stack[v]:                back_edges.append((u, v))        in_stack[u] = False     for i in range(num_blocks):        if not visited[i]:            dfs(i)    return back_edges  def main():    # 一个简单的循环函数: eax 从 0 加到 10    instrs = [        (0, 'mov', None),   # eax = 0        (1, 'cmp', None),   # eax 与 10 比较        (2, 'jge', 6),      # 若 eax >= 10 跳到结束        (3, 'inc', None),   # eax += 1        (4, 'jmp', 1),      # 回到比较处(回边)        (5, 'nop', None),   # 不可达的死代码        (6, 'ret', None),   # 返回    ]    blocks = build_basic_blocks(instrs)    edges = build_edges(blocks)    back_edges = find_back_edges(edges, len(blocks))    print('基本块:')    for i, (start, body) in enumerate(blocks):        print('  B%d: 入口@%d, 指令数=%d' % (i, start, len(body)))    print('控制流边:')    for u, v in sorted(edges):        print('  B%d -> B%d' % (u, v))    print('回边(循环):')    for u, v in back_edges:        print('  B%d -> B%d' % (u, v))  if __name__ == '__main__':    main()
目的:用纯 Python 复刻 Hopper 最核心的两步——把指令流切成基本块、构建控制流图并检测回边。跑一遍就能直观理解“基本块”和“循环”在代码里到底长什么样。
  1. targets = set()` 和 `targets.add(target)先收集所有跳转目标地址,它们是新基本块的入口,就像地图上标注的岔路口。
  2. if addr in targets and cur:遇到标签地址时,把已积累的指令“结账”成一个基本块,然后重新开一个新块。
  3. if op.startswith('j') or op == 'ret':基本块的结束条件:跳转或返回指令之后不再有“顺路直行”。
  4. last_op = body[-1][1]每个块的“结局”由最后一条指令决定,Hopper 也是靠这一条来画后继边。
  5. edges.add((i, index[last_tgt]))把跳转目标地址翻译成块编号,形成一条有向边。
  6. elif last_op.startswith('j'):条件跳转产生两条边:命中目标,以及不命中时顺序落入下一块。
  7. elif in_stack[v]: back_edges.append((u, v))若邻居仍在 DFS 栈上,说明存在回路,这条边就是回边,对应程序里的循环。

常见误区

误区我打开 Hopper 的伪代码视图,以为看到了开发者写的源码,直接照着改逻辑就可以了。

正解伪代码是反编译器“翻译导游”的大白话:语义等价,但变量名、类型是推断出来的,循环可能被改写成 goto 形式,复杂调用会被简化成宏。它应该被当作“高质量注释”而非源码。关键判断必须回到汇编逐条对照——就像导游说“前面左转”,你得自己看路牌确认方向。

为何常见:伪代码长得太像 C 代码,初学者很容易忘记它本质上是重建产物;加上 Hopper 的伪代码可读性确实好,更容易产生“这就是源码”的错觉。

误区我以为装了 Hopper,Windows PE、ARM 固件、MIPS 路由器都能像 IDA 一样随便分析。

正解Hopper 的舒适区是 macOS/Linux 下的 Mach-O(含 iOS)二进制,对 Windows PE 和嵌入式架构的支持有限。选工具要按街区类型来:老城区速写本够用,工业区就得请测绘局。动手前先确认文件格式是否在工具支持范围内。

为何常见:反汇编工具界面长得都差不多,宣传材料又都突出优点,新手很容易忽略架构支持范围这种硬边界。

误区我看见伪代码里有一个 `jne` 跳过注册校验,心想:“把这个 `jne` 改成 `jmp` 不就破解了?”

正解现代 macOS 应用通常有多层防线:代码签名校验、加密字符串、反调试、服务器端校验。单点 patch 可能让应用直接崩溃或被检测到篡改。正确做法是用 CFG 完整梳理校验网络,用 Hopper 定位所有相关分支,再配合动态调试验证每一处修改。

为何常见:网上教程常拿“改一个跳转”举例,给人造成“绕过=单点”的印象,忽略了真实软件工程的防御纵深。

误区我以为 CFG 里从上到下、从左到右排列的块就是程序真实的执行顺序,顺着读就能看懂逻辑。

正解CFG 的节点布局是 Hopper 为了可读性重排的。真实的执行顺序必须沿边追踪:条件跳转的“命中/不命中”、循环回边、间接跳转都会打乱直觉上的顺序。看 CFG 要像看地铁图——站点画在哪里不重要,线路连接才是关键。

为何常见:人脑天然按阅读顺序理解图形,而图布局算法优先考虑减少线条交叉,而不是还原时间顺序,二者经常冲突。

面试题

Q1. 作为反汇编工具,Hopper 最突出的定位特点是?

单选题 · easy

  • A.面向 macOS 与 Linux 的轻量工具← 正确答案
  • B.面向 Windows 平台的重量级平台
  • C.面向嵌入式固件的专用分析器
  • D.面向数据库的通用查询优化器

答案:A。Hopper 的定位就是 macOS 与 Linux 上的轻量反汇编工具,像一本速写本——启动快、界面清爽,是 IDA 的补充。B 说反了平台,C 和 D 描述的根本不是反汇编工具,属于混淆项。

Q2. macOS 可执行文件所使用的标准容器格式是?

单选题 · easy

  • A.PE 可移植可执行格式
  • B.ELF 可执行与链接格式
  • C.Mach-O 对象文件格式← 正确答案
  • D.COFF 通用对象文件格式

答案:C。Mach-O 是 macOS/iOS 的“城市土地登记册”,Hopper 解析它就是靠读这份档案。PE 属于 Windows,ELF 是 Linux 通用格式,COFF 是早期 Unix 的遗留格式,都不是 macOS 的标准容器。

Q3. 控制流图中的回边(back edge)通常对应哪种程序结构?

单选题 · mid

  • A.顺序执行的平坦代码段
  • B.循环体与循环条件的跳回← 正确答案
  • C.函数之间的递归调用层级
  • D.多线程之间的互斥同步原语

答案:B。回边指从后访问的基本块跳回先访问的块,DFS 里就是指向栈中祖先的边,程序上对应循环——就像地铁环线。顺序执行不产生回边;递归属于调用图,互斥同步是运行时概念,都不在 CFG 回边的定义范围内。

Q4. Objective-C 的 [obj setTitle:x] 编译后调用的是哪个统一派发函数?

单选题 · mid

  • A.发送消息给对象并附带选择器← 正确答案
  • B.对精确地址执行间接跳转
  • C.从函数表偏移量取出入口
  • D.将代码展开成逐条赋值语句

答案:A。ObjC 方法调用编译后统一变成 objc_msgSend(obj, @selector(setTitle:), x),由运行时动态派发,像总机转接分机。B 是直接跳转,C 是虚函数表,D 是内联展开——这些都不是 ObjC 的运行时模型。

Q5. 基本块(basic block)最准确的定义是?

单选题 · easy

  • A.单一入口且单一出口的指令序列← 正确答案
  • B.包含多个调用的完整函数体
  • C.从程序入口到出口的全部指令
  • D.编译器循环展开后的代码片段

答案:A。基本块是中间没有任何跳转进出的连续指令段,只有一个入口一个出口,就像没有岔路的长街。B 是函数,C 是整段程序路径,D 是编译器优化产物,都不符合基本块的定义。

Q6. 关于 Hopper 的伪代码视图,下列说法正确的是?

单选题 · hard

  • A.是反编译器重建的语义等价代码← 正确答案
  • B.是调试信息还原的原始源码
  • C.是链接器导出的中间表示文件
  • D.是运行时自动解密后的明文数据

答案:A。伪代码是反编译器 Φ 变换的输出,语义与汇编等价但形式不同,变量名是猜测结果,要当“高质量注释”用。B 混淆了调试符号与反编译,C 不是反汇编产物,D 更是运行时概念,均不成立。

Q7. Swift 符号修饰(mangling)的核心作用是?

单选题 · hard

  • A.在符号名中编码类型与参数信息← 正确答案
  • B.对可执行代码进行运行时加密
  • C.将 Swift 代码翻译为 Objective-C
  • D.删除函数名以阻止逆向工程

答案:A。Swift 把模块名、类型、参数、返回类型都编码进符号(如 $s1A1AC3fooyyF),本质是为了解决重载冲突,像门牌编码。它不做加密、不翻译语言、也不以对抗逆向为目的——虽然客观上增加了阅读难度。

Q8. Hopper 针对 Objective-C 的识别优化主要体现在哪些方面?

多选题 · mid

  • A.自动解析选择器并标注消息调用← 正确答案
  • B.还原类与方法的映射关系表← 正确答案
  • C.追踪完整消息发送调用链← 正确答案
  • D.启动虚拟机动态调试运行

答案:ABC。Hopper 的 ObjC 优化是静态层面的“招牌识别”:解析 @selector、还原类方法映射、让 objc_msgSend 链可追踪。D 是动态调试手段,不属于静态识别优化,且 Hopper 本身不提供完整的虚拟机调试能力。

Q9. 相比 IDA Pro,Hopper 在 macOS 生态中的显著优势通常包括?

多选题 · mid

  • A.启动速度更快且界面更轻← 正确答案
  • B.授权价格相对更加亲民← 正确答案
  • C.对 macOS 生态针对性更强← 正确答案
  • D.内置了完整的图形化调试器

答案:ABC。Hopper 的优势是速写本式的轻快、平价和对 Mach-O/ObjC/Swift 的针对性优化。IDA 强在架构广度和插件生态;而“内置完整图形化调试器”并非 Hopper 的公认优势,调试通常搭配外部 LLDB 完成。

Q10. 在 macOS 应用漏洞验证中,Hopper 的典型用法包括哪些?

多选题 · hard

  • A.分析控制流图定位校验分支← 正确答案
  • B.回溯调用链确认输入路径← 正确答案
  • C.对比补丁前后的汇编差异← 正确答案
  • D.自动生成正式的漏洞利用代码

答案:ABC。漏洞验证中 Hopper 承担“静态地图”角色:用 CFG 找关键分叉、用 Xrefs 回溯输入路径、用不同版本二进制对比补丁差异。自动生成利用代码需要人工分析加额外工具链,Hopper 并不负责这一步。