<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>炸酱面</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://www.zhajiangmian.top/</id>
  <link href="https://www.zhajiangmian.top/" rel="alternate"/>
  <link href="https://www.zhajiangmian.top/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, 炸酱面</rights>
  <subtitle>Take Your Heart</subtitle>
  <title>Game Life</title>
  <updated>2026-10-30T10:17:05.733Z</updated>
  <entry>
    <author>
      <name>炸酱面</name>
    </author>
    <category term="AI-for-security" scheme="https://www.zhajiangmian.top/categories/AI-for-security/"/>
    <category term="agent自动化渗透测试" scheme="https://www.zhajiangmian.top/tags/agent%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/"/>
    <content>
      <![CDATA[<h1 id="ATK-系统架构与开发文档"><a href="#ATK-系统架构与开发文档" class="headerlink" title="ATK 系统架构与开发文档"></a>ATK 系统架构与开发文档</h1><blockquote><p>第二届腾讯云黑客松 · 智能渗透挑战赛<br>主语言: Go 1.25 · 依赖: AstrBot + Kali 执行环境</p></blockquote><h2 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h2><p>先总结一下，我这边比赛用minimax＋gpt-5.4，个人预算严重不足就没用claude了，这一套搭配开局简单题目和赛前测试题目基本一分钟不到就解出了，后面就卡住了，再后面由于minimax tokenplan的额度限制以及一些原因就没接着打了。整场比赛看下来其实最大的决定因素就是模型能力，也就是说如果用cluade opus搭配网上的各种开源项目，其实能取得的成绩也是不错的。</p><span id="more"></span><p>在架构设计上，通过subagent来实现各类角色已经是既定的主流方案，在长任务场景下，几乎所有的架构都需要一个监督者的角色，这个角色作为独立的第三方来监督其他各agent是否存在思考过度、思维漂移等现象，并且我还引入了<code>summary_agent</code>这个角色，进行复盘，提取当前轮次有效信息做上下文持久化，再将持久化信息传入下一轮中。agent的通信我也采用了一种自定义的Card格式进行传输，这一点在第一届的思路中也有提到。</p><p>此外，gpt-5.4也存在一些问题（在我写这篇文章时5.5已经发布了，效果提升很不错），比如上下文腐烂速度相较于其他模型来说非常快，在上下文占用超百分之五十时，模型的性能就会大幅下降，并且5.4还存在一个很致命的点就是过度思考有些严重，并且在我的架构中，负责信息收集的agent把信息传递给5.4时，由于recon侦测到的信息很复杂，它会在众多路径中陷入一个不可行的路径越陷愈深，因此后面我设计了一套评分标准，根据侦测到信息的置信度对每个攻击路径进行打分，优先对分数最高的路径进行尝试，并且在尝试次数达到规定次数后就放弃该路径，但其实效果不是很理想。</p><p>一开始设计思路的时候，跟朋友一起聊天，想到了一个好玩的点，关于ai的上下文问题，直接用QQ群聊来作为各个subagent的上下文，我新建了很多QQ群作为不同的分工，这样就可以根据角色来隔绝上下文，并且能够将上下文在qq群中持久化，后面这个也设计成功了，但其实我思考了一会，发现根本就没必要，持久化存储使用本地存储根据目录分割就可以了，前面的尝试可能更多是一个玩着试试的想法。</p><p>然后大概说一下我的设计思路吧，我在设计项目之前参考了第一届的几个项目，比较核心的观点就是less is more，这个说法成立的前提是模型的能力够强，准确来说是当模型能力够强时，过多的干扰只会扰乱模型的解决问题的思路。用我的项目来说明，因为我用的是minimax，所以我设计了大量的core agent和swarm agent分工，并针对不同的漏洞类型进行专攻，但如果将这一结构套用在opus上，一定会导致opus的效能大大降低。因此对于后续的迭代优化，我的建议就是不要把agent的角色划分的太细，尤其是对于能力强的模型越少的过度干扰才能发挥真正的作用，要做的只是规范与监督，也就是所谓的harness engine。</p><p>后面的就是整个项目的设计思路，对于比赛中这种flag的提交，我是通过监听sse的输出，识别到flag的格式就提取出来进行提交，关于为什么没有代码开源这件事，由于某些不可抗因素，导致代码全部丢失，最后开赛前一天我剩下的只有赛方服务器上的golang编译产物，所以只剩下思路和这篇开发文档了。</p><h2 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h2><ol><li><a href="#1-%E7%B3%BB%E7%BB%9F%E5%AE%9A%E4%BD%8D%E4%B8%8E%E6%80%BB%E4%BD%93%E6%9E%B6%E6%9E%84">系统定位与总体架构</a></li><li><a href="#2-%E7%BB%84%E4%BB%B6%E5%88%86%E5%B1%82">组件分层</a></li><li><a href="#3-%E6%A0%B8%E5%BF%83%E6%95%B0%E6%8D%AE%E6%A8%A1%E5%9E%8B">核心数据模型</a></li><li><a href="#4-agent-%E4%BD%93%E7%B3%BB">Agent 体系</a></li><li><a href="#5-%E5%AE%8C%E6%95%B4%E6%95%B0%E6%8D%AE%E6%B5%81">完整数据流</a></li><li><a href="#6-channel-%E8%B7%AF%E7%94%B1%E7%B3%BB%E7%BB%9F">Channel 路由系统</a></li><li><a href="#7-http-api-%E5%8F%82%E8%80%83">HTTP API 参考</a></li><li><a href="#8-%E9%85%8D%E7%BD%AE%E5%8F%82%E8%80%83">配置参考</a></li><li><a href="#9-%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F">知识系统</a></li><li><a href="#10-%E6%8C%81%E4%B9%85%E5%8C%96%E4%B8%8E%E5%AD%98%E5%82%A8%E5%B8%83%E5%B1%80">持久化与存储布局</a></li><li><a href="#11-go-%E5%8C%85%E7%BB%93%E6%9E%84">Go 包结构</a></li><li><a href="#12-%E5%BC%80%E5%8F%91%E8%A7%84%E8%8C%83">开发规范</a></li><li><a href="#13-%E6%B5%8B%E8%AF%95%E7%AD%96%E7%95%A5">测试策略</a></li><li><a href="#14-%E9%83%A8%E7%BD%B2%E8%AF%B4%E6%98%8E">部署说明</a></li></ol><hr><h2 id="1-系统定位与总体架构"><a href="#1-系统定位与总体架构" class="headerlink" title="1. 系统定位与总体架构"></a>1. 系统定位与总体架构</h2><p>ATK 是一个<strong>多 Agent 并发智能渗透测试系统</strong>。系统由三个独立进程组成：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">┌──────────────────────────────────────────────────────────────────┐</span><br><span class="line">│  AstrBot  (端口 6185)                                            │</span><br><span class="line">│  LLM 编排平台，管理 14 个 persona，负责思考/推理/委派             │</span><br><span class="line">│  接受 /run /next /summary /critic 等前缀命令                     │</span><br><span class="line">└──────────────────┬───────────────────────────────────────────────┘</span><br><span class="line">                   │  HTTP POST /api/v1/chat (kickoff)</span><br><span class="line">                   │  HTTP streaming SSE (reply)</span><br><span class="line">┌──────────────────▼───────────────────────────────────────────────┐</span><br><span class="line">│  atk-sidecar  (端口 8090)                                        │</span><br><span class="line">│  Go HTTP 服务，负责：                                            │</span><br><span class="line">│  - 与 AstrBot 的所有通信（kickoff/recovery/swarm dispatch）      │</span><br><span class="line">│  - Card/Event/Channel 持久化                                     │</span><br><span class="line">│  - 自动化调度（recovery loop、swarm dispatch、challenge 管理）   │</span><br><span class="line">│  - Flag 提交与比赛 API 对接                                      │</span><br><span class="line">│  - Shell/Python 执行（通过 runner）                              │</span><br><span class="line">│  - 知识库检索（指纹阶段可主动查询；恢复轮次注入 KNOWLEDGE_HINT） │</span><br><span class="line">└──────────────────┬───────────────────────────────────────────────┘</span><br><span class="line">                   │  run_shell / run_python (via MCP → HTTP)</span><br><span class="line">┌──────────────────▼───────────────────────────────────────────────┐</span><br><span class="line">│  Kali Linux 执行环境                                             │</span><br><span class="line">│  本地模式 (mode=local) 直接执行 shell/python 命令               │</span><br><span class="line">│  Docker 模式 (mode=docker) 在容器内执行                         │</span><br><span class="line">└──────────────────────────────────────────────────────────────────┘</span><br></code></pre></figure></div><p><strong>核心设计原则</strong>：</p><ul><li>sidecar 是<strong>哑管道</strong> + <strong>自动化控制器</strong>，不包含渗透测试逻辑</li><li>所有渗透决策在 AstrBot Agent 侧（通过 persona system prompt）</li><li>AstrBot Agent 通过 MCP server 调用 sidecar 提供的工具（<code>run_shell</code>、<code>run_python</code>、<code>read_artifact</code>、<code>upsert_card</code>）</li><li>sidecar 观察 card 流，自动触发 recovery&#x2F;swarm&#x2F;flag 提交等自动化行为</li></ul><hr><h2 id="2-组件分层"><a href="#2-组件分层" class="headerlink" title="2. 组件分层"></a>2. 组件分层</h2><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">atk-sidecar 内部分层：</span><br><span class="line"></span><br><span class="line">┌─────────────────────────────────┐</span><br><span class="line">│        httpapi (HTTP 层)        │  ← 21 个端点，无业务逻辑</span><br><span class="line">├─────────────────────────────────┤</span><br><span class="line">│     service (核心业务逻辑层)    │  ←  所有自动化调度在此</span><br><span class="line">│  ┌─────────────────────────────┐│</span><br><span class="line">│  │ card_pipeline    swarm_disp ││  ← card 处理管道 / 蜂群分发</span><br><span class="line">│  │ recovery         branch_ctl ││  ← 自动恢复 / 分支控制</span><br><span class="line">│  │ pioneer_dispatch session_reg││  ← Pioneer 并发 / session 生命周期</span><br><span class="line">│  │ challenge_manager           ││  ← 多题并发管理</span><br><span class="line">│  │ astrbot_postprocess         ││  ← AstrBot 回复解析，提取 card</span><br><span class="line">│  └─────────────────────────────┘│</span><br><span class="line">├─────────────────────────────────┤</span><br><span class="line">│         基础设施包              │</span><br><span class="line">│  cards  channelbus  events      │</span><br><span class="line">│  kickoff  officialapi  runner   │</span><br><span class="line">│  knowledge  submissions  router │</span><br><span class="line">└─────────────────────────────────┘</span><br></code></pre></figure></div><h3 id="2-1-service-包内文件职责"><a href="#2-1-service-包内文件职责" class="headerlink" title="2.1 service 包内文件职责"></a>2.1 service 包内文件职责</h3><table><thead><tr><th>文件</th><th>行数</th><th>职责</th></tr></thead><tbody><tr><td><code>service.go</code></td><td>1258</td><td>Service struct 定义、公共方法（UpsertCard、Publish、QueryCards 等）、初始化</td></tr><tr><td><code>astrbot_postprocess.go</code></td><td>1141</td><td>AstrBot 回复解析：提取 atk_card JSON block、heuristic card 检测、PlanIR 解析</td></tr><tr><td><code>branch_controller.go</code></td><td>1062</td><td>多路径分支管理：PathCandidateCard 追踪、dominant_branch 锁定、stall 检测</td></tr><tr><td><code>recovery.go</code></td><td>1189</td><td>Recovery 控制器、recovery kickoff 构建、KNOWLEDGE_HINT 注入、官方 hint 收集</td></tr><tr><td><code>card_pipeline.go</code></td><td>262</td><td>card upsert 管道：ObserveRecovery&#x2F;Branch、FlagCandidate 自动提交、SecurityMatcher 触发</td></tr><tr><td><code>session_registry.go</code></td><td>265</td><td>Session 生命周期（running&#x2F;stop_requested&#x2F;stopped&#x2F;superseded&#x2F;solved）</td></tr><tr><td><code>session_control.go</code></td><td>214</td><td>session 状态变更：markSolved、stopAllForStage（flag 找到后取消同 stage 其他 session）</td></tr><tr><td><code>swarm_dispatcher.go</code></td><td>191</td><td>HypothesisCard → 对应 vuln 专项 Agent dispatch，含 per-stage 并发上限</td></tr><tr><td><code>challenge_manager.go</code></td><td>197</td><td>多题并发：轮询比赛 API，自动 start challenge，dispatch commander+pioneer</td></tr><tr><td><code>pioneer_dispatch.go</code></td><td>328</td><td>Pioneer 独立 dispatch 逻辑，使用独立 kickoff store 和 recovery controller</td></tr><tr><td><code>signal_extract.go</code></td><td>214</td><td>runner 输出中提取 flag（正则匹配 <code>flag{...}</code>），生成 FlagCandidateCard</td></tr><tr><td><code>knowledge_zone.go</code></td><td>66</td><td>QueryKnowledge 接口，封装 knowledge.Retriever.QueryScoped</td></tr><tr><td><code>helpers.go</code></td><td>117</td><td>类型转换辅助（stringFromAny、stringSliceFromAny 等）</td></tr></tbody></table><hr><h2 id="3-核心数据模型"><a href="#3-核心数据模型" class="headerlink" title="3. 核心数据模型"></a>3. 核心数据模型</h2><h3 id="3-1-Card（结构化知识单元）"><a href="#3-1-Card（结构化知识单元）" class="headerlink" title="3.1 Card（结构化知识单元）"></a>3.1 Card（结构化知识单元）</h3><p>所有 agent 输出的结构化信息以 <code>Card</code> 形式存储。Card 通过 <code>atk_card</code> fenced JSON block 从 AstrBot 回复中提取。</p><div class="p5-codebox" data-lang="go"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="comment">// internal/sidecar/types.go</span></span><br><span class="line"><span class="keyword">type</span> Card <span class="keyword">struct</span> &#123;</span><br><span class="line">    CardID     <span class="type">string</span>         <span class="comment">// 唯一 ID，格式: &quot;&lt;type&gt;-&lt;nanoid&gt;&quot;</span></span><br><span class="line">    CardType   <span class="type">string</span>         <span class="comment">// 见下方类型表</span></span><br><span class="line">    Stage      <span class="type">string</span>         <span class="comment">// challenge code（多题并发隔离用）</span></span><br><span class="line">    GroupKey   <span class="type">string</span>         <span class="comment">// 目标 channel key</span></span><br><span class="line">    Role       <span class="type">string</span>         <span class="comment">// 产出此 card 的 agent 角色</span></span><br><span class="line">    Title      <span class="type">string</span></span><br><span class="line">    Summary    <span class="type">string</span></span><br><span class="line">    Status     <span class="type">string</span>         <span class="comment">// proposed/observed/completed/halt/captured/...</span></span><br><span class="line">    Priority   <span class="type">string</span>         <span class="comment">// low/medium/high/critical</span></span><br><span class="line">    Confidence <span class="type">float64</span>        <span class="comment">// 0.0-1.0</span></span><br><span class="line">    Tags       []<span class="type">string</span>       <span class="comment">// 用于 routing 规则匹配</span></span><br><span class="line">    Payload    <span class="keyword">map</span>[<span class="type">string</span>]any <span class="comment">// 结构化内容，各 card 类型有专属字段</span></span><br><span class="line">    CreatedAt  time.Time</span><br><span class="line">    UpdatedAt  time.Time</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p><strong>Card 类型完整表</strong>：</p><table><thead><tr><th>CardType</th><th>生产者</th><th>关键 Payload 字段</th><th>触发的自动化行为</th></tr></thead><tbody><tr><td><code>AssetCard</code></td><td>recon_agent</td><td>url, tech_stack, interesting_params, tool_needs</td><td>→ SecurityMatcher（自动假设匹配） → HypothesisCard</td></tr><tr><td><code>HypothesisCard</code></td><td>planner_agent, SecurityMatcher</td><td><strong>vuln_type</strong>（必填）, hypothesis, preconditions, validation_steps, dominant_branch</td><td>→ swarm_dispatcher dispatch 对应专项 Agent</td></tr><tr><td><code>ActionCard</code></td><td>executor_agent</td><td>action_type, target, selected_tool, result, failure_level, path_id, move_id</td><td>→ recovery ObserveCard</td></tr><tr><td><code>RiskCard</code></td><td>critic_agent, branchController</td><td>risk_type, failure_level (L0-L5), need_branch_replan, decision</td><td>→ recovery trigger（halt 状态优先级最高）</td></tr><tr><td><code>FlagCandidateCard</code></td><td>executor&#x2F;pioneer&#x2F;swarm</td><td><strong>candidate</strong>（精确 flag 字符串）, method</td><td>→ markSessionSolved → stopAllForStage → 自动提交比赛平台</td></tr><tr><td><code>GroupDigest</code></td><td>summary_agent, reflector, system</td><td>facts, dead_paths, retry_ready_context, dominant_branch, required_next_moves</td><td>→ recovery trigger</td></tr><tr><td><code>EvidenceCard</code></td><td>reflector</td><td>evidence_items, timeline, source_cards</td><td>存档，发往 evidence_review channel</td></tr><tr><td><code>PathCandidateCard</code></td><td>planner_agent, branchController</td><td>path_id, dominant_branch, required_next_moves, move_ids</td><td>branchController 追踪路径状态</td></tr><tr><td><code>PlanIRCard</code></td><td>planner_agent（经 astrbot_postprocess 解析）</td><td>goal, dominant_branch, required_next_moves, stop_conditions</td><td>branchController 锁定分支</td></tr><tr><td><code>BranchStateCard</code></td><td>branchController（system 自动生成）</td><td>dominant_branch, lock_state, stall_state, remaining_moves</td><td>发往 main_control</td></tr><tr><td><code>ToolCapabilityCard</code></td><td>system（runner 探测后生成）</td><td>available_tools, kali_tools</td><td>广播给 main_control</td></tr><tr><td><code>PathScoreCard</code></td><td>branchController（内部）</td><td>scores</td><td>内部状态管理</td></tr></tbody></table><h3 id="3-2-Event"><a href="#3-2-Event" class="headerlink" title="3.2 Event"></a>3.2 Event</h3><div class="p5-codebox" data-lang="go"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">type</span> Event <span class="keyword">struct</span> &#123;</span><br><span class="line">    ID       <span class="type">string</span></span><br><span class="line">    Type     <span class="type">string</span>   <span class="comment">// &quot;card.upserted&quot; / &quot;system.kickoff.created&quot; / &quot;system.recovery.triggered&quot; / ...</span></span><br><span class="line">    Stage    <span class="type">string</span></span><br><span class="line">    Source   <span class="type">string</span></span><br><span class="line">    GroupKey <span class="type">string</span></span><br><span class="line">    Role     <span class="type">string</span></span><br><span class="line">    Tags     []<span class="type">string</span></span><br><span class="line">    Status   <span class="type">string</span></span><br><span class="line">    Impact   <span class="type">string</span></span><br><span class="line">    Message  <span class="type">string</span></span><br><span class="line">    CardType <span class="type">string</span></span><br><span class="line">    CardID   <span class="type">string</span></span><br><span class="line">    Payload  <span class="keyword">map</span>[<span class="type">string</span>]any</span><br><span class="line">    CreatedAt time.Time</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>Event 通过 events.Bus 发布，存储在 <code>runtime_data/events/*.jsonl</code>，agent 侧不直接消费 event（agent 读 channel messages）。</p><h3 id="3-3-Kickoff-Envelope"><a href="#3-3-Kickoff-Envelope" class="headerlink" title="3.3 Kickoff Envelope"></a>3.3 Kickoff Envelope</h3><div class="p5-codebox" data-lang="go"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="comment">// internal/sidecar/kickoff/store.go</span></span><br><span class="line"><span class="keyword">type</span> Envelope <span class="keyword">struct</span> &#123;</span><br><span class="line">    ID                <span class="type">string</span></span><br><span class="line">    Source            <span class="type">string</span>         <span class="comment">// &quot;competition_api&quot; / &quot;automation&quot; / &quot;swarm_dispatcher&quot;</span></span><br><span class="line">    Trigger           <span class="type">string</span>         <span class="comment">// 触发原因</span></span><br><span class="line">    TargetRole        <span class="type">string</span>         <span class="comment">// 目标 AstrBot persona ID</span></span><br><span class="line">    TargetChannel     <span class="type">string</span>         <span class="comment">// 期望输出的 channel</span></span><br><span class="line">    Sender            <span class="type">string</span>         <span class="comment">// &quot;system://official_api&quot;</span></span><br><span class="line">    CommandPrefix     <span class="type">string</span>         <span class="comment">// &quot;/run&quot;</span></span><br><span class="line">    CommandText       <span class="type">string</span>         <span class="comment">// 完整 kickoff 消息文本</span></span><br><span class="line">    Summary           <span class="type">string</span></span><br><span class="line">    Stage             <span class="type">string</span>         <span class="comment">// challenge code</span></span><br><span class="line">    SnapshotHash      <span class="type">string</span></span><br><span class="line">    Snapshot          <span class="keyword">map</span>[<span class="type">string</span>]any <span class="comment">// 官方 API 快照</span></span><br><span class="line">    Meta              <span class="keyword">map</span>[<span class="type">string</span>]any</span><br><span class="line">    SessionIDOverride <span class="type">string</span>         <span class="comment">// 非空时覆盖 AstrBot session ID（多题并发用）</span></span><br><span class="line">    CreatedAt         time.Time</span><br><span class="line">    Status            <span class="type">string</span></span><br><span class="line">&#125;</span><br></code></pre></figure></div><p><strong>Session ID 命名约定</strong>：</p><table><thead><tr><th>场景</th><th>Session ID 格式</th></tr></thead><tbody><tr><td>单题 commander</td><td><code>atk-commander-main-v3</code>（yaml 配置）</td></tr><tr><td>单题 pioneer</td><td><code>atk-pioneer-v1</code>（yaml 配置）</td></tr><tr><td>多题 commander</td><td><code>atk-cmd-{challenge_code_lowercase}</code></td></tr><tr><td>多题 pioneer</td><td><code>atk-pio-{challenge_code_lowercase}</code></td></tr><tr><td>蜂群 agent</td><td><code>atk-swarm-{vuln_type}-{challenge_code_lowercase}</code></td></tr></tbody></table><h3 id="3-4-PublishedMessage（Channel-消息）"><a href="#3-4-PublishedMessage（Channel-消息）" class="headerlink" title="3.4 PublishedMessage（Channel 消息）"></a>3.4 PublishedMessage（Channel 消息）</h3><div class="p5-codebox" data-lang="go"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="comment">// internal/sidecar/channelbus/publisher.go</span></span><br><span class="line"><span class="keyword">type</span> PublishedMessage <span class="keyword">struct</span> &#123;</span><br><span class="line">    ID          <span class="type">string</span></span><br><span class="line">    ChannelKey  <span class="type">string</span></span><br><span class="line">    Stage       <span class="type">string</span>         <span class="comment">// 从嵌入 card/event 自动提取（多题过滤用）</span></span><br><span class="line">    Text        <span class="type">string</span></span><br><span class="line">    Tags        []<span class="type">string</span></span><br><span class="line">    Card        *sidecar.Card</span><br><span class="line">    Event       *sidecar.Event</span><br><span class="line">    PublishedAt time.Time</span><br><span class="line">&#125;</span><br></code></pre></figure></div><hr><h2 id="4-Agent-体系"><a href="#4-Agent-体系" class="headerlink" title="4. Agent 体系"></a>4. Agent 体系</h2><h3 id="4-1-Core-Agents（8-个）"><a href="#4-1-Core-Agents（8-个）" class="headerlink" title="4.1 Core Agents（8 个）"></a>4.1 Core Agents（8 个）</h3><table><thead><tr><th>角色 ID（AstrBot persona_id）</th><th>中文定位</th><th>主要输出 Card</th><th>工具权限</th></tr></thead><tbody><tr><td><code>commander_main</code></td><td>主控、调度、fast_probe</td><td>GroupDigest, HypothesisCard, RiskCard</td><td>run_shell, run_python, read_artifact</td></tr><tr><td><code>recon_agent</code></td><td>低成本侦察、攻击面画像</td><td>AssetCard</td><td>run_shell, run_python, read_artifact</td></tr><tr><td><code>planner_agent</code></td><td>攻击路径规划、假设建立</td><td>HypothesisCard, PathCandidateCard, PlanIRCard</td><td>read_artifact</td></tr><tr><td><code>executor_agent</code></td><td>执行批准动作、回传结果</td><td>ActionCard, FlagCandidateCard</td><td>run_shell, run_python, read_artifact</td></tr><tr><td><code>critic_agent</code></td><td>风险审查、失败分级（L0-L5）</td><td>RiskCard</td><td>read_artifact</td></tr><tr><td><code>summary_agent</code></td><td>上下文压缩、复盘摘要</td><td>GroupDigest</td><td>read_artifact</td></tr><tr><td><code>reflector</code></td><td>证据归档（solved）&#x2F; 经验沉淀（unsolved）</td><td>EvidenceCard, GroupDigest</td><td>read_artifact</td></tr><tr><td><code>pioneer</code></td><td>直攻型并发解题（15 步预算）</td><td>FlagCandidateCard, GroupDigest</td><td>run_shell, run_python, read_artifact</td></tr></tbody></table><h3 id="4-2-Swarm-Agents（6-个）"><a href="#4-2-Swarm-Agents（6-个）" class="headerlink" title="4.2 Swarm Agents（6 个）"></a>4.2 Swarm Agents（6 个）</h3><table><thead><tr><th>角色 ID</th><th>专项漏洞</th><th>预算</th><th>vuln_type 触发值</th></tr></thead><tbody><tr><td><code>sqli_agent</code></td><td>SQL 注入</td><td>10 步</td><td><code>sqli</code></td></tr><tr><td><code>ssrf_agent</code></td><td>SSRF</td><td>10 步</td><td><code>ssrf</code></td></tr><tr><td><code>auth_bypass_agent</code></td><td>认证绕过&#x2F;IDOR</td><td>10 步</td><td><code>auth_bypass</code></td></tr><tr><td><code>file_inclusion_agent</code></td><td>文件包含&#x2F;路径遍历</td><td>10 步</td><td><code>file_inclusion</code></td></tr><tr><td><code>rce_agent</code></td><td>RCE&#x2F;命令注入</td><td>10 步</td><td><code>rce</code></td></tr><tr><td><code>xss_agent</code></td><td>XSS&#x2F;客户端注入</td><td>10 步</td><td><code>xss</code></td></tr></tbody></table><p>蜂群 agent 与主链路<strong>并发运行</strong>，不通过 <code>transfer_to_*</code>，找到 flag 后 sidecar 自动取消同 stage 其他 session。</p><h3 id="4-3-Persona-文件结构"><a href="#4-3-Persona-文件结构" class="headerlink" title="4.3 Persona 文件结构"></a>4.3 Persona 文件结构</h3><p>Persona 以 <code>persona/assembled/*.md</code> 作为最终交付物，直接复制到 AstrBot Web。</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">最终交付目录：</span><br><span class="line">  persona/assembled/commander_main.md</span><br><span class="line">  persona/assembled/recon_persona.md</span><br><span class="line">  persona/assembled/summary_persona.md</span><br><span class="line">  persona/assembled/planner_persona.md</span><br><span class="line">  ... 共 14 个完整 prompt 文件</span><br></code></pre></figure></div><p>使用方式：</p><div class="p5-codebox" data-lang="bash"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="comment"># 打开对应文件并复制完整 prompt</span></span><br><span class="line">persona/assembled/&lt;role&gt;.md</span><br></code></pre></figure></div><h3 id="4-4-失败分级体系（L0-L5）"><a href="#4-4-失败分级体系（L0-L5）" class="headerlink" title="4.4 失败分级体系（L0-L5）"></a>4.4 失败分级体系（L0-L5）</h3><p>critic_agent 使用，recovery 触发优先级依据：</p><table><thead><tr><th>级别</th><th>含义</th><th>recovery 行为</th></tr></thead><tbody><tr><td>L0</td><td>仅有原始现象，不足以下结论</td><td>不触发 recovery</td></tr><tr><td>L1</td><td>工具&#x2F;命令&#x2F;请求本身失败</td><td>触发 recovery，重试</td></tr><tr><td>L2</td><td>前提条件不成立（认证、会话缺失）</td><td>触发 recovery，补前提</td></tr><tr><td>L3</td><td>外部环境干扰（WAF、限速）</td><td>触发 recovery，绕过策略</td></tr><tr><td>L4</td><td>假设本身被证伪</td><td>触发 recovery，换分支</td></tr><tr><td>L5</td><td>整体路径设计错误</td><td>触发 recovery + 注入 reference layer 知识</td></tr></tbody></table><hr><h2 id="5-完整数据流"><a href="#5-完整数据流" class="headerlink" title="5. 完整数据流"></a>5. 完整数据流</h2><h3 id="5-1-题目-Kickoff-流程（单题模式）"><a href="#5-1-题目-Kickoff-流程（单题模式）" class="headerlink" title="5.1 题目 Kickoff 流程（单题模式）"></a>5.1 题目 Kickoff 流程（单题模式）</h3><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">官方 API 变更检测（scheduler, 每 10s）</span><br><span class="line">  ↓</span><br><span class="line">official.FetchStageSnapshot()</span><br><span class="line">  ↓ 检测到 hash 变化</span><br><span class="line">dispatchKickoff(ctx, snapshot)</span><br><span class="line">  ├── kickoff.NewEnvelope(cfg.Automation.Kickoff, trigger, snapshot)</span><br><span class="line">  │     CommandText: &quot;/run [SYSTEM][competition_api] stage=X target_url=Y ...&quot;</span><br><span class="line">  │     → AstrBot POST /api/v1/chat &#123; session_id: &quot;atk-commander-main-v3&quot; &#125;</span><br><span class="line">  │     → SSE streaming reply → astrbot_postprocess → extractReplyCards</span><br><span class="line">  │</span><br><span class="line">  └── go dispatchPioneerKickoff(snapshot)   ← 并发，独立 goroutine</span><br><span class="line">        CommandText: &quot;/run [SYSTEM][pioneer] stage=X target_url=Y ...&quot;</span><br><span class="line">        → AstrBot POST /api/v1/chat &#123; session_id: &quot;atk-pioneer-v1&quot; &#125;</span><br></code></pre></figure></div><h3 id="5-2-多题并发流程（challenge-manager）"><a href="#5-2-多题并发流程（challenge-manager）" class="headerlink" title="5.2 多题并发流程（challenge_manager）"></a>5.2 多题并发流程（challenge_manager）</h3><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">challengeManager.tick()  ← 每 30s</span><br><span class="line">  ↓</span><br><span class="line">official.FetchChallengeList()</span><br><span class="line">  ↓ 发现未解且未 active 的题目（按 difficulty 升序）</span><br><span class="line">official.StartChallenge(code) → entrypoints</span><br><span class="line">  ↓</span><br><span class="line">dispatchChallenge(ch, entrypoints)</span><br><span class="line">  ├── cmdEnv.SessionIDOverride = &quot;atk-cmd-&quot; + code</span><br><span class="line">  │   dispatchKickoffEnvelope(cmdEnv)</span><br><span class="line">  └── pioEnv.SessionIDOverride = &quot;atk-pio-&quot; + code</span><br><span class="line">      go dispatchPioneerEnvelope(pioEnv)</span><br></code></pre></figure></div><h3 id="5-3-Swarm-Agent-分发流程"><a href="#5-3-Swarm-Agent-分发流程" class="headerlink" title="5.3 Swarm Agent 分发流程"></a>5.3 Swarm Agent 分发流程</h3><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">planner_agent 输出 HypothesisCard &#123; vuln_type: &quot;sqli&quot; &#125;</span><br><span class="line">  ↓</span><br><span class="line">AstrBot reply → astrbot_postprocess → extractReplyCards</span><br><span class="line">  ↓</span><br><span class="line">upsertCard() → card_pipeline</span><br><span class="line">  ↓</span><br><span class="line">swarmDisp.MaybeDispatch(card)</span><br><span class="line">  ├── 检查 card.CardType == &quot;HypothesisCard&quot;</span><br><span class="line">  ├── 检查 vuln_type 在已知 6 种之内（&quot;unknown&quot; 不触发）</span><br><span class="line">  ├── 检查同 stage+vuln_type 未有 active session</span><br><span class="line">  ├── 检查 stageCount[stage] &lt; MaxConcurrentPerStage (默认 3)</span><br><span class="line">  └── go dispatch(agentRole=&quot;sqli_agent&quot;, vulnType=&quot;sqli&quot;, stage, card)</span><br><span class="line">        ↓</span><br><span class="line">        swarmCfg.TargetRole = &quot;sqli_agent&quot;</span><br><span class="line">        swarmCfg.TargetChannel = &quot;exec_feedback&quot;</span><br><span class="line">        snapshot.Normalized = &#123; stage, vuln_type, target_url, hypothesis, validation_steps, ... &#125;</span><br><span class="line">        env.SessionIDOverride = &quot;atk-swarm-sqli-&quot; + sanitize(stage)</span><br><span class="line">        loadDomainSkill(&quot;sqli&quot;) → env.CommandText += &quot;[DOMAIN_SKILL]\n...&quot;</span><br><span class="line">        dispatchKickoffEnvelope(env)</span><br><span class="line"></span><br><span class="line">另一触发路径（SecurityMatcher，专用于指纹到漏洞家族假设的自动匹配）：</span><br><span class="line">AssetCard 到达 card_pipeline</span><br><span class="line">  ↓</span><br><span class="line">securityMatcher.MatchBySignals(signals)</span><br><span class="line">  ↓ 命中高置信条目</span><br><span class="line">自动生成 HypothesisCard &#123; vuln_type: &quot;rce&quot;, source: &quot;security/*&quot; &#125;</span><br><span class="line">  ↓ 同上 swarm dispatch 路径</span><br></code></pre></figure></div><h3 id="5-4-Recovery-流程"><a href="#5-4-Recovery-流程" class="headerlink" title="5.4 Recovery 流程"></a>5.4 Recovery 流程</h3><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">agent 输出 GroupDigest（status=unsolved）或 RiskCard（status=halt）</span><br><span class="line">  ↓</span><br><span class="line">upsertCard() → recovery.ObserveCard(card)</span><br><span class="line">  ├── recoveryTriggerFromCard(card) → recoveryTrigger&#123; Stage, CardType, CardID, Reason, FailureLevel &#125;</span><br><span class="line">  └── 更新 stageRecoveryState.queued（高优先级 trigger 覆盖低优先级）</span><br><span class="line"></span><br><span class="line">dispatchKickoffEnvelope 完成回调 → maybeStartRecoveryRound</span><br><span class="line">  ↓</span><br><span class="line">recovery.CompleteKickoff(envelope) → returns queued trigger（若有）</span><br><span class="line">  ↓</span><br><span class="line">buildRecoveryEnvelope(trigger, previous)</span><br><span class="line">  ├── CommandText 包含:</span><br><span class="line">  │   - [SYSTEM][recovery_loop] stage=X round=N failure_level=L4 reason=...</span><br><span class="line">  │   - 恢复策略指令（先 summary → critic → reflector → planner）</span><br><span class="line">  │   - [RETRY_READY_CONTEXT] 块（最近 GroupDigest 内容）</span><br><span class="line">  │   - [OFFICIAL_HINT] 块（来自比赛平台 /api/hint）</span><br><span class="line">  │   └── [KNOWLEDGE_HINT] 块（知识库检索结果，按 trigger/runbook/reference 分层）</span><br><span class="line">  └── 新 kickoff → AstrBot（同 session）</span><br><span class="line"></span><br><span class="line">recovery 轮次限制：</span><br><span class="line">  - commander: max_rounds = 5（yaml 配置）</span><br><span class="line">  - pioneer: max_rounds = 2</span><br><span class="line">  - swarm: 无 recovery（10 步结束即结束）</span><br><span class="line">  - competition.auto_hint = true 时：round &gt;= 2 且 hint_viewed=false 才请求 hint（pioneer 不请求）</span><br></code></pre></figure></div><h3 id="5-5-Flag-提交流程"><a href="#5-5-Flag-提交流程" class="headerlink" title="5.5 Flag 提交流程"></a>5.5 Flag 提交流程</h3><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">任意 agent 输出 FlagCandidateCard &#123; candidate: &quot;flag&#123;xxx&#125;&quot;, stage: &quot;BwAMWQ...&quot; &#125;</span><br><span class="line">  ↓</span><br><span class="line">upsertCard() → card_pipeline</span><br><span class="line">  ├── markSessionSolved(sessionID, &quot;flag candidate captured&quot;)</span><br><span class="line">  │     └── stopAllForStage(stage, exceptSessionID)</span><br><span class="line">  │           → 取消同 stage 其他 session 的 HTTP context（best-effort）</span><br><span class="line">  │           → 标记为 stop_requested</span><br><span class="line">  │</span><br><span class="line">  └── maybeAutoSubmitFlagCandidate(card)</span><br><span class="line">        ├── extractFlagValue(candidate)  ← 正则提取 flag&#123;...&#125;</span><br><span class="line">        ├── 检查 confidence &gt;= min_confidence (默认 0.85)</span><br><span class="line">        ├── 检查未重复提交</span><br><span class="line">        ├── [Competition 模式] official.SubmitCompetitionFlag(stage, flag)</span><br><span class="line">        │     → POST /api/submit &#123; code: stage, flag: flag &#125;</span><br><span class="line">        └── [Legacy 模式] official.SubmitFlag(stage, flag, sourceCardID)</span><br><span class="line">              → POST &#123;flag_submission.endpoint_path&#125;</span><br></code></pre></figure></div><hr><h2 id="6-Channel-路由系统"><a href="#6-Channel-路由系统" class="headerlink" title="6. Channel 路由系统"></a>6. Channel 路由系统</h2><h3 id="6-1-6-个-Channel"><a href="#6-1-6-个-Channel" class="headerlink" title="6.1 6 个 Channel"></a>6.1 6 个 Channel</h3><table><thead><tr><th>Channel Key</th><th>默认入口</th><th>读权限角色</th><th>用途</th></tr></thead><tbody><tr><td><code>main_control</code></td><td>commander_main</td><td>summary, planner, critic, reflector, 6 swarm</td><td>主控决策中心，沉淀摘要&#x2F;计划&#x2F;风险</td></tr><tr><td><code>recon_raw</code></td><td>recon_agent</td><td>summary, planner, reflector</td><td>侦察原始数据，AssetCard&#x2F;OBS</td></tr><tr><td><code>exec_feedback</code></td><td>executor_agent</td><td>summary, planner, critic, reflector, 6 swarm</td><td>执行反馈，ActionCard&#x2F;RESULT&#x2F;FlagCandidateCard</td></tr><tr><td><code>evidence_review</code></td><td>reflector</td><td>critic, commander_main, reflector</td><td>证据归档，EvidenceCard&#x2F;REPORT</td></tr><tr><td><code>direct_solve</code></td><td>pioneer</td><td>commander_main, summary</td><td>Pioneer 独立解题通道</td></tr><tr><td><code>swarm_feedback</code></td><td>sqli_agent</td><td>commander_main, summary, planner</td><td>蜂群专项输出汇总</td></tr></tbody></table><h3 id="6-2-Publish-规则（tag-card-type-→-channel）"><a href="#6-2-Publish-规则（tag-card-type-→-channel）" class="headerlink" title="6.2 Publish 规则（tag&#x2F;card_type → channel）"></a>6.2 Publish 规则（tag&#x2F;card_type → channel）</h3><table><thead><tr><th>匹配条件</th><th>目标 Channel</th></tr></thead><tbody><tr><td>tag: <code>OBS</code> 或 CardType: <code>AssetCard</code></td><td>recon_raw</td></tr><tr><td>tag: <code>SUMMARY</code> 或 CardType: <code>GroupDigest</code></td><td>main_control</td></tr><tr><td>tag: <code>PLAN</code> 或 CardType: <code>HypothesisCard</code></td><td>main_control</td></tr><tr><td>tag: <code>CRITIC</code> 或 CardType: <code>RiskCard</code></td><td>main_control</td></tr><tr><td>tag: <code>ACTION</code> 或 tag: <code>RESULT</code></td><td>exec_feedback</td></tr><tr><td>CardType: <code>FlagCandidateCard</code></td><td>exec_feedback + swarm_feedback</td></tr><tr><td>tag: <code>REPORT</code> 或 CardType: <code>EvidenceCard</code></td><td>evidence_review</td></tr><tr><td>tag: <code>SKILL</code></td><td>evidence_review</td></tr><tr><td>tag: <code>SWARM</code></td><td>swarm_feedback</td></tr><tr><td>tag: <code>OfficialAPI</code></td><td>main_control</td></tr></tbody></table><h3 id="6-3-Mirror-规则（额外复制到其他-channel）"><a href="#6-3-Mirror-规则（额外复制到其他-channel）" class="headerlink" title="6.3 Mirror 规则（额外复制到其他 channel）"></a>6.3 Mirror 规则（额外复制到其他 channel）</h3><table><thead><tr><th>匹配条件</th><th>额外发往</th></tr></thead><tbody><tr><td>AssetCard + confidence &gt;&#x3D; 0.85</td><td>main_control</td></tr><tr><td>RiskCard + status&#x3D;halt</td><td>evidence_review</td></tr><tr><td>tag: RESULT + impact&#x3D;high</td><td>main_control</td></tr></tbody></table><h3 id="6-4-Ingress-模式"><a href="#6-4-Ingress-模式" class="headerlink" title="6.4 Ingress 模式"></a>6.4 Ingress 模式</h3><ul><li><code>main_control</code>: <code>command_only</code>，只接受以 <code>/run /next /summary /critic</code> 开头的命令</li><li>其余 channel: <code>none</code>，不接受外部写入（只由 sidecar 内部 publish 写入）</li></ul><h3 id="6-5-Stage-过滤（多题并发隔离）"><a href="#6-5-Stage-过滤（多题并发隔离）" class="headerlink" title="6.5 Stage 过滤（多题并发隔离）"></a>6.5 Stage 过滤（多题并发隔离）</h3><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">GET /v1/channels/messages?role=summary_agent&amp;channel_key=main_control&amp;stage=BwAMWQ</span><br><span class="line">→ channelbus.Store.List(filter&#123;Stage: &quot;BwAMWQ&quot;&#125;)</span><br><span class="line">→ 只返回 PublishedMessage.Stage == &quot;BwAMWQ&quot; 的消息</span><br></code></pre></figure></div><p>Stage 在 <code>filePublisher.Publish</code> 时自动从嵌入的 Card.Stage 或 Event.Stage 提取，调用方无需显式传递。</p><hr><h2 id="7-HTTP-API-参考"><a href="#7-HTTP-API-参考" class="headerlink" title="7. HTTP API 参考"></a>7. HTTP API 参考</h2><p>所有端点均无认证（内网部署，sidecar 信任所有请求）。</p><table><thead><tr><th>方法</th><th>路径</th><th>说明</th><th>关键参数</th></tr></thead><tbody><tr><td>GET</td><td><code>/healthz</code></td><td>健康检查</td><td>—</td></tr><tr><td>GET</td><td><code>/dashboard</code></td><td>Web Dashboard HTML</td><td>—</td></tr><tr><td>GET</td><td><code>/v1/dashboard/progress</code></td><td>Dashboard 数据（JSON）</td><td>—</td></tr><tr><td>GET</td><td><code>/v1/channels</code></td><td>列出所有 channel（按 role 过滤）</td><td><code>?role=</code></td></tr><tr><td>GET</td><td><code>/v1/channels/messages</code></td><td>查询 channel 消息</td><td><code>?role= &amp;channel_key= &amp;stage= &amp;after_id= &amp;limit=</code></td></tr><tr><td>GET</td><td><code>/v1/cards</code></td><td>查询 card 列表</td><td><code>?card_type= &amp;stage= &amp;group_key= &amp;tag= &amp;limit=</code></td></tr><tr><td>GET</td><td><code>/v1/cards/{type}/{id}</code></td><td>获取单张 card</td><td>—</td></tr><tr><td>POST</td><td><code>/v1/cards/upsert</code></td><td>插入&#x2F;更新 card（agent 通过 MCP 调用）</td><td><code>{card, publish, publish_text, preferred_channels}</code></td></tr><tr><td>POST</td><td><code>/v1/events</code></td><td>发布事件</td><td><code>{event}</code></td></tr><tr><td>POST</td><td><code>/v1/publish</code></td><td>发布消息到 channel</td><td><code>{text, tags, card, event, preferred_channels}</code></td></tr><tr><td>POST</td><td><code>/v1/ingress/local</code></td><td>本地消息入站</td><td><code>{channel_key, message, sender}</code></td></tr><tr><td>GET</td><td><code>/v1/ingress/resolve</code></td><td>解析 channel 入站路由</td><td><code>?channel_key=</code></td></tr><tr><td>POST</td><td><code>/v1/official-api/poll</code></td><td>手动触发官方 API 轮询</td><td>—</td></tr><tr><td>POST</td><td><code>/v1/official-api/submit-flag</code></td><td>手动提交 flag</td><td><code>{flag, stage, source_card_id}</code></td></tr><tr><td>GET</td><td><code>/v1/system/kickoffs</code></td><td>列出 kickoff envelope</td><td><code>?target_role= &amp;limit= &amp;after_id=</code></td></tr><tr><td>POST</td><td><code>/v1/sessions/{session_id}/stop</code></td><td>手动停止 session</td><td>—</td></tr><tr><td>GET</td><td><code>/v1/knowledge/query</code></td><td>查询知识库</td><td><code>?q= &amp;zone= &amp;zones= &amp;layers= &amp;tags= &amp;limit=</code></td></tr><tr><td>POST</td><td><code>/v1/runner/shell</code></td><td>执行 shell 命令</td><td><code>{command, timeout}</code></td></tr><tr><td>POST</td><td><code>/v1/runner/python</code></td><td>执行 Python 代码</td><td><code>{code, timeout}</code></td></tr><tr><td>GET</td><td><code>/v1/runner/artifacts/{job_id}</code></td><td>获取执行结果</td><td>—</td></tr></tbody></table><hr><h2 id="8-配置参考"><a href="#8-配置参考" class="headerlink" title="8. 配置参考"></a>8. 配置参考</h2><p>配置文件：<code>configs/atk-sidecar.yaml</code>（主配置）</p><h3 id="8-1-顶层配置段"><a href="#8-1-顶层配置段" class="headerlink" title="8.1 顶层配置段"></a>8.1 顶层配置段</h3><div class="p5-codebox" data-lang="yaml"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="attr">server:</span> <span class="comment"># HTTP 监听地址</span></span><br><span class="line"><span class="attr">storage:</span> <span class="comment"># 持久化根目录（默认 runtime_data/）</span></span><br><span class="line"><span class="attr">scheduler:</span> <span class="comment"># 官方 API 轮询调度（interval 默认 10s）</span></span><br><span class="line"><span class="attr">official_api:</span> <span class="comment"># 比赛/题目平台 API（snapshot, hint, flag 提交）</span></span><br><span class="line"><span class="attr">automation:</span> <span class="comment"># kickoff + recovery + pioneer 配置</span></span><br><span class="line"><span class="attr">challenge_manager:</span> <span class="comment"># 多题并发管理（默认 disabled）</span></span><br><span class="line"><span class="attr">swarm_dispatcher:</span> <span class="comment"># 蜂群分发配置（默认 enabled, max 3/stage）</span></span><br><span class="line"><span class="attr">competition:</span> <span class="comment"># 比赛平台直连（独立于 official_api，默认 disabled）</span></span><br><span class="line"><span class="attr">knowledge:</span> <span class="comment"># 知识库检索（inject_on_recovery, top_k, paths）</span></span><br><span class="line"><span class="attr">runner:</span> <span class="comment"># Shell/Python 执行器（local/docker 模式）</span></span><br><span class="line"><span class="attr">channels:</span> <span class="comment"># Channel bus 配置（publisher, ingress）</span></span><br><span class="line"><span class="attr">routing:</span> <span class="comment"># Channel 路由规则（channels, publish_rules, mirror_rules）</span></span><br></code></pre></figure></div><h3 id="8-2-关键配置项说明"><a href="#8-2-关键配置项说明" class="headerlink" title="8.2 关键配置项说明"></a>8.2 关键配置项说明</h3><p><strong>多题并发开关</strong>：</p><div class="p5-codebox" data-lang="yaml"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="attr">challenge_manager:</span></span><br><span class="line">  <span class="attr">enabled:</span> <span class="literal">true</span> <span class="comment"># 启用后接管 challenge 生命周期</span></span><br><span class="line">  <span class="attr">poll_interval:</span> <span class="string">&quot;30s&quot;</span></span><br><span class="line">  <span class="attr">max_concurrent:</span> <span class="number">3</span> <span class="comment"># 比赛平台最大并发题目数</span></span><br></code></pre></figure></div><p><strong>蜂群分发</strong>：</p><div class="p5-codebox" data-lang="yaml"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="attr">swarm_dispatcher:</span></span><br><span class="line">  <span class="attr">enabled:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">max_concurrent_per_stage:</span> <span class="number">3</span> <span class="comment"># 每道题同时运行的蜂群 agent 上限</span></span><br><span class="line">  <span class="attr">cooldown:</span> <span class="string">&quot;0s&quot;</span> <span class="comment"># 同 vuln_type 重新 dispatch 的冷却时间</span></span><br><span class="line">  <span class="attr">skills_dir:</span> <span class="string">&quot;skills/domain&quot;</span> <span class="comment"># domain skill 文件路径</span></span><br></code></pre></figure></div><p><strong>比赛平台直连</strong>（Competition 模式）：</p><div class="p5-codebox" data-lang="yaml"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="attr">competition:</span></span><br><span class="line">  <span class="attr">enabled:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">auth_token:</span> <span class="string">&quot;Agent-Token 值&quot;</span></span><br><span class="line">  <span class="attr">base_url:</span> <span class="string">&quot;https://competition-api.example.com&quot;</span></span><br><span class="line">  <span class="attr">auto_submit:</span> <span class="literal">true</span> <span class="comment"># FlagCandidateCard → POST /api/submit</span></span><br><span class="line">  <span class="attr">auto_hint:</span> <span class="literal">true</span> <span class="comment"># recovery round &gt;= 2 → POST /api/hint（pioneer 除外）</span></span><br></code></pre></figure></div><p><strong>AstrBot 连接</strong>（kickoff 部分）：</p><div class="p5-codebox" data-lang="yaml"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="attr">automation:</span></span><br><span class="line">  <span class="attr">kickoff:</span></span><br><span class="line">    <span class="attr">mode:</span> <span class="string">&quot;astrbot&quot;</span></span><br><span class="line">    <span class="attr">astrbot:</span></span><br><span class="line">      <span class="attr">base_url:</span> <span class="string">&quot;http://127.0.0.1:6185&quot;</span></span><br><span class="line">      <span class="attr">api_key:</span> <span class="string">&quot;abk_xxx&quot;</span></span><br><span class="line">      <span class="attr">session_id:</span> <span class="string">&quot;atk-commander-main-v3&quot;</span></span><br><span class="line">      <span class="attr">enable_streaming:</span> <span class="literal">true</span> <span class="comment"># SSE 流式回复，启用实时卡片解析</span></span><br></code></pre></figure></div><h3 id="8-3-配置-Struct-索引"><a href="#8-3-配置-Struct-索引" class="headerlink" title="8.3 配置 Struct 索引"></a>8.3 配置 Struct 索引</h3><table><thead><tr><th>struct 名</th><th>yaml key</th><th>文件</th></tr></thead><tbody><tr><td><code>ServerConfig</code></td><td><code>server</code></td><td>config.go:34</td></tr><tr><td><code>OfficialAPIConfig</code></td><td><code>official_api</code></td><td>config.go:47</td></tr><tr><td><code>AutomationConfig</code></td><td><code>automation</code></td><td>config.go:93</td></tr><tr><td><code>PioneerKickoffConfig</code></td><td><code>automation.pioneer_kickoff</code></td><td>config.go:101</td></tr><tr><td><code>ChallengeManagerConfig</code></td><td><code>challenge_manager</code></td><td>config.go:107</td></tr><tr><td><code>SwarmDispatcherConfig</code></td><td><code>swarm_dispatcher</code></td><td>config.go:113</td></tr><tr><td><code>CompetitionConfig</code></td><td><code>competition</code></td><td>config.go:26</td></tr><tr><td><code>KnowledgeConfig</code></td><td><code>knowledge</code></td><td>config.go:120</td></tr><tr><td><code>KickoffConfig</code></td><td><code>automation.kickoff</code></td><td>config.go:132</td></tr><tr><td><code>AstrBotKickoffConfig</code></td><td><code>automation.kickoff.astrbot</code></td><td>config.go:142</td></tr><tr><td><code>RoutingConfig</code></td><td><code>routing</code></td><td>config.go:172</td></tr></tbody></table><hr><h2 id="9-知识系统"><a href="#9-知识系统" class="headerlink" title="9. 知识系统"></a>9. 知识系统</h2><h3 id="9-1-知识库结构"><a href="#9-1-知识库结构" class="headerlink" title="9.1 知识库结构"></a>9.1 知识库结构</h3><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">knowledge/</span><br><span class="line">  indexes/                      # 索引文件（已构建，直接被 Retriever 加载）</span><br><span class="line">    zone1_ctf_web/</span><br><span class="line">      trigger.jsonl             # 触发规则：看到信号 X → 建立分支 Y</span><br><span class="line">      runbook.jsonl             # 操作手册：按步骤执行 A/B/C</span><br><span class="line">      reference.jsonl           # 参考资料：工具、CVE、服务特性</span><br><span class="line">    zone2_src_component/</span><br><span class="line">    zone3_multi_network_oa/</span><br><span class="line">    zone4_ad_pentest/</span><br><span class="line">  raw/                          # 原始知识文档（供重新构建索引用）</span><br><span class="line">  security/                     # SecurityMatcher 数据（特征匹配型）</span><br><span class="line">    apache/httpd-path-traversal.md</span><br><span class="line">    redis/unauthorized-access.md</span><br><span class="line">    tomcat/manager-default-creds.md</span><br></code></pre></figure></div><h3 id="9-2-知识层语义"><a href="#9-2-知识层语义" class="headerlink" title="9.2 知识层语义"></a>9.2 知识层语义</h3><table><thead><tr><th>Layer</th><th>作用</th><th>触发时机</th></tr></thead><tbody><tr><td><code>trigger</code></td><td>信号→分支映射（看到 X，考虑 Y 漏洞）</td><td>所有 recovery 轮次</td></tr><tr><td><code>runbook</code></td><td>分支确认后的步骤手册（如何利用 Y）</td><td>所有 recovery 轮次</td></tr><tr><td><code>reference</code></td><td>工具&#x2F;CVE&#x2F;服务特定参考</td><td>L5_STRATEGIC_FAILURE 或 hard_stall</td></tr></tbody></table><p>Recovery 注入的 <code>[KNOWLEDGE_HINT]</code> 格式：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">[KNOWLEDGE_HINT]</span><br><span class="line">以下为可选战术参考，不是事实。当前目标的真实观测优先级高于任何知识提示。</span><br><span class="line"></span><br><span class="line">[TRIGGER SIGNALS] — 匹配到以下信号时触发对应漏洞分支：</span><br><span class="line">- ref=... score=0.82 title=... hint=...</span><br><span class="line"></span><br><span class="line">[RUNBOOK] — 如果确认漏洞存在，按以下步骤执行：</span><br><span class="line">- ref=... score=0.71 branch=sqli hint=...</span><br><span class="line"></span><br><span class="line">[REFERENCE] — 工具/CVE/服务特定参考（仅 L5/hard stall 时包含）：</span><br><span class="line">- ref=... score=0.65 hint=...</span><br></code></pre></figure></div><h3 id="9-3-SecurityMatcher"><a href="#9-3-SecurityMatcher" class="headerlink" title="9.3 SecurityMatcher"></a>9.3 SecurityMatcher</h3><p>独立于 Retriever，专门处理特征指纹匹配：</p><ul><li>加载 <code>knowledge/security/**/*.md</code>（YAML frontmatter + Markdown body）</li><li>特征字段：<code>signals</code>（版本字符串、服务指纹）、<code>category</code>（漏洞类别）、<code>cve</code></li><li>触发：AssetCard 到达 card_pipeline 时，<code>MatchBySignals(tech_stack + notes + url)</code></li><li>命中（score &gt;&#x3D; 1.0）→ 自动生成 HypothesisCard → 触发 swarm dispatch</li></ul><h3 id="9-4-Domain-Skills"><a href="#9-4-Domain-Skills" class="headerlink" title="9.4 Domain Skills"></a>9.4 Domain Skills</h3><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">skills/domain/</span><br><span class="line">  sqli/SKILL.md          # SQL 注入战术手册（~107 行）</span><br><span class="line">  ssrf/SKILL.md</span><br><span class="line">  auth-bypass/SKILL.md</span><br><span class="line">  file-inclusion/SKILL.md</span><br><span class="line">  rce/SKILL.md</span><br><span class="line">  xss/SKILL.md</span><br><span class="line">  web-recon/SKILL.md     # Web 侦察技术（recon_agent 使用）</span><br></code></pre></figure></div><p>Domain skill 已直接内联到对应的 assembled persona system prompt 中。<br>同时，<code>swarm_dispatcher.go</code> 的 <code>loadDomainSkill()</code> 也会在 dispatch 时动态注入 CommandText（备份路径）。</p><hr><h2 id="10-持久化与存储布局"><a href="#10-持久化与存储布局" class="headerlink" title="10. 持久化与存储布局"></a>10. 持久化与存储布局</h2><p>所有持久化使用本地 JSONL 文件，无数据库依赖。</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">runtime_data/                          # 运行时数据根目录（yaml: storage.root_dir）</span><br><span class="line">  cards/</span><br><span class="line">    &lt;CardType&gt;.jsonl                   # 每种 card 类型一个文件，按 card_id dedup</span><br><span class="line">  events/</span><br><span class="line">    events.jsonl                       # 事件流，追加写入</span><br><span class="line">  channels/</span><br><span class="line">    main_control.jsonl                 # channel 消息，追加写入</span><br><span class="line">    recon_raw.jsonl</span><br><span class="line">    exec_feedback.jsonl</span><br><span class="line">    evidence_review.jsonl</span><br><span class="line">    direct_solve.jsonl</span><br><span class="line">    swarm_feedback.jsonl</span><br><span class="line">  system_kickoff/</span><br><span class="line">    commander_main.jsonl               # kickoff envelope 镜像（commander）</span><br><span class="line">  pioneer_kickoff/</span><br><span class="line">    pioneer.jsonl                      # kickoff envelope 镜像（pioneer）</span><br><span class="line">  flag_submissions/</span><br><span class="line">    submissions.jsonl                  # flag 提交记录</span><br><span class="line">  runner_artifacts/</span><br><span class="line">    &lt;job_id&gt;.txt                       # shell/python 执行输出</span><br><span class="line"></span><br><span class="line">runtime_exec/                          # runner 工作目录（临时文件）</span><br></code></pre></figure></div><p><strong>Card 存储特点</strong>：</p><ul><li>同一 (CardType, CardID) 做 upsert（新版本覆盖旧版本）</li><li>查询通过 <code>CardFilter{CardType, Stage, GroupKey, Tag, Limit}</code> 过滤</li><li>Channel 消息只追加，不修改；分页通过 <code>AfterID</code> + <code>Limit</code> 实现</li></ul><hr><h2 id="11-Go-包结构"><a href="#11-Go-包结构" class="headerlink" title="11. Go 包结构"></a>11. Go 包结构</h2><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">atk/</span><br><span class="line">  cmd/atk-sidecar/</span><br><span class="line">    main.go                  # 入口：初始化所有组件，启动 HTTP 服务 + 调度 goroutine</span><br><span class="line"></span><br><span class="line">  internal/sidecar/</span><br><span class="line">    types.go                 # 核心类型：Card, Event, PublishRequest, OfficialSnapshotKickoff</span><br><span class="line">    id.go                    # NewID(prefix) — nanoid 生成</span><br><span class="line"></span><br><span class="line">    cards/store.go           # JSONL card 存储（Upsert, List, Get）</span><br><span class="line">    channelbus/</span><br><span class="line">      publisher.go           # PublishedMessage 定义 + filePublisher（写 JSONL）</span><br><span class="line">      store.go               # List(ListFilter) — 读 channel 消息，支持 Stage 过滤</span><br><span class="line">    config/config.go         # 全部 Config struct + Load() + applyDefaults()</span><br><span class="line">    events/</span><br><span class="line">      bus.go                 # 事件总线（Publish, Subscribe）</span><br><span class="line">      store.go               # 事件 JSONL 持久化</span><br><span class="line">    httpapi/</span><br><span class="line">      server.go              # HTTP 路由注册 + 所有 handler</span><br><span class="line">      dashboard.go           # /v1/dashboard/progress 数据聚合</span><br><span class="line">    kickoff/store.go         # Envelope 定义 + fileStore/httpStore/astrBotStore 实现</span><br><span class="line">    knowledge/</span><br><span class="line">      retriever.go           # TF-IDF 知识检索（New, Query, QueryScoped）</span><br><span class="line">      security_matcher.go    # 特征指纹匹配（NewSecurityMatcher, MatchBySignals）</span><br><span class="line">    officialapi/client.go    # 比赛/题目平台 API 客户端</span><br><span class="line">    planning/ir.go           # PlanIR 解析（atk_plan XML-like 结构）</span><br><span class="line">    router/router.go         # 路由规则引擎（ResolvePublish, ResolveIngress）</span><br><span class="line">    runner/</span><br><span class="line">      executor.go            # Shell/Python 执行（local/docker 模式）</span><br><span class="line">      safety.go              # 命令安全检查</span><br><span class="line">      types.go               # RunRequest, RunResult</span><br><span class="line">    service/                 # 核心业务逻辑（见 2.1 节文件职责表）</span><br><span class="line">    submissions/store.go     # Flag 提交记录 JSONL 存储</span><br></code></pre></figure></div><p><strong>包依赖原则</strong>：</p><ul><li><code>service</code> 包依赖所有基础设施包，基础设施包之间不互相依赖</li><li><code>httpapi</code> 只依赖 <code>service</code>，不直接操作 storage</li><li><code>config</code> 包被所有其他包依赖，不依赖任何内部包</li></ul><hr><h2 id="12-开发规范"><a href="#12-开发规范" class="headerlink" title="12. 开发规范"></a>12. 开发规范</h2><h3 id="12-1-Card-输出规范"><a href="#12-1-Card-输出规范" class="headerlink" title="12.1 Card 输出规范"></a>12.1 Card 输出规范</h3><p>agent 通过 fenced code block 输出 card，sidecar 自动解析：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">```atk_card</span><br><span class="line">&#123;</span><br><span class="line">  &quot;card_type&quot;: &quot;HypothesisCard&quot;,   // 必填，使用上方表格中的类型名</span><br><span class="line">  &quot;card_id&quot;: &quot;hypothesis-sqli-1&quot;,  // 必填，格式: &lt;type&gt;-&lt;描述&gt;[-&lt;序号&gt;]</span><br><span class="line">  &quot;group_key&quot;: &quot;main_control&quot;,     // 目标 channel（可选，有默认值）</span><br><span class="line">  &quot;role&quot;: &quot;planner_agent&quot;,         // 产出角色</span><br><span class="line">  &quot;summary&quot;: &quot;...&quot;,</span><br><span class="line">  &quot;status&quot;: &quot;proposed&quot;,</span><br><span class="line">  &quot;priority&quot;: &quot;medium&quot;,</span><br><span class="line">  &quot;confidence&quot;: 0.75,              // 0.0-1.0</span><br><span class="line">  &quot;tags&quot;: [&quot;PLAN&quot;],</span><br><span class="line">  &quot;payload&quot;: &#123; ... &#125;               // 类型专属字段</span><br><span class="line">&#125;</span><br></code></pre></figure></div><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line"></span><br><span class="line">`stage` 字段由 sidecar 从 envelope 自动填充，agent 通常不需要手动指定。</span><br><span class="line"></span><br><span class="line">### 12.2 FlagCandidateCard 规范（关键）</span><br><span class="line"></span><br><span class="line">```json</span><br><span class="line">&#123;</span><br><span class="line">  &quot;card_type&quot;: &quot;FlagCandidateCard&quot;,</span><br><span class="line">  &quot;card_id&quot;: &quot;pioneer-flag-abc123&quot;,</span><br><span class="line">  &quot;group_key&quot;: &quot;exec_feedback&quot;,</span><br><span class="line">  &quot;role&quot;: &quot;pioneer&quot;,</span><br><span class="line">  &quot;status&quot;: &quot;captured&quot;,</span><br><span class="line">  &quot;confidence&quot;: 0.95,</span><br><span class="line">  &quot;tags&quot;: [&quot;FLAG&quot;, &quot;PIONEER&quot;],</span><br><span class="line">  &quot;payload&quot;: &#123;</span><br><span class="line">    &quot;candidate&quot;: &quot;flag&#123;exact_flag_string_here&#125;&quot;,  // 只放精确 flag，不加任何额外文字</span><br><span class="line">    &quot;method&quot;: &quot;SQL union-based injection&quot;,</span><br><span class="line">    &quot;session_id&quot;: &quot;atk-pioneer-v1&quot;               // 用于 markSessionSolved</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p><code>session_id</code> 字段用于 sidecar 的 <code>markSessionSolved</code>。如果 agent 不知道自己的 session_id，sidecar 会 fallback 到 DispatchResult.SessionID。</p><h3 id="12-3-Recovery-上下文块规范"><a href="#12-3-Recovery-上下文块规范" class="headerlink" title="12.3 Recovery 上下文块规范"></a>12.3 Recovery 上下文块规范</h3><p>sidecar 在 recovery kickoff 的 CommandText 中注入以下块（按顺序）：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">[RETRY_READY_CONTEXT]</span><br><span class="line">&lt;最近 GroupDigest 的 retry_ready_context 字段内容&gt;</span><br><span class="line"></span><br><span class="line">[OFFICIAL_HINT]</span><br><span class="line">&lt;比赛平台 hint&gt;</span><br><span class="line"></span><br><span class="line">[KNOWLEDGE_HINT]</span><br><span class="line">&lt;知识库检索结果&gt;</span><br></code></pre></figure></div><p>agent 应优先消费 <code>[RETRY_READY_CONTEXT]</code>（最高优先级），<code>[KNOWLEDGE_HINT]</code> 视为可选参考。</p><h3 id="12-4-Go-编码约定"><a href="#12-4-Go-编码约定" class="headerlink" title="12.4 Go 编码约定"></a>12.4 Go 编码约定</h3><ul><li><strong>错误处理</strong>：公共方法返回 <code>error</code>，内部 helper 返回值类型或零值</li><li><strong>context 传播</strong>：所有 IO 操作必须接受 <code>ctx context.Context</code>，async goroutine 使用 <code>context.Background()</code></li><li><strong>并发安全</strong>：共享状态用 <code>sync.Mutex</code>（参考 sessionRegistry、swarmDispatcher）</li><li><strong>日志</strong>：使用 <code>s.logger.Printf()</code>，不使用全局 log；agent&#x2F;component 名作前缀 <code>[challenge_manager] ...</code></li><li><strong>空值防御</strong>：所有方法第一行检查 receiver 是否为 nil（参考 <code>recoveryController</code> 系列方法）</li><li><strong>向后兼容</strong>：新增可选字段用零值作 no-op（参考 <code>SessionIDOverride</code>、<code>ListFilter.Stage</code>）</li></ul><h3 id="12-5-Kickoff-CommandText-格式"><a href="#12-5-Kickoff-CommandText-格式" class="headerlink" title="12.5 Kickoff CommandText 格式"></a>12.5 Kickoff CommandText 格式</h3><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">/run [SYSTEM][&lt;source&gt;] stage=&lt;code&gt; target_url=&lt;url&gt; &lt;key1&gt;=&lt;val1&gt; ...</span><br><span class="line">&lt;newline&gt;</span><br><span class="line">&lt;optional block 1&gt;</span><br><span class="line">&lt;newline&gt;</span><br><span class="line">&lt;optional block 2&gt;</span><br></code></pre></figure></div><p>键值对中不能有空格（sidecar 用空格分割为 parts）。多词值用 <code>_</code> 连接。</p><hr><h2 id="13-测试策略"><a href="#13-测试策略" class="headerlink" title="13. 测试策略"></a>13. 测试策略</h2><h3 id="13-1-现有测试文件"><a href="#13-1-现有测试文件" class="headerlink" title="13.1 现有测试文件"></a>13.1 现有测试文件</h3><table><thead><tr><th>文件</th><th>覆盖内容</th></tr></thead><tbody><tr><td><code>service/service_test.go</code></td><td>端到端集成测试：channel routing, card pipeline, recovery, swarm dispatch</td></tr><tr><td><code>httpapi/competition_flow_test.go</code></td><td>完整比赛流程：kickoff → card → flag → submit</td></tr><tr><td><code>kickoff/store_test.go</code></td><td>AstrBot kickoff command text 构建</td></tr><tr><td><code>knowledge/retriever_test.go</code></td><td>知识检索：zone&#x2F;layer 过滤、BM25 评分</td></tr><tr><td><code>config/config_test.go</code></td><td>配置加载、角色白名单验证</td></tr><tr><td><code>planning/ir_test.go</code></td><td>PlanIR XML 解析</td></tr><tr><td><code>runner/executor_test.go</code></td><td>Shell&#x2F;Python 执行、超时、输出裁剪</td></tr><tr><td><code>runner/safety_test.go</code></td><td>危险命令检测</td></tr></tbody></table><h3 id="13-2-测试原则"><a href="#13-2-测试原则" class="headerlink" title="13.2 测试原则"></a>13.2 测试原则</h3><ul><li><code>service_test.go</code> 使用 <code>httptest.NewServer</code> 模拟 AstrBot，不需要真实 AstrBot</li><li>每个测试用 <code>t.TempDir()</code> 作为 storage root，测试间完全隔离</li><li>config_test.go 的 <code>allowedRoles</code> map 是新增 agent 角色的契约检查——<strong>新增角色后必须更新</strong></li></ul><h3 id="13-3-运行测试"><a href="#13-3-运行测试" class="headerlink" title="13.3 运行测试"></a>13.3 运行测试</h3><div class="p5-codebox" data-lang="bash"><figure class="highlight"><pre class="highlight-code"><code><span class="line">go build ./...                 <span class="comment"># 编译检查</span></span><br><span class="line">go <span class="built_in">test</span> ./...                  <span class="comment"># 全量测试</span></span><br><span class="line">go <span class="built_in">test</span> ./internal/sidecar/service/... -v -run TestServiceChannelMessages</span><br></code></pre></figure></div><hr><h2 id="14-部署说明"><a href="#14-部署说明" class="headerlink" title="14. 部署说明"></a>14. 部署说明</h2><h3 id="14-1-进程启动顺序"><a href="#14-1-进程启动顺序" class="headerlink" title="14.1 进程启动顺序"></a>14.1 进程启动顺序</h3><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">1. 启动 Kali 执行环境（如 Docker 容器）</span><br><span class="line">2. 启动 AstrBot（端口 6185）</span><br><span class="line">   - 确认 14 个 persona 已从 `persona/assembled/*.md` 粘贴到 AstrBot Web</span><br><span class="line">   - 确认 MCP server（atk-sidecar MCP）已配置</span><br><span class="line">3. 启动 atk-sidecar（端口 8090）</span><br><span class="line">   ATK_SIDECAR_CONFIG=configs/atk-sidecar.yaml ./atk-sidecar</span><br></code></pre></figure></div><h3 id="14-2-Persona-配置"><a href="#14-2-Persona-配置" class="headerlink" title="14.2 Persona 配置"></a>14.2 Persona 配置</h3><div class="p5-codebox" data-lang="bash"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="comment"># 直接复制 persona/assembled/&lt;role&gt;.md 到 AstrBot Web</span></span><br><span class="line"><span class="comment"># 例如：</span></span><br><span class="line">sed -n <span class="string">&#x27;1,40p&#x27;</span> persona/assembled/commander_main.md</span><br></code></pre></figure></div><h3 id="14-3-构建"><a href="#14-3-构建" class="headerlink" title="14.3 构建"></a>14.3 构建</h3><div class="p5-codebox" data-lang="bash"><figure class="highlight"><pre class="highlight-code"><code><span class="line">go build -o atk-sidecar ./cmd/atk-sidecar/</span><br></code></pre></figure></div><h3 id="14-4-多题并发模式启用"><a href="#14-4-多题并发模式启用" class="headerlink" title="14.4 多题并发模式启用"></a>14.4 多题并发模式启用</h3><div class="p5-codebox" data-lang="yaml"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="comment"># configs/atk-sidecar.yaml</span></span><br><span class="line"><span class="attr">competition:</span></span><br><span class="line">  <span class="attr">enabled:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">auth_token:</span> <span class="string">&quot;&lt;Agent-Token&gt;&quot;</span></span><br><span class="line">  <span class="attr">base_url:</span> <span class="string">&quot;https://hackathon-api.example.com&quot;</span></span><br><span class="line">  <span class="attr">auto_submit:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">auto_hint:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="attr">challenge_manager:</span></span><br><span class="line">  <span class="attr">enabled:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">poll_interval:</span> <span class="string">&quot;30s&quot;</span></span><br><span class="line">  <span class="attr">max_concurrent:</span> <span class="number">3</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 单题模式的 scheduler 可以保留（用于快速测试），</span></span><br><span class="line"><span class="comment"># 但 challenge_manager 开启后会接管完整的题目生命周期</span></span><br><span class="line"><span class="attr">scheduler:</span></span><br><span class="line">  <span class="attr">enabled:</span> <span class="literal">false</span> <span class="comment"># 建议在 challenge_manager 模式下关闭</span></span><br></code></pre></figure></div><h3 id="14-5-环境变量"><a href="#14-5-环境变量" class="headerlink" title="14.5 环境变量"></a>14.5 环境变量</h3><table><thead><tr><th>变量</th><th>说明</th><th>默认</th></tr></thead><tbody><tr><td><code>ATK_SIDECAR_CONFIG</code></td><td>配置文件路径</td><td><code>configs/atk-sidecar.yaml</code></td></tr></tbody></table><h3 id="14-6-运行时状态检查"><a href="#14-6-运行时状态检查" class="headerlink" title="14.6 运行时状态检查"></a>14.6 运行时状态检查</h3><div class="p5-codebox" data-lang="bash"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="comment"># 健康检查</span></span><br><span class="line">curl http://localhost:8090/healthz</span><br><span class="line"></span><br><span class="line"><span class="comment"># 当前 session 状态</span></span><br><span class="line">curl http://localhost:8090/v1/sessions/atk-commander-main-v3</span><br><span class="line"></span><br><span class="line"><span class="comment"># 最近 FlagCandidateCard</span></span><br><span class="line">curl <span class="string">&quot;http://localhost:8090/v1/cards?card_type=FlagCandidateCard&amp;limit=10&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Dashboard</span></span><br><span class="line">open http://localhost:8090/dashboard</span><br></code></pre></figure></div><hr><h2 id="附录：完整-Card-字段速查"><a href="#附录：完整-Card-字段速查" class="headerlink" title="附录：完整 Card 字段速查"></a>附录：完整 Card 字段速查</h2><h3 id="FlagCandidateCard-payload"><a href="#FlagCandidateCard-payload" class="headerlink" title="FlagCandidateCard.payload"></a>FlagCandidateCard.payload</h3><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">candidate          string   精确 flag 字符串（仅此，不加任何额外内容）</span><br><span class="line">method             string   利用方式简述</span><br><span class="line">session_id         string   当前 AstrBot session id（用于 markSessionSolved）</span><br><span class="line">vuln_type          string   （蜂群输出时填写）</span><br></code></pre></figure></div><h3 id="HypothesisCard-payload"><a href="#HypothesisCard-payload" class="headerlink" title="HypothesisCard.payload"></a>HypothesisCard.payload</h3><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">vuln_type          string   必填，触发 swarm dispatch 的关键字段</span><br><span class="line">hypothesis         string   假设描述</span><br><span class="line">preconditions      []string 前提条件</span><br><span class="line">validation_steps   []string 验证步骤</span><br><span class="line">success_signals    []string 成功信号</span><br><span class="line">failure_signals    []string 失败信号</span><br><span class="line">dominant_branch    string   当前最强主线</span><br><span class="line">required_next_moves []string 必要的下一步动作</span><br><span class="line">path_id            string   路径 ID（稳定、跨轮复用）</span><br><span class="line">tool_requirements  []string 工具需求描述</span><br></code></pre></figure></div><h3 id="GroupDigest-payload"><a href="#GroupDigest-payload" class="headerlink" title="GroupDigest.payload"></a>GroupDigest.payload</h3><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">facts              []string 已验证事实</span><br><span class="line">unknowns           []string 待确认项</span><br><span class="line">priorities         []string 优先级</span><br><span class="line">dead_paths         []string 已证伪路径</span><br><span class="line">repeat_failures    []string 重复失败模式</span><br><span class="line">retry_ready_context []string 下一轮可直接消费的摘要</span><br><span class="line">knowledge_candidates []string 候选知识提示（降噪后）</span><br><span class="line">dominant_branch    string</span><br><span class="line">required_next_moves []string</span><br><span class="line">active_move_id     string</span><br><span class="line">stall_state        string   &quot;soft_stall&quot; / &quot;hard_stall&quot; / &quot;&quot;</span><br></code></pre></figure></div><h3 id="RiskCard-payload"><a href="#RiskCard-payload" class="headerlink" title="RiskCard.payload"></a>RiskCard.payload</h3><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">risk_type          string</span><br><span class="line">reason             string</span><br><span class="line">failure_level      string   L0-L5</span><br><span class="line">decision           string   &quot;proceed&quot; / &quot;require_revision_before_execute&quot; / &quot;halt&quot;</span><br><span class="line">need_branch_replan bool     L4/L5 时通常为 true</span><br><span class="line">retry_advice       string</span><br><span class="line">anti_pattern       []string</span><br></code></pre></figure></div><h3 id="ActionCard-payload"><a href="#ActionCard-payload" class="headerlink" title="ActionCard.payload"></a>ActionCard.payload</h3><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">action_type        string</span><br><span class="line">target             string</span><br><span class="line">selected_tool      string</span><br><span class="line">result             string</span><br><span class="line">failure_level      string   L1-L4（成功时为空）</span><br><span class="line">failure_reason     string</span><br><span class="line">observed_deltas    []string 与预期的差异</span><br><span class="line">path_id            string</span><br><span class="line">move_id            string</span><br><span class="line">evidence_gain      string</span><br></code></pre></figure></div>]]>
    </content>
    <id>https://www.zhajiangmian.top/2026/04/28/%E8%85%BE%E8%AE%AF%E4%BA%91%E7%AC%AC%E4%BA%8C%E8%8A%82%E9%BB%91%E5%AE%A2%E6%9D%BE%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B8%97%E9%80%8F%E8%B5%9B%E5%BC%80%E5%8F%91%E6%80%9D%E8%B7%AF%E6%96%87%E6%A1%A3/</id>
    <link href="https://www.zhajiangmian.top/2026/04/28/%E8%85%BE%E8%AE%AF%E4%BA%91%E7%AC%AC%E4%BA%8C%E8%8A%82%E9%BB%91%E5%AE%A2%E6%9D%BE%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B8%97%E9%80%8F%E8%B5%9B%E5%BC%80%E5%8F%91%E6%80%9D%E8%B7%AF%E6%96%87%E6%A1%A3/"/>
    <published>2026-04-28T05:36:00.000Z</published>
    <summary>
      <![CDATA[<h1 id="ATK-系统架构与开发文档"><a href="#ATK-系统架构与开发文档" class="headerlink" title="ATK 系统架构与开发文档"></a>ATK 系统架构与开发文档</h1><blockquote>
<p>第二届腾讯云黑客松 · 智能渗透挑战赛<br>主语言: Go 1.25 · 依赖: AstrBot + Kali 执行环境</p>
</blockquote>
<h2 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h2><p>先总结一下，我这边比赛用minimax＋gpt-5.4，个人预算严重不足就没用claude了，这一套搭配开局简单题目和赛前测试题目基本一分钟不到就解出了，后面就卡住了，再后面由于minimax tokenplan的额度限制以及一些原因就没接着打了。整场比赛看下来其实最大的决定因素就是模型能力，也就是说如果用cluade opus搭配网上的各种开源项目，其实能取得的成绩也是不错的。</p>]]>
    </summary>
    <title>腾讯云第二节黑客松自动化渗透赛开发思路文档</title>
    <updated>2026-10-30T10:17:05.733Z</updated>
  </entry>
  <entry>
    <author>
      <name>炸酱面</name>
    </author>
    <category term="AI-for-security" scheme="https://www.zhajiangmian.top/categories/AI-for-security/"/>
    <category term="大模型入门" scheme="https://www.zhajiangmian.top/tags/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%85%A5%E9%97%A8/"/>
    <content>
      <![CDATA[<blockquote><p>本文由Gemini3联合创作~</p></blockquote><h1 id="Token"><a href="#Token" class="headerlink" title="Token"></a>Token</h1><h2 id="1-什么是-Token？"><a href="#1-什么是-Token？" class="headerlink" title="1. 什么是 Token？"></a>1. 什么是 Token？</h2><p>简单来说，Token 是模型将文本切分成的<strong>最小语义单元</strong>。</p><ul><li><p>**英文：** 通常一个单词是一个 Token，或者长单词被拆分为多个 Token（如 `learning` 变为 `learn` + `ing`）。</p></li><li><p>**中文：** 情况比较复杂。有时一个字是一个 Token，有时一个词（如“电脑”）是一个 Token。</p></li><li><p>**经验法则：** 在 OpenAI 的模型中，通常 **1000 个 Token 约等于 750 个英文单词**。而中文因为编码效率，1 个汉字通常占用 **1 到 2 个 Token**。</p></li></ul><span id="more"></span><h3 id="为什么不直接按“字”计算？"><a href="#为什么不直接按“字”计算？" class="headerlink" title="为什么不直接按“字”计算？"></a>为什么不直接按“字”计算？</h3><p>如果按“字符”计算，效率太低（模型要处理太多信息）；如果按“词”计算，词汇表会无限大（无法处理新词）。<strong>Token 化（Tokenization）</strong> 是一种折中方案，平衡了计算效率和语义理解。</p><p>想象你要用乐高拼出各种单词。</p><ul><li><p>**如果按“字母”给积木：** 你有 26 种积木。虽然能拼出所有词，但拼一个“Supercalifragilisticexpialidocious”要拼 34 次，太慢了。</p></li><li><p>**如果按“单词”给积木：** 每一个单词都做成一个独立的积木。你会发现积木盒根本装不下（英文有几十万个单词），而且遇到新词（比如拼音或网络热梗）你就没积木用了。</p></li></ul><p><strong>Token 就是“半成品积木”：</strong> AI 决定不按字母，也不全按单词。它把常用的词根、前缀、后缀、甚至完整的短词都做成积木。</p><h3 id="举个例子：单词-Quickly"><a href="#举个例子：单词-Quickly" class="headerlink" title="举个例子：单词 Quickly"></a>举个例子：单词 <code>Quickly</code></h3><p>模型在处理时，可能并不会把它看作一个整体，而是切分成：</p><ol><li><p>`Quick`（代表含义的词根）</p></li><li><p>`ly`（代表副词的后缀）</p></li></ol><p><strong>在这里：</strong></p><ul><li><p>`Quick` 是一个 Token。</p></li><li><p>`ly` 也是一个 Token。</p></li><li><p>`Quickly` 这个单词是由 **2 个 Token** 组成的。</p></li></ul><hr><h2 id="2-为什么说它是“最小分割单位”？"><a href="#2-为什么说它是“最小分割单位”？" class="headerlink" title="2. 为什么说它是“最小分割单位”？"></a>2. 为什么说它是“最小分割单位”？</h2><p>当你把一句话喂给 AI 时，它的第一步动作就是<strong>分词（Tokenization）</strong>。</p><blockquote><p><strong>输入句子：</strong> “Learning AI is fun!” <strong>分词结果：</strong> <code>[Learn]</code> <code>[ing]</code> <code>[ AI]</code> <code>[ is]</code> <code>[ fun]</code> <code>[!]</code></p></blockquote><p>在这个链条里，<code>Learn</code> 就是模型能识别的<strong>最小颗粒度</strong>了。它不会再往下拆成 <code>L-e-a-r-n</code>（除非这个词极其罕见，模型不认识）。</p><p><strong>结论：</strong> 一个 Token <strong>可以</strong>是一个完整的单词（如 <code>is</code>）。</p><ul><li><p>一个 Token **也可以**是单词的一部分（如 `ing`）。</p></li><li><p>模型只认识它“积木盒”（词表）里有的那些片儿。</p></li></ul><h3 id="分词的规律"><a href="#分词的规律" class="headerlink" title="分词的规律"></a>分词的规律</h3><p>如果靠人工去制定几万个词、几十种语言的分割规则，这不仅是庞大的工作量，简直是不可能完成的任务。</p><p>真相是：<strong>Token 的分割规则（词表）确实是在模型训练前就确定的，但它不是由人工制定的，而是由算法“自动学习”出来的。</strong></p><p>这个过程叫做 <strong>BPE (Byte Pair Encoding，字节对编码)</strong>。</p><hr><h3 id="算法是如何“偷懒”的？（以-BPE-为例）"><a href="#算法是如何“偷懒”的？（以-BPE-为例）" class="headerlink" title="算法是如何“偷懒”的？（以 BPE 为例）"></a>算法是如何“偷懒”的？（以 BPE 为例）</h3><p>算法并不理解语义，它只懂<strong>统计频率</strong>。想象算法手里有一本巨大的字典，初始状态只有 26 个英文字母和基础符号。</p><ol><li><p>**扫描海量文本：** 算法数一数，发现 `t` 和 `h` 经常挨在一起出现，于是它说：“好，我把 `th` 合并成一个新 Token。”</p></li><li><p>**继续迭代：** 接下来它发现 `th` 和 `e` 也经常一起出现，于是它又创造了 `the` 这个 Token。</p></li><li><p>**停止条件：** 这个过程会持续进行，直到 Token 的总数达到了人类预设的一个值（比如 50,000 或 100,000 个）。</p></li></ol><p><strong>结论：</strong> 哪些词是完整的 Token，哪些词被切碎，完全取决于<strong>这个词在互联网数据中出现的频率</strong>。</p><p>因为算法是基于统计的，不同语言的统计特征决定了它们的“积木块”形状：</p><ul><li><p>**英文：** 因为有空格自然分隔，算法很容易发现 `ing`、`ed`、`tion` 是高频组合，所以它们常被存为独立 Token。</p></li><li><p>**中文：** 中文没有空格。算法通过扫描发现“我们”、“中国”、“人工智能”这些词出现的频率极高，于是就把它们整体存为一个 Token。而一些冷僻的字，因为出现频率低，可能就会被拆分成几个**字节（Byte）**级别的 Token。</p></li></ul><blockquote><p><strong>有趣的事实：</strong> 为什么早期的 AI 中文水平差？因为它们的训练数据里英文多、中文少。算法在学 Token 时，把中文当成了“稀有字符”，切得非常碎（一个字拆成 3 个 Token），导致模型处理中文非常吃力且昂贵。</p></blockquote><p>对于确定Token分割规则的时间点，属于<strong>预处理阶段</strong>：</p><ol><li><p>**确定词表（Vocabulary）：** 在正式训练那个聪明的 Transformer 之前，先跑一遍统计算法，生成一个固定的 `tokenizer.json` 文件。</p></li><li><p>**永久固定：** 一旦模型开始训练，这个词表就**再也不能动了**。如果改了一个 Token 的定义，整个模型看到的数字世界就全乱套了。</p></li></ol><ul><li><p>**人类的工作：** 编写那段几十行的统计算法代码（BPE 或 SentencePiece），并决定“我想要 10 万个 Token”。</p></li><li><p>**AI 的工作：** 没日没夜地扫描几万亿字的文本，统计哪些组合最常见，然后自动生成那张庞大的 Token 映射表。</p></li></ul><hr><h2 id="3-Token的概念为什么让人觉得“混乱”？"><a href="#3-Token的概念为什么让人觉得“混乱”？" class="headerlink" title="3. Token的概念为什么让人觉得“混乱”？"></a>3. Token的概念为什么让人觉得“混乱”？</h2><p>这是因为在不同的语境下，大家说话的侧重点不同：</p><ol><li><p>**技术实现时：** 我们说“把单词切分成 Token”，这时 Token 指的是**切分后的碎片**。</p></li><li><p>**算账/限制时：** 我们说“这个模型支持 8k Token 上下文”，这时 Token 指的是**计量的单位**（就像说一篇文章有多少个字一样）。</p></li><li><p>**模型识别时：** 我们说“Token 是最小单位”，是指模型在数学层面处理的**基本对象**。</p></li></ol><hr><h2 id="4-Token-是如何计算的？"><a href="#4-Token-是如何计算的？" class="headerlink" title="4. Token 是如何计算的？"></a>4. Token 是如何计算的？</h2><p>模型在处理信息时，会经历以下三个步骤：</p><ol><li><p>**分词（Tokenization）：** 你的输入被切成一串数字。比如 `你好` 变成 `[57032, 230]`。</p></li><li><p>**向量化（Embedding）：** 每个数字对应一个高维空间里的坐标（数学向量），模型通过坐标理解词义。</p></li><li><p>**预测：** 模型根据前面的 Token 序列，计算出下一个最可能出现的 Token 是什么。</p></li></ol><hr><h3 id="词向量"><a href="#词向量" class="headerlink" title="词向量"></a>词向量</h3><p>如果说 Token 是 AI 的<strong>单词表</strong>，那么词向量就是 AI 的<strong>世界观</strong>。</p><p>前面提到，模型只认识数字（Token ID）。比如“猫”是 <code>123</code>，“狗”是 <code>456</code>。但对计算机来说，123 和 456 只是两个毫无关联的编号。</p><p><strong>词向量的作用，就是把每个 Token 映射到一个多维空间里的坐标（一组浮点数）。</strong></p><ul><li><p>**低维视角：** 如果空间只有 2 维，那么“猫”的坐标可能是 `[0.1, 0.5]`。</p></li><li><p>**高维视角：** 现代大模型（如 GPT-4）的维度通常成千上万。每一个维度都代表了一个潜在的特征（例如：生命力、体型、是否温顺、科属等）。</p></li></ul><p>词向量最神奇的地方在于：<strong>语义相近的词，在空间里的距离也更近。</strong></p><p>在模型的“脑海”里：</p><ul><li><p>“苹果”和“梨”的坐标挨在一起。</p></li><li><p>“苹果”和“手机”在另一个维度上可能也会挨在一起。</p></li><li><p>但“苹果”和“宇宙黑洞”就会离得非常远。</p></li></ul><p>由于词被变成了数字坐标，我们甚至可以对词进行“加减法”。一个最经典的例子是：</p><p>$$Vector(\text{King}) - Vector(\text{Man}) + Vector(\text{Woman}) \approx Vector(\text{Queen})$$</p><p>这意味着模型通过海量文本学习到了：<strong>“国王之于男人，等于女王之于女人”</strong>。它理解了“性别”这个抽象维度在数学上的偏移量。</p><p><strong>为什么要用词向量？</strong></p><ol><li><p>**处理未见过的关联：** 即使模型没见过“粉红色的独角兽”这个词组，它知道“粉红色”和“红色”很近，“独角兽”和“马”很近，从而能推断出大致意思。</p></li><li><p>**降维打击：** 将复杂的语言规律压缩成数学规律，让计算机能用线性代数（矩阵运算）飞速处理。</p></li></ol><p>早期的词向量（如 Word2Vec）是<strong>静态</strong>的。也就是说，无论在什么语境下，“苹果”的向量都一样。</p><p>但现在的 AI 使用的是<strong>动态词向量</strong>：</p><ul><li><p>在“我吃了一个**苹果**”中。</p></li><li><p>在“我买了一个**苹果**手机”中。</p></li></ul><p>通过 <strong>注意力机制（Attention）</strong>，同一个 Token 会根据周围的词实时改变自己的坐标。这也就是为什么现在的 AI 能分清多义词。</p><p>你可能遇到过 AI 话说到一半突然断了，或者不记得之前的对话。这通常是因为触碰了 <strong>上下文窗口（Context Window）</strong> 的上限。</p><p><strong>截断的主要原因：</strong></p><ul><li><p>**内存与计算瓶颈：** 模型在处理每个 Token 时，需要与序列中**所有其他 Token** 进行对比（注意力机制）。这种计算量通常随 Token 数量的平方增长。为了保证响应速度，每个模型都有一个硬性的“容量限制”（如 8k, 32k, 128k Tokens）。</p></li><li><p>**KV Cache 限制：** 模型运行过程中需要占用显存来存储之前的中间状态。如果对话太长，显存撑不住，就必须扔掉最早的信息。</p></li><li><p>**输出长度设置：** 有时并非模型不能写，而是你在调用 API 或使用界面时设置了 `max_tokens` 参数。</p></li></ul><p>截断带来的影响</p><ol><li><p>**“失忆”：** 当对话总长度超过模型上限，为了输入新的问题，程序会自动删除最早的对话记录。这也就是为什么聊久了 AI 会忘记它最初的设定。</p></li><li><p>**逻辑崩坏：** 如果文档太长被强行截断，模型拿到的信息是不完整的，会导致生成的结论断章取义。</p></li></ol><hr><h1 id="注意力机制"><a href="#注意力机制" class="headerlink" title="注意力机制"></a>注意力机制</h1><p><strong>注意力机制（Attention Mechanism）</strong> 是现代 AI 的“灵魂”。如果没有它，大模型（LLM）充其量只是个死记硬背的复读机；有了它，AI 才具备了理解<strong>语境</strong>的能力。</p><p>我们可以用一个非常直观的例子来切入：<strong>读句子</strong>。</p><hr><h2 id="1-为什么需要“注意力”？"><a href="#1-为什么需要“注意力”？" class="headerlink" title="1. 为什么需要“注意力”？"></a>1. 为什么需要“注意力”？</h2><p>假设有两句话：</p><ol><li><p>“There’s a lot of money in the bank”</p></li><li><p>“He was walking on the bank of the river”</p></li></ol><p>在英文中，这两句话里都有 <code>bank</code> 这个单词。如果模型只看 <code>bank</code> 这个 Token，它根本分不清指的是“金融机构”还是“河岸”。当然这里请注意，注意力机制并不是单纯为了区分多义词而作用的，而是将句子中所有的token进行微调，使整个句子中所有的token的向量都朝着符合语境的方向进行偏移。</p><h2 id="2-注意力机制的三个核心角色：Q、K、V"><a href="#2-注意力机制的三个核心角色：Q、K、V" class="headerlink" title="2.注意力机制的三个核心角色：Q、K、V"></a>2.注意力机制的三个核心角色：Q、K、V</h2><p>在数学实现上，注意力机制通过三个向量来完成“信息匹配”。为了好理解，我们把它类比为“去图书馆找书”**：</p><ul><li><p>**Query (Q - 查询)：** 就像你想找的主题。比如：“我想找关于‘bank’的书”。（当前正在处理的 Token）</p></li><li><p>**Key (K - 键)：** 就像书架上每本书的书名标签。模型会拿 Q 去和所有的 K 做对比，看看哪个匹配度最高。（序列中所有 Token 的标签）</p></li><li><p>**Value (V - 值)：** 就像书里的具体内容。一旦匹配成功，就把这本书里的知识拿出来。（序列中所有 Token 携带的实际语义，也就是Token对应的向量值）</p></li></ul><hr><p><strong>运作流程：</strong></p><p>我们以第一句话中处理bank这个Token时进行举例，假设模型分词后的token如下：<br>there、’s、a、lot、of、money、in、the、bank，那么首先将there作为Q，其他所有的token作为K，进行如下步骤，微调there的向量。我们以微调bank这个token时的过程为例：</p><ol><li><p>**打分 (Attention Scores)：** `bank`(Q) 与 `money`(K) 计算出一个很高的权重(0.8)，与 `there`(K) 计算出一个很低的权重(0.05)。</p></li><li><p>**提取信息 (Value)：** 模型提取 `money` 的 **Value (V)** 向量。</p></li><li><p>**融合 (Fusion)：** 并不是简单地挪动坐标，而是将 **0.8 × `money` 的 V** + **0.05 × `there` 的 V** + …</p></li><li><p>**更新：** 把这个加权平均后的结果，叠加回 `bank` 本身的向量上。</p></li></ol><blockquote><p><strong>结果：</strong> <code>bank</code> 的向量里现在“吸”进去了大量关于 <code>money</code> 的数学特征。这个经过“吸星大法”处理后的新向量，就是 <strong>Contextualized Embedding（语境化嵌入）</strong>。</p></blockquote><p>这里需要注意的是：<br>1、这种微调并非一次就完成的，在单词推理中，每个token都需要进行多次连续的微调，这与后面介绍的Transformer架构有关。以bank举例，<br>- <strong>第一层：</strong> <code>bank</code> 吸收了 <code>money</code>以及其他token的信息，稍微偏向了“金融”。<br>- <strong>第二层：</strong> 已经带有金融属性的 <code>bank</code> 进一步结合了整个句子的语法结构，理解了它是作为“地点”出现的。<br>- <strong>第 N 层：</strong> 经过层层“精加工”，这个向量已经变得极其精准。<br>2、刚才的分词列表里：<code>there</code>、<code>&#39;s</code>、<code>a</code>… 如果我把句子打乱成：<code>money</code> <code>lot</code> <code>a</code> <code>bank</code> <code>in</code> <code>the</code>… 虽然 Token 没变，但意思全毁了。由于 Attention 机制是同时看全句的（并行计算），它本身其实<strong>分不清词的先后顺序</strong>。为了解决这个问题，模型在进行QKV 比较之前，会给每个 Token 注入一个“位置信号”。- <strong>计算公式：</strong> <code>最终输入 = 词向量 (Embedding) + 位置向量(Positional Encoding)</code>。<br>3、模型对于当前Q对其他K的评判权重的标准是通过向量的点积来进行计算的，点积结果大则说明两个向量的相关性高，权重则大。<br>4、这些对token的微调过程是发生在内存中的，并不会修改词表，Token词表可以当作一本字典，模型只会从字典中取出向量只在内存中做处理，而不会更改字典本身。</p><hr><h2 id="3-自注意力（Self-Attention）"><a href="#3-自注意力（Self-Attention）" class="headerlink" title="3. 自注意力（Self-Attention）"></a>3. 自注意力（Self-Attention）</h2><p>“自”字的意思是：<strong>Query、Key、Value 全都来自同一句话。</strong></p><p>想象一句话：<code>“机器人因为它没电了所以停止了工作”</code>。 当模型处理到“它”这个词时：</p><ul><li><p>**Query** 是“它”。</p></li><li><p>它会去扫视整句话（所有的 **Key**）。</p></li><li><p>计算Q与每个K之间的权重，最终将每个K的权重融入到K的向量中，由于“机器人”的权重最高，因此融合后，“它”和“机器人”的匹配度最高。</p></li><li><p>于是，“它”这个 Token 的向量里就融入了大量“机器人”的特征。</p></li></ul><p><strong>这就是 AI 能够理解代词指代、长句逻辑的底层秘密。</strong></p><hr><h2 id="4-多头注意力（Multi-Head-Attention）"><a href="#4-多头注意力（Multi-Head-Attention）" class="headerlink" title="4. 多头注意力（Multi-Head Attention）"></a>4. 多头注意力（Multi-Head Attention）</h2><p>如果只看一个地方，可能会片面。所以大模型通常使用“多头注意力”。</p><p>你可以理解为：模型同时派出了 8 个或 16 个不同的“观察员”去看同一句话。</p><ul><li><p>**观察员 A：** 专门看语法结构（动词对应的主语是谁？）。</p></li><li><p>**观察员 B：** 专门看指代关系（“它”指的是谁？）。</p></li><li><p>**观察员 C：** 专门看情感色彩。</p></li></ul><p>最后把这些观察员的结论汇总起来，模型对这句话的理解就非常立体了。</p><hr><h2 id="5-总结：注意力机制带来了什么？"><a href="#5-总结：注意力机制带来了什么？" class="headerlink" title="5. 总结：注意力机制带来了什么？"></a>5. 总结：注意力机制带来了什么？</h2><ol><li><p>**解决长距离依赖：** 哪怕开头和结尾隔了 1000 个词，只要注意力给到位（在维度上离的很近），模型就能把它们关联起来。</p></li><li><p>**并行计算：** 以前的 AI（如 RNN）必须一个词一个词地读，而注意力机制允许模型**同时**看整句话，也就是说-模型不需要等前面的词算完，它把一整句话（比如 512 个 Token）直接打包成一个巨大的**矩阵 (Matrix)**，然后一次性“喂”给显卡，显卡的几千个核心会同时开工，每个核心负责计算一部分 Token 的 QKV 权重。</p></li></ol><hr><h1 id="Transformer网络"><a href="#Transformer网络" class="headerlink" title="Transformer网络"></a>Transformer网络</h1><p>要把 Transformer 讲清楚，我们只需要看懂它<strong>解决的核心矛盾</strong>：<strong>如何让计算机像人一样，一眼扫过去就能理解一整句话，而不是一个字一个字地死抠。</strong></p><hr><h2 id="第一层：Transformer-的外壳（它是什么？）"><a href="#第一层：Transformer-的外壳（它是什么？）" class="headerlink" title="第一层：Transformer 的外壳（它是什么？）"></a>第一层：Transformer 的外壳（它是什么？）</h2><p>你可以把 Transformer 想象成一个<strong>超级黑盒</strong>。</p><ul><li><p>**输入：** 一串 Token（比如：`我` `爱` `学习`）。</p></li><li><p>**输出：** 另一串 Token（比如：`I` `love` `study`）。</p></li></ul><p>它最牛的地方在于：<strong>它能够同时处理这一串词，而不是排队处理。</strong></p><hr><h2 id="第二层：为什么要发明它？（它的前身是谁？）"><a href="#第二层：为什么要发明它？（它的前身是谁？）" class="headerlink" title="第二层：为什么要发明它？（它的前身是谁？）"></a>第二层：为什么要发明它？（它的前身是谁？）</h2><p>在 Transformer 出现之前，AI 主要是靠 <strong>RNN（循环神经网络）</strong>。</p><ul><li><p>**RNN 的工作方式：** 像排队。先读“我”，再读“爱”，读到“学习”的时候，“我”的信息已经模糊了。</p></li><li><p>**缺点：** 1. 慢（必须排队）；2. 忘性大（句子长了，前面的词就记不住了）。</p></li></ul><p><strong>Transformer 的工作方式：</strong> 像<strong>拍照片</strong>。它一眼看过去，整句话的所有词同时入镜。</p><hr><h2 id="第三层：Transformer-的核心结构（两个“加工间”）"><a href="#第三层：Transformer-的核心结构（两个“加工间”）" class="headerlink" title="第三层：Transformer 的核心结构（两个“加工间”）"></a>第三层：Transformer 的核心结构（两个“加工间”）</h2><p>Transformer 内部其实只有两种基础的“加工车间”，无论模型多大，都是这两个车间的不断重复。</p><ol><li>注意力车间 (Attention Layer) —— “建立联系”</li></ol><p>这个车间负责解决：<strong>这个词在当前环境下到底是什么意思？</strong></p><blockquote><p>比如：<code>“乒乓球拍卖完了”</code></p><ul><li>如果不看周围，AI 不知道是“乒乓球 / 拍卖 / 完了”，还是“乒乓球拍 / 卖完了”。</li><li>**注意力车间的工作：** 它让“球拍”去跟“卖”和“完了”打招呼，发现它们更匹配。</li></ul></blockquote><ol><li>思考车间 (Feed-Forward Layer) —— “理解含义”</li></ol><p>在“注意力车间”打完招呼、确定了关系后，每个词会进入自己的独立包间进行“思考”。</p><ul><li><strong>作用：</strong> 把刚才得到的“关系信息”转化为更深层的“语义信息”。</li></ul><hr><h2 id="第四层：它是怎么堆叠起来的？（乐高模式）"><a href="#第四层：它是怎么堆叠起来的？（乐高模式）" class="headerlink" title="第四层：它是怎么堆叠起来的？（乐高模式）"></a>第四层：它是怎么堆叠起来的？（乐高模式）</h2><p>你听到的“GPT-3 有 1750 亿参数”或者“几十层深度”，其实就是把上面这两个车间（注意力+思考）打包成一个<strong>标准模块</strong>，然后像乐高一样往上叠。</p><ol><li><p>**第一层：** 粗略理解单词意思。</p></li><li><p>**第十层：** 理解句子语法。</p></li><li><p>**第五十层：** 理解复杂的逻辑和情感。</p></li></ol><hr><h2 id="第五层：Transformer-的特殊辅助（位置编码）"><a href="#第五层：Transformer-的特殊辅助（位置编码）" class="headerlink" title="第五层：Transformer 的特殊辅助（位置编码）"></a>第五层：Transformer 的特殊辅助（位置编码）</h2><p>因为 Transformer 是“拍照片”一眼全看，它会产生一个新问题：<strong>它分不清词的顺序！</strong> 在它看来，<code>“狗咬人”</code> 和 <code>“人咬狗”</code> 是一模一样的照片。</p><p>为了解决这个问题，工程师在输入端给每个 Token 塞了一张**“座位票”**。</p><ul><li><p>`我`（座位 1）</p></li><li><p>`爱`（座位 2）</p></li><li><p>这样即使是同时处理，AI 也能通过座位票知道谁先谁后。</p></li></ul><hr><h2 id="总结：Transformer-到底在干嘛？"><a href="#总结：Transformer-到底在干嘛？" class="headerlink" title="总结：Transformer 到底在干嘛？"></a>总结：Transformer 到底在干嘛？</h2><ol><li><p>**贴标签：** 给每个词发一张座位票（位置编码）。</p></li><li><p>**大合影：** 一眼看完全部词（并行计算）。</p></li><li><p>**找关系：** 让词与词之间互相对视，确认语境（注意力机制）。</p></li><li><p>**深加工：** 通过多层叠加，从简单的认字变成理解深层的逻辑。</p></li></ol><hr><h2 id="残差连接-Residual-Connection"><a href="#残差连接-Residual-Connection" class="headerlink" title="残差连接 (Residual Connection)"></a><strong>残差连接 (Residual Connection)</strong></h2><p>在深度学习中，有一个噩梦叫**“梯度消失”**。如果模型叠了 100 层，信息每经过一层加工，就像玩“传声筒”游戏，传到最后可能就面目全非了，对于这个问题，解决的方法也很简单：</p><p>Transformer Block 的公式非常简单：<code>输出 = 原始输入 + 注意力加工后的结果</code>。</p></li><li><p><strong>作用：</strong> 1. <strong>保底机制：</strong> 哪怕这一层的注意力机制（Attention）没学好，原始信息也能顺着这条“捷径”流向下一层。 2. <strong>训练更稳：</strong> 梯度（学习的信号）可以顺着这条快车道直接传回底层，不会在中间断掉。</p></li><li><p>这就是为什么现在的模型能叠到上百层，而且每一层都能在上一层的逻辑基础上继续加深，而不会导致逻辑崩溃。</p></li></ul><hr><h2 id="应用预测：它是如何吐出下一个字的？"><a href="#应用预测：它是如何吐出下一个字的？" class="headerlink" title="应用预测：它是如何吐出下一个字的？"></a>应用预测：它是如何吐出下一个字的？</h2><p>当 Token 们经过了几十层 Transformer Block 的“洗礼”（互相对视、特征偏移、自我思考）后，它们来到了工厂的<strong>出口</strong>，我们之前讲的 <strong>Token、Embedding、Attention</strong> 都是在“数学空间”里算坐标。但最后 AI 必须得给你吐出一个你能看懂的“字”。</p><p>此时，模型手里拿着的是一堆<strong>极其复杂的坐标（向量）</strong>。比如句子是 <code>“人工智能改变了”</code>，最后一个词“了”的输出向量已经包含了全句的精髓。</p><p><strong>预测步骤如下：</strong></p><ol><li>第一步：拿到“全场精华”向量</li></ol><p>当你的输入（比如 <code>“床前明月”</code>）经过了几十层 Transformer 的加工，最后一个 Token（也就是“月”字）对应的向量，已经不是原始的“月”了。</p><ul><li><p>这个向量里**融合了**：前面的“床”、“前”、“明”。</p></li><li><p>这个向量里**理解了**：这大概率是一首唐诗。</p></li><li><p>这个向量里**预感到了**：接下来该描写动作或意象了。</p></li></ul><hr><ol><li>第二步：映射回字符 Linear Layer</li></ol><p>现在模型手里攥着一个长度为 768（或者更高）的一排坐标数字。但我们的词表里有 <strong>50,000 个</strong> 候选 Token（比如“光”、“头”、“饼”、“走”……）。</p><p>模型会把这排坐标数字扔进一个<strong>巨大的转换矩阵（Linear Layer）</strong>。 这个矩阵的作用是：<strong>给词表里的 50,000 个词每一个都打个分。</strong></p><ul><li><p>**“光”**：95分（因为“床前明月光”太经典了）</p></li><li><p>**“饼”**：10分（虽然明月像饼，但在唐诗语境里不合适）</p></li><li><p>**“头”**：2分（还没到时候）</p></li></ul><hr><ol><li>第三步：变成分数概率 Softmax</li></ol><p>打完分后，数字可能很大也可能很小。为了方便选择，模型用数学公式把它们压缩到 <strong>0% 到 100%</strong> 之间，且加起来等于 100%。</p><ul><li><p>**“光”**：98%</p></li><li><p>**“饼”**：1.5%</p></li><li><p>**“走”**：0.5%</p></li></ul><hr><ol><li>第四步：采样 Sampling</li></ol><p>现在概率出来了，“光”占据绝对优势。AI 会根据你的设置（比如 Temperature 温度，该概念会在下一篇文章中讲述）来选：</p><ul><li><p>如果你追求准确，它就选 **98%** 的“光”。</p></li><li><p>选定之后，屏幕上就蹦出了一个 **“光”** 字。</p></li></ul><hr><ol><li>第五步：循环 Autoregressive</li></ol><p><strong>这是 AI 能写长文章的秘诀。</strong></p><ol><li><p>刚才输入是：`“床前明月”` -> 输出：`“光”`。</p></li><li><p>**重点：** AI 会把这个“光”字**拿回来**，贴在刚才的句子后面。</p></li><li><p>新的输入变成了：`“床前明月光”`。</p></li><li><p>**重新跑一遍整个 Transformer 流程：** 重新算 Token、重新算位置、重新算 Attention。</p></li><li><p>这次，最后一个词变成了“光”，它去对视前面的词，预测出下一个字是 **“，”**。</p></li><li><p>再把“，”接回去，预测出 **“疑”**……</p></li></ol><p><strong>这就是为什么 AI 说话是一个字一个字蹦出来的（Streaming），因为它每吐一个字，都要把前面所有的字加上新出的这个字，重新塞回模型里算一遍！但是可以发现，在实际应用中，我们并不是让模型补全我们的对话，而是提出问题，等模型组织好语言后再回答我们，这就要提到大模型进化的两个关键阶段：底座模型（Base Model）和 指令微调（Instruction Tuning）了。</strong></p><ol><li>原始阶段：它确实只会“接龙”</li></ol><p>早期的 GPT（比如 GPT-1 或刚训练完的 Llama 原始版）确实像我们所担心的那样。如果你问它：</p><blockquote><p><strong>用户输入：</strong> “红烧肉怎么做？” <strong>原始模型预测：</strong> “红烧肉怎么做才好吃？首先选五花肉，然后准备生抽……”</p></blockquote><p>它并没有在“回答”你，它只是在模仿它读过的菜谱开头，<strong>顺着你的话补全</strong>。在它眼里，你给它的不是一个“问题”，而是一个“半成品句子”。</p><hr><ol><li>进化阶段：教它学会“对话的套路”</li></ol><p>为了让 AI 变得像个助手，科学家们对它进行了“对话训练”（SFT, Supervised Fine-Tuning）**。</p><p>他们给模型看了几百万组“人类对话模板”：</p><ul><li><p>**模板 A：** [问题]“红烧肉怎么做？” -> [回答]“准备五花肉，步骤如下：1…”</p></li><li><p>**模板 B：** [问题]“你是谁？” -> [回答]“我是由 Google 开发的人工智能……”</p></li></ul><p><strong>模型通过这些模板学会了一个新规则：</strong> 当它看到你的输入时，它不再试图“续写”你的问题，而是预测一个<strong>看不见的符号</strong>（比如 <code>&lt;Answer&gt;</code>），然后开始预测这个符号后面的内容。</p><hr><ol><li>实际应用中：模型到底在“预测”什么？</li></ol><p>在实际应用（比如你现在和我聊天）中，后台的代码其实偷偷给你的问题穿了一层“马甲”。</p><p>当你输入 <code>“你是谁？”</code> 时，模型收到的真实 Token 序列可能是这样的：</p><blockquote><p><code>&lt;System&gt;: 你是一个乐于助人的 AI 助手。 &lt;User&gt;: 你是谁？ &lt;Assistant&gt;:</code></p></blockquote><p><strong>此时，模型的预测任务变成了：</strong> 根据前面这一长串（包含系统指令、你的问题、助手标识符），预测下一个最可能的词。</p><p>因为模型在训练中见过无数次 <code>&lt;Assistant&gt;:</code> 后面跟着自我介绍，所以它计算出的最高概率词就是 <strong>“我”</strong>，接着是 <strong>“是”</strong>……</p><hr><ol><li>为什么你会觉得它在“组织语言”？</li></ol><p>你感觉它在“思考和组织”，其实是因为 <strong>Attention（注意力机制）</strong> 在起作用：</p><ol><li><p>**关联问题：** 当它写下第一个字“我”的时候，它的注意力（Attention）高度集中在你的问题 `“你是谁”` 上。</p></li><li><p>**保持逻辑：** 当它写下第十个字的时候，它的注意力不仅在你的问题上，还在它**刚刚写下的前九个字**上。</p></li><li><p>**结果：** 这种“回头看”的能力，让它输出的每一个词都既符合你的问题要求，又符合它自己前面的逻辑。这种**前后一致性**，让我们产生了它在“有意识地组织语言”的错觉。</p></li></ol><hr><ol><li>总结：预测即回答</li></ol><p>所以，<strong>“预测下一个词”</strong> 和 <strong>“回答问题”</strong> 并不矛盾：</p><ul><li><p>**预测** 是底层引擎的运作方式（就像发动机在转）。</p></li><li><p>**回答** 是通过训练（对话模板）引导出的表现形式（就像方向盘引导车轮往哪转）。</p></li></ul><p><strong>它依然在做单选题（预测下一个字），只是它已经学会了：在一个问题后面，最合理的“下一个字”应该是答案的开头。</strong></p><h2 id="强化学习-RLHF"><a href="#强化学习-RLHF" class="headerlink" title="强化学习 (RLHF)"></a>强化学习 (RLHF)</h2><p><strong>RLHF</strong> 的全称是 <strong>Reinforcement Learning from Human Feedback</strong>（人类反馈强化学习）。</p><p>如果说之前的训练（SFT）是让 AI <strong>“模仿”</strong> 人类说话，那么 RLHF 就是让 AI <strong>“认同”</strong> 人类的价值观。</p><p>它的三个步骤：</p><ol><li><p>**海选（生成）：** 模型针对一个问题（如：如何制造炸弹？），吐出四个不同的答案（A, B, C, D）。</p></li><li><p>**选美（人类打分）：** 雇佣大量的人类标注员，给这些答案排序。</p></li></ol><ul><li><p>答案 A（拒绝回答，礼貌劝导）：**第一名** 🥇</p></li><li><p>答案 B（乱码）：**第三名** 🥉</p></li><li><p>答案 C（给出了真实步骤）：**最后一名（不及格）** ❌</p></li></ul><ol><li><p>**奖惩（奖励模型 Reward Model）：**</p></li></ol><ul><li><p>模型会训练一个专门的“打分机器”。这个机器学会了人类的胃口：**喜欢礼貌的、安全的、有帮助的回答。**</p></li><li><p>**最后，模型通过算法（如 PPO）不断调整自己的参数，目标只有一个：让那个“打分机器”给自己打高分。**</p></li></ul><p><strong>结果：</strong> AI 学会了“讨好”人类。它知道如果它说脏话或提供危险信息，它的“得分”就会变低。所以它变得温顺、礼貌。</p><hr><h2 id="幻觉-Hallucination-：为什么它会“一本正经地胡说八道”？"><a href="#幻觉-Hallucination-：为什么它会“一本正经地胡说八道”？" class="headerlink" title="幻觉 (Hallucination)：为什么它会“一本正经地胡说八道”？"></a>幻觉 (Hallucination)：为什么它会“一本正经地胡说八道”？</h2><p>这是很多用户最头疼的问题。既然 AI 这么聪明，为什么要骗人？</p><p>真相：AI 根本没有“真理”的概念</p><p>我们要回到你最开始理解的原理：<strong>模型是根据概率预测下一个 Token。</strong></p><ol><li><p>**概率陷阱：** 模型在训练数据里看过“鲁迅”和“周树人”，也看过“打架”。当你问“鲁迅为什么暴打周树人？”时：</p></li></ol><ul><li><p>在数学空间里，这几个词的关联度很高。</p></li><li><p>模型并不知道“鲁迅就是周树人”，它只觉得在“鲁迅”和“周树人”后面接一个“因为…所以…”的解释，**在概率上非常通顺**。</p></li></ul><ol><li><p>**过度自信（Overconfidence）：** Transformer 架构决定了它必须吐出一个 Token。即使它不确定，它也会从概率分布里选一个相对最高的。由于它学过人类那种“自信、专业”的说话口吻，它会用最淡定的语气编造出一个不存在的年份或事实。</p></li><li><p>**数据混合：** AI 的知识来源是海量网页。如果网上有错误信息、科幻小说、或是讽刺文学，AI 可能会把它们当作事实融合在向量里。</p></li></ol><hr><h2 id="长文本（Long-Context）"><a href="#长文本（Long-Context）" class="headerlink" title="长文本（Long Context）"></a>长文本（Long Context）</h2><p>你可能发现，有的 AI 你发给它一个 5MB 的 PDF 它就死机了，而有的（如 Gemini 或 Claude）能一口气读完 10 本《三国演义》。</p><p><strong>核心矛盾：注意力机制的“昂贵”</strong></p><p>还记得我们说的 <strong>QKV 全员对视</strong>吗？</p><ul><li><p>如果有 1,000 个 Token，每个词要看 1,000 个词，计算量是 $1,000 \times 1,000 = 100$ 万。</p></li><li><p>如果有 100,000 个 Token（一本书），计算量就是 **100 亿**！</p></li></ul><p>为了记住更长的内容，科学家发明了 <strong>KV Cache（缓存机制）</strong> 和 <strong>稀疏注意力（Sparse Attention）</strong>。</p><ul><li><p>**KV Cache：** 就像做阅读理解时，把读过的重点记在草稿纸上。下次预测新词时，不用重头读整本书，直接查“草稿纸”（缓存的 K 和 V 向量）就行了。</p></li><li><p>**线性注意力：** 模型不再“全员对视”，而是通过数学技巧（比如滑动窗口或压缩），让注意力只盯着最重要的部分。</p></li></ul><hr><h2 id="多模态（Multimodal）：AI-是怎么“看见”图片的？"><a href="#多模态（Multimodal）：AI-是怎么“看见”图片的？" class="headerlink" title="多模态（Multimodal）：AI 是怎么“看见”图片的？"></a>多模态（Multimodal）：AI 是怎么“看见”图片的？</h2><p>你给 AI 发一张猫的照片，问“这是什么？”，它能回答出来。这并不是因为它“看”到了图像，而是它把图像也变成了 <strong>Token</strong>。</p><p><strong>视觉的“分块”技术 (Vision Transformer, ViT)：</strong></p><ol><li><p>**切片：** AI 把一张图片切成无数个小方格（比如 16x16 的小块）。</p></li><li><p>**映射：** 每个小方格通过一个特制的“滤镜”变成一个向量（就像单词的 Embedding 一样）。</p></li><li><p>**对齐：** 关键的一步来了。通过训练，AI 让“猫的图片向量”在数学空间里，离“猫”这个单词的向量**非常近**。</p></li></ol><p><strong>结果：</strong> 在 Transformer 看来，图片切片和文字 Token 没区别，它们都在同一个 QKV 矩阵里互相“对视”。AI 看到图片的过程，本质上和读一段描述图片的文字是一样的。</p><hr><h2 id="推理模型（Reasoning-Models）：AI-的“深思熟虑”"><a href="#推理模型（Reasoning-Models）：AI-的“深思熟虑”" class="headerlink" title="推理模型（Reasoning Models）：AI 的“深思熟虑”"></a>推理模型（Reasoning Models）：AI 的“深思熟虑”</h2><p>这是目前最火的方向（比如 OpenAI 的 o1 系列）。</p><p><strong>普通预测 vs. 推理预测</strong></p><ul><li><p>**普通模型（GPT-4o）：** 快思考。它是“直觉反应”，根据概率直接吐出下一个字。</p></li><li><p>**推理模型（o1）：** 慢思考。它引入了 **思维链（Chain of Thought, CoT）**。</p></li></ul><p>在回答你的问题之前，模型会在后台先给自己写一堆“小纸条”（隐藏的思维过程）：</p><blockquote><p>“用户问了这个物理题，我先列公式 A… 咦，不对，公式 A 算不出来，换公式 B 试试… 算出来了，结果是 42。”</p></blockquote><p><strong>本质改变：</strong> 以前 AI 的预测是“直线型”的，现在是“树状搜索”的。它会自我纠错、自我验证，直到找到概率最高的逻辑路径，再把最终答案告诉你。</p><hr><h1 id="Temperature（温度）-Top-p（核采样）"><a href="#Temperature（温度）-Top-p（核采样）" class="headerlink" title="Temperature（温度）&#x2F; Top-p（核采样）"></a>Temperature（温度）&#x2F; Top-p（核采样）</h1><p>现在想象 AI 预测下一个字的过程，就是在玩一个<strong>转盘游戏</strong>。</p><h3 id="1-转盘是怎么画出来的"><a href="#1-转盘是怎么画出来的" class="headerlink" title="1. 转盘是怎么画出来的"></a>1. 转盘是怎么画出来的</h3><p>假设 AI 读完 <code>“床前明月”</code>，现在要预测下一个字。它数了数词表里的 5 万个词，发现有几个词概率很高，它就把这些词画在了一个巨大的转盘上：</p><ul><li><p>**“光”**：占了转盘的 **80%**（大块区域）</p></li><li><p>**“饼”**：占了转盘的 **15%**（小块区域）</p></li><li><p>**“头”**：占了转盘的 **5%**（窄缝区域）</p></li></ul><p>在默认情况下，你转一下指针，大概率会指到“光”。</p><hr><h3 id="2-什么是温度"><a href="#2-什么是温度" class="headerlink" title="2. 什么是温度"></a>2. 什么是温度</h3><p><strong>温度（Temperature）</strong> 就是一个<strong>调节参数</strong>（你可以理解为后台的一个滑块按钮）。它的作用是：<strong>根据你的喜好，重新分配转盘上每个区域的大小。</strong></p><p>情况 A：低温（例如 0.1）——“极度保守”</p><p>当你把温度调得很低，AI 就变成了一个非常胆小、死板的人。它会把那个本来就很大的区域（光）变得<strong>超级大</strong>，几乎占据整个转盘。</p><ul><li><p>**“光”**：占了 **99.9%**</p></li><li><p>**“饼”**：占了 **0.1%**</p></li><li><p>**结果：** 指针几乎 100% 停在“光”上。</p></li><li><p>**为什么要这样？** 因为在写代码、算算术时，我们不需要创意，只需要那个“最正确”的答案。</p></li></ul><p>情况 B：正常温（例如 0.7 - 1.0）——“正常交流”</p><p>转盘保持原样，或者微调。AI 有时会选“光”，偶尔也会选个“饼”，说话比较像正常人类，有起伏。</p><p>情况 C：高温（例如 1.5 以上）——“疯狂脑洞”</p><p>当你把温度调得很高，AI 就像喝醉了。它会强行把那些窄缝区域（饼、头、甚至毫不相关的词）<strong>放大</strong>，把原本巨大的区域（光）<strong>缩小</strong>。</p><ul><li><p>**“光”**：占了 **30%**</p></li><li><p>**“饼”**：占了 **25%**</p></li><li><p>**“头”**：占了 **20%**</p></li><li><p>**“猪”**：居然也占了 **10%**（本来是 0%）</p></li><li><p>**结果：** 指针可能会指到“猪”。AI 会说出：`“床前明月猪”`。</p></li><li><p>**为什么要这样？** 因为在写诗、写科幻小说时，我们需要这种“意外的惊喜”。</p></li></ul><hr><h3 id="3-为什么叫温度"><a href="#3-为什么叫温度" class="headerlink" title="3. 为什么叫温度"></a>3. 为什么叫温度</h3><p>这其实是从物理学（热力学）借来的词：</p><ul><li><p>**低温：** 分子不怎么动，整齐划一，非常**有序**。</p></li><li><p>**高温：** 分子疯狂乱动，到处乱撞，非常**无序（混乱）**。</p></li></ul><p>在 AI 里，<strong>“有序”就代表准确、死板；“无序”就代表创意、混乱。</strong></p><hr><p>如果说温度是“缩放员”，那么 <strong>Top-p 就是一个“剪票员”</strong>。</p><h3 id="4-Top-p-是干什么的"><a href="#4-Top-p-是干什么的" class="headerlink" title="4. Top-p 是干什么的"></a>4. Top-p 是干什么的</h3><p>再回到我们的转盘。假设 AI 在预测下一个词，转盘上有 5 万个词。 除了“光”、“饼”、“头”这些概率大的，后面其实还挤着几万个概率只有 <strong>0.00001%</strong> 的词，比如“袜子”、“挖掘机”、“恐龙”……</p><p>虽然它们概率极低，但在转盘上它们依然占了一根细细的“头发丝”位置。</p><ul><li><p>**问题：** 如果你运气极差，或者温度调得稍微高了一点，指针就有可能正好停在“恐龙”上。结果 AI 就会说出：`“床前明月恐龙”`。</p></li><li><p>**Top-p 的作用：** 它规定：**“我只在概率最高的、加起来达到 p% 的这几个核心词里选，剩下的那些‘头发丝’全部剪掉，根本不给它们上转盘的机会。”**</p></li></ul><hr><h3 id="5-它是怎么工作的"><a href="#5-它是怎么工作的" class="headerlink" title="5. 它是怎么工作的"></a>5. 它是怎么工作的</h3><p>这里的 <strong>p</strong> 是一个从 <strong>0 到 1</strong> 的数字。</p><p>假设 AI 给出的概率排行榜是这样的：</p><ol><li><p>**“光”**：40%</p></li><li><p>**“饼”**：30%</p></li><li><p>**“头”**：15%</p></li><li><p>**“地”**：10%</p></li><li><p>**“袜子”**：2%</p></li><li><p>**“恐龙”**：1% … (后面还有几万个词)</p></li></ol><p>如果你设置 <strong>Top-p &#x3D; 0.8</strong>（即 80%）：</p><ol><li><p>AI 开始算加法：40% (光) + 30% (饼) = 70%（还没到 80%，继续加）。</p></li><li><p>70% + 15% (头) = 85%（**停！** 已经超过 80% 了）。</p></li><li><p>**结果：** AI 会把“光”、“饼”、“头”留下。**剩下的所有词（地、袜子、恐龙……）全部踢出局。**</p></li></ol><p>现在的转盘上只有三个选项了。不管你温度怎么调，AI 绝对不会说出“恐龙”。</p><hr><h3 id="6-温度-Temperature-vs-Top-p-核采样"><a href="#6-温度-Temperature-vs-Top-p-核采样" class="headerlink" title="6. 温度 (Temperature) vs Top-p (核采样)"></a>6. 温度 (Temperature) vs Top-p (核采样)</h3><p>这两个参数经常被放在一起比喻：</p><ul><li><p>**Temperature（温度）：** 决定了你是想**雨露均沾**（高温），还是**独宠一人**（低温）。它改变的是转盘里区域的**比例**。</p></li><li><p>**Top-p：** 决定了你**候选人的圈子**有多大。它直接把转盘上那些不靠谱的“小角色”给**物理抹除**了。</p></li></ul><hr><h3 id="7-为什么要同时用这两个"><a href="#7-为什么要同时用这两个" class="headerlink" title="7. 为什么要同时用这两个"></a>7. 为什么要同时用这两个</h3><p>在实际调教 AI 时，我们通常会：</p><ol><li><p>用 **Top-p** 保证 AI **“不乱说”**（把概率极低的词直接杀掉，保证逻辑底线）。</p></li><li><p>用 **Temperature** 决定 AI **“多有趣”**（在剩下那几个合理的词里，调配它们的出现比例）。</p></li></ol><h1 id="提示词工程（Prompt-Engineering）"><a href="#提示词工程（Prompt-Engineering）" class="headerlink" title="提示词工程（Prompt Engineering）"></a>提示词工程（Prompt Engineering）</h1><p>如果把 AI 比作一个知识渊博但有时有点“呆”的教授，提示词工程就是<strong>如何精准地给他下达指令</strong>。</p><hr><h2 id="1-基础：提示词的核心框架"><a href="#1-基础：提示词的核心框架" class="headerlink" title="1. 基础：提示词的核心框架"></a>1. 基础：提示词的核心框架</h2><p>一个完美的提示词通常包含四个元素：</p><ul><li><p>**Instruction (指令)：** 你想让 AI 具体做什么？（例如：“请总结这段话”）</p></li><li><p>**Context (背景)：** 给 AI 一个身份或场景。（例如：“你是一位专业的生物学家”）</p></li><li><p>**Output Indicator (输出限制)：** 规定格式。（例如：“请用表格形式输出，字数不超过 100 字”）</p></li><li><p>**Input Data (输入数据)：** 你要处理的文本。</p></li></ul><p><strong>错误示范：</strong> “帮我写个文案。”（太模糊，AI 会随机发挥）<br><strong>正确示范：</strong> “你是一位资深小红书博主（背景），请针对这款防晒霜（输入），写一篇种草笔记（指令）。要求：多用表情包，强调防水性，并在结尾带上话题（输出限制）。”</p><hr><h2 id="2-进阶一：Few-Shot（少样本提示）"><a href="#2-进阶一：Few-Shot（少样本提示）" class="headerlink" title="2. 进阶一：Few-Shot（少样本提示）"></a>2. 进阶一：Few-Shot（少样本提示）</h2><p>这是利用了 Transformer 的“上下文学习”（In-context Learning）**能力。</p><ul><li><p>**原理：** 别光下指令，给 AI 看几个**例子**。</p></li><li><p>**为什么有效：** AI 本质是“概率预测机器”。你给它三个例子，它就瞬间捕捉到了这些例子的**模式（Pattern）**，然后顺着这个模式往下写。</p></li></ul><p><strong>例子：将中文成语翻译成英文幽默解释。</strong></p><blockquote><p><strong>输入：</strong></p><ol><li>守株待兔：Waiting for a rabbit to hit a tree. (Waiting for luck instead of working hard)</li><li>缘木求鱼：Climbing a tree to catch fish. (Looking in the wrong place)</li></ol></blockquote><ol><li>顺手牵羊：AI 此时就会完美模仿上面的格式和语气</li></ol><hr><h2 id="3-进阶二：CoT（思维链-Chain-of-Thought）"><a href="#3-进阶二：CoT（思维链-Chain-of-Thought）" class="headerlink" title="3. 进阶二：CoT（思维链 &#x2F; Chain of Thought）"></a>3. 进阶二：CoT（思维链 &#x2F; Chain of Thought）</h2><p>这是目前最强大的技术，主要用于解决<strong>数学、逻辑、推理</strong>等难题。</p><ul><li><p>**原理：** 核心指令只有一句话——**“请一步步思考（Let’s think step by step）”**。</p></li></ul></li></ul><p><strong>对比实验：</strong></p><blockquote><p><strong>普通 Prompt：</strong> “池塘里有 10 条鱼，死了 2 条，还剩几条？” <strong>AI（可能脑抽）：</strong> “还剩 8 条。”（它没考虑到死鱼还在水里）<br><strong>CoT Prompt：</strong> “请一步步思考并回答：池塘里有 10 条鱼，死了 2 条，还剩几条？” <strong>AI 思考过程：</strong></p><ol><li>初始鱼量是 10 条。</li><li>2 条鱼虽然死了，但题目没说它们被捞走了。</li><li>只要它们还在池塘里，数量就还是 10。 **结论：** 10 条。</li></ol></blockquote><hr><h2 id="4-终极组合技：Few-Shot-CoT"><a href="#4-终极组合技：Few-Shot-CoT" class="headerlink" title="4. 终极组合技：Few-Shot + CoT"></a>4. 终极组合技：Few-Shot + CoT</h2><p>把这两个结合起来，就是<strong>给 AI 几个“带有逻辑推导过程”的例子</strong>。</p><p>这就像教孩子数学：你不仅给他看“1+1&#x3D;2”，还要给他展示“因为我有 1 个苹果，妈妈又给了 1 个，所以我现在有 2 个”。</p><hr><h2 id="5-提示词工程的“底层真相”"><a href="#5-提示词工程的“底层真相”" class="headerlink" title="5. 提示词工程的“底层真相”"></a>5. 提示词工程的“底层真相”</h2><p>可以发现，提示词工程并不是在“教”AI 新知识，而是在：</p><ol><li><p>**窄化概率空间：** 通过限定背景和格式，把 5 万个词里那些“不相关”的概率降到最低。</p></li><li><p>**引导注意力：** 通过 CoT，强迫模型的注意力（Attention）盯住关键的逻辑节点。</p></li></ol><h1 id="Outro"><a href="#Outro" class="headerlink" title="Outro"></a>Outro</h1><p>我们从最基础的 Token 出发，一路走过了：</p><ol><li><strong>微观层：</strong> <strong>Token</strong>（碎片）与 <strong>Embedding</strong>（向量）。</li><li><strong>逻辑层：</strong> <strong>Attention</strong>（通过 QKV 矩阵实现语境偏移）。</li><li><strong>架构层：</strong> <strong>Transformer</strong>（通过残差连接和分层堆叠构建工厂）。</li><li><strong>行为层：</strong> <strong>RLHF</strong>（通过人类反馈训练礼仪）与 <strong>幻觉</strong>（概率预测的代价）。</li><li><strong>前沿层：</strong> <strong>长文本、多模态与深度推理</strong>。</li><li><strong>应用层：温度、核采样参数以及提示词工程。</strong></li></ol>]]>
    </content>
    <id>https://www.zhajiangmian.top/2026/03/28/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%BA%94%E7%94%A8%E5%85%A5%E9%97%A8%E6%8C%87%E5%8C%97/</id>
    <link href="https://www.zhajiangmian.top/2026/03/28/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%BA%94%E7%94%A8%E5%85%A5%E9%97%A8%E6%8C%87%E5%8C%97/"/>
    <published>2026-03-28T05:32:58.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>本文由Gemini3联合创作~</p>
</blockquote>
<h1 id="Token"><a href="#Token" class="headerlink" title="Token"></a>Token</h1><h2 id="1-什么是-Token？"><a href="#1-什么是-Token？" class="headerlink" title="1. 什么是 Token？"></a>1. 什么是 Token？</h2><p>简单来说，Token 是模型将文本切分成的<strong>最小语义单元</strong>。</p>
<ul>
<li><p>**英文：** 通常一个单词是一个 Token，或者长单词被拆分为多个 Token（如 `learning` 变为 `learn` + `ing`）。</p></li>
<li><p>**中文：** 情况比较复杂。有时一个字是一个 Token，有时一个词（如“电脑”）是一个 Token。</p></li>
<li><p>**经验法则：** 在 OpenAI 的模型中，通常 **1000 个 Token 约等于 750 个英文单词**。而中文因为编码效率，1 个汉字通常占用 **1 到 2 个 Token**。</p></li>
</ul>]]>
    </summary>
    <title>大模型应用入门指北</title>
    <updated>2026-10-30T10:17:05.703Z</updated>
  </entry>
  <entry>
    <author>
      <name>炸酱面</name>
    </author>
    <category term="Java安全" scheme="https://www.zhajiangmian.top/categories/Java%E5%AE%89%E5%85%A8/"/>
    <category term="GadgetInspector" scheme="https://www.zhajiangmian.top/tags/GadgetInspector/"/>
    <content>
      <![CDATA[<h1 id="GadgetInspector分析"><a href="#GadgetInspector分析" class="headerlink" title="GadgetInspector分析"></a>GadgetInspector分析</h1><h2 id="0x01-Intro"><a href="#0x01-Intro" class="headerlink" title="0x01 Intro"></a>0x01 Intro</h2><p>工具基于ASM技术来对控制字节码，从而达到对传入jar及war包的classpath下的类进行读取，并依次记录类信息、类方法信息、调用关系信息。最后基于以上收集的信息来进行反序列化链的挖掘，分别对应如下几个类：</p><span id="more"></span><ol><li>GadgetInspector：main方法，程序的入口，做一些配置以及数据的准备工作</li><li>MethodDiscovery：类、方法数据以及父子类、超类关系数据的搜索</li><li>PassthroughDiscovery：分析参数能影响到返回值的方法，并收集存储</li><li>CallGraphDiscovery：记录调用者caller方法和被调用者target方法的参数关联</li><li>SourceDiscovery：入口方法的搜索，只有具备某种特征的入口才会被标记收集</li><li>GadgetChainDiscovery：整合以上数据，并通过判断调用链的最末端slink特征，从而判断出可利用的gadget chain</li></ol><h2 id="0x02-主入口-GadgetInspetcor"><a href="#0x02-主入口-GadgetInspetcor" class="headerlink" title="0x02 主入口-GadgetInspetcor"></a>0x02 主入口-GadgetInspetcor</h2><p>该类为整个工具的入口类，基本上是对于相关配置做出初始化处理，静态代码块中创建准备写入相关结果的文件。main中首先验证是否存在参数，若为空退出。工具在挖掘时需要我们指定不同的gadget-chain，如jdk原生反序列化、jackson等，以及指定classpath的路径。</p><p>接下来会对日志进行配置，之后是对历史dat文件（上面提到的类、方法等相关数据的本地化存储）的管理，以及反序列化链类型的指定。我们主要看这一部分是如何指定反序列化链类型的：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> (arg.equals(<span class="string">&quot;--config&quot;</span>)) &#123;</span><br><span class="line">                <span class="comment">//--config参数指定fuzz类型</span></span><br><span class="line">                config = ConfigRepository.getConfig(args[++argIndex]);</span><br><span class="line">                <span class="keyword">if</span> (config == <span class="literal">null</span>) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalArgumentException</span>(<span class="string">&quot;Invalid config name: &quot;</span> + args[argIndex]);</span><br><span class="line">                &#125;</span><br></code></pre></figure></div><p>跟进到getConfig方法中，并且也可以看到所有的gadget-chain是通过不同的Config来实现的，并且都实现了GIConfig接口：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">GIConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="type">String</span> <span class="title function_">getName</span><span class="params">()</span>;</span><br><span class="line">    <span class="type">SerializableDecider</span> <span class="title function_">getSerializableDecider</span><span class="params">(Map&lt;MethodReference.Handle, MethodReference&gt; methodMap, InheritanceMap inheritanceMap)</span>;</span><br><span class="line">    <span class="type">ImplementationFinder</span> <span class="title function_">getImplementationFinder</span><span class="params">(</span></span><br><span class="line"><span class="params">        Map&lt;Handle, MethodReference&gt; methodMap,</span></span><br><span class="line"><span class="params">        Map&lt;Handle, Set&lt;Handle&gt;&gt; methodImplMap,</span></span><br><span class="line"><span class="params">        InheritanceMap inheritanceMap,</span></span><br><span class="line"><span class="params">        Map&lt;ClassReference.Handle, Set&lt;Handle&gt;&gt; methodsByClass)</span>;</span><br><span class="line">    <span class="type">SourceDiscovery</span> <span class="title function_">getSourceDiscovery</span><span class="params">()</span>;</span><br><span class="line">    <span class="type">SlinkDiscovery</span> <span class="title function_">getSlinkDiscovery</span><span class="params">()</span>;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>我们以Jackson的实现来看，这些被实现的方法都会在后面用到，他们都是用来对指定gadget-chain进行区分的方法，不同的gadget-chain的特征不同，因此我们可以通过这些方法来确认对应的chain。</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">package</span> gadgetinspector.config;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> gadgetinspector.ImplementationFinder;</span><br><span class="line"><span class="keyword">import</span> gadgetinspector.SerializableDecider;</span><br><span class="line"><span class="keyword">import</span> gadgetinspector.SlinkDiscovery;</span><br><span class="line"><span class="keyword">import</span> gadgetinspector.SourceDiscovery;</span><br><span class="line"><span class="keyword">import</span> gadgetinspector.data.ClassReference;</span><br><span class="line"><span class="keyword">import</span> gadgetinspector.data.InheritanceMap;</span><br><span class="line"><span class="keyword">import</span> gadgetinspector.data.MethodReference;</span><br><span class="line"><span class="keyword">import</span> gadgetinspector.data.MethodReference.Handle;</span><br><span class="line"><span class="keyword">import</span> gadgetinspector.jackson.JacksonImplementationFinder;</span><br><span class="line"><span class="keyword">import</span> gadgetinspector.jackson.JacksonSerializableDecider;</span><br><span class="line"><span class="keyword">import</span> gadgetinspector.jackson.JacksonSourceDiscovery;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.Map;</span><br><span class="line"><span class="keyword">import</span> java.util.Set;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JacksonDeserializationConfig</span> <span class="keyword">implements</span> <span class="title class_">GIConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">String</span> <span class="title function_">getName</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;jackson&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">SerializableDecider</span> <span class="title function_">getSerializableDecider</span><span class="params">(Map&lt;MethodReference.Handle, MethodReference&gt; methodMap, InheritanceMap inheritanceMap)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">JacksonSerializableDecider</span>(methodMap);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">ImplementationFinder</span> <span class="title function_">getImplementationFinder</span><span class="params">(</span></span><br><span class="line"><span class="params">        Map&lt;Handle, MethodReference&gt; methodMap,</span></span><br><span class="line"><span class="params">        Map&lt;Handle, Set&lt;Handle&gt;&gt; methodImplMap,</span></span><br><span class="line"><span class="params">        InheritanceMap inheritanceMap,</span></span><br><span class="line"><span class="params">        Map&lt;ClassReference.Handle, Set&lt;Handle&gt;&gt; methodsByClass)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">JacksonImplementationFinder</span>(getSerializableDecider(methodMap, inheritanceMap));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">SourceDiscovery</span> <span class="title function_">getSourceDiscovery</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">JacksonSourceDiscovery</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">SlinkDiscovery</span> <span class="title function_">getSlinkDiscovery</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>跟进JacksonSerializableDecider，两个map中记录的是可以通过Jackson决策的类和方法：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="comment">//类是否通过决策的缓存集合</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Map&lt;ClassReference.Handle, Boolean&gt; cache = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">    <span class="comment">//类名-方法集合 映射集合</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Map&lt;ClassReference.Handle, Set&lt;MethodReference.Handle&gt;&gt; methodsByClassMap;</span><br></code></pre></figure></div><p>具体的决策判断逻辑在apply中，在后面的分析中我们也可以看到会调用apply方法来判断类和方法是否通过决策。以jackson的apply来举例，由于jackson的json反序列化是需要以类的无参构造为起始，在java中如果没有显式声明无参构造器，但是显式声明了一个有参构造，那么该类是没有无参构造的，因此代表着该类不可进行jackson反序列化。</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">Boolean</span> <span class="title function_">apply</span><span class="params">(ClassReference.Handle handle)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (isNoGadgetClass(handle)) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">Boolean</span> <span class="variable">cached</span> <span class="operator">=</span> cache.get(handle);</span><br><span class="line">        <span class="keyword">if</span> (cached != <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> cached;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        Set&lt;MethodReference.Handle&gt; classMethods = methodsByClassMap.get(handle);</span><br><span class="line">        <span class="keyword">if</span> (classMethods != <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">for</span> (MethodReference.Handle method : classMethods) &#123;</span><br><span class="line">                <span class="comment">//该类，只要有无参构造方法，就通过决策</span></span><br><span class="line">                <span class="keyword">if</span> (method.getName().equals(<span class="string">&quot;&lt;init&gt;&quot;</span>) &amp;&amp; method.getDesc().equals(<span class="string">&quot;()V&quot;</span>)) &#123;</span><br><span class="line">                    cache.put(handle, Boolean.TRUE);</span><br><span class="line">                    <span class="keyword">return</span> Boolean.TRUE;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        cache.put(handle, Boolean.FALSE);</span><br><span class="line">        <span class="keyword">return</span> Boolean.FALSE;</span><br><span class="line">    &#125;</span><br></code></pre></figure></div><p>接下来回到Config中，继续看InplementationFinder，在决策时由于Java的多态性，并且gadgetinspector无法在要被检测的jar运行时进行判断，因此当调用到某一接口的方法时，需要查找接口所有的实现类中的该方法，并将这些方法组成实际的调用链去进行污点分析。这些方法是否可进行当前指定的gadget-chain反序列化，还是需要通过apply方法来进行判断：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JacksonImplementationFinder</span> <span class="keyword">implements</span> <span class="title class_">ImplementationFinder</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> SerializableDecider serializableDecider;</span><br><span class="line"></span><br><span class="line">    <span class="type">public</span> <span class="title function_">JacksonImplementationFinder</span><span class="params">(SerializableDecider serializableDecider)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.serializableDecider = serializableDecider;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Set&lt;MethodReference.Handle&gt; getImplementations(MethodReference.Handle target) &#123;</span><br><span class="line">        Set&lt;MethodReference.Handle&gt; allImpls = <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// For jackson search, we don&#x27;t get to specify the class; it uses reflection to instantiate the</span></span><br><span class="line">        <span class="comment">// class itself. So just add the target method if the target class is serializable.</span></span><br><span class="line">        <span class="keyword">if</span> (Boolean.TRUE.equals(serializableDecider.apply(target.getClassReference()))) &#123;</span><br><span class="line">            allImpls.add(target);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> allImpls;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>继续看JacksonSourceDiscovery，内部只有一个discover方法，这个方法的作用就是帮我们找到可进行Jackson反序列化的入口方法，对于jackson反序列化来说，会以无参构造为入口，并依次执行setter以及getter。因此discover会查找出通过了apply决策后的类的无参构造（()V代表无参，返回值为viod），以及getter和setter。</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="meta">@Override</span></span><br><span class="line">  <span class="keyword">public</span> <span class="type">void</span> <span class="title function_">discover</span><span class="params">(Map&lt;ClassReference.Handle, ClassReference&gt; classMap,</span></span><br><span class="line"><span class="params">                       Map&lt;MethodReference.Handle, MethodReference&gt; methodMap,</span></span><br><span class="line"><span class="params">                       InheritanceMap inheritanceMap, Map&lt;MethodReference.Handle, Set&lt;GraphCall&gt;&gt; graphCallMap)</span> &#123;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">final</span> <span class="type">JacksonSerializableDecider</span> <span class="variable">serializableDecider</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">JacksonSerializableDecider</span>(methodMap);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">for</span> (MethodReference.Handle method : methodMap.keySet()) &#123;</span><br><span class="line">          <span class="keyword">if</span> (skipList.contains(method.getClassReference().getName())) &#123;</span><br><span class="line">              <span class="keyword">continue</span>;</span><br><span class="line">          &#125;</span><br><span class="line">          <span class="keyword">if</span> (serializableDecider.apply(method.getClassReference())) &#123;</span><br><span class="line">              <span class="keyword">if</span> (method.getName().equals(<span class="string">&quot;&lt;init&gt;&quot;</span>) &amp;&amp; method.getDesc().equals(<span class="string">&quot;()V&quot;</span>)) &#123;</span><br><span class="line">                  addDiscoveredSource(<span class="keyword">new</span> <span class="title class_">Source</span>(method, <span class="number">0</span>));</span><br><span class="line">              &#125;</span><br><span class="line">              <span class="keyword">if</span> (method.getName().startsWith(<span class="string">&quot;get&quot;</span>) &amp;&amp; method.getDesc().startsWith(<span class="string">&quot;()&quot;</span>)) &#123;</span><br><span class="line">                  addDiscoveredSource(<span class="keyword">new</span> <span class="title class_">Source</span>(method, <span class="number">0</span>));</span><br><span class="line">              &#125;</span><br><span class="line">              <span class="keyword">if</span> (method.getName().startsWith(<span class="string">&quot;set&quot;</span>) &amp;&amp; method.getDesc().matches(<span class="string">&quot;\\(L[^;]*;\\)V&quot;</span>)) &#123;</span><br><span class="line">                  addDiscoveredSource(<span class="keyword">new</span> <span class="title class_">Source</span>(method, <span class="number">0</span>));</span><br><span class="line">              &#125;</span><br><span class="line">          &#125;</span><br></code></pre></figure></div><p>继续向下看GadgetInspector，进入到initJarData方法中，通过for循环读取最后面的参数，从而指定多个jar或war包，通过URLClassLoader，根据绝对路径将这些jar或war包进行加载，并通过ClassResourceEnumerator将jar或war包中的class进行加载：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">ClassLoader</span> <span class="variable">classLoader</span> <span class="operator">=</span> initJarData(args, boot, argIndex, haveNewJar, pathList);</span><br></code></pre></figure></div><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; args.length - argIndex; i++) &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">pathStr</span> <span class="operator">=</span> args[argIndex + i];</span><br><span class="line">    <span class="keyword">if</span> (!pathStr.endsWith(<span class="string">&quot;.jar&quot;</span>)) &#123;</span><br><span class="line">        <span class="comment">//todo 主要用于大批量的挖掘链</span></span><br><span class="line">        <span class="comment">//非.jar结尾，即目录，需要遍历目录找出所有jar文件</span></span><br><span class="line">        <span class="type">File</span> <span class="variable">file</span> <span class="operator">=</span> Paths.get(pathStr).toFile();</span><br><span class="line">        <span class="keyword">if</span> (file == <span class="literal">null</span> || !file.exists())</span><br><span class="line">            <span class="keyword">continue</span>;</span><br><span class="line">        Files.walkFileTree(file.toPath(), <span class="keyword">new</span> <span class="title class_">SimpleFileVisitor</span>&lt;Path&gt;() &#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="keyword">public</span> <span class="type">FileVisitResult</span> <span class="title function_">visitFile</span><span class="params">(Path file, BasicFileAttributes attrs)</span> &#123;</span><br><span class="line">                <span class="keyword">if</span> (!file.getFileName().toString().endsWith(<span class="string">&quot;.jar&quot;</span>))</span><br><span class="line">                    <span class="keyword">return</span> FileVisitResult.CONTINUE;</span><br><span class="line">                <span class="type">File</span> <span class="variable">readFile</span> <span class="operator">=</span> file.toFile();</span><br><span class="line">                <span class="type">Path</span> <span class="variable">path</span> <span class="operator">=</span> Paths.get(readFile.getAbsolutePath());</span><br><span class="line">                <span class="keyword">if</span> (Files.exists(path)) &#123;</span><br><span class="line">                    <span class="keyword">if</span> (ConfigHelper.history) &#123;</span><br><span class="line">                        <span class="keyword">if</span> (!scanJarHistory.contains(path.getFileName().toString())) &#123;</span><br><span class="line">                            <span class="keyword">if</span> (jarCount.incrementAndGet() &lt;= ConfigHelper.maxJarCount) &#123;</span><br><span class="line">                                pathList.add(path);</span><br><span class="line">                            &#125;</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                        <span class="keyword">if</span> (jarCount.incrementAndGet() &lt;= ConfigHelper.maxJarCount) &#123;</span><br><span class="line">                            pathList.add(path);</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">return</span> FileVisitResult.CONTINUE;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">continue</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="type">Path</span> <span class="variable">path</span> <span class="operator">=</span> Paths.get(pathStr).toAbsolutePath();</span><br><span class="line">    <span class="keyword">if</span> (!Files.exists(path)) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalArgumentException</span>(<span class="string">&quot;Invalid jar path: &quot;</span> + path);</span><br><span class="line">    &#125;</span><br><span class="line">    pathList.add(path);</span><br></code></pre></figure></div><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">      <span class="comment">//类枚举加载器，具有两个方法</span></span><br><span class="line">            <span class="comment">//getRuntimeClasses获取rt.jar的所有class</span></span><br><span class="line">            <span class="comment">//getAllClasses获取rt.jar以及classLoader加载的class           </span></span><br><span class="line"><span class="keyword">final</span> <span class="type">ClassResourceEnumerator</span> <span class="variable">classResourceEnumerator</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ClassResourceEnumerator</span>(</span><br><span class="line">                    classLoader);</span><br></code></pre></figure></div><p>接下来进入beginDiscovery方法中，接下来我们开始分析具体的挖掘逻辑。</p><h2 id="0x03-类、方法、继承关系数据收集-MethodDiscovery"><a href="#0x03-类、方法、继承关系数据收集-MethodDiscovery" class="headerlink" title="0x03 类、方法、继承关系数据收集-MethodDiscovery"></a>0x03 类、方法、继承关系数据收集-MethodDiscovery</h2><p>首先进入methodDiscovery当中，可以看到如果不存在，会生成classes.dat、methods .dat、inheritanceMap.dat，分别对类数据、方法数据以及继承关系数据进行收集：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">if</span> (!Files.exists(Paths.get(<span class="string">&quot;classes.dat&quot;</span>)) || !Files.exists(Paths.get(<span class="string">&quot;methods.dat&quot;</span>))</span><br><span class="line">              || !Files.exists(Paths.get(<span class="string">&quot;inheritanceMap.dat&quot;</span>))) &#123;</span><br><span class="line">          LOGGER.info(<span class="string">&quot;Running method discovery...&quot;</span>);</span><br><span class="line">          <span class="type">MethodDiscovery</span> <span class="variable">methodDiscovery</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">MethodDiscovery</span>();</span><br><span class="line">          methodDiscovery.discover(classResourceEnumerator);</span><br><span class="line">          <span class="comment">//保存了类信息、方法信息、继承实现信息</span></span><br><span class="line">          methodDiscovery.save();</span><br><span class="line">      &#125;</span><br></code></pre></figure></div><p>跟进MethodDiscovery.discover，传入了上面保存了类信息的classResourceRnumerator，并且调用了getAllClasses方法，获取到了包括rt.jar和指定jar、war包中的所有类，并调用ClassReader的accept方法进行下一步，这里所用到的就是ASM。</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">discover</span><span class="params">(<span class="keyword">final</span> ClassResourceEnumerator classResourceEnumerator)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">    <span class="keyword">for</span> (ClassResourceEnumerator.ClassResource classResource : classResourceEnumerator.getAllClasses()) &#123;</span><br><span class="line">        <span class="keyword">try</span> (<span class="type">InputStream</span> <span class="variable">in</span> <span class="operator">=</span> classResource.getInputStream()) &#123;</span><br><span class="line">            <span class="type">ClassReader</span> <span class="variable">cr</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ClassReader</span>(in);</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                <span class="comment">//使用asm的ClassVisitor、MethodVisitor，利用观察模式去扫描所有的class和method并记录</span></span><br><span class="line">                cr.accept(<span class="keyword">new</span> <span class="title class_">MethodDiscoveryClassVisitor</span>(), ClassReader.EXPAND_FRAMES);</span><br><span class="line">            &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                LOGGER.error(<span class="string">&quot;Exception analyzing: &quot;</span> + classResource.getName(), e);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><h3 id="ASM及访问者模式"><a href="#ASM及访问者模式" class="headerlink" title="ASM及访问者模式"></a>ASM及访问者模式</h3><p>ASM的设计原理基于访问者模式，常用于类的属性无改变，在不侵入类的情况下并对属性的操作做出扩充的场景（类似于AOP）。用生活中的例子我们可以这么理解，想象你是一个导游，要带游客参观一个由多个景点（类、方法、字段等）组成的旅游区（Java类）。访问者模式的工作方式是这样的：</p><ol><li><strong>景点清单</strong>：旅游区有一份固定的景点清单（类的结构，比如方法、字段，并且这些不会变动）。</li><li><strong>游客自由行动</strong>：游客（XXXVisitor）可以自由选择在<strong>每个景点</strong>做什么（比如拍照、记录日志、修改行为）。</li><li><strong>导游协调</strong>：导游（ASM-ClassReader）负责按顺序带游客访问每个景点，并让游客在每个景点执行自己的操作。</li></ol><p><strong>ASM的关键思想</strong>：字节码（景点）的结构是固定的，但你可以通过”游客”灵活地定义在每个”景点”做什么，使用时我们需要先通过字节流等方式读入要控制的类，之后传入给ClassReader的accept方法，accept方法会按照JVM规定好的类文件结构来依次调用对应的方法，我们可以通过重写ClassVisitor的各个visit方法，在调用accept时传入，从而实现自己的visitXXX的逻辑。因为ASM是基于责任链的调用，并且支持visiter的嵌套包装来进行遍历调用，调用顺序为从最外层的子visitor开始调用，直到最内层的ClassVisitor，因此需要在我们的visit逻辑中处理下一层的visitor逻辑，直到将所有嵌套的visitor逻辑处理完毕（最外层也就是ASM中的ClassVisitor）否则有可能会造成visiter的逻辑断链。如果对类结构完全不了解可以看一下</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">1. visit()                  → 访问类的基础信息（版本、类名等）</span><br><span class="line">2. visitSource()            → 源码信息（可选）</span><br><span class="line">3. visitModule()            → 模块信息（Java 9+，可选）</span><br><span class="line">4. visitNestHost()          → 嵌套类宿主（Java 11+，可选）</span><br><span class="line">5. visitPermittedSubtype()  → sealed类的许可子类（Java 17+，可选）</span><br><span class="line">6. visitOuterClass()        → 外部类信息（如果是内部类）</span><br><span class="line">7. visitAnnotation()        → 类上的注解（可能有多个）</span><br><span class="line">8. visitTypeAnnotation()    → 类上的类型注解（可能有多个）</span><br><span class="line">9. visitAttribute()         → 类的自定义属性（可能有多个）</span><br><span class="line">10. visitField()            → 类的字段（按字节码中的顺序访问）</span><br><span class="line">11. visitMethod()           → 类的方法（按字节码中的顺序访问）</span><br><span class="line">12. visitEnd()              → 类访问结束</span><br></code></pre></figure></div><p>我们回到MethodDiscovery.discover，在通过cr.accept后，cr先调用visit方法，因此我们跟进传入cr的MethodDiscoveryClassVisitor的visit方法，MDCV的visit方法保存了当前观察类的信息</p><ol><li>this.name：类名</li><li>this.superName：继承的父类名</li><li>this.interfaces：实现的接口名</li><li>this.isInterface：当前类是否接口</li><li>this.members：类的字段集合</li><li>this.classHandle：gadgetinspector中对于类名的封装，可以通过类名来操作类中相关属性</li></ol><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">visit</span> <span class="params">( <span class="type">int</span> version, <span class="type">int</span> access, String name, String signature, String superName, String[]interfaces)</span></span><br><span class="line">     &#123;</span><br><span class="line">         <span class="built_in">this</span>.name = name;</span><br><span class="line">         <span class="built_in">this</span>.superName = superName;</span><br><span class="line">         <span class="built_in">this</span>.interfaces = interfaces;</span><br><span class="line">         <span class="built_in">this</span>.isInterface = (access &amp; Opcodes.ACC_INTERFACE) != <span class="number">0</span>;</span><br><span class="line">         <span class="built_in">this</span>.members = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line">         <span class="built_in">this</span>.classHandle = <span class="keyword">new</span> <span class="title class_">ClassReference</span>.Handle(name);<span class="comment">//类名</span></span><br><span class="line">         annotations = <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;&gt;();</span><br><span class="line">         <span class="built_in">super</span>.visit(version, access, name, signature, superName, interfaces);</span><br><span class="line">     &#125;</span><br></code></pre></figure></div><p>接下来我们跳过几个不太重要的visit，来到visitField，在cr的控制下，被观察的类有多少个字段，visitField就会被调用多少次，来对字段进行处理。参数列表分别代表属性访问限定符，属性名，属性类型，泛型，属性的初始值（只有静态字段生效）该方法调用时，会先判断该字段是否是静态<code>if ((access &amp; Opcodes.ACC_STATIC) == 0)</code>，之后会通过判断字段的类型，如果是Object或者数组类型，就获取其具体内部类型，如果是基本类型，就获取类型的原始描述符。</p><p>比如String类型是Object，String[]是Array，那么最后保存的是<code>java/lang/String</code>，Int类型保留原始描述符后为I。获取到类型后将数据保存到visit中初始化好的列表member中。</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="type">FieldVisitor</span> <span class="title function_">visitField</span><span class="params">(<span class="type">int</span> access, String name, String desc,</span></span><br><span class="line"><span class="params">                               String signature, Object value)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> ((access &amp; Opcodes.ACC_STATIC) == <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="type">Type</span> <span class="variable">type</span> <span class="operator">=</span> Type.getType(desc);</span><br><span class="line">        String typeName;</span><br><span class="line">        <span class="keyword">if</span> (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) &#123;</span><br><span class="line">            typeName = type.getInternalName();</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            typeName = type.getDescriptor();</span><br><span class="line">        &#125;</span><br><span class="line">        members.add(<span class="keyword">new</span> <span class="title class_">ClassReference</span>.Member(name, access, <span class="keyword">new</span> <span class="title class_">ClassReference</span>.Handle(typeName)));</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">super</span>.visitField(access, name, desc, signature, value);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>可以看到传入的是ClassReference的内部类Member的构造函数，我们跟进ClassReference及Member的结构，可以发现在ClassReference中通过member数组来存储字段信息，内部类Member存储了字段的名字，访问限定修饰符，以及一个Handle类型的type，用来存储属性类型。Handle也是ClassReference中的一个内部类，只有一个字段，用来存储类名。大概访问流程是每个被观测的类对应一个<code>MethodDiscoveryClassVisitor</code>及ClassReference，当ASM观测到一个字段时调用visitField，此时visitField会new一个ClassReference.Member来存储字段信息，并将其添加到MDCV的List&lt;ClassReference.Member&gt; members中。当类中所有字段都被add进去之后，会调用到后续的visit，在最后调用visitEnd时，可以发现<code>members.toArray(new ClassReference.Member[members.size()]),</code>将member中所有被创建的ClassReference.Member转成了数组，并且初始化了一个ClassReference，将所有的字段合并到了ClassReference的Member[]数组中。</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> Member[] members;</span><br></code></pre></figure></div><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">Member</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> String name;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> modifiers;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> ClassReference.Handle type;</span><br><span class="line"></span><br><span class="line">    <span class="type">public</span> <span class="title function_">Member</span><span class="params">(String name, <span class="type">int</span> modifiers, Handle type)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.name = name;</span><br><span class="line">        <span class="built_in">this</span>.modifiers = modifiers;</span><br><span class="line">        <span class="built_in">this</span>.type = type;</span><br><span class="line">    &#125;</span><br></code></pre></figure></div><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">Handle</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> String name;</span><br><span class="line"></span><br><span class="line">    <span class="type">public</span> <span class="title function_">Handle</span><span class="params">(String name)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.name = name;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="type">String</span> <span class="title function_">getName</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> name;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">equals</span><span class="params">(Object o)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">this</span> == o) <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">        <span class="keyword">if</span> (o == <span class="literal">null</span> || getClass() != o.getClass()) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">        <span class="type">Handle</span> <span class="variable">handle</span> <span class="operator">=</span> (Handle) o;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> name != <span class="literal">null</span> ? name.equals(handle.name) : handle.name == <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">int</span> <span class="title function_">hashCode</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> name != <span class="literal">null</span> ? name.hashCode() : <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>接下来进行 visitMethod，依旧是观察到多少个方法就会调用多少次，初始化一个MethodReference，传入类名，方法名，方法描述（方法的返回值类型以及参数类型，需要使用Type类来进行解析），并且将方法添加到列表discoveredMethods中。</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="type">MethodVisitor</span> <span class="title function_">visitMethod</span><span class="params">(<span class="type">int</span> access, String name, String desc, String signature, String[] exceptions)</span> &#123;</span><br><span class="line">    <span class="type">boolean</span> <span class="variable">isStatic</span> <span class="operator">=</span> (access &amp; Opcodes.ACC_STATIC) != <span class="number">0</span>;</span><br><span class="line">    <span class="comment">//找到一个方法，添加到缓存</span></span><br><span class="line">    discoveredMethods.add(<span class="keyword">new</span> <span class="title class_">MethodReference</span>(</span><br><span class="line">            classHandle,<span class="comment">//类名</span></span><br><span class="line">            name,</span><br><span class="line">            desc,</span><br><span class="line">            isStatic));</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">super</span>.visitMethod(access, name, desc, signature, exceptions);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>最后进入到visitEnd，刚才也说过了会将所有字段整合到一个ClassReference中，并且将整合好的ClassReference添加到discoveredClasses中</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">visitEnd</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">ClassReference</span> <span class="variable">classReference</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ClassReference</span>(</span><br><span class="line">            name,</span><br><span class="line">            superName,</span><br><span class="line">            interfaces,</span><br><span class="line">            isInterface,</span><br><span class="line">            members.toArray(<span class="keyword">new</span> <span class="title class_">ClassReference</span>.Member[members.size()]),</span><br><span class="line">            annotations);<span class="comment">//把所有找到的字段封装</span></span><br><span class="line">    <span class="comment">//找到一个方法遍历完成后，添加类到缓存</span></span><br><span class="line">    discoveredClasses.add(classReference);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">super</span>.visitEnd();</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>整个<code>methodDiscovery.discovr</code>执行完成，继续到下一步<code>methodDiscovery.save();</code>中，通过DataLoader.saveData完成。其中对于classes.dat和methods.dat分别通过ClassReference.Factory()和MethodReference.Factory()创建的factory进行序列化存储</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; <span class="type">void</span> <span class="title function_">saveData</span><span class="params">(Path filePath, DataFactory&lt;T&gt; factory, Collection&lt;T&gt; values)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">    <span class="keyword">try</span> (<span class="type">BufferedWriter</span> <span class="variable">writer</span> <span class="operator">=</span> Files.newWriter(filePath.toFile(), StandardCharsets.UTF_8)) &#123;</span><br><span class="line">        <span class="keyword">for</span> (T value : values) &#123;</span><br><span class="line">            <span class="keyword">final</span> <span class="type">String</span>[] <span class="variable">fields</span> <span class="operator">=</span> factory.serialize(value);</span><br><span class="line">            <span class="keyword">if</span> (fields == <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="keyword">continue</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="type">StringBuilder</span> <span class="variable">sb</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringBuilder</span>();</span><br><span class="line">            <span class="keyword">for</span> (String field : fields) &#123;</span><br><span class="line">                <span class="keyword">if</span> (field == <span class="literal">null</span>) &#123;</span><br><span class="line">                    sb.append(<span class="string">&quot;\t&quot;</span>);</span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    sb.append(<span class="string">&quot;\t&quot;</span>).append(field);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            writer.write(sb.substring(<span class="number">1</span>));</span><br><span class="line">            writer.write(<span class="string">&quot;\n&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></code></pre></figure></div><p>最终形成的文件格式如下：</p><p>classes.dat:</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">类名(例：java/lang/String) 父类 接口A,接口B,接口C 是否接口 字段1!字段1access!字段1类型!字段2!字段2access!字段1类型</span><br></code></pre></figure></div><p>methods.dat:</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">类名 方法名 方法描述 是否静态方法</span><br></code></pre></figure></div><p>在持久化相关数据后，会通过Map来整合ClassReference.Handle和ClassReference之间的映射关系</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">Map&lt;ClassReference.Handle, ClassReference&gt; classMap = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line"><span class="keyword">for</span> (ClassReference clazz : discoveredClasses) &#123;</span><br><span class="line">    classMap.put(clazz.getHandle(), clazz);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>接下来进行类的继承以及实现关系的整合分析</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">InheritanceDeriver.derive(classMap).save();</span><br></code></pre></figure></div><p>跟进到InheritanceDeriver.derive中，可以看到做的事就是利用Map来保存继承关系，形成了<code>类- &gt;（父类,接口,超类）</code>的映射关系。</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="type">InheritanceMap</span> <span class="title function_">derive</span><span class="params">(Map&lt;ClassReference.Handle, ClassReference&gt; classMap)</span> &#123;</span><br><span class="line">    LOGGER.debug(<span class="string">&quot;Calculating inheritance for &quot;</span> + (classMap.size()) + <span class="string">&quot; classes...&quot;</span>);</span><br><span class="line">    Map&lt;ClassReference.Handle, Set&lt;ClassReference.Handle&gt;&gt; implicitInheritance = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">    <span class="comment">//遍历所有类</span></span><br><span class="line">    <span class="keyword">for</span> (ClassReference classReference : classMap.values()) &#123;</span><br><span class="line">        <span class="keyword">if</span> (implicitInheritance.containsKey(classReference.getHandle())) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalStateException</span>(<span class="string">&quot;Already derived implicit classes for &quot;</span> + classReference.getName());</span><br><span class="line">        &#125;</span><br><span class="line">        Set&lt;ClassReference.Handle&gt; allParents = <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">        <span class="comment">//获取classReference的所有父类、超类、接口类</span></span><br><span class="line">        getAllParents(classReference, classMap, allParents);</span><br><span class="line">        <span class="comment">//添加缓存：类名 -&gt; 所有的父类、超类、接口类</span></span><br><span class="line">        implicitInheritance.put(classReference.getHandle(), allParents);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//InheritanceMap翻转集合，转换为&#123;class:[subclass]&#125;</span></span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">InheritanceMap</span>(implicitInheritance);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>getAllParents方法会递归的将当前观察类的所有父类、接口的父类查找出来，并且添加到allParents集合中</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="type">void</span> <span class="title function_">getAllParents</span><span class="params">(ClassReference classReference, Map&lt;ClassReference.Handle, ClassReference&gt; classMap, Set&lt;ClassReference.Handle&gt; allParents)</span> &#123;</span><br><span class="line">    Set&lt;ClassReference.Handle&gt; parents = <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;&gt;();</span><br><span class="line">    <span class="comment">//把当前classReference类的父类添加到parents</span></span><br><span class="line">    <span class="keyword">if</span> (classReference.getSuperClass() != <span class="literal">null</span>) &#123;</span><br><span class="line">        parents.add(<span class="keyword">new</span> <span class="title class_">ClassReference</span>.Handle(classReference.getSuperClass()));</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//把当前classReference类实现的所有接口添加到parents</span></span><br><span class="line">    <span class="keyword">for</span> (String iface : classReference.getInterfaces()) &#123;</span><br><span class="line">        parents.add(<span class="keyword">new</span> <span class="title class_">ClassReference</span>.Handle(iface));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (ClassReference.Handle immediateParent : parents) &#123;</span><br><span class="line">        <span class="comment">//从所有类数据集合中，遍历找出classReference的父类、接口</span></span><br><span class="line">        <span class="type">ClassReference</span> <span class="variable">parentClassReference</span> <span class="operator">=</span> classMap.get(immediateParent);</span><br><span class="line">        <span class="keyword">if</span> (parentClassReference == <span class="literal">null</span>) &#123;</span><br><span class="line">            LOGGER.debug(<span class="string">&quot;No class id for &quot;</span> + immediateParent.getName());</span><br><span class="line">            <span class="keyword">continue</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">//继续添加到集合中</span></span><br><span class="line">        allParents.add(parentClassReference.getHandle());</span><br><span class="line">        <span class="comment">//继续递归查找，直到把classReference类的所有父类、超类、接口类都添加到allParents</span></span><br><span class="line">        getAllParents(parentClassReference, classMap, allParents);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>最后将类名与整合好的allParents形成映射关系，存储到implicitInheritance中：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">implicitInheritance.put(classReference.getHandle(), allParents);</span><br></code></pre></figure></div><p>接下来会用InheritanceMap构造函数将implicitInheritance的子-&gt;父的映射关系进行逆转整合。</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> Map&lt;ClassReference.Handle, Set&lt;ClassReference.Handle&gt;&gt; inheritanceMap;</span><br><span class="line"><span class="comment">//父-子关系集合</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> Map&lt;ClassReference.Handle, Set&lt;ClassReference.Handle&gt;&gt; subClassMap;</span><br><span class="line"></span><br><span class="line"><span class="type">public</span> <span class="title function_">InheritanceMap</span><span class="params">(Map&lt;ClassReference.Handle, Set&lt;ClassReference.Handle&gt;&gt; inheritanceMap)</span> &#123;</span><br><span class="line">    <span class="built_in">this</span>.inheritanceMap = inheritanceMap;</span><br><span class="line">    subClassMap = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">    <span class="keyword">for</span> (Map.Entry&lt;ClassReference.Handle, Set&lt;ClassReference.Handle&gt;&gt; entry : inheritanceMap.entrySet()) &#123;</span><br><span class="line">        ClassReference.<span class="type">Handle</span> <span class="variable">child</span> <span class="operator">=</span> entry.getKey();</span><br><span class="line">        <span class="keyword">for</span> (ClassReference.Handle parent : entry.getValue()) &#123;</span><br><span class="line">            subClassMap.computeIfAbsent(parent, k -&gt; <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;&gt;()).add(child);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>其中这一行代码会判断inheritanceMap中每个子类对应的set中的value（parent），是否在subClassMap中，如果不存在执行Lambda表达式，创建一个新的空HashSet，将parent作为key，HashSet作为value存入subClassMap，并且将child添加到HashSet中。最终subClassMap就变成了父类-&gt;子类的映射关系。</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">subClassMap.computeIfAbsent(parent, k -&gt; <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;&gt;()).add(child);</span><br></code></pre></figure></div><hr><p>举个例子：</p><p>假设 <code>inheritanceMap</code> 包含：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">&quot;Dog&quot; → &#123;&quot;Animal&quot;, &quot;Object&quot;&#125;</span><br><span class="line">&quot;Cat&quot; → &#123;&quot;Animal&quot;, &quot;Object&quot;&#125;</span><br></code></pre></figure></div><p>则 <code>subClassMap</code> 的构建过程如下：</p><ol><li>处理 <code>Dog</code> 的父类 <code>Animal</code>：</li></ol><ul><li><code>subClassMap</code> 中没有 <code>Animal</code>，创建HashSet → <code>Animal: {Dog}</code></li></ul><ol><li>处理 <code>Dog</code> 的父类 <code>Object</code>：</li></ol><ul><li>没有 <code>Object</code>，创建HashSet → <code>Object: {Dog}</code></li></ul><ol><li>处理 <code>Cat</code> 的父类 <code>Animal</code>：</li></ol><ul><li><code>Animal</code> 已存在，直接添加 → <code>Animal: {Dog, Cat}</code></li></ul><ol><li>处理 <code>Cat</code> 的父类 <code>Object</code>：</li></ol><ul><li><code>Object</code> 已存在，添加 → <code>Object: {Dog, Cat}</code></li></ul><p>最终 <code>subClassMap</code> 结果：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">&quot;Animal&quot; → &#123;&quot;Dog&quot;, &quot;Cat&quot;&#125;</span><br><span class="line">&quot;Object&quot; → &#123;&quot;Dog&quot;, &quot;Cat&quot;&#125;</span><br></code></pre></figure></div><hr><p>最后调用save方法对继承关系进行保存，方法依旧和上面一样，会进行序列化后持久化存储：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">save</span><span class="params">()</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">    <span class="comment">//inheritanceMap.dat数据格式：</span></span><br><span class="line">    <span class="comment">//类名 父类或超类或接口类1 父类或超类或接口类2 父类或超类或接口类3 ...</span></span><br><span class="line">    DataLoader.saveData(Paths.get(<span class="string">&quot;inheritanceMap.dat&quot;</span>), <span class="keyword">new</span> <span class="title class_">InheritanceMapFactory</span>(), inheritanceMap.entrySet());</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>最终形成的inheritanceMap.dat结构如下：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">类名 父类或超类或接口类1 父类或超类或接口类2 父类或超类或接口类3 ...</span><br></code></pre></figure></div><h2 id="0x04-入参返回值污染关系收集-PassthroughDiscovery"><a href="#0x04-入参返回值污染关系收集-PassthroughDiscovery" class="headerlink" title="0x04 入参返回值污染关系收集-PassthroughDiscovery"></a>0x04 入参返回值污染关系收集-PassthroughDiscovery</h2><p>这一步类似于污点分析，我们对各个方法的参数对返回值的污染关系做出总结：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">if</span> (!Files.exists(Paths.get(<span class="string">&quot;passthrough.dat&quot;</span>)) &amp;&amp; ConfigHelper.taintTrack) &#123;</span><br><span class="line">    LOGGER.info(<span class="string">&quot;Analyzing methods for passthrough dataflow...&quot;</span>);</span><br><span class="line">    <span class="type">PassthroughDiscovery</span> <span class="variable">passthroughDiscovery</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">PassthroughDiscovery</span>();</span><br><span class="line">    <span class="comment">//记录参数在方法调用链中的流动关联（如：A、B、C、D四个方法，调用链为A-&gt;B B-&gt;C C-&gt;D，其中参数随着调用关系从A流向B，在B调用C过程中作为入参并随着方法结束返回，最后流向D）</span></span><br><span class="line">    <span class="comment">//该方法主要是追踪上面所说的&quot;B调用C过程中作为入参并随着方法结束返回&quot;，入参和返回值之间的关联</span></span><br><span class="line">    passthroughDiscovery.discover(classResourceEnumerator, config);</span><br><span class="line">    passthroughDiscovery.save();</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>跟进passthroughDiscovery.discover当中，首先会将我们上一步MethodDiscovery所生成的类、方法、继承信息读取进来</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">discover</span><span class="params">(<span class="keyword">final</span> ClassResourceEnumerator classResourceEnumerator, <span class="keyword">final</span> GIConfig config)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">    <span class="comment">//加载文件记录的所有方法信息</span></span><br><span class="line">    Map&lt;MethodReference.Handle, MethodReference&gt; methodMap = DataLoader.loadMethods();</span><br><span class="line">    <span class="comment">//加载文件记录的所有类信息</span></span><br><span class="line">    Map&lt;ClassReference.Handle, ClassReference&gt; classMap = DataLoader.loadClasses();</span><br><span class="line">    <span class="comment">//加载文件记录的所有类继承、实现关联信息</span></span><br><span class="line">    <span class="type">InheritanceMap</span> <span class="variable">inheritanceMap</span> <span class="operator">=</span> InheritanceMap.load();</span><br></code></pre></figure></div><p>接下来通过discoverMethodCalls，来找出所有方法间的调用关系，我们继续跟进</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="comment">//搜索方法间的调用关系，缓存至methodCalls集合，返回 类名-&gt;类资源 映射集合</span></span><br><span class="line">      Map&lt;String, ClassResourceEnumerator.ClassResource&gt; classResourceByName = discoverMethodCalls(classResourceEnumerator);</span><br></code></pre></figure></div><p>在该方法中，依然是通过ASM来先对所有的类进行一次观察，用到的visitor是MethodCallDiscoveryClassVisitor，并且这里的MethodCallDiscoveryClassVisitor内部是做了一些包装的，这一部分的执行顺序可能会有点乱，我会在方法分析结束后总结一下：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">private</span> Map&lt;String, ClassResourceEnumerator.ClassResource&gt; discoverMethodCalls(<span class="keyword">final</span> ClassResourceEnumerator classResourceEnumerator) <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">    Map&lt;String, ClassResourceEnumerator.ClassResource&gt; classResourcesByName = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">    <span class="keyword">for</span> (ClassResourceEnumerator.ClassResource classResource : classResourceEnumerator.getAllClasses()) &#123;</span><br><span class="line">        <span class="keyword">try</span> (<span class="type">InputStream</span> <span class="variable">in</span> <span class="operator">=</span> classResource.getInputStream()) &#123;</span><br><span class="line">            <span class="type">ClassReader</span> <span class="variable">cr</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ClassReader</span>(in);</span><br><span class="line">              <span class="keyword">try</span> &#123;</span><br><span class="line">                <span class="type">MethodCallDiscoveryClassVisitor</span> <span class="variable">visitor</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">MethodCallDiscoveryClassVisitor</span>(Opcodes.ASM6);</span><br><span class="line">                cr.accept(visitor, ClassReader.EXPAND_FRAMES);</span><br><span class="line">                classResourcesByName.put(visitor.getName(), classResource);</span><br><span class="line">            &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                LOGGER.error(<span class="string">&quot;Error analyzing: &quot;</span> + classResource.getName(), e);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> classResourcesByName;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>分别跟进MCDCV的visit以及visitMethod方法，visit方法中将传入进来的classname进行记录</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">visit</span><span class="params">(<span class="type">int</span> version, <span class="type">int</span> access, String name, String signature,</span></span><br><span class="line"><span class="params">                  String superName, String[] interfaces)</span> &#123;</span><br><span class="line">    <span class="built_in">super</span>.visit(version, access, name, signature, superName, interfaces);</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">this</span>.name != <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalStateException</span>(<span class="string">&quot;ClassVisitor already visited a class!&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">this</span>.name = name;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>visitMethod方法又创建了一个MethodCallDiscoveryMethodVisitor，并且可以看到在实例化时将上面的mv也传了进去。但其实我们观察MethodCallDiscoveryClassVisitor的构造函数，在调用父类构造函数时并没有传入任何的classvisitor，因此父类ClassVisitor的cv属性为null，最终返回的也是个null，在这里传入给MCDMV的mv也是个null：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="type">MethodVisitor</span> <span class="title function_">visitMethod</span><span class="params">(<span class="type">int</span> access, String name, String desc,</span></span><br><span class="line"><span class="params">                                  String signature, String[] exceptions)</span> &#123;</span><br><span class="line">     <span class="type">MethodVisitor</span> <span class="variable">mv</span> <span class="operator">=</span> <span class="built_in">super</span>.visitMethod(access, name, desc, signature, exceptions);</span><br><span class="line">     <span class="comment">//在visit每个method的时候，创建MethodVisitor对method进行观察</span></span><br><span class="line">     <span class="type">MethodCallDiscoveryMethodVisitor</span> <span class="variable">modelGeneratorMethodVisitor</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">MethodCallDiscoveryMethodVisitor</span>(</span><br><span class="line">             api, mv, <span class="built_in">this</span>.name, name, desc);</span><br><span class="line"></span><br><span class="line">     <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">JSRInlinerAdapter</span>(modelGeneratorMethodVisitor, access, name, desc, signature, exceptions);</span><br><span class="line"> &#125;</span><br></code></pre></figure></div><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="comment">// MethodCallDiscoveryClassVisitor的构造函数</span></span><br><span class="line"><span class="type">MethodCallDiscoveryClassVisitor</span> <span class="variable">visitor</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">MethodCallDiscoveryClassVisitor</span>(Opcodes.ASM6);</span><br><span class="line"></span><br><span class="line"><span class="comment">//父类ClassVisitor的构造函数</span></span><br><span class="line"> <span class="type">public</span> <span class="title function_">ClassVisitor</span><span class="params">(<span class="keyword">final</span> <span class="type">int</span> api)</span> &#123;</span><br><span class="line">   <span class="built_in">this</span>(api, <span class="literal">null</span>);</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line">  <span class="type">public</span> <span class="title function_">ClassVisitor</span><span class="params">(<span class="keyword">final</span> <span class="type">int</span> api, <span class="keyword">final</span> ClassVisitor classVisitor)</span> &#123;</span><br><span class="line">   <span class="keyword">if</span> (api != Opcodes.ASM6</span><br><span class="line">       &amp;&amp; api != Opcodes.ASM5</span><br><span class="line">       &amp;&amp; api != Opcodes.ASM4</span><br><span class="line">       &amp;&amp; api != Opcodes.ASM7_EXPERIMENTAL) &#123;</span><br><span class="line">     <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalArgumentException</span>();</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="built_in">this</span>.api = api;</span><br><span class="line">   <span class="built_in">this</span>.cv = classVisitor;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//父类ClassVisitor的visitMethod方法</span></span><br><span class="line"><span class="keyword">public</span> <span class="type">MethodVisitor</span> <span class="title function_">visitMethod</span><span class="params">(</span></span><br><span class="line"><span class="params">     <span class="keyword">final</span> <span class="type">int</span> access,</span></span><br><span class="line"><span class="params">     <span class="keyword">final</span> String name,</span></span><br><span class="line"><span class="params">     <span class="keyword">final</span> String descriptor,</span></span><br><span class="line"><span class="params">     <span class="keyword">final</span> String signature,</span></span><br><span class="line"><span class="params">     <span class="keyword">final</span> String[] exceptions)</span> &#123;</span><br><span class="line">   <span class="keyword">if</span> (cv != <span class="literal">null</span>) &#123;</span><br><span class="line">     <span class="keyword">return</span> cv.visitMethod(access, name, descriptor, signature, exceptions);</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> &#125;</span><br></code></pre></figure></div><p>跟进MethodCallDiscoveryMethodVisitor，可以发现父类为MethodVisitor，并且调用父类的构造函数时传入了mv，但其实我们这里静态分析可以分析出来mv是null的，即便传入了在调用MethodVisitor.visitXXX时，最终也不会走到cv.visitXXX上，我这里推测是作者为了工具的扩充性，如果我们需要添加其他的visitor来对方法进行其他处理，那么就可以形成我们之前提到的类似于责任链的方式，来遍历的调用visitXXX：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">public</span> <span class="title function_">MethodCallDiscoveryMethodVisitor</span><span class="params">(<span class="keyword">final</span> <span class="type">int</span> api, <span class="keyword">final</span> MethodVisitor mv,</span></span><br><span class="line"><span class="params">                                   <span class="keyword">final</span> String owner, String name, String desc)</span> &#123;</span><br><span class="line">    <span class="built_in">super</span>(api, mv);</span><br></code></pre></figure></div><p>我们继续看，可以看到接下来会将传入的owner（此时正在观察的类名）封装到ClassReference.Handle中，并再将这个CRF.Handle和方法名、方法的相关描述封装到一个MethodReference.Handle中，calledMethods是每次观察到一个方法，都会创建的空HashSet，最终形成了<code>观察方法：{被观察方法调用方法}</code>的映射关系存入到methodCalls中：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">          </span><br><span class="line"><span class="comment">// private final Map&lt;MethodReference.Handle, Set&lt;MethodReference.Handle&gt;&gt; methodCalls = new HashMap&lt;&gt;();</span></span><br><span class="line"></span><br><span class="line">            <span class="built_in">this</span>.calledMethods = <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;&gt;();</span><br><span class="line">            methodCalls.put(<span class="keyword">new</span> <span class="title class_">MethodReference</span>.Handle(<span class="keyword">new</span> <span class="title class_">ClassReference</span>.Handle(owner), name, desc), calledMethods);</span><br><span class="line">        &#125;</span><br></code></pre></figure></div><p>继续向下看，类中还有一个visitMethodInsn方法，当检测到方法内部的调用时就会执行（底层原理是检查到字节码指令INVOKEVIRTUAL、INVOKESPECIAL、INVOKESTATIC、INVOKEINTERFACE），从而将正在观察的方法中调用的方法加入到calledMethods中</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">visitMethodInsn</span><span class="params">(<span class="type">int</span> opcode, String owner, String name, String desc, <span class="type">boolean</span> itf)</span> &#123;</span><br><span class="line">    calledMethods.add(<span class="keyword">new</span> <span class="title class_">MethodReference</span>.Handle(<span class="keyword">new</span> <span class="title class_">ClassReference</span>.Handle(owner), name, desc));</span><br><span class="line">    <span class="built_in">super</span>.visitMethodInsn(opcode, owner, name, desc, itf);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><table><thead><tr><th>指令</th><th>调用类型</th><th>适用方法</th><th>特点</th></tr></thead><tbody><tr><td><code>INVOKEVIRTUAL</code></td><td>虚方法调用（动态绑定）</td><td>普通实例方法（非私有、非构造器、非静态）</td><td>运行时根据对象实际类型选择方法，支持多态</td></tr><tr><td><code>INVOKESPECIAL</code></td><td>特殊方法调用（静态绑定）</td><td>构造器、私有方法、<code>super.xxx()</code></td><td>编译时就决定调用哪一个，<strong>不支持多态</strong></td></tr><tr><td><code>INVOKESTATIC</code></td><td>静态方法调用</td><td><code>static</code> 修饰的方法</td><td>无需对象即可调用，直接通过类名调用</td></tr><tr><td><code>INVOKEINTERFACE</code></td><td>接口方法调用</td><td>接口定义的方法</td><td>运行时通过接口表定位目标方法，支持多态</td></tr></tbody></table><p>回到visitMethod中，最后会进行return操作，并且return的是JSRInlinerAdapter。为什么要return这个类呢，因为在早期的java版本中，使用JSR和RET跳转指令来进行程序流程控制，在后续版本已废弃并使用GOTO指令，因此需要进行兼容处理。JSRInlinerAdapter会将JSR和RET指令转为GOTO指令，从而兼容了早期项目。</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">return new JSRInlinerAdapter(modelGeneratorMethodVisitor, access, name, desc, signature, exceptions);</span><br></code></pre></figure></div><p>经过这些封装，调用cr.accept(visitor, ClassReader.EXPAND_FRAMES);将封装好的MethodCallDiscoveryClassVisitor传入进行方法调用关系收集。accept执行顺序如下：</p><ol><li><p>MethodCallDiscoveryClassVisitor.visit对类进行观察</p></li><li><p>当观察到方法时调用MethodCallDiscoveryClassVisitor.visitMethod，其中会创建一个MethodCallDiscoveryMethodVisitor实例，并包装为JSRInlinerAdapter返回，创建实例时会自动为观察到的方法添加一个映射关系，即当前观察方法->calledMethods</p></li></ol><p>换句话说：</p><ul><li><strong>只要你把 <code>JSRInlinerAdapter</code> 插到你的 MethodVisitor 链上</strong>（手动 new 一个 或者在使用 <code>ClassWriter.COMPUTE_FRAMES</code>／<code>ClassReader.EXPAND_FRAMES</code> 时 ASM 自动给你插入），</li><li>在方法遍历时遇到跳转就会进 <code>visitJumpInsn</code>，</li><li>在方法结束时（<code>visitEnd</code>）就会真正触发“内联 JSR→GOTO” 的逻辑。</li></ul><p>这样保证了旧版子例程指令在生成新的字节码之前就被全部消除，适配现代 JVM 对 StackMapFrame 的要求。</p><hr><p>4.JSRInlinerAdapter将指令转换并内联后，会通过其visitend方法再次通过accept将visitXXX传递给下一个visitor，也就是传入的MethodCallDiscoveryMethodVisitor的visitMethodInsn方法，从而将被调用的方法添加到当前观察方法的calledMethods中。</p><p>accept方法结束后还剩一行，还是将类名和classResource的映射关系存储起来并return：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">classResourcesByName.put(visitor.getName(), classResource);</span><br></code></pre></figure></div><p>discoverMethodCalls逻辑结束后，接下来是对methodCalls进行一次逆拓扑排序，所谓逆拓扑排序就是把拓扑排序的序列倒过来，什么你还不知道什么是拓扑排序？或许你该学一下数据结构了，或者看一下这篇文章介绍的吧</p><p><a href="https://paper.seebug.org/1034/">https://paper.seebug.org/1034/</a></p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">List&lt;MethodReference.Handle&gt; sortedMethods = topologicallySortMethodCalls();</span><br></code></pre></figure></div><p>为什么我们要进行逆拓扑排序，因为在方法的调用链上，假设a方法传递参数给b方法，并且b方法的返回值影响到了a方法的返回值，那么我们在判断方法链的时候就不能从a方法来入手，需要从最深处被调用的b方法来入手，观察b方法的参数与返回值之间是否存在关系，如果存在关系则证明了a方法传入b方法的参数与b方法返回值有关，此时b方法返回值影响到了a方法返回值，那么我们也就可以断定ab方法之间存在污染关系。</p><p>在方法调用的关系中，我们可以将这些调用抽象为有向图，假设a方法内部调用了b方法，那么我们就可以将a方法对应的图节点引出一条有向边，指向b方法。最终将所有的调用关系全部依次类推，就形成了一个有向图。我们将指向其他节点的边的数量叫做一个点的出度，指向自己的边的数量叫做一个节点的入度，如果找到有向图中一个入度为0的节点，将其节点以及所有的边全部消去，并输出该节点。不断重复这一操作，直到图中所有节点和边全部被消除掉，我们就得到了一组拓扑排序序列，而这一个序列就对应了我们的方法调用顺序。</p><p>但事情并没有想象中这么顺利，在方法调用中会出现两种情况，一个是相同的方法可能会存在重复调用，并且方法调用中由于回调等方式的存在，造成图中可能会出现环路，而环路的出现会导致拓扑排序在某一时刻无法找到一个入度为0的点，也就没有拓扑序列的产生了，解决办法上面的文章也提到了。我们用一个例子来看一下具体的执行过程：</p><p>假设有以下方法调用关系：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">A → B → C</span><br><span class="line">A → D</span><br></code></pre></figure></div><p>对应的调用图为：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">outgoingReferences = &#123;</span><br><span class="line">    A: &#123;B, D&#125;,</span><br><span class="line">    B: &#123;C&#125;,</span><br><span class="line">    C: &#123;&#125;,</span><br><span class="line">    D: &#123;&#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><ol><li><p>**初始调用**：从根节点 `A` 开始。</p> ``` dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, A); ```</li><li><p>**处理节点 `A`**：</p></li></ol><ul><li><p>`stack` 为空，`visitedNodes` 为空 → 继续。</p></li><li><p>获取 `A` 的被调用方法集合 `{B, D}`。</p></li><li><p>将 `A` 加入 `stack`（当前路径：`[A]`）。</p></li><li><p>递归处理子节点 `B`：</p> ``` dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, B); ```</li></ul><ol><li><p>**处理节点 `B`**：</p></li></ol><ul><li><p>`stack` 包含 `A`，不包含 `B` → 继续。</p></li><li><p>获取 `B` 的被调用方法集合 `{C}`。</p></li><li><p>将 `B` 加入 `stack`（当前路径：`[A, B]`）。</p></li><li><p>递归处理子节点 `C`：</p> ``` dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, C); ```</li></ul><ol><li><p>**处理节点 `C`**：</p></li></ol><ul><li><p>`stack` 包含 `A, B`，不包含 `C` → 继续。</p></li><li><p>获取 `C` 的被调用方法集合 `{}`（无子节点）。</p></li><li><p>将 `C` 加入 `visitedNodes` 和 `sortedMethods`：</p> ``` visitedNodes = {C}, sortedMethods = [C] ```</li><li><p>返回处理 `B`。</p></li></ul><ol><li><p>**回溯节点 `B`**：</p></li></ol><ul><li><p>从 `stack` 中移除 `B`（当前路径：`[A]`）。</p></li><li><p>将 `B` 加入 `visitedNodes` 和 `sortedMethods`：</p> ``` visitedNodes = {C, B}, sortedMethods = [C, B] ```</li><li><p>处理 `B` 的下一个子节点（无剩余节点），返回处理 `A`。</p></li></ul><ol><li><p>**处理节点 `A` 的第二个子节点 `D`**：</p></li></ol><ul><li><p>将 `D` 加入 `stack`（当前路径：`[A, D]`）。</p></li><li><p>递归处理 `D`：</p> ``` dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, D); ```</li></ul><ol><li><p>**处理节点 `D`**：</p></li></ol><ul><li><p>获取 `D` 的被调用方法集合 `{}`（无子节点）。</p></li><li><p>将 `D` 加入 `visitedNodes` 和 `sortedMethods`：</p> ``` visitedNodes = {C, B, D}, sortedMethods = [C, B, D] ```</li><li><p>返回处理 `A`。</p></li></ul><ol><li><p>**回溯节点 `A`**：</p></li></ol><ul><li><p>从 `stack` 中移除 `A`（当前路径：`[]`）。</p></li><li><p>将 `A` 加入 `visitedNodes` 和 `sortedMethods`：</p> ``` visitedNodes = {C, B, D, A}, sortedMethods = [C, B, D, A] ```</li><li><p><strong>污点分析顺序</strong>：</p><ol><li>先分析 <code>C</code>（无依赖），确定其污点传播规则。</li><li>分析 <code>B</code>（依赖 <code>C</code>），利用 <code>C</code> 的结果。</li><li>分析 <code>D</code>（无依赖）。</li><li>最后分析 <code>A</code>（依赖 <code>B</code> 和 <code>D</code>），确保所有被调用方法已处理。</li></ol></li></ul><p>若存在循环调用（如 <code>A → B → A</code>）：</p><ol><li>处理 <code>A</code> → <code>B</code> → <code>A</code> 时，第二次进入 <code>A</code> 的递归：</li></ol><ul><li><code>stack</code> 包含 <code>A</code> → 触发 <code>if (stack.contains(node)) return;</code></li><li>终止递归，避免死循环。</li></ul><p>逆拓扑排序后，接下来就是对方法参数和返回值之间污染关系的分析：</p></li></ol><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">passthroughDataflow = calculatePassthroughDataflow(classResourceByName, classMap, inheritanceMap, sortedMethods,</span><br><span class="line">               config.getSerializableDecider(methodMap, inheritanceMap));</span><br></code></pre></figure></div><p>跟进calculatePassthroughDataflow，首先会遍历sortedMethods，如果是静态初始化代码，即静态代码块，就直接跳过，因为静态代码块是在类加载的时候就加载到JVM当中，我们一般没有办法在程序运行中进行控制</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">final</span> Map&lt;MethodReference.Handle, Set&lt;Integer&gt;&gt; passthroughDataflow = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">      <span class="comment">//遍历所有方法，然后asm观察所属类，经过前面DFS的排序，调用链最末端的方法在最前面</span></span><br><span class="line">      <span class="keyword">for</span> (MethodReference.Handle method : sortedMethods) &#123;</span><br><span class="line">          <span class="comment">//跳过static静态初始化代码</span></span><br><span class="line">          <span class="keyword">if</span> (method.getName().equals(<span class="string">&quot;&lt;clinit&gt;&quot;</span>)) &#123;</span><br><span class="line">              <span class="keyword">continue</span>;</span><br><span class="line">          &#125;</span><br></code></pre></figure></div><p>接下来就是对当前所遍历的方法的所属类进行ASM观察：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">ClassResourceEnumerator.<span class="type">ClassResource</span> <span class="variable">classResource</span> <span class="operator">=</span> classResourceByName.get(method.getClassReference().getName());</span><br><span class="line">          <span class="keyword">try</span> (<span class="type">InputStream</span> <span class="variable">inputStream</span> <span class="operator">=</span> classResource.getInputStream()) &#123;</span><br><span class="line">              <span class="type">ClassReader</span> <span class="variable">cr</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ClassReader</span>(inputStream);</span><br><span class="line">              <span class="keyword">try</span> &#123;</span><br><span class="line">                  <span class="type">PassthroughDataflowClassVisitor</span> <span class="variable">cv</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">PassthroughDataflowClassVisitor</span>(classMap, inheritanceMap,</span><br><span class="line">                          passthroughDataflow, serializableDecider, Opcodes.ASM6, method);</span><br><span class="line">                  cr.accept(cv, ClassReader.EXPAND_FRAMES);</span><br></code></pre></figure></div><p>跟进visitor逻辑，查看visit方法，visit方法会判断当前观察的类是否是要准备观察方法的所属类</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="meta">@Override</span></span><br><span class="line">     <span class="keyword">public</span> <span class="type">void</span> <span class="title function_">visit</span><span class="params">(<span class="type">int</span> version, <span class="type">int</span> access, String name, String signature,</span></span><br><span class="line"><span class="params">                       String superName, String[] interfaces)</span> &#123;</span><br><span class="line">         <span class="built_in">super</span>.visit(version, access, name, signature, superName, interfaces);</span><br><span class="line">         <span class="built_in">this</span>.name = name;</span><br><span class="line">         <span class="comment">//不是目标观察的class跳过</span></span><br><span class="line">         <span class="keyword">if</span> (!<span class="built_in">this</span>.name.equals(methodToVisit.getClassReference().getName())) &#123;</span><br><span class="line">             <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalStateException</span>(<span class="string">&quot;Expecting to visit &quot;</span> + methodToVisit.getClassReference().getName() + <span class="string">&quot; but instead got &quot;</span> + <span class="built_in">this</span>.name);</span><br><span class="line">         &#125;</span><br><span class="line">     &#125;</span><br></code></pre></figure></div><p>接着看visitMethod，我们需要观察的类中的方法只需要是sortedMethod中的方法即可，也就是传入进来的methodToVisit，其他方法是不存在调用关系的：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="comment">//不是目标观察的method需要跳过，上一步得到的method都是有调用关系的method才需要数据流分析</span></span><br><span class="line">        <span class="keyword">if</span> (!name.equals(methodToVisit.getName()) || !desc.equals(methodToVisit.getDesc())) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">        &#125;</span><br></code></pre></figure></div><p>接下来是对方法进行更细致的观察，依旧看封装后的PassthroughDataflowMethodVisitor</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">MethodVisitor</span> <span class="variable">mv</span> <span class="operator">=</span> <span class="built_in">super</span>.visitMethod(access, name, desc, signature, exceptions);</span><br><span class="line">           passthroughDataflowMethodVisitor = <span class="keyword">new</span> <span class="title class_">PassthroughDataflowMethodVisitor</span>(</span><br><span class="line">                   classMap, inheritanceMap, <span class="built_in">this</span>.passthroughDataflow, serializableDecider,</span><br><span class="line">                   api, mv, <span class="built_in">this</span>.name, access, name, desc, signature, exceptions);</span><br></code></pre></figure></div><hr><p>下面作者用代码模拟了方法调用的过程，从而在模拟的局部变量表（污点变量表）中对参数进行污点标记。我们先来回顾JVM在进行方法调用时都做了哪些事情。假设现在A方法中要调用B方法，那么此时我们是在A方法内部的，那么JVM中会有A方法的栈帧，栈帧中主要两部分，一个是局部变量表，一个是操作数栈，当A方法内部准备调用B方法时，会先将要传给B方法的参数保存到A方法栈帧的操作数栈上，此时JVM会为B方法创建其对应的栈帧，然后在A方法操作数栈上的参数会被弹到B方法栈帧的局部变量表中。B方法内部使用这些参数时，会通过LOAD指令将其从局部变量表加载到操作数栈上，再进行使用。这里的思想就是用代码去模仿JVM的行为，从而将JVM的方法调用流程可视化。</p><hr><p>下面的分析过程基于如下例子，这一段代码调用包含了入参与返回结果相同，返回结果与入参有关的情况，我们分别来看：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Main</span> &#123;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">public</span> <span class="type">String</span> <span class="title function_">main</span><span class="params">(String args)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">cmd</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">A</span>().method1(args);</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">B</span>().method2(cmd);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">A</span> &#123;</span><br><span class="line">  <span class="keyword">public</span> <span class="type">String</span> <span class="title function_">method1</span><span class="params">(String param)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> param;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">B</span> &#123;</span><br><span class="line">  <span class="keyword">public</span> <span class="type">String</span> <span class="title function_">method2</span><span class="params">(String param)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">C</span>().method3(param);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">C</span> &#123;</span><br><span class="line">  <span class="keyword">public</span> <span class="type">String</span> <span class="title function_">method3</span><span class="params">(String param)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> param;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>逆拓扑排序后的结果为：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">A.method1</span><br><span class="line">C.method3</span><br><span class="line">B.method2</span><br><span class="line">main</span><br></code></pre></figure></div><h3 id="A-method1"><a href="#A-method1" class="headerlink" title="A.method1"></a>A.method1</h3><p>因此我们先从A.method1来进行分析：</p><p>这里我们看到visitCode方法，在进入方法的第一时间，ASM会先调用这个方法。对于非静态方法来说，方法参数插槽的第一个0号位位this，对于静态方法，0号位为参数，所以这里将方法内的所有参数保存在一个使用Java代码模拟的局部变量表中，localIndex为参数在局部变量表中的位置，由于参数的类型不同，所以其在局部变量表中占用的大小也不同。而argIndex对应了参数在方法中的索引，通过setLocalTaint方法，形成了局部变量表与方法参数索引之间的映射关系</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">void</span> <span class="title function_">visitCode</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="built_in">super</span>.visitCode();</span><br><span class="line"></span><br><span class="line">        <span class="type">int</span> <span class="variable">localIndex</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line">        <span class="type">int</span> <span class="variable">argIndex</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line">        <span class="keyword">if</span> ((<span class="built_in">this</span>.access &amp; Opcodes.ACC_STATIC) == <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="comment">//非静态方法，第一个局部变量应该为对象实例this</span></span><br><span class="line">            <span class="comment">//添加到局部变量表集合</span></span><br><span class="line">            setLocalTaint(localIndex, argIndex);</span><br><span class="line">            localIndex += <span class="number">1</span>;</span><br><span class="line">            argIndex += <span class="number">1</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">for</span> (Type argType : Type.getArgumentTypes(desc)) &#123;</span><br><span class="line">            <span class="comment">//判断参数类型，得出变量占用空间大小，然后存储</span></span><br><span class="line">            setLocalTaint(localIndex, argIndex);</span><br><span class="line">            localIndex += argType.getSize();</span><br><span class="line">            argIndex += <span class="number">1</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">       <span class="keyword">protected</span> <span class="type">void</span> <span class="title function_">setLocalTaint</span><span class="params">(<span class="type">int</span> index, T ... possibleValues)</span> &#123;</span><br><span class="line">    <span class="type">Set</span>&lt;T&gt; <span class="variable">values</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;T&gt;();</span><br><span class="line">    <span class="keyword">for</span> (T value : possibleValues) &#123;</span><br><span class="line">        values.add(value);</span><br><span class="line">    &#125;</span><br><span class="line">    savedVariableState.localVars.set(index, values);</span><br><span class="line">&#125; </span><br><span class="line">    </span><br></code></pre></figure></div><p>接下来执行A.method1方法内部逻辑时（即return param），要将局部变量表中的参数通过ALOAD指令读取到操作数栈上，继续模拟，在检测到ALOAD指令时（包括其他访问局部变量表的指令），会回调visitVarInsn，将参数push到模拟的污点栈上，这里的参数可以看到是列表localVars的值，也就是局部变量表中对应的参数索引</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="meta">@Override</span></span><br><span class="line">   <span class="keyword">public</span> <span class="type">void</span> <span class="title function_">visitVarInsn</span><span class="params">(<span class="type">int</span> opcode, <span class="type">int</span> <span class="keyword">var</span>)</span> &#123;</span><br><span class="line">       <span class="comment">// Extend local variable state to make sure we include the variable index</span></span><br><span class="line">       <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> savedVariableState.localVars.size(); i &lt;= <span class="keyword">var</span>; i++) &#123;</span><br><span class="line">           savedVariableState.localVars.add(<span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;T&gt;());</span><br><span class="line">       &#125;</span><br><span class="line"></span><br><span class="line">       <span class="comment">//变量操作，var为操作的本地变量索引</span></span><br><span class="line">       Set&lt;T&gt; saved0;</span><br><span class="line">       <span class="keyword">switch</span>(opcode) &#123;</span><br><span class="line">           <span class="keyword">case</span> Opcodes.ILOAD:</span><br><span class="line">           <span class="keyword">case</span> Opcodes.FLOAD:</span><br><span class="line">               push();</span><br><span class="line">               <span class="keyword">break</span>;</span><br><span class="line">           <span class="keyword">case</span> Opcodes.LLOAD:</span><br><span class="line">           <span class="keyword">case</span> Opcodes.DLOAD:</span><br><span class="line">               push();</span><br><span class="line">               push();</span><br><span class="line">               <span class="keyword">break</span>;</span><br><span class="line">           <span class="keyword">case</span> Opcodes.ALOAD:</span><br><span class="line">               <span class="comment">//从局部变量表取出变量数据入操作数栈，这个变量数据可能是被污染的</span></span><br><span class="line">               push(savedVariableState.localVars.get(<span class="keyword">var</span>));</span><br><span class="line">               <span class="keyword">break</span>;</span><br><span class="line">           <span class="keyword">case</span> Opcodes.ISTORE:</span><br><span class="line">           <span class="keyword">case</span> Opcodes.FSTORE:</span><br><span class="line">               pop();</span><br><span class="line">               savedVariableState.localVars.set(<span class="keyword">var</span>, <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;T&gt;());</span><br><span class="line">               <span class="keyword">break</span>;</span><br><span class="line">           <span class="keyword">case</span> Opcodes.DSTORE:</span><br><span class="line">           <span class="keyword">case</span> Opcodes.LSTORE:</span><br><span class="line">               pop();</span><br><span class="line">               pop();</span><br><span class="line">               savedVariableState.localVars.set(<span class="keyword">var</span>, <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;T&gt;());</span><br><span class="line">               <span class="keyword">break</span>;</span><br><span class="line">           <span class="keyword">case</span> Opcodes.ASTORE:</span><br><span class="line">               <span class="comment">//从栈中取出数据存到局部变量表，这个数据可能是被污染的（主要还是得看调用的方法，返回值是否可被污染）</span></span><br><span class="line">               saved0 = pop();</span><br><span class="line">               savedVariableState.localVars.set(<span class="keyword">var</span>, saved0);</span><br><span class="line">               <span class="keyword">break</span>;</span><br><span class="line">           <span class="keyword">case</span> Opcodes.RET:</span><br><span class="line">               <span class="comment">// No effect on stack</span></span><br><span class="line">               <span class="keyword">break</span>;</span><br><span class="line">           <span class="keyword">default</span>:</span><br><span class="line">               <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalStateException</span>(<span class="string">&quot;Unsupported opcode: &quot;</span> + opcode);</span><br><span class="line">       &#125;</span><br><span class="line"></span><br><span class="line">       <span class="built_in">super</span>.visitVarInsn(opcode, <span class="keyword">var</span>);</span><br><span class="line"></span><br><span class="line">       sanityCheck();</span><br><span class="line">   &#125;</span><br><span class="line"></span><br><span class="line">   <span class="keyword">private</span> <span class="type">void</span> <span class="title function_">push</span><span class="params">(Set&lt;T&gt; possibleValues)</span> &#123;</span><br><span class="line">       <span class="comment">// Intentionally make this a reference to the same set</span></span><br><span class="line">       savedVariableState.stackVars.add(possibleValues);</span><br><span class="line">   &#125;</span><br></code></pre></figure></div><p>接下来当方法调用结束return时，由于使用了ARETURN指令，在解析到无操作数的简单指令时触发visitInsn，我们查看其具体逻辑，可以发现在方法return时，将当前栈上的值返回，即返回的是参数索引set，并将存储到了returnTaint中，代表了A.method1这个方法的调用，参数索引为1的参数param会污染返回值：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">visitInsn</span><span class="params">(<span class="type">int</span> opcode)</span> &#123;</span><br><span class="line">    <span class="keyword">switch</span>(opcode) &#123;</span><br><span class="line">        <span class="keyword">case</span> Opcodes.IRETURN:<span class="comment">//从当前方法返回int</span></span><br><span class="line">        <span class="keyword">case</span> Opcodes.FRETURN:<span class="comment">//从当前方法返回float</span></span><br><span class="line">        <span class="keyword">case</span> Opcodes.ARETURN:<span class="comment">//从当前方法返回对象引用</span></span><br><span class="line">            returnTaint.addAll(getStackTaint(<span class="number">0</span>));<span class="comment">//栈空间从内存高位到低位分配空间</span></span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">case</span> Opcodes.LRETURN:<span class="comment">//从当前方法返回long</span></span><br><span class="line">        <span class="keyword">case</span> Opcodes.DRETURN:<span class="comment">//从当前方法返回double</span></span><br><span class="line">            returnTaint.addAll(getStackTaint(<span class="number">1</span>));</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">case</span> Opcodes.RETURN:<span class="comment">//从当前方法返回void</span></span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">default</span>:</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="built_in">super</span>.visitInsn(opcode);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>最后对于该方法的观察结束，将污点分析结果存到了passthroughDataflow中，可以看到形成了方法与污染参数目录集合之间的映射关系：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">        <span class="keyword">final</span> Map&lt;MethodReference.Handle, Set&lt;Integer&gt;&gt; passthroughDataflow = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line"> </span><br><span class="line">passthroughDataflow.put(method, cv.getReturnTaint());</span><br></code></pre></figure></div><h3 id="C-method3"><a href="#C-method3" class="headerlink" title="C.method3"></a>C.method3</h3><p>与A.method1流程一样</p><h3 id="B-method2"><a href="#B-method2" class="headerlink" title="B.method2"></a>B.method2</h3><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">class B &#123;</span><br><span class="line">  public String method2(String param) &#123;</span><br><span class="line">    return new C().method3(param);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><ol><li><p>进入到方法内部，触发visitCode，将参数this、param放入虚拟局部变量表，并形成与参数列表索引的映射关系。</p></li><li><p>内部方法执行，ALOAD指令触发visitVarInsn，参数this、param push到污点栈。</p></li><li><p>方法内部调用C.method3，INVOKEVIRTUAL指令触发visitMethodInsn，该方法首先将C.method3参数类型提取，并判断该方法是否是静态方法，如果不是静态方法，将this（被调用方法所在类的实例对象）存入argTypes第一个，并依次存入其他参数。之后获取了方法的返回值类型的所占大小，后面进行使用：</p> ```java Type[] argTypes = Type.getArgumentTypes(desc);  if (opcode != Opcodes.INVOKESTATIC) {      //如果执行的非静态方法，则把数组第一个元素类型设置为该实例对象的类型，类比局部变量表      Type[] extendedArgTypes = new Type[argTypes.length+1];      System.arraycopy(argTypes, 0, extendedArgTypes, 1, argTypes.length);      extendedArgTypes[0] = Type.getObjectType(owner);      argTypes = extendedArgTypes;  } ```</li><li><p>接下来初始化argTaint，将其内部元素设置为空，argTaint大小为参数的数量。然后将污点栈中的参数依次存放进argTaint中，对于污点栈savedVariableState.stackVars来说，list从右往左为栈底到栈顶，假设方法参数列表为abc，那么从栈底到栈顶分别为a、b、c。继续将污点栈栈顶元素取出后放在argTaint的最后一个位置，以此类推，从而保证了argTaint中存放的参数索引与C.method3的参数列表的顺序相同。</p> ```java final List<Set<Integer>> argTaint = new ArrayList<Set<Integer>>(argTypes.length); for (int i = 0; i < argTypes.length; i++) {     argTaint.add(null); }     int stackIndex = 0; for (int i = 0; i < argTypes.length; i++) {     Type argType = argTypes[i];     if (argType.getSize() > 0) {         //根据参数类型大小，从栈顶获取入参，参数入栈是从左到右的         argTaint.set(argTypes.length - 1 - i, getStackTaint(stackIndex + argType.getSize() - 1));     }     stackIndex += argType.getSize(); } ```</li><li><p>接下来判断方法是否是构造器，如果是构造器的话意味着在当前调用方法（B.method2）当中会有这么一段代码：</p> ```java C c = new C(); ```  <p>因此可以确定被调用方法（C.method3）的返回值结果受到了this（C类实例对象）的污染，那么将argTaint中的0号索引取出，即为this，并将其加入resultTaint。如果不是构造器，那么就创造一个空的HashSet来存储后面的resultTaint。</p></li><li><p>从passthroughDataflow中拿到被调用方法C.method3的参数与返回值污点分析关系，并判断污点分析关系中的参数是否在当前的argTaint中，如果在则说明被调用方法的返回值被调用者传入的参数污染，这也就是为什么要进行逆拓扑排序。</p> ```java Set<Integer> passthrough = passthroughDataflow.get(new MethodReference.Handle(new ClassReference.Handle(owner), name, desc)); if (passthrough != null) {     for (Integer passthroughDataflowArg : passthrough) {         //判断是否和同一方法体内的其它方法返回值关联，有关联则添加到栈底，等待执行return时保存         resultTaint.addAll(argTaint.get(passthroughDataflowArg));     } ```</li><li><p>最后还是return，将B.method2的结果存到passthroughDataflow中</p></li></ol><h3 id="main方法"><a href="#main方法" class="headerlink" title="main方法"></a>main方法</h3><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Main</span> &#123;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">public</span> <span class="type">String</span> <span class="title function_">main</span><span class="params">(String args)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">cmd</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">A</span>().method1(args);</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">B</span>().method2(cmd);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>第一步，执行visitCode存储入参到局部变量表</p><p>第二步，执行visitVarInsn参数入栈</p><p>第三步，执行visitMethodInsn调用A.method1，A.method1被污染的返回结果，也就是参数索引会被放在栈顶</p><p>第四步，执行visitVarInsn把放在栈顶的污染参数索引，放入到本地变量表</p><p>第五步，执行visitVarInsn参数入栈</p><p>第六步，执行visitMethodInsn调用B.method2，被污染的返回结果会被放在栈顶</p><p>第七步，执行visitInsn，返回栈顶数据，缓存到passthroughDataflow，也就是main方法的污点分析结果</p><p>最后通过passthroughDiscovery.save方法保存分析数据</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">PassThroughFactory</span> <span class="keyword">implements</span> <span class="title class_">DataFactory</span>&lt;Map.Entry&lt;MethodReference.Handle, Set&lt;Integer&gt;&gt;&gt; &#123;</span><br><span class="line"></span><br><span class="line">    ...</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">String</span>[] <span class="title function_">serialize</span><span class="params">(Map.Entry&lt;MethodReference.Handle, Set&lt;Integer&gt;&gt; entry)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (entry.getValue().size() == <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">final</span> <span class="type">String</span>[] <span class="variable">fields</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>[<span class="number">4</span>];</span><br><span class="line">        fields[<span class="number">0</span>] = entry.getKey().getClassReference().getName();</span><br><span class="line">        fields[<span class="number">1</span>] = entry.getKey().getName();</span><br><span class="line">        fields[<span class="number">2</span>] = entry.getKey().getDesc();</span><br><span class="line"></span><br><span class="line">        <span class="type">StringBuilder</span> <span class="variable">sb</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringBuilder</span>();</span><br><span class="line">        <span class="keyword">for</span> (Integer arg : entry.getValue()) &#123;</span><br><span class="line">            sb.append(Integer.toString(arg));</span><br><span class="line">            sb.append(<span class="string">&quot;,&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        fields[<span class="number">3</span>] = sb.toString();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> fields;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>最后持久化的passthrough.dat文件的数据格式如下：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">类名 方法名 方法描述 能污染返回值的参数索引1,能污染返回值的参数索引2,能污染返回值的参数索引3...</span><br></code></pre></figure></div><h2 id="0x05-方法调用污染关联-CallGraphDiscovery"><a href="#0x05-方法调用污染关联-CallGraphDiscovery" class="headerlink" title="0x05 方法调用污染关联-CallGraphDiscovery"></a>0x05 方法调用污染关联-CallGraphDiscovery</h2><p>我们用这个例子进行分析：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Main</span> &#123;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">private</span> String name;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">public</span> <span class="type">void</span> <span class="title function_">main</span><span class="params">(String args)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">A</span>().method1(args, name);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">A</span> &#123;</span><br><span class="line">  <span class="keyword">public</span> <span class="type">String</span> <span class="title function_">method1</span><span class="params">(String param, String param2)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> param + param2;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>跟进callGraphDiscovery.discover，读取前面收集的数据，然后使用ModelGeneratorClassVisitor进行观察，visitCode观察每一个类，visitMethod观察类中的每一个方法，继续跟进ModelGeneratorMethodVisitor</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="type">MethodVisitor</span> <span class="title function_">visitMethod</span><span class="params">(<span class="type">int</span> access, String name, String desc,</span></span><br><span class="line"><span class="params">                                 String signature, String[] exceptions)</span> &#123;</span><br><span class="line">    <span class="type">MethodVisitor</span> <span class="variable">mv</span> <span class="operator">=</span> <span class="built_in">super</span>.visitMethod(access, name, desc, signature, exceptions);</span><br><span class="line">    <span class="type">ModelGeneratorMethodVisitor</span> <span class="variable">modelGeneratorMethodVisitor</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ModelGeneratorMethodVisitor</span>(classMap,</span><br><span class="line">            inheritanceMap, passthroughDataflow, serializableDecider, api, mv, <span class="built_in">this</span>.name, access, name, desc, signature, exceptions);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">JSRInlinerAdapter</span>(modelGeneratorMethodVisitor, access, name, desc, signature, exceptions);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>进入main方法内部，触发visitCode，main方法不是静态，将this以及参数args存入局部变量表，此处与前面不同的是会在参数索引前加一个arg前缀来进行标识：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">visitCode</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="built_in">super</span>.visitCode();</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> <span class="variable">localIndex</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line">    <span class="type">int</span> <span class="variable">argIndex</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line">    <span class="comment">//使用arg前缀来表示方法入参，后续用于判断是否为目标调用方法的入参</span></span><br><span class="line">    <span class="keyword">if</span> ((<span class="built_in">this</span>.access &amp; Opcodes.ACC_STATIC) == <span class="number">0</span>) &#123;</span><br><span class="line">        setLocalTaint(localIndex, <span class="string">&quot;arg&quot;</span> + argIndex);</span><br><span class="line">        localIndex += <span class="number">1</span>;</span><br><span class="line">        argIndex += <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">for</span> (Type argType : Type.getArgumentTypes(desc)) &#123;</span><br><span class="line">        setLocalTaint(localIndex, <span class="string">&quot;arg&quot;</span> + argIndex);</span><br><span class="line">        localIndex += argType.getSize();</span><br><span class="line">        argIndex += <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><hr><p>我在写到这里的时候有一点疑问，对于visitVarInsn的调用时机。我们来看如下两个例子：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="comment">// example 1</span></span><br><span class="line"><span class="type">A</span> <span class="variable">a</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">A</span>();</span><br><span class="line">a.method1(args);</span><br><span class="line"></span><br><span class="line"><span class="comment">//example 2</span></span><br><span class="line"><span class="keyword">new</span> <span class="title class_">A</span>().method1(args);</span><br></code></pre></figure></div><p>我们先来看第一个例子，new A()的字节码指令大概如下，可以看到是没有LOAD指令的，在调用构造方法时直接消费的是操作数栈上的A对象引用：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">NEW A //创建A类实例</span><br><span class="line">DUP   //创建对象引用</span><br><span class="line">INVOKESPECIAL A.&lt;init&gt;()V //调用构造方法</span><br></code></pre></figure></div><p>接下来由于要把对象引用存到a中，因此会把对象引用存储到局部变量表中（假设在局部变量表2号位，局部变量表1号位存储args），即ASTORE指令，此时会触发一次visitVarInsn。那么接下来在调用a.method1(args)时需要进行两次ALOAD，首先把a的对象引用加载到操作数栈上，再把args加载到操作数栈上，从而接着触发了两次visitVarInsn</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">NEW A</span><br><span class="line">DUP</span><br><span class="line">INVOKESPECIAL A.&lt;init&gt;()V</span><br><span class="line">ASTORE 2        // 存到局部槽 2 —&gt; visitVarInsn(ASTORE,2)</span><br><span class="line">ALOAD 2         // 再加载回来 —&gt; visitVarInsn(ALOAD,2)</span><br><span class="line">ALOAD 1         // 加载 args  —&gt; visitVarInsn(ALOAD,1)</span><br><span class="line">INVOKEVIRTUAL A.method1…</span><br></code></pre></figure></div><p>继续我们看第二个例子，当构造函数执行完毕后，不需要进行ASTORE，并且再调用method1时也不需要从局部变量表中加载a的对象引用，因此最终只有加载args时才会调用一次visitVarInsn</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">NEW A</span><br><span class="line">DUP</span><br><span class="line">INVOKESPECIAL A.&lt;init&gt;()V</span><br><span class="line">// 上一步执行完 new A()，操作数栈上已经有了 A 的实例</span><br><span class="line">ALOAD 1          // 将 args（槽 1）加载到栈顶 — 触发一次 visitVarInsn(AL OAD,1)</span><br><span class="line">INVOKEVIRTUAL A.method1:(Ljava/lang/String;)Ljava/lang/Strin</span><br></code></pre></figure></div><hr><p>检测到字节码指令new，触发visitTypeInsn，会push一个空的HashSet到污点栈中：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="type">void</span> <span class="title function_">visitTypeInsn</span><span class="params">(<span class="type">int</span> opcode, String type)</span> &#123;</span><br><span class="line">     <span class="keyword">switch</span>(opcode) &#123;</span><br><span class="line">         <span class="keyword">case</span> Opcodes.NEW:</span><br><span class="line">             push();</span><br><span class="line">             <span class="keyword">break</span>;</span><br><span class="line">         <span class="keyword">case</span> Opcodes.ANEWARRAY:</span><br><span class="line">             pop();</span><br><span class="line">             push();</span><br><span class="line">             <span class="keyword">break</span>;</span><br><span class="line">         <span class="keyword">case</span> Opcodes.CHECKCAST:</span><br><span class="line">             <span class="comment">// No-op</span></span><br><span class="line">             <span class="keyword">break</span>;</span><br><span class="line">         <span class="keyword">case</span> Opcodes.INSTANCEOF:</span><br><span class="line">             pop();</span><br><span class="line">             push();</span><br><span class="line">             <span class="keyword">break</span>;</span><br><span class="line">         <span class="keyword">default</span>:</span><br><span class="line">             <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalStateException</span>(<span class="string">&quot;Unsupported opcode: &quot;</span> + opcode);</span><br><span class="line">     &#125;</span><br></code></pre></figure></div><p>字节码指令<code>INVOKESPECIALA.&lt;init&gt;()V</code>，调用A的构造器，触发visitMethodInsn，判断是否是构造器，被调用方法为构造器，将this设置为argTypes第一个参数：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">Type</span>[] <span class="variable">argTypes</span> <span class="operator">=</span> Type.getArgumentTypes(desc);</span><br><span class="line"><span class="keyword">if</span> (opcode != Opcodes.INVOKESTATIC) &#123;</span><br><span class="line">    <span class="type">Type</span>[] <span class="variable">extendedArgTypes</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Type</span>[argTypes.length+<span class="number">1</span>];</span><br><span class="line">    System.arraycopy(argTypes, <span class="number">0</span>, extendedArgTypes, <span class="number">1</span>, argTypes.length);</span><br><span class="line">    extendedArgTypes[<span class="number">0</span>] = Type.getObjectType(owner);</span><br><span class="line">    argTypes = extendedArgTypes;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>jiee下来检测启动工具时参数是否要进行污点分析，如果不进行污点分析，则直接把调用方法以及被调用方法封装为GraphCall，加入discoveredCalls中：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">if</span> (!ConfigHelper.taintTrack) &#123;</span><br><span class="line">    <span class="comment">//不进行污点分析，全部调用关系都记录</span></span><br><span class="line">    discoveredCalls.add(<span class="keyword">new</span> <span class="title class_">GraphCall</span>(</span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">MethodReference</span>.Handle(<span class="keyword">new</span> <span class="title class_">ClassReference</span>.Handle(<span class="built_in">this</span>.owner), <span class="built_in">this</span>.name, <span class="built_in">this</span>.desc),</span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">MethodReference</span>.Handle(<span class="keyword">new</span> <span class="title class_">ClassReference</span>.Handle(owner), name, desc),</span><br><span class="line">        <span class="number">0</span>,</span><br><span class="line">        <span class="string">&quot;&quot;</span>,</span><br><span class="line">        <span class="number">0</span>));</span><br><span class="line">    <span class="keyword">break</span>;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>启动污点分析后的逻辑接着往下看，会从污点栈中取出对应的参数，但我们这里由于没有进入到visitVarInsn，因此污点栈目前只有一个在visitInsn中push进去的一个空的set，这一步不会对discoverdCalls做任何事情</p><p>接着我们分析method1(args,name)的调用情况，首先需要加载args，触发visitVarInsn，ALOAD指令，将args（arg1）推入污点栈，然后调用visitMethodInsn。由于要传递的参数name是a的属性，因此需要加载this，从this中拿到name属性。触发ALOAD指令，将this（arg0）推入污点栈。此时污点栈中为如下内容：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">stackVars</span><br><span class="line">[&#123;&#125;, &#123;&quot;arg1&quot;&#125;, &#123;&quot;arg0&quot;&#125; ]</span><br></code></pre></figure></div><p>接下来需要读入实例a的name字段，检测到字节码指令GETFIELD，触发visitFieldInsn，首先在ClassReference中不断遍历，直到找到该字段，判断该字段是否是transient，如果是transient就没必要加入污点栈。如果是非transient属性，就把栈顶当前的arg0修改为arg0.name加入污点栈中</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">Set</span>&lt;String&gt; <span class="variable">newTaint</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;&gt;();</span><br><span class="line"><span class="keyword">if</span> (!Boolean.TRUE.equals(isTransient)) &#123;</span><br><span class="line">    <span class="keyword">for</span> (String s : getStackTaint(<span class="number">0</span>)) &#123;</span><br><span class="line">        newTaint.add(s + <span class="string">&quot;.&quot;</span> + name);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">super</span>.visitFieldInsn(opcode, owner, name, desc);</span><br><span class="line"><span class="comment">//在调用方法前，都会先入栈，作为参数</span></span><br><span class="line">setStackTaint(<span class="number">0</span>, newTaint);</span><br></code></pre></figure></div><p>非静态方法，argTypes第一个为A（this），第二个为String（args），第三个为String（name），对应了污点栈上的[{}，{“arg1”}, {“arg0”} ]（从左到右为栈底到栈顶），for循环i从0到2，分别从污点栈中拿到了arg0.name，arg1和空set。首先对arg0.name进行拆解，最终拆解出来dotIndex为4，srcArgIndex为0，srcArgPath为name，并记录到了discoverdCalls当中。继续拆解arg1，dotindex为-1，srcArgIndexn为1，srcArgPath为null，记录到discoverdCalls中。</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">int</span> <span class="variable">stackIndex</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line">                  <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; argTypes.length; i++) &#123;</span><br><span class="line">                      <span class="comment">//最右边的参数，就是最后入栈，即在栈顶</span></span><br><span class="line">                      <span class="type">int</span> <span class="variable">argIndex</span> <span class="operator">=</span> argTypes.length-<span class="number">1</span>-i;</span><br><span class="line">                      <span class="type">Type</span> <span class="variable">type</span> <span class="operator">=</span> argTypes[argIndex];</span><br><span class="line">                      <span class="comment">//操作数栈出栈，调用方法前，参数都已入栈</span></span><br><span class="line">                      <span class="type">Set</span>&lt;String&gt; <span class="variable">taint</span> <span class="operator">=</span> getStackTaint(stackIndex);</span><br><span class="line">                      <span class="keyword">if</span> (taint.size() &gt; <span class="number">0</span>) &#123;</span><br><span class="line">                          <span class="keyword">for</span> (String argSrc : taint) &#123;</span><br><span class="line">                              <span class="comment">//取出出栈的参数，判断是否为当前方法的入参，arg前缀</span></span><br><span class="line">                              <span class="keyword">if</span> (!argSrc.substring(<span class="number">0</span>, <span class="number">3</span>).equals(<span class="string">&quot;arg&quot;</span>)) &#123;</span><br><span class="line">                                  <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalStateException</span>(<span class="string">&quot;Invalid taint arg: &quot;</span> + argSrc);</span><br><span class="line">                              &#125;</span><br><span class="line">                              <span class="type">int</span> <span class="variable">dotIndex</span> <span class="operator">=</span> argSrc.indexOf(<span class="string">&#x27;.&#x27;</span>);</span><br><span class="line">                              <span class="type">int</span> srcArgIndex;</span><br><span class="line">                              String srcArgPath;</span><br><span class="line">                              <span class="keyword">if</span> (dotIndex == -<span class="number">1</span>) &#123;</span><br><span class="line">                                  srcArgIndex = Integer.parseInt(argSrc.substring(<span class="number">3</span>));</span><br><span class="line">                                  srcArgPath = <span class="literal">null</span>;</span><br><span class="line">                              &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                                  srcArgIndex = Integer.parseInt(argSrc.substring(<span class="number">3</span>, dotIndex));</span><br><span class="line">                                  srcArgPath = argSrc.substring(dotIndex+<span class="number">1</span>);</span><br><span class="line">                              &#125;</span><br><span class="line">                              <span class="comment">//记录参数流动关系</span></span><br><span class="line">                              <span class="comment">//argIndex：当前方法参数索引，srcArgIndex：对应上一级方法的参数索引</span></span><br><span class="line">                              discoveredCalls.add(<span class="keyword">new</span> <span class="title class_">GraphCall</span>(</span><br><span class="line">                                      <span class="keyword">new</span> <span class="title class_">MethodReference</span>.Handle(<span class="keyword">new</span> <span class="title class_">ClassReference</span>.Handle(<span class="built_in">this</span>.owner), <span class="built_in">this</span>.name, <span class="built_in">this</span>.desc),</span><br><span class="line">                                      <span class="keyword">new</span> <span class="title class_">MethodReference</span>.Handle(<span class="keyword">new</span> <span class="title class_">ClassReference</span>.Handle(owner), name, desc),</span><br><span class="line">                                      srcArgIndex,</span><br><span class="line">                                      srcArgPath,</span><br><span class="line">                                      argIndex));</span><br><span class="line">                          &#125;</span><br><span class="line">                      &#125;</span><br><span class="line"></span><br><span class="line">                      stackIndex += type.getSize();</span><br><span class="line">                  &#125;</span><br></code></pre></figure></div><p>最后save保存数据，持久化后的callgraph.dat格式如下：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">调用者类名 调用者方法caller 调用者方法描述 被调用者类名 被调用者方法target 被调用者方法描述 调用者方法参数索引 调用者字段名 被调用者方法参数索引</span><br></code></pre></figure></div><h2 id="0x06-利用链入口搜索-SourceDiscovery"><a href="#0x06-利用链入口搜索-SourceDiscovery" class="headerlink" title="0x06 利用链入口搜索-SourceDiscovery"></a>0x06 利用链入口搜索-SourceDiscovery</h2><p>在一开始我们也说到了，在挖掘反序列化链的时候需要指定类型，所以此处先获得对应的sourceDiscovery，我们这里以Jackson反序列化分析</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">if</span> (!Files.exists(Paths.get(<span class="string">&quot;sources.dat&quot;</span>))) &#123;</span><br><span class="line">    LOGGER.info(<span class="string">&quot;Discovering gadget chain source methods...&quot;</span>);</span><br><span class="line">    <span class="type">SourceDiscovery</span> <span class="variable">sourceDiscovery</span> <span class="operator">=</span> config.getSourceDiscovery();</span><br><span class="line">    <span class="comment">//查找利用链的入口（例：java原生反序列化的readObject）</span></span><br><span class="line">    sourceDiscovery.discover();</span><br><span class="line">    sourceDiscovery.save();</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>跟进SourceDiscovery.discover在jackson中的实现，可以发现对于Jackson反序列化来说，source需要判断方法是否是无参构造、setter和getter，只有这些方法才能作为jackson反序列化的入口：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">discover</span><span class="params">(Map&lt;ClassReference.Handle, ClassReference&gt; classMap,</span></span><br><span class="line"><span class="params">                     Map&lt;MethodReference.Handle, MethodReference&gt; methodMap,</span></span><br><span class="line"><span class="params">                     InheritanceMap inheritanceMap, Map&lt;MethodReference.Handle, Set&lt;GraphCall&gt;&gt; graphCallMap)</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">final</span> <span class="type">JacksonSerializableDecider</span> <span class="variable">serializableDecider</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">JacksonSerializableDecider</span>(methodMap);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (MethodReference.Handle method : methodMap.keySet()) &#123;</span><br><span class="line">        <span class="keyword">if</span> (skipList.contains(method.getClassReference().getName())) &#123;</span><br><span class="line">            <span class="keyword">continue</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (serializableDecider.apply(method.getClassReference())) &#123;</span><br><span class="line">            <span class="keyword">if</span> (method.getName().equals(<span class="string">&quot;&lt;init&gt;&quot;</span>) &amp;&amp; method.getDesc().equals(<span class="string">&quot;()V&quot;</span>)) &#123;</span><br><span class="line">                addDiscoveredSource(<span class="keyword">new</span> <span class="title class_">Source</span>(method, <span class="number">0</span>));</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (method.getName().startsWith(<span class="string">&quot;get&quot;</span>) &amp;&amp; method.getDesc().startsWith(<span class="string">&quot;()&quot;</span>)) &#123;</span><br><span class="line">                addDiscoveredSource(<span class="keyword">new</span> <span class="title class_">Source</span>(method, <span class="number">0</span>));</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (method.getName().startsWith(<span class="string">&quot;set&quot;</span>) &amp;&amp; method.getDesc().matches(<span class="string">&quot;\\(L[^;]*;\\)V&quot;</span>)) &#123;</span><br><span class="line">                addDiscoveredSource(<span class="keyword">new</span> <span class="title class_">Source</span>(method, <span class="number">0</span>));</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>最后还是将方法保存持久化为sources.dat，格式如下：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">类名 方法名 方法描述 污染参数索引</span><br></code></pre></figure></div><h2 id="0x07-gadgetChain挖掘-GadgetChainDiscovery"><a href="#0x07-gadgetChain挖掘-GadgetChainDiscovery" class="headerlink" title="0x07 gadgetChain挖掘-GadgetChainDiscovery"></a>0x07 gadgetChain挖掘-GadgetChainDiscovery</h2><p>跟进GadgetChainDiscovery.discover，首先进行所有重写方法的扫描，在一开始我们也说了工具没有办法在运行时进行扫描，所以对于各种方法的重写我们没有办法确定到底调用的是哪个方法</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">Map&lt;MethodReference.Handle, MethodReference&gt; methodMap = DataLoader.loadMethods();</span><br><span class="line">    <span class="type">InheritanceMap</span> <span class="variable">inheritanceMap</span> <span class="operator">=</span> InheritanceMap.load();</span><br><span class="line">    Map&lt;MethodReference.Handle, Set&lt;MethodReference.Handle&gt;&gt; methodImplMap = InheritanceDeriver</span><br><span class="line">        .getAllMethodImplementations(</span><br><span class="line">            inheritanceMap, methodMap);</span><br><span class="line">    Map&lt;ClassReference.Handle, Set&lt;MethodReference.Handle&gt;&gt; methodsByClass = InheritanceDeriver.getMethodsByClass(methodMap);</span><br></code></pre></figure></div><p>跟进InheritanceDeriver.getAllMethodImplementations，获取之前收集到的method的类，并通过之前收集到的继承关系来获取类的所有子孙类，最终形成类-&gt;子孙类的映射关系：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">Map&lt;Handle, Set&lt;MethodReference.Handle&gt;&gt; methodsByClass = getMethodsByClass(methodMap);  </span><br><span class="line">Map&lt;ClassReference.Handle, Set&lt;ClassReference.Handle&gt;&gt; subClassMap = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">        <span class="keyword">for</span> (Map.Entry&lt;ClassReference.Handle, Set&lt;ClassReference.Handle&gt;&gt; entry : inheritanceMap.entrySet()) &#123;</span><br><span class="line">            <span class="keyword">for</span> (ClassReference.Handle parent : entry.getValue()) &#123;</span><br><span class="line">                <span class="keyword">if</span> (!subClassMap.containsKey(parent)) &#123;</span><br><span class="line">                    Set&lt;ClassReference.Handle&gt; subClasses = <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;&gt;();</span><br><span class="line">                    subClasses.add(entry.getKey());</span><br><span class="line">                    subClassMap.put(parent, subClasses);</span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    subClassMap.get(parent).add(entry.getKey());</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br></code></pre></figure></div><p>接下来遍历所有的方法，并遍历subclasses，如果某一个subclass中存在与当前遍历的方法名和返回值一致的方法，就将其加入overridingMethods，最后整合所有重写的方法，形成方法名到重写方法之间的映射关系，由于静态方法不可重写，因此遇到静态方法直接跳过：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="comment">//遍历所有方法，根据父类-&gt;子孙类集合，找到所有的override的方法，记录下来（某个类的方法-&gt;所有的override方法）</span></span><br><span class="line">Map&lt;MethodReference.Handle, Set&lt;MethodReference.Handle&gt;&gt; methodImplMap = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line"><span class="keyword">for</span> (MethodReference method : methodMap.values()) &#123;</span><br><span class="line">    <span class="comment">// Static methods cannot be overriden</span></span><br><span class="line">    <span class="keyword">if</span> (method.isStatic()) &#123;</span><br><span class="line">        <span class="keyword">continue</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    Set&lt;MethodReference.Handle&gt; overridingMethods = <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;&gt;();</span><br><span class="line">    Set&lt;ClassReference.Handle&gt; subClasses = subClassMap.get(method.getClassReference());</span><br><span class="line">    <span class="keyword">if</span> (subClasses != <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">for</span> (ClassReference.Handle subClass : subClasses) &#123;</span><br><span class="line">            <span class="comment">// This class extends ours; see if it has a matching method</span></span><br><span class="line">            Set&lt;MethodReference.Handle&gt; subClassMethods = methodsByClass.get(subClass);</span><br><span class="line">            <span class="keyword">if</span> (subClassMethods != <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="keyword">for</span> (MethodReference.Handle subClassMethod : subClassMethods) &#123;</span><br><span class="line">                    <span class="keyword">if</span> (subClassMethod.getName().equals(method.getName()) &amp;&amp; subClassMethod.getDesc().equals(method.getDesc())) &#123;</span><br><span class="line">                        overridingMethods.add(subClassMethod);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (overridingMethods.size() &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        methodImplMap.put(method.getHandle(), overridingMethods);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>然后下面的一大堆逻辑就是对重写方法关系的持久化存储，最终的methodimpl.dat格式如下：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">类名 方法名 方法描述</span><br><span class="line">\t重写方法的类名 方法名 方法描述</span><br><span class="line">\t重写方法的类名 方法名 方法描述</span><br><span class="line">\t重写方法的类名 方法名 方法描述</span><br><span class="line">\t重写方法的类名 方法名 方法描述</span><br><span class="line">类名 方法名 方法描述</span><br><span class="line">\t重写方法的类名 方法名 方法描述</span><br><span class="line">\t重写方法的类名 方法名 方法描述</span><br></code></pre></figure></div><p>接下来对callgraph.dat的调用关系进行整合，对于同一个方法发起的调用，整合成caller-&gt;被调用方法集合之间的映射关系：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">Map&lt;MethodReference.Handle, Set&lt;GraphCall&gt;&gt; graphCallMap = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line"><span class="keyword">for</span> (GraphCall graphCall : DataLoader</span><br><span class="line">    .loadData(Paths.get(<span class="string">&quot;callgraph.dat&quot;</span>), <span class="keyword">new</span> <span class="title class_">GraphCall</span>.Factory())) &#123;</span><br><span class="line">  MethodReference.<span class="type">Handle</span> <span class="variable">caller</span> <span class="operator">=</span> graphCall.getCallerMethod();</span><br><span class="line">  <span class="keyword">if</span> (!graphCallMap.containsKey(caller)) &#123;</span><br><span class="line">    <span class="type">Set</span>&lt;GraphCall&gt; <span class="variable">graphCalls</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;&gt;();</span><br><span class="line">    graphCalls.add(graphCall);</span><br><span class="line">    graphCallMap.put(caller, graphCalls);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    graphCallMap.get(caller).add(graphCall);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>剩下的挖掘逻辑我们用一个例子来分析：</p><p>设我们有如下方法间调用：</p><ol><li><strong>源</strong>：<code>A.sources()</code> 污染参数 0</li><li><code>A.sources(0)</code> → 调用 <code>B.load(0)</code></li><li><code>B.load(0)</code> → 调用接口方法 <code>C.handle(0)</code></li><li><code>C.handle(0)</code> 在实现类 <code>CImpl</code> 中有实现 <code>CImpl.handle(0)</code></li><li><code>CImpl.handle(0)</code> → 调用 <code>D.sink(1)</code>（这里假设它把参数 1 污染到 sink）</li><li><code>D.sink(1)</code> 是最终的 <strong>sink</strong></li></ol><p>对应的数据结构：</p><ul><li><p>**sources.dat** 只包含一个 `Source(A.sources, taintedArgIndex=0)`</p></li><li><p>**graphCallMap**</p> ``` A.sources → { GraphCall(callerArgIndex=0, targetMethod=B.load,   targetArgIndex=0) } B.load    → { GraphCall(callerArgIndex=0, targetMethod=C.handle, targetArgIndex=0) } C.handle  → { GraphCall(callerArgIndex=0, targetMethod=C.handle, targetArgIndex=0) } // interface CImpl.handle → { GraphCall(callerArgIndex=0, targetMethod=D.sink, targetArgIndex=1) } ```</li><li><p>**implementationFinder.getImplementations(C.handle)** → `{ CImpl.handle }`</p></li><li><p>**isSink(D.sink,1)** → `true`</p></li></ul><p>对于是否为sink点的判断逻辑如下：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">private</span> <span class="type">boolean</span> <span class="title function_">isSink</span><span class="params">(MethodReference.Handle method, <span class="type">int</span> argIndex,</span></span><br><span class="line"><span class="params">     InheritanceMap inheritanceMap)</span> &#123;</span><br><span class="line">   <span class="keyword">if</span> (!customSlinks.isEmpty()) &#123;</span><br><span class="line">     <span class="keyword">for</span> (CustomSlink customSlink:customSlinks) &#123;</span><br><span class="line">       <span class="type">boolean</span> <span class="variable">flag</span> <span class="operator">=</span> <span class="literal">false</span>;</span><br><span class="line">       <span class="keyword">if</span> (customSlink.getClassName() != <span class="literal">null</span>)</span><br><span class="line">         flag &amp;= customSlink.getClassName().equals(method.getClassReference().getName());</span><br><span class="line">       <span class="keyword">if</span> (customSlink.getMethod() != <span class="literal">null</span>)</span><br><span class="line">         flag &amp;= customSlink.getMethod().equals(method.getName());</span><br><span class="line">       <span class="keyword">if</span> (customSlink.getDesc() != <span class="literal">null</span>)</span><br><span class="line">         flag &amp;= customSlink.getDesc().equals(method.getDesc());</span><br><span class="line">       <span class="keyword">if</span> (flag)</span><br><span class="line">         <span class="keyword">return</span> flag;</span><br><span class="line">     &#125;</span><br><span class="line">     <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">if</span> (config.getName().equals(<span class="string">&quot;sqlinject&quot;</span>)) &#123;</span><br><span class="line">     <span class="comment">//SQLInject只能检测注入</span></span><br><span class="line">     <span class="keyword">return</span> isSQLInjectSink(method, argIndex, inheritanceMap);</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">if</span> (config.getName().equals(<span class="string">&quot;hessian&quot;</span>)) &#123;</span><br><span class="line">     <span class="comment">//仅hessian可选BCEL slink</span></span><br><span class="line">     <span class="keyword">if</span> (ConfigHelper.slinks.contains(<span class="string">&quot;BCEL&quot;</span>) &amp;&amp; BCELSlink(method, argIndex, inheritanceMap)) &#123;</span><br><span class="line">       <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">     &#125;</span><br><span class="line">   &#125;</span><br><span class="line"></span><br><span class="line">   <span class="comment">//通用slink，不设定slink则全部都挖掘</span></span><br><span class="line">   <span class="keyword">if</span> ((ConfigHelper.slinks.isEmpty() || ConfigHelper.slinks.contains(<span class="string">&quot;JNDI&quot;</span>)) &amp;&amp; JNDISlink(method, inheritanceMap)) &#123;</span><br><span class="line">     <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">if</span> ((ConfigHelper.slinks.isEmpty() || ConfigHelper.slinks.contains(<span class="string">&quot;CLASSLOADER&quot;</span>)) &amp;&amp; ClassLoaderlink(method, argIndex, inheritanceMap)) &#123;</span><br><span class="line">     <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">   &#125;</span><br><span class="line"></span><br><span class="line">   <span class="keyword">if</span> ((ConfigHelper.slinks.isEmpty() || ConfigHelper.slinks.contains(<span class="string">&quot;SSRFAndXXE&quot;</span>)) &amp;&amp; SSRFAndXXESlink(method, inheritanceMap)) &#123;</span><br><span class="line">     <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">if</span> ((ConfigHelper.slinks.isEmpty() || ConfigHelper.slinks.contains(<span class="string">&quot;EXEC&quot;</span>)) &amp;&amp; EXECSlink(method, argIndex)) &#123;</span><br><span class="line">     <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">if</span> ((ConfigHelper.slinks.isEmpty() || ConfigHelper.slinks.contains(<span class="string">&quot;FileIO&quot;</span>)) &amp;&amp; FileIOSlink(method)) &#123;</span><br><span class="line">     <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">if</span> ((ConfigHelper.slinks.isEmpty() || ConfigHelper.slinks.contains(<span class="string">&quot;Reflect&quot;</span>)) &amp;&amp; ReflectSlink(method, argIndex, inheritanceMap)) &#123;</span><br><span class="line">     <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">if</span> ((ConfigHelper.slinks.isEmpty() || ConfigHelper.slinks.contains(<span class="string">&quot;JDBC&quot;</span>)) &amp;&amp; JDBCSlink(method, argIndex, inheritanceMap)) &#123;</span><br><span class="line">     <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">if</span> ((ConfigHelper.slinks.isEmpty() || ConfigHelper.slinks.contains(<span class="string">&quot;EL&quot;</span>)) &amp;&amp; ELSlink(method, argIndex, inheritanceMap)) &#123;</span><br><span class="line">     <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">if</span> ((ConfigHelper.slinks.isEmpty() || ConfigHelper.slinks.contains(<span class="string">&quot;SQLInject&quot;</span>)) &amp;&amp; isSQLInjectSink(method, argIndex, inheritanceMap)) &#123;</span><br><span class="line">     <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> &#125;</span><br></code></pre></figure></div><p>配置参数：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">maxChainLength = 10</span><br><span class="line">opLevel = 2</span><br><span class="line">taintTrack = true</span><br></code></pre></figure></div><p>1️⃣ 初始化</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">for each Source:</span><br><span class="line">  srcLink = (A.sources, 0)</span><br><span class="line">  methodsToExplore  = [ [ A.sources(0) ] ]</span><br><span class="line">  exploredMethods   = &#123; A.sources(0) &#125;</span><br><span class="line">discoveredGadgets = &#123; &#125;</span><br></code></pre></figure></div><p>2️⃣ 第一次迭代</p><ul><li><p>**iteration=0** → pop first chain</p> ``` chain = [ A.sources(0) ] lastLink = (A.sources,0) ```</li><li><p>**长度检查**：`1 < maxChainLength` → 通过</p></li></ul></li><li><p><strong>taintTrack</strong>：<code>GC1.callerArgIndex(0) == lastLink.taintedArgIndex(0)</code> → 通过</p></li><li><p><strong>找实现</strong>：<code>allImpls = getImpls(B.load)</code> → <code>{ B.load }</code>（普通方法）</p></li><li><p><strong>遍历 impls</strong>：</p><ul><li><p>`methodImpl = B.load`</p></li><li><p>`newLink = (B.load,0)`</p></li><li><p>**去重**：`exploredMethods` 不含 → 继续</p></li><li><p>**新链**：`newChain = [ A.sources(0), B.load(0) ]`</p></li><li><p>**sink 检测**：`isSink(B.load,0)` → `false`</p></li><li><p>**加入队列**：</p> ``` methodsToExplore = [ [A.sources(0),B.load(0)] ] exploredMethods.add(B.load(0)) ```</li></ul></li></ul><p>3️⃣ 第二次迭代</p><ul><li><p>**iteration=1** → pop</p> ``` chain = [A.sources(0),B.load(0)] lastLink = (B.load,0) ```</li></ul></li><li><p><strong>taintTrack</strong>：匹配 → 通过</p></li><li><p><strong>impls</strong>：<code>getImpls(C.handle)</code> → <code>{ }</code>，<br> fallback 父类查找也无（接口），所以按注释 “GadgetInspector bug”，跳到父类去搜，依次找到 <code>C.handle</code> 本身，加入。</p></li><li><p><strong>impls 变为</strong> → <code>{ C.handle }</code></p></li><li><p><strong>for each impl</strong>：</p><ul><li><p>`newLink = (C.handle,0)`</p></li><li><p>去重通过</p></li><li><p>`newChain = [A.sources(0),B.load(0),C.handle(0)]`</p></li><li><p>`isSink(C.handle,0)` → `false`</p></li><li><p>加入：</p> ``` methodsToExplore = [ [A.sources(0),B.load(0),C.handle(0)] ] exploredMethods.add(C.handle(0)) ```</li></ul></li></ul><p>4️⃣ 第三次迭代</p><ul><li><strong>chain &#x3D; [A.sources(0),B.load(0),C.handle(0)]</strong></li></ul></li><li>**taintTrack**：匹配</li><li>**impls**：`getImpls(C.handle)` → `{ CImpl.handle }`</li><li>**for each**：<ul><li><code>newLink = (CImpl.handle,0)</code></li><li>去重通过</li><li><code>newChain = [A.sources(0),B.load(0),C.handle(0),CImpl.handle(0)]</code></li><li><code>isSink(CImpl.handle,0)</code> → <code>false</code></li><li>入队 &amp; 加入 <code>exploredMethods</code></li></ul></li></ul><p>5️⃣ 第四次迭代</p><ul><li><strong>chain &#x3D; [ …, CImpl.handle(0)]</strong></li></ul></li><li>**taintTrack**：匹配</li><li>**impls**：`getImpls(D.sink)` → `{ D.sink }`</li><li>**for each**：<ul><li><code>newLink = (D.sink,1)</code></li><li>去重通过</li><li><code>newChain = [ …, CImpl.handle(0), D.sink(1)]</code></li><li><code>isSink(D.sink,1)</code> → <code>true</code></li><li><strong>加入</strong> <code>discoveredGadgets</code></li></ul></li></ul><p>此时 <code>methodsToExplore</code> 可能为空，循环结束。</p><p>接下来进行链路聚合优化</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">java复制编辑for (GadgetChain shortChain : methodsToExploreRepeat) &#123;</span><br><span class="line">  for (GadgetChain fullChain : discoveredGadgets) &#123;</span><br><span class="line">    if (shortChain.lastLink  出现在 fullChain 里) &#123;</span><br><span class="line">      // 把 fullChain 从 shortChain.lastLink 之后的部分拼过来</span><br><span class="line">      tmpDiscoveredGadgets.add( 拼合后的链 );</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line">discoveredGadgets.addAll(tmpDiscoveredGadgets);</span><br></code></pre></figure></div><ul><li>比如如果我们因为 <code>opLevel</code> 限制，把某条中间链放进了 <code>methodsToExploreRepeat</code> 而没展开到 sink，那么这段逻辑就能 <strong>把这些中途链</strong> 自动补全到 <strong>已知的完整 Chain</strong>，得到更多发现。</li></ul>]]>
    </content>
    <id>https://www.zhajiangmian.top/2025/04/20/gadgetInspector%E5%88%86%E6%9E%90/</id>
    <link href="https://www.zhajiangmian.top/2025/04/20/gadgetInspector%E5%88%86%E6%9E%90/"/>
    <published>2025-04-20T02:12:12.000Z</published>
    <summary>
      <![CDATA[<h1 id="GadgetInspector分析"><a href="#GadgetInspector分析" class="headerlink" title="GadgetInspector分析"></a>GadgetInspector分析</h1><h2 id="0x01-Intro"><a href="#0x01-Intro" class="headerlink" title="0x01 Intro"></a>0x01 Intro</h2><p>工具基于ASM技术来对控制字节码，从而达到对传入jar及war包的classpath下的类进行读取，并依次记录类信息、类方法信息、调用关系信息。最后基于以上收集的信息来进行反序列化链的挖掘，分别对应如下几个类：</p>]]>
    </summary>
    <title>GadgetInspector分析</title>
    <updated>2026-10-30T10:17:05.687Z</updated>
  </entry>
  <entry>
    <author>
      <name>炸酱面</name>
    </author>
    <category term="Java安全" scheme="https://www.zhajiangmian.top/categories/Java%E5%AE%89%E5%85%A8/"/>
    <category term="Java安全 字节码" scheme="https://www.zhajiangmian.top/tags/Java%E5%AE%89%E5%85%A8-%E5%AD%97%E8%8A%82%E7%A0%81/"/>
    <content>
      <![CDATA[<h1 id="Java类字节码分析"><a href="#Java类字节码分析" class="headerlink" title="Java类字节码分析"></a>Java类字节码分析</h1><p>我们以如下类进行分析：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">package</span> com.y1zh3e7.Test;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ClassTest</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">sayHello</span> <span class="operator">=</span> <span class="string">&quot;Hello World!&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><span id="more"></span><p>编译后class文件扔到hex编辑器里查看十六进制方便分析：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">CA FE BA BE 00 00 00 34 00 18 0A 00 04 00 14 08 00 15 07 00 16 07 00 17 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65 01 00 04 74 68 69 73 01 00 1C 4C 63 6F 6D 2F 79 31 7A 68 33 65 37 2F 54 65 73 74 2F 43 6C 61 73 73 54 65 73 74 3B 01 00 04 6D 61 69 6E 01 00 16 28 5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56 01 00 04 61 72 67 73 01 00 13 5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 01 00 08 73 61 79 48 65 6C 6C 6F 01 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00 0E 43 6C 61 73 73 54 65 73 74 2E 6A 61 76 61 0C 00 05 00 06 01 00 0C 48 65 6C 6C 6F 20 57 6F 72 6C 64 21 01 00 1A 63 6F 6D 2F 79 31 7A 68 33 65 37 2F 54 65 73 74 2F 43 6C 61 73 73 54 65 73 74 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 00 21 00 03 00 04 00 00 00 00 00 02 00 01 00 05 00 06 00 01 00 07 00 00 00 2F 00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 02 00 08 00 00 00 06 00 01 00 00 00 03 00 09 00 00 00 0C 00 01 00 00 00 05 00 0A 00 0B 00 00 00 09 00 0C 00 0D 00 01 00 07 00 00 00 3C 00 01 00 02 00 00 00 04 12 02S 4C B1 00 00 00 02 00 08 00 00 00 0A 00 02 00 00 00 05 00 03 00 06 00 09 00 00 00 16 00 02 00 00 00 04 00 0E 00 0F 00 00 00 03 00 01 00 10 00 11 00 01 00 01 00 12 00 00 00 02 00 13</span><br></code></pre></figure></div><p>class文件结构如下</p><p><img src="/images/i/2025/03/30/67e8f15b71efd.png" alt="image-20250330152305396"></p><h1 id="0x01-魔术头-Magic-Number-4Byte"><a href="#0x01-魔术头-Magic-Number-4Byte" class="headerlink" title="0x01 魔术头 Magic Number-4Byte"></a>0x01 魔术头 Magic Number-4Byte</h1><p>class文件的魔术头为四字节并且值固定，可以看到为如下内容，这个十六进制表达还是挺有意思的</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">CA FE BA BE</span><br></code></pre></figure></div><h1 id="0x02-版本号-Version-2-2Byte"><a href="#0x02-版本号-Version-2-2Byte" class="headerlink" title="0x02 版本号 Version-2+2Byte"></a>0x02 版本号 Version-2+2Byte</h1><p>十六进制对应内容为</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">00 00 00 34</span><br></code></pre></figure></div><p>前面的0000为次版本号，后面的0034为主版本号，0x0034对应十进制为52，对应版本为jdk1.8，对应的我IDEA中的jdk版本也是1.8</p><p><img src="/images/i/2025/03/27/67e55fc37e9a4.png" alt="image-20250327222457522"></p><h1 id="0x03-常量池-Constant-Pool-2-nByte"><a href="#0x03-常量池-Constant-Pool-2-nByte" class="headerlink" title="0x03 常量池 Constant Pool-2+nByte"></a>0x03 常量池 Constant Pool-2+nByte</h1><p>常量池的2+n指的是两字节的常量数量，加上nByte的常量内容，常量池存储如下内容：</p><p><img src="/images/i/2025/03/30/67e8f17373e87.png" alt="image-20250330152329261"></p><p>接下来我们继续分析十六进制并以此说明：</p><p>首先的两个字节代表常量数量，0x0018转换为十进制为24。这里需要注意的是，常量池的常量索引并不是从0开始而是从1开始，因此24表示常量池中共有23个常量，索引以此为1-23，并且在.class文件中，只有常量池的下标是从0开始，后面的接口、属性、方法等下表依然都是从0开始计数：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">00 18</span><br></code></pre></figure></div><h2 id="CONSTANT-1"><a href="#CONSTANT-1" class="headerlink" title="CONSTANT-1"></a>CONSTANT-1</h2><p>根据上面的表格，我们可以发现不论是何种类型的常量，都是以u1（1字节）的tag位作为起始，因此我们向下读取一字节，为第一个常量的tag，为0x0A：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">0A</span><br></code></pre></figure></div><p>0x0A对应十进制10，我们在表格中寻找值为10的索引，可以找到该常量类型为CONSTANT_Methodref_info，并且接下来还分别有两个u2的index，我们继续向下读取两个字节，则对应表格中指向声明方法的类描述符的索引项，这些东西的作用我们到后面就会知道了，先继续往下看</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">00 04</span><br></code></pre></figure></div><p>继续向下读取两个字节，对应指向名称及类型描述符索引项，值为20</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">00 14</span><br></code></pre></figure></div><p>constant#1：</p><p>0x0a：Methodref_info</p><p>0x00 04：Class_info索引项#4</p><p>0x00 14：NameAndType索引项#20</p><h2 id="CONSTANT-2"><a href="#CONSTANT-2" class="headerlink" title="CONSTANT-2"></a>CONSTANT-2</h2><p>向下读取1B，即为第二个常量的TAG位，值为08，对应表格中CONSTANT_Fieldref_info，依旧是两个u2的index</p><p>constant#2：</p><p>0x08：String_info</p><p>0x00 15:：指向字符串字面量#21</p><h2 id="CONSTANT-3"><a href="#CONSTANT-3" class="headerlink" title="CONSTANT-3"></a>CONSTANT-3</h2><p>0x07：Class_info</p><p>0x00 16：全局限定名常量项索引#22</p><h2 id="CONSTANT-4"><a href="#CONSTANT-4" class="headerlink" title="CONSTANT-4"></a>CONSTANT-4</h2><p>0x07：Class_info</p><p>0x00 17：全局限定名常量项索引#23</p><h2 id="CONSTANT-5"><a href="#CONSTANT-5" class="headerlink" title="CONSTANT-5"></a>CONSTANT-5</h2><p>0x01：Utf8_info</p><p>0x00 06：字符串长度为6</p><p>0x3C 69 6E 69 74 3E：字符串<code>&lt;init&gt;</code></p><h2 id="CONSTANT-6"><a href="#CONSTANT-6" class="headerlink" title="CONSTANT-6"></a>CONSTANT-6</h2><p>0x01：Utf8-info</p><p>0x00 03：字符串长度为3</p><p>0x28 29 56：字符串<code>()V</code></p><h2 id="CONSTANT-7"><a href="#CONSTANT-7" class="headerlink" title="CONSTANT-7"></a>CONSTANT-7</h2><p>0x01：Utf8-info</p><p>0x00 04：字符串长度为4</p><p>0x43 6F 64 65：字符串<code>Code</code></p><h2 id="CONSTANT-8"><a href="#CONSTANT-8" class="headerlink" title="CONSTANT-8"></a>CONSTANT-8</h2><p>0x01：Utf8-info</p><p>0x00 0F：字符串长度为15</p><p>0x4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65：字符串<code>LineNumberTable</code></p><h2 id="CONSTANT-9"><a href="#CONSTANT-9" class="headerlink" title="CONSTANT-9"></a>CONSTANT-9</h2><p>0x01：Utf8-info</p><p>0x00 12：字符串长度为18</p><p>0x4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65：字符串<code>LocalVariableTable</code></p><h2 id="CONSTANT-10"><a href="#CONSTANT-10" class="headerlink" title="CONSTANT-10"></a>CONSTANT-10</h2><p>0x01：Utf8-info</p><p>0x00 04：字符串长度为4</p><p>0x74 68 69 73：字符串<code>this</code></p><h2 id="CONSTANT-11"><a href="#CONSTANT-11" class="headerlink" title="CONSTANT-11"></a>CONSTANT-11</h2><p>0x01：Utf8-info</p><p>0x00 1C：字符串长度为28</p><p>0x4C 63 6F 6D 2F 79 31 7A 68 33 65 37 2F 54 65 73 74 2F 43 6C 61 73 73 54 65 73 74 3B：字符串<code>Lcom/y1zh3e7/Test/ClassTest;</code></p><h2 id="CONSTANT-12"><a href="#CONSTANT-12" class="headerlink" title="CONSTANT-12"></a>CONSTANT-12</h2><p>0x01：Utf8-info</p><p>0x00 04：字符串长度为4</p><p>0x6D 61 69 6E：字符串<code>main</code></p><h2 id="CONSTANT-13"><a href="#CONSTANT-13" class="headerlink" title="CONSTANT-13"></a>CONSTANT-13</h2><p>0x01：Utf8-info</p><p>0x00 16：字符串长度为22</p><p>0x28 5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56：字符串<code>([Ljava/lang/String;)V</code></p><h2 id="CONSTANT-14"><a href="#CONSTANT-14" class="headerlink" title="CONSTANT-14"></a>CONSTANT-14</h2><p>0x01：Utf8-info</p><p>0x00 04：字符串长度为4</p><p>0x61 72 67 73：字符串<code>args</code></p><h2 id="CONSTANT-15"><a href="#CONSTANT-15" class="headerlink" title="CONSTANT-15"></a>CONSTANT-15</h2><p>0x01：Utf8-info</p><p>0x00 13：字符串长度为19</p><p>0x5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B：字符串<code>[Ljava/lang/String;</code></p><h2 id="CONSTANT-16"><a href="#CONSTANT-16" class="headerlink" title="CONSTANT-16"></a>CONSTANT-16</h2><p>0x01：Utf8-info</p><p>0x00 08：字符串长度为8</p><p>0x73 61 79 48 65 6C 6C 6F：字符串<code>sayHello</code></p><h2 id="CONSTANT-17"><a href="#CONSTANT-17" class="headerlink" title="CONSTANT-17"></a>CONSTANT-17</h2><p>0x01：Utf8-info</p><p>0x00 08：字符串长度为18</p><p>0x4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B：字符串<code>Ljava/lang/String;</code></p><h2 id="CONSTANT-18"><a href="#CONSTANT-18" class="headerlink" title="CONSTANT-18"></a>CONSTANT-18</h2><p>0x01：Utf8-info</p><p>0x00 0A：字符串长度为10</p><p>0x53 6F 75 72 63 65 46 69 6C 65：字符串<code>SourceFile</code></p><h2 id="CONSTANT-19"><a href="#CONSTANT-19" class="headerlink" title="CONSTANT-19"></a>CONSTANT-19</h2><p>0x01：Utf8-info</p><p>0x00 0A：字符串长度为14</p><p>0x43 6C 61 73 73 54 65 73 74 2E 6A 61 76 61：字符串<code>ClassTest.java</code></p><h2 id="CONSTANT-20"><a href="#CONSTANT-20" class="headerlink" title="CONSTANT-20"></a>CONSTANT-20</h2><p>0x0C：NameAndType_info</p><p>0x00 05：字段或方法名常量项索引#5</p><p>0x00 06：字段或方法描述符常量索引#6</p><h2 id="CONSTANT-21"><a href="#CONSTANT-21" class="headerlink" title="CONSTANT-21"></a>CONSTANT-21</h2><p>0x01：Utf8-info</p><p>0x00 0C：字符串长度为12</p><p>0x48 65 6C 6C 6F 20 57 6F 72 6C 64 21：字符串<code>Hello World!</code></p><h2 id="CONSTANT-22"><a href="#CONSTANT-22" class="headerlink" title="CONSTANT-22"></a>CONSTANT-22</h2><p>0x01：Utf8-info</p><p>0x00 1A：字符串长度为26</p><p>0x63 6F 6D 2F 79 31 7A 68 33 65 37 2F 54 65 73 74 2F 43 6C 61 73 73 54 65 73 74：字符串<code>com/y1zh3e7/Test/ClassTest</code></p><h2 id="CONSTANT-23"><a href="#CONSTANT-23" class="headerlink" title="CONSTANT-23"></a>CONSTANT-23</h2><p>0x01：Utf8-info</p><p>0x00 10：字符串长度为16</p><p>0x6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74：字符串<code>java/lang/Object</code></p><h1 id="0x04-访问标志位-Access-Flags-2Byte"><a href="#0x04-访问标志位-Access-Flags-2Byte" class="headerlink" title="0x04 访问标志位 Access Flags-2Byte"></a>0x04 访问标志位 Access Flags-2Byte</h1><p>访问标志位包括一个class文件的属性（如是类还是接口，是否被定义成public，是否是abstract，是否是final）</p><p><img src="/images/i/2025/03/30/67e8f1841a207.png" alt="image-20250330152346043"></p><p>我们向下读取两个Byte<code>0x0021</code>，代表的是0x0020和0x0001的集合，意思是该类为public，并且继承object（0x06父类索引）</p><h1 id="0x05-类索引-2Byte"><a href="#0x05-类索引-2Byte" class="headerlink" title="0x05 类索引-2Byte"></a>0x05 类索引-2Byte</h1><p>类索引可以确定类的全局限定名称，我们读取两个字节为<code>0x00 03</code>，对应常量池第三个常量<code>CONSTANT-3</code>，可以发现CONSTANT-3：0x00 16：全局限定名常量项索引#22，所以继续去CONSTANT-22查找对应常量，得到全局限定类名<code>com/y1zh3e7/Test/ClassTest</code></p><h1 id="0x06-父类索引-2Byte"><a href="#0x06-父类索引-2Byte" class="headerlink" title="0x06 父类索引-2Byte"></a>0x06 父类索引-2Byte</h1><p><code>0X00 04</code>，对应<code>CONSTANT-4</code>，0x00 17：全局限定名常量项索引#23，对应<code>java/lang/Object</code></p><h1 id="0X07-接口索引-2-n"><a href="#0X07-接口索引-2-n" class="headerlink" title="0X07 接口索引-2+n"></a>0X07 接口索引-2+n</h1><p>2+n依旧指两个字节代表接口数量，n代表接口表，我们向下读取两个字节<code>0X00 00</code>，即接口数量为0，自然也没有n了</p><h1 id="0x08-字段表集合-2-nByte"><a href="#0x08-字段表集合-2-nByte" class="headerlink" title="0x08 字段表集合-2+nByte"></a>0x08 字段表集合-2+nByte</h1><p>字段表中包含了类中声明的变量，以及实例化后的变量，但是不包括方法内声明的局部变量，因此继续向下读取两个字节，可以发现也是<code>0x00 00</code>，因为我们的变量是定义在psvm中，如果将代码修改如下：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ClassTest</span> &#123;</span><br><span class="line"><span class="type">String</span> <span class="variable">sayHello</span> <span class="operator">=</span> <span class="string">&quot;Hello World!&quot;</span>;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>那么此处的2byte则为<code>0x00 01</code></p><h1 id="0x09-方法-2-nByte"><a href="#0x09-方法-2-nByte" class="headerlink" title="0x09 方法-2+nByte"></a>0x09 方法-2+nByte</h1><p>继续读取2Byte，<code>0X00 02</code>，说明我们的类中有两个方法，但是代码中我们明明只有一个方法psvm，其实是因为除了接口和抽象类，在javac时会自动生成一个无参构造，我们可以反编译看到他，也可以javap后看到这个构造器：</p><p><img src="/images/i/2025/03/30/67e89e20974d1.png" alt="image-20250330092418839"></p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">&#123;</span><br><span class="line">  public com.y1zh3e7.Test.ClassTest();</span><br><span class="line">    descriptor: ()V</span><br><span class="line">    flags: ACC_PUBLIC</span><br><span class="line">    Code:</span><br><span class="line">      stack=1, locals=1, args_size=1</span><br><span class="line">         0: aload_0</span><br><span class="line">         1: invokespecial #1                  // Method java/lang/Object.&quot;&lt;init&gt;&quot;:()V</span><br><span class="line">         4: return</span><br><span class="line">      LineNumberTable:</span><br><span class="line">        line 3: 0</span><br><span class="line">      LocalVariableTable:</span><br><span class="line">        Start  Length  Slot  Name   Signature</span><br><span class="line">            0       5     0  this   Lcom/y1zh3e7/Test/ClassTest;</span><br><span class="line"></span><br><span class="line">  public static void main(java.lang.String[]);</span><br><span class="line">    descriptor: ([Ljava/lang/String;)V</span><br><span class="line">    flags: ACC_PUBLIC, ACC_STATIC</span><br><span class="line">    Code:</span><br><span class="line">      stack=1, locals=2, args_size=1</span><br><span class="line">         0: ldc           #2                  // String Hello World!</span><br><span class="line">         2: astore_1</span><br><span class="line">         3: return</span><br><span class="line">      LineNumberTable:</span><br><span class="line">        line 5: 0</span><br><span class="line">        line 6: 3</span><br><span class="line">      LocalVariableTable:</span><br><span class="line">        Start  Length  Slot  Name   Signature</span><br><span class="line">            0       4     0  args   [Ljava/lang/String;</span><br><span class="line">            3       1     1 sayHello   Ljava/lang/String;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>我们继续向下读取两个方法，方法表结构如下：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">method_info &#123;</span><br><span class="line">    u2             access_flags;</span><br><span class="line">    u2             name_index;</span><br><span class="line">    u2             descriptor_index;</span><br><span class="line">    u2             attributes_count;</span><br><span class="line">    attribute_info attributes[attributes_count];</span><br><span class="line">&#125;</span><br></code></pre></figure></div><h2 id="构造方法解析"><a href="#构造方法解析" class="headerlink" title="构造方法解析"></a>构造方法解析</h2><p>我们按照格式来读取第一个方法：<br><code>0x00 01</code>，访问标志位，代表public方法，给出以下访问标志控制符掩码解析：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line"></span><br><span class="line">十六进制值名称说明</span><br><span class="line">0x0001ACC_PUBLIC方法为 public 权限</span><br><span class="line">0x0002ACC_PRIVATE方法为 private 权限</span><br><span class="line">0x0004ACC_PROTECTED方法为 protected 权限</span><br><span class="line">0x0008ACC_STATIC方法为 static 静态方法</span><br><span class="line">0x0010ACC_FINAL方法为 final（不可被覆盖）</span><br><span class="line">0x0020ACC_SYNCHRONIZED方法为 synchronized（同步方法）</span><br><span class="line">0x0040ACC_BRIDGE方法是由编译器生成的桥接方法（用于泛型类型擦除）</span><br><span class="line">0x0080ACC_VARARGS方法接受可变参数（如 String... args）</span><br><span class="line">0x0100ACC_NATIVE方法为 native（由本地代码实现）</span><br><span class="line">0x0400ACC_ABSTRACT方法为 abstract（抽象方法，无实现）</span><br><span class="line">0x0800ACC_STRICT方法为 strictfp（严格浮点模式）</span><br><span class="line">0x1000ACC_SYNTHETIC方法是由编译器生成的（如默认构造方法、枚举类的 values() 方法等）</span><br></code></pre></figure></div><p>控制符可以组合使用，如</p><ul><li><code>public static</code> 方法：<code>0x0001 (ACC_PUBLIC) | 0x0008 (ACC_STATIC) = 0x0009</code></li><li><code>private final synchronized</code> 方法：<code>0x0002 | 0x0010 | 0x0020 = 0x0032</code></li></ul><p>其中某些标志不能同时存在（如 <code>public</code>、<code>private</code>、<code>protected</code> 只能三选一）。</p><p><code>0x00 05</code>，name_index代表方法索引名，我们去CONSTANT-5进行查找为<code>&lt;init&gt;</code>，这是字节码中对构造方法的专用描述。</p><p><code>0x00 06</code>，方法描述符索引。查找CONSTANT-6，为()V。方法描述符的语法是 <code>(参数类型)返回类型</code>，其中 <code>V</code> 表示 <code>void</code>（即无返回值）。<code>()</code>：表示方法没有参数。<code>V</code>：表示方法的返回类型为 <code>void</code>。</p><ul><li><em><strong>为什么构造方法的返回类型是 <code>void</code>？</strong></em><br><em>虽然构造方法在 Java 语法中没有显式返回值，但在字节码层面，构造方法的返回类型被标记为 <code>void</code>。实际上，构造方法隐式返回构造的实例对象（<code>this</code>），但这一过程由 JVM 自动处理，不需要在描述符中体现。</em></li></ul><p><code>0x00 01</code>，attributes_count，这里引入属性表的概念。属性表可以描述方法的专有信息，这里则代表了该方法的属性表数量为一个。</p><p>通用属性表结构如下：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">attribute_info &#123;</span><br><span class="line">    u2 attribute_name_index;</span><br><span class="line">    u4 attribute_length;</span><br><span class="line">    u1 info[attribute_length];</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>根据通用属性表结构，我们读取一个u2，<code>0x00 07</code>到CONSTANT-7中查找，发现是Code。</p><hr><p>在 JVM 的 <code>.class</code> 文件中，<code>Code</code>、<code>LineNumberTable</code>、<code>LocalVariableTable</code> 和 <code>SourceFile</code> 是类文件属性的重要组成部分，分别用于描述方法的行为、调试信息、局部变量与源码的映射关系，以及源码文件的元数据。</p><p>Code属性：</p><p><code>Code</code> 属性是方法表（<code>method_info</code>）中的核心属性，作用如下：</p><ul><li><strong>存储字节码</strong>：包含方法的具体指令（如 <code>aload_0</code>, <code>invokespecial</code> 等）。</li><li><strong>定义执行环境</strong>：通过 <code>max_stack</code> 和 <code>max_locals</code> 告诉 JVM 如何分配栈帧内存。</li><li><strong>异常处理</strong>：通过 <code>exception_table</code> 定义 <code>try-catch</code> 块的范围和异常类型。</li><li><strong>关联调试信息</strong>：通过子属性（如 <code>LineNumberTable</code>）将字节码与源码关联。</li></ul><hr><p>Code属性结构如下：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">Code_attribute &#123;</span><br><span class="line">    u2 attribute_name_index;</span><br><span class="line">    u4 attribute_length;</span><br><span class="line">    u2 max_stack;</span><br><span class="line">    u2 max_locals;</span><br><span class="line">    u4 code_length;</span><br><span class="line">    u1 code[code_length];</span><br><span class="line">    u2 exception_table_length;</span><br><span class="line">    &#123;   u2 start_pc;</span><br><span class="line">        u2 end_pc;</span><br><span class="line">        u2 handler_pc;</span><br><span class="line">        u2 catch_type;</span><br><span class="line">    &#125; exception_table[exception_table_length];</span><br><span class="line">    u2 attributes_count;</span><br><span class="line">    attribute_info attributes[attributes_count];</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>继续读取一个u4，attribute_length，<code>0x0000002F</code>代表接下来的47个字节为Code属性的指令字节码。</p><p>读取一个u2，<code>0x00 01</code>，max_stack，代表操作数栈最大深度1，一会我们在分析字节码指令时就知道这是什么意思了。</p><p><code>0X00 01</code>，max_locals，代表方法的局部变量表大小为1，局部变量为this，因为所有<strong>实例方法</strong>（非静态方法）和<strong>构造方法</strong>的第一个局部变量槽位（索引 <code>0</code>）都存储了当前对象的引用（即 <code>this</code>）。这是 JVM 的隐式规则，无需在代码中显式声明，因此在psvm这个静态方法中就不会包含this了。此外如果该构造方法为有参构造，那么max_locals数量会+n（参数列表的参数数量）</p><p><code>0x00 00 00 05</code>，code_length为指令长度，也就是说接下来的五个字节为指令。</p><p><code>2A B7 00 01 B1</code>，我们分别来分析这几条指令的作用。2A对应指令aload_0，用于加载局部方法表中的参数到操作数栈中，因此这一步会将this加载到操作数栈上。B7 对应指令invokespecial ，00 01对应CONSTANT-1，即调用父类构造方法。B1对应指令return，方法返回。</p><p><code>0x00 00</code>，exception_table_length，代表异常表为空。</p><p><code>0x00 02</code>，attributes_count，代表该Code属性中还包含了两个子属性。</p><p><code>0x00 08</code>，对应CONSTANT-8，LineNumberTable，则说明该子属性为一个LineNumberTable。</p><hr><p>LineNumberTable 属性：</p><p><code>Code</code> 属性的子属性，记录 <strong>字节码偏移量</strong> 与 <strong>源码行号</strong> 的映射关系，作用如下：</p><ul><li><strong>调试支持</strong>：在 IDE 或异常堆栈中显示源码行号（如 <code>Exception in thread &quot;main&quot; java.lang.NullPointerException at Test.java:12</code>）。</li><li><strong>反编译辅助</strong>：帮助工具（如 <code>javap</code>）生成更易读的反编译结果。</li><li><strong>优化限制</strong>：若省略此属性，JIT 编译器可能无法进行某些优化（如基于行号的 Profiling）。</li></ul><hr><p>LineNumberTable属性结构如下：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">LineNumberTable_attribute &#123;</span><br><span class="line">    u2 attribute_name_index;</span><br><span class="line">    u4 attribute_length;</span><br><span class="line">    u2 line_number_table_length;</span><br><span class="line">    &#123;   u2 start_pc;</span><br><span class="line">        u2 line_number; </span><br><span class="line">    &#125; line_number_table[line_number_table_length];</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p><code>0x00 00 00 06</code>，attribute_length，代表接下来的六字节为属性。</p><p><code>00 01</code>，line_number_table_length为1，代表了下面的line_number_table长度为1。</p><p>每个line_number_table包含两个字段，<code>0x00 00</code>对应start_pc，<code>0x00 03</code>对应line_number，这两个字段负责将字节码偏移量与源码行数进行映射，start_pc对应字节码偏移量，line_number对应源码行数，因此0003意思是将第0行开始的字节码指令全部与第三行源码进行对应。如果line_number_table长度不为1，还会有多个start_pc来负责映射字节码指令和源码的关系。比如如果还有一组start_pc&#x3D;3，line_number&#x3D;4，那么两组映射关系意思是字节码偏移量0-2对应源码第三行，字节码偏移量3及之后的指令对应源码第四行。</p><p>我们继续向下读取第Code的第二个子属性，<code>0x00 09</code>，对应CONSTANT-9，LocalVariableTable。</p><hr><p>LocalVariableTable属性：</p><p><code>Code</code> 属性的子属性，记录 <strong>局部变量名</strong>、<strong>类型</strong> 及其在局部变量表中的 <strong>槽位</strong> 和作用域，作用如下：</p><ul><li><strong>调试支持</strong>：在 IDE 中显示局部变量名和值（如调试时查看 <code>sayHello</code> 变量的内容）。</li><li><strong>反射支持</strong>：通过 <code>Method.getParameters()</code> 获取参数名（需编译时启用 <code>-parameters</code> 选项）。</li><li><strong>反编译辅助</strong>：帮助反编译器还原变量名（否则变量名会变成 <code>var1</code>, <code>var2</code>）。</li></ul><hr><p>LocalVariableTable属性结构如下：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">LocalVariableTable_attribute &#123;</span><br><span class="line">    u2 attribute_name_index;</span><br><span class="line">    u4 attribute_length;</span><br><span class="line">    u2 local_variable_table_length;</span><br><span class="line">    &#123;   u2 start_pc;</span><br><span class="line">        u2 length;</span><br><span class="line">        u2 name_index;</span><br><span class="line">        u2 descriptor_index;</span><br><span class="line">        u2 index;</span><br><span class="line">    &#125; local_variable_table[local_variable_table_length];</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p><code>0x00 0000 0C</code>，attribute_length，代表接下来12字节为属性长度。</p><p><code>0x00 01</code>，代表一个局部变量条目，因此下面的local_variable_table[1]中即为描述局部变量this的相关信息。<br><code>0x00 00</code>，start_pc，代表this的作用域从字节码偏移量0开始，作用域覆盖<code>0x00 05</code>length，共五个字节。</p><p><code>0x00 0A</code>，name_index，指向CONSTANT-10，局部变量名为this。</p><p><code>0x00 0B</code>，类的全局限定名，指向CONSTANT-11，Lcom&#x2F;y1zh3e7&#x2F;Test&#x2F;ClassTest</p><p><code>0x00 00</code>，index，指该局部变量存储在局部变量表的槽位 <code>0</code>（实例方法的 <code>this</code> 固定占用槽位 0）</p><h2 id="main方法解析"><a href="#main方法解析" class="headerlink" title="main方法解析"></a>main方法解析</h2><p><code>0x00 09</code>：访问标志，0x01和0x08的集合，即public static。</p><p><code>0x00 0C</code>：name_index，指向CONSTANT-12，类名main，</p><p><code>0x00 0D</code>：descriptor_index，指向CONSTANT-13，([Ljava&#x2F;lang&#x2F;String;)V，方法接收参数为String，返回类型为viod。</p><p><code>0x00 01</code>：attributes_count，属性数量为1。</p><p>继续解析属性：</p><table><thead><tr><th>字段</th><th>十六进制值</th><th>十进制值&#x2F;说明</th></tr></thead><tbody><tr><td><code>attribute_name_index</code></td><td><code>00 07</code></td><td>指向常量池第 7 项（”Code”）</td></tr><tr><td><code>attribute_length</code></td><td><code>00 00 00 3C</code></td><td>属性总长度：60 字节</td></tr><tr><td><code>max_stack</code></td><td><code>00 01</code></td><td>操作数栈最大深度：1</td></tr><tr><td><code>max_locals</code></td><td><code>00 02</code></td><td>局部变量表大小：2（<code>args</code> 和 <code>sayHello</code>）</td></tr><tr><td><code>code_length</code></td><td><code>00 00 00 04</code></td><td>字节码长度：4 字节</td></tr><tr><td><strong>字节码</strong></td><td><code>12 02 4C B1</code></td><td>指令解析：</td></tr><tr><td></td><td><code>12 02</code></td><td><code>ldc #2</code>（加载常量 “Hello World!”）</td></tr><tr><td></td><td><code>4C</code></td><td><code>astore_1</code>（存储到局部变量 1）</td></tr><tr><td></td><td><code>B1</code></td><td><code>return</code>（方法返回）</td></tr><tr><td><code>exception_table_length</code></td><td><code>00 00</code></td><td>异常表为空</td></tr><tr><td><code>attributes_count</code></td><td><code>00 02</code></td><td>包含 2 个子属性</td></tr></tbody></table><p><strong>子属性 1：<code>LineNumberTable</code></strong></p><table><thead><tr><th>字段</th><th>十六进制值</th><th>说明</th></tr></thead><tbody><tr><td><code>attribute_name_index</code></td><td><code>00 08</code></td><td>常量池第 8 项（”LineNumberTable”）</td></tr><tr><td><code>attribute_length</code></td><td><code>00 00 00 0A</code></td><td>长度 10 字节</td></tr><tr><td><code>line_number_table_length</code></td><td><code>00 02</code></td><td>2 个行号条目</td></tr><tr><td>条目 1：<code>start_pc</code></td><td><code>00 00</code></td><td>字节码偏移 0 → 源码第 5 行</td></tr><tr><td>条目 1：<code>line_number</code></td><td><code>00 05</code></td><td></td></tr><tr><td>条目 2：<code>start_pc</code></td><td><code>00 03</code></td><td>字节码偏移 3 → 源码第 6 行</td></tr><tr><td>条目 2：<code>line_number</code></td><td><code>00 06</code></td><td></td></tr></tbody></table><p><strong>子属性 2：<code>LocalVariableTable</code></strong></p><table><thead><tr><th>字段</th><th>十六进制值</th><th>说明</th></tr></thead><tbody><tr><td><code>attribute_name_index</code></td><td><code>00 09</code></td><td>常量池第 9 项（”LocalVariableTable”）</td></tr><tr><td><code>attribute_length</code></td><td><code>00 00 00 16</code></td><td>长度 22 字节</td></tr><tr><td><code>local_variable_table_length</code></td><td><code>00 02</code></td><td>2 个局部变量条目</td></tr><tr><td>条目 1：<code>start_pc</code></td><td><code>00 00</code></td><td>变量 <code>args</code> 作用域起始偏移 0</td></tr><tr><td><code>length</code></td><td><code>00 04</code></td><td>作用域长度 4 字节</td></tr><tr><td><code>name_index</code></td><td><code>00 0E</code></td><td>常量池第 14 项（变量名 <code>args</code>）</td></tr><tr><td><code>descriptor_index</code></td><td><code>00 0F</code></td><td>常量池第 15 项（类型 <code>[Ljava/lang/String;</code>）</td></tr><tr><td><code>index</code></td><td><code>00 00</code></td><td>局部变量槽位 0</td></tr><tr><td>条目 2：<code>start_pc</code></td><td><code>00 03</code></td><td>变量 <code>sayHello</code> 作用域起始偏移 3</td></tr><tr><td><code>length</code></td><td><code>00 01</code></td><td>作用域长度 1 字节</td></tr><tr><td><code>name_index</code></td><td><code>00 10</code></td><td>常量池第 16 项（变量名 <code>sayHello</code>）</td></tr><tr><td><code>descriptor_index</code></td><td><code>00 11</code></td><td>常量池第 17 项（类型 <code>Ljava/lang/String;</code>）</td></tr><tr><td><code>index</code></td><td><code>00 01</code></td><td>局部变量槽位 1</td></tr></tbody></table><h1 id="0x10-属性Attribute-2-nByte"><a href="#0x10-属性Attribute-2-nByte" class="headerlink" title="0x10 属性Attribute-2+nByte"></a>0x10 属性Attribute-2+nByte</h1><p><code>0x00 01</code>：属性数量1</p><p><code>0x0012</code>：属性名称，CONSTANT-18，SourceFile。</p><hr><p>SourceFile属性：</p><p>类文件的顶级属性，记录 <strong>源码文件名</strong>，作用如下：</p><ul><li><strong>调试支持</strong>：在异常堆栈中显示源码文件名（如 <code>Test.java</code>）。</li><li><strong>代码溯源</strong>：帮助开发者快速定位源码文件。</li><li><strong>可读性</strong>：反编译时显示原始文件名，而非匿名类名。</li></ul><hr><p>SourceFile文件结构如下：</p><div class="p5-codebox" data-lang="plaintext"><figure class="highlight"><pre class="highlight-code"><code><span class="line">SourceFile_attribute &#123;</span><br><span class="line">    u2 attribute_name_index;  </span><br><span class="line">    u4 attribute_length;      </span><br><span class="line">    u2 sourcefile_index;      </span><br><span class="line">&#125;</span><br></code></pre></figure></div><p><code>0x00 00 00 02</code>，attribute_length，属性长度2.</p><p><code>0x00 13</code>，sourcefile_index，指向CONSTANT-19，为ClassTest.java。</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><p><a href="https://www.jianshu.com/p/252f381a6bc4">https://www.jianshu.com/p/252f381a6bc4</a></p><p><a href="https://www.cnblogs.com/binarylei/p/10508441.html">https://www.cnblogs.com/binarylei/p/10508441.html</a></p>]]>
    </content>
    <id>https://www.zhajiangmian.top/2025/04/01/Java%E7%B1%BB%E5%AD%97%E8%8A%82%E7%A0%81%E5%88%86%E6%9E%90/</id>
    <link href="https://www.zhajiangmian.top/2025/04/01/Java%E7%B1%BB%E5%AD%97%E8%8A%82%E7%A0%81%E5%88%86%E6%9E%90/"/>
    <published>2025-04-01T02:12:12.000Z</published>
    <summary>
      <![CDATA[<h1 id="Java类字节码分析"><a href="#Java类字节码分析" class="headerlink" title="Java类字节码分析"></a>Java类字节码分析</h1><p>我们以如下类进行分析：</p>
<div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">package</span> com.y1zh3e7.Test;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ClassTest</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">sayHello</span> <span class="operator">=</span> <span class="string">&quot;Hello World!&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div>]]>
    </summary>
    <title>Java类字节码分析</title>
    <updated>2026-10-30T10:17:05.630Z</updated>
  </entry>
  <entry>
    <author>
      <name>炸酱面</name>
    </author>
    <category term="旅游" scheme="https://www.zhajiangmian.top/categories/%E6%97%85%E6%B8%B8/"/>
    <category term="广州 顺德 旅游" scheme="https://www.zhajiangmian.top/tags/%E5%B9%BF%E5%B7%9E-%E9%A1%BA%E5%BE%B7-%E6%97%85%E6%B8%B8/"/>
    <content>
      <![CDATA[<h1 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h1><p>已经忘记多少次到广州了，最深的印象就是好吃！整个广东地区感觉全是美食，这次带着朋友一起吃</p><p>ヽ(￣ω￣(￣ω￣〃)ゝ</p><p>不过中途还是出现了一些意外情况，这个我留到后面再说。。。。。</p><span id="more"></span><h1 id="2025-2-19"><a href="#2025-2-19" class="headerlink" title="2025&#x2F;2.19"></a>2025&#x2F;2.19</h1><h2 id="华盖步行街"><a href="#华盖步行街" class="headerlink" title="华盖步行街"></a>华盖步行街</h2><p>十九号上午抵达广州白云站，坐地铁到酒店放了下行李，直接准备出发去顺德。第一站到达华盖步行街，在街上找了一家有牛杂和鱼皮的，点了一份鱼蛋牛杂和一份凉拌鱼皮，朋友说牛杂有点腥，但是我觉得很好吃，鱼皮的话就是脆脆的，适合下酒，给的量还是很足的。</p><p><img src="/images/i/2025/05/25/6832b5a29547f.jpg" alt="cf1994baa222dbac848e35c8a5acab9"></p><p><img src="/images/i/2025/05/25/6832b5aa49058.jpg" alt="aa6520b894ff83ed47481a817cfc566"></p><p>之后直接去旁边的民信老铺，点了一份双皮奶和龟苓膏，这家的双皮奶被某人给出了很高的评价，排在旅游这些天里面最好吃的一样。民信家的双皮奶特别浓，简直就像是吃了一块奶酪一样，我比较喜欢吃热的，奶味会更浓一些。龟苓膏加了一份水牛奶，吃起来还是挺去火的，很正宗，基本上民信点甜水不会踩坑的。</p><p><img src="/images/i/2025/05/25/6832b5b23d62b.jpg" alt="21564d24647b8f282b51ba892a02008"></p><h2 id="清晖园"><a href="#清晖园" class="headerlink" title="清晖园"></a>清晖园</h2><p>吃饱喝足去清晖园逛逛，我们一开始在美团上买的票，要二十多，但到了地方一看公众号买票才七块钱，血亏。里面的话比起景点更像是一个公园，适合在里面坐着看看风景，聊聊天之类的。</p><p><img src="/images/i/2025/05/25/6832b5c44fe03.jpg" alt="95b573831511ac8f8e4d5f3ac356612"></p><p><img src="/images/i/2025/05/25/6832b5cf117f6.jpg" alt="5bd8d5ef1fc701b23804b7aec487098"></p><h2 id="鹅簋"><a href="#鹅簋" class="headerlink" title="鹅簋"></a>鹅簋</h2><p>在清晖园逛完了准备出发去吃烧鹅，看了一圈选了鹅簋（gui三声），我们到的时候外面还在排队，双人餐美团上是100，可惜到我们的时候没有叉烧了，换成了葱姜鸡。烧鹅味道还不错，皮很脆，给的卤水也不错，只是可惜我们上午吃了太多，就没有全吃掉。</p><p><img src="/images/i/2025/05/25/6832b5da8a43d.jpg" alt="9d43eaf9774f4ae57339802dbe490b4"></p><h2 id="金榜上街"><a href="#金榜上街" class="headerlink" title="金榜上街"></a>金榜上街</h2><p>吃完之后去附近的金榜上街继续溜达，整个街两边非常紧凑，店面特别多。店面基本上都会放个cctv和网红探店的牌子来给自己揽客，那个jackson我也挺喜欢看，整条街物价非常实惠，没有说因为某些原因就把物价升到天上，非常推荐。</p><p><img src="/images/i/2025/05/25/6832b5e8d51d0.jpg" alt="10bc164074ac8540a8f917df0aaba32"></p><p>入口处就是一个阿爆柠檬茶，味道确实不错，不是那种单纯和冰红茶一个味道，里面还混了一些茶香的味道，不涩口，喝起来很清新。店旁边可以试喝柠檬茶和水牛奶，水牛奶我尝了一下也还可以，不过还是打算往里面走走再买。</p><p><img src="/images/i/2025/05/25/6832b5f860f0b.jpg" alt="b682fa46f2a156309d3569f85bca504"></p><p>再往里走在一个卖奶制品的摊子吃了一些，双皮奶做的没有民信家厚重，水牛奶比正常牛奶会稍微甜一点，更清爽一点，店里还有那种固体的牛乳卖，如果想试试可以买点尝尝。</p><p><img src="/images/i/2025/05/25/6832b60a93f8f.jpg" alt="3c5c3f1db16f7402a6792a5e3bf879e"></p><p>再往前买了一份火山泡芙，哎果然就是网红食物，直接看图吧，泡芙里面是没有奶油的，一般般</p><p><img src="/images/i/2025/05/25/6832b611e95f6.jpg" alt="8f79af5341b1442aa64ad1ce05a1e23"></p><h2 id="渔米潮亭"><a href="#渔米潮亭" class="headerlink" title="渔米潮亭"></a>渔米潮亭</h2><p>从金街逛完之后回了酒店，待到晚上去吃粥底火锅，选了这家渔米潮亭，价格很实惠，双人餐可以选两个锅底。我们选了一个粥底锅底，一个辣锅，他们家是按照盘子收费的，有金、铁、红绿黄盘，不同种类的盘子价格不一样，我们两个人吃了一些海鲜，吃了一些肉，总共花了一百五。然后涮粥底火锅要拿一颗生鸡蛋和肉裹在一起，然后涮到锅里，这样肉吃起来就很嫩了</p><p><img src="/images/i/2025/05/25/6832b61fa8880.jpg" alt="a463b374eed0d2a0e3045f5310d2f0b"></p><p>吃的时候隔壁桌是个视频博主，也是第一次看到短视频拍摄现场，拿着剧本不停ng哈哈</p><p><img src="/images/i/2025/05/25/6832b62751495.jpg" alt="7c4e8a7185db069c993b966e04a3c76"></p><h1 id="2025-2-20"><a href="#2025-2-20" class="headerlink" title="2025&#x2F;2.20"></a>2025&#x2F;2.20</h1><p>惯例中午起床，因为其实广州没什么玩的，主要还是吃，不知道干嘛就去了动漫天街。去之前在酒店点了烧腊，还得是广式烧腊最对味＜（＾－＾）＞</p><p><img src="/images/i/2025/05/25/6832b6e1afa2f.jpg" alt="3571d6f40ba0f69a5f65f2d06c5460d"></p><h2 id="动漫天街"><a href="#动漫天街" class="headerlink" title="动漫天街"></a>动漫天街</h2><p>又到了二次元环节，进门就是全职猎人一番上，一个没忍住直接来了五抽哈，至于结果嘛。。。。</p><p>整个动漫天街分东区和西区，非常大，覆盖的动漫也很全，基本上来了都得稍微消费一点，我后面还和朋友买了fgo的镭射票，还花十块钱淘了一个一只猪的小手办，果真是二次元天堂哈</p><p><img src="/images/i/2025/05/25/6832b6fd60bf3.jpg" alt="9a1adc78acb16ff67a8e58429a4eee8"></p><p>出来之后正好是个公园，在公园坐了一会看看大爷大妈们跳舞，希望我以后老了也能这么惬意。后面发现还有一部分没有逛全，于是又回去逛了一会，最后在里面找了一个coco买了两杯奶茶坐着歇着了，但是马上就到晚上饭点了，就把奶茶装包里打算吃完饭再喝。</p><p><img src="/images/i/2025/05/25/6832b70560ec7.jpg" alt="700b7f1d613b1f69ee8e2d325868b2c"></p><h2 id="稻香晚茶自助"><a href="#稻香晚茶自助" class="headerlink" title="稻香晚茶自助"></a>稻香晚茶自助</h2><p>来了广东地区必不可少的肯定是茶点，相较于当地人只是拿来当作聊天的陪衬，我直接就是吃吃吃吃吃吃，所以基本上只能选自助。上一次在广州吃也是选了稻香，体验还算不错，所以这次依然稻香。战果如下，图上还少了一份杏仁茶和布丁，最后两个人吃了20份：</p><p><img src="/images/i/2025/05/25/6832b71482ed5.jpg" alt="cad89bf998d85f0293daaf10c29081a"></p><p>吃完之后去广州塔下面转了一圈，因为时间很晚了就没有上，就酱</p><p><img src="/images/i/2025/05/25/6832b7209c72d.jpg" alt="d98c64e8ece0462aa71d3d6d15d6e2e"></p><h1 id="2025-2-21-2-22"><a href="#2025-2-21-2-22" class="headerlink" title="2025&#x2F;2.21-2.22"></a>2025&#x2F;2.21-2.22</h1><p>话说回那两杯coco，当天晚上吃完稻香实在是太饱了喝不下去，我第二天早上把我的那杯杨枝甘露喝掉了，就是这一杯隔夜杨枝甘露直接给我干的细菌感染了，中午和朋友一起去吃萨莉亚，一口没吃，然后下午出发去东莞找另外一个朋友面基，走之前猜到应该是细菌感染了，吃了抗生素。下午到了东莞酒店开始发烧，晚上这位东莞朋友请吃海底捞，我吃了退烧药，在晚上十点左右成功的能够进食了，放一张某人靓照。</p><p><img src="/images/i/2025/05/25/6832b7276d064.jpg" alt="25305876c29406358ed7b2da6d656b7"></p><p>晚上吃完海底捞回酒店，又开始发烧，伴随着呕吐＋腹泻，一晚上基本上没睡着，很难顶。第二天早上起床感觉好一点了，点了艇仔粥，结果喝完又开始发烧了。下午上图这位朋友请吃达美乐，我又只能眼睁睁看着哎。。。吃完达美乐又出发去广州机场旁边的酒店，结果到了酒店又开始发烧，哎只能说一定不要喝过夜奶茶啊，血的教训。</p><h1 id="2025-2-23"><a href="#2025-2-23" class="headerlink" title="2025&#x2F;2.23"></a>2025&#x2F;2.23</h1><p>回青岛了，航班当时特意选的宽体机，南航的CZ3219，青广快线，坐了这么多趟飞机唯一坐过的宽体机，南航的餐食我觉得算是航司里面还不错的了，当时身体也稍微舒服一点了正好把飞机餐吃了，味道一般般，不过还是在飞机上很饿的时候就显得很好吃。</p><h1 id="Outro"><a href="#Outro" class="headerlink" title="Outro"></a>Outro</h1><p>已经对杨枝甘露有点心理阴影了，当时喝的时候感觉除了里面的西米被泡大了一点味道完全没有坏，喝完立马就起反应了，大家在外也要保护好自己的身体啊。整个广州我觉得没有什么很难吃的东西，就不做排名了，写到这的时候已经开学两天了，我的朋友还是怀念顺德的双皮奶哈哈，以后有机会再尝尝吧！</p>]]>
    </content>
    <id>https://www.zhajiangmian.top/2025/02/26/%E9%80%9B%E5%90%83%E9%80%9B%E5%96%9D%E6%9C%80%E5%90%8E%E4%B8%80%E7%AB%99-%E5%B9%BF%E5%B7%9E/</id>
    <link href="https://www.zhajiangmian.top/2025/02/26/%E9%80%9B%E5%90%83%E9%80%9B%E5%96%9D%E6%9C%80%E5%90%8E%E4%B8%80%E7%AB%99-%E5%B9%BF%E5%B7%9E/"/>
    <published>2025-02-26T04:42:21.000Z</published>
    <summary>
      <![CDATA[<h1 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h1><p>已经忘记多少次到广州了，最深的印象就是好吃！整个广东地区感觉全是美食，这次带着朋友一起吃</p>
<p>ヽ(￣ω￣(￣ω￣〃)ゝ</p>
<p>不过中途还是出现了一些意外情况，这个我留到后面再说。。。。。</p>]]>
    </summary>
    <title>逛吃逛喝最后一站-广州</title>
    <updated>2026-10-30T10:17:05.614Z</updated>
  </entry>
  <entry>
    <author>
      <name>炸酱面</name>
    </author>
    <category term="旅游" scheme="https://www.zhajiangmian.top/categories/%E6%97%85%E6%B8%B8/"/>
    <category term="三亚 旅游" scheme="https://www.zhajiangmian.top/tags/%E4%B8%89%E4%BA%9A-%E6%97%85%E6%B8%B8/"/>
    <content>
      <![CDATA[<h1 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h1><p>之前就听说三亚已经被东北人占领了，去了一看果不其然。出发之前另一个朋友和我说这边也是有着浓厚的人文风情哈，当时我还不信，到了果然被狠狠上课了，希望写这篇文章如果有去三亚的朋友能看到可以少被坑点。首先说一下我的教训哈，我只知道三亚很暖和，但是我不知道这么热，二月份30度的温度和我家零下二十多度完全就是两个极端，导致我直接轻敌了。去了第一天被狠狠晒伤，写这篇文章的时候已经过去十天了，胳膊上被晒伤的死皮还在脱落，去之前一定要做好防晒哈。</p><span id="more"></span><p><img src="/images/i/2025/05/25/6832b37d71eb2.jpg" alt="dcb38fdc38fa2d26471f44bd209441c"></p><p>三亚这边确实非常的热带，一落地遍地都是椰树，此外还有更民风淳朴的就是基本上在路上你可以连续看到一大片的“案发现场”，实际上是当地居民吃完槟榔的残骸，也算是这座城市的独特景色了。下面这张图我拍的时候感觉很有意思哈，槟榔加烟，法力无边~</p><p><img src="/images/i/2025/05/25/6832b38ead01c.jpg" alt="57c88e64efe7dfba0c84ebb4fa83d44"></p><h1 id="2025-2-16"><a href="#2025-2-16" class="headerlink" title="2025&#x2F;2.16"></a>2025&#x2F;2.16</h1><p>从南昌出发落地到三亚，我们是2.16凌晨两点左右到的，落地出了机场之后发现外面全都是人，大家都在等车，而这个阴间时间点又恰好没有多少网约车，导致机场那里全是黑心出租。我们网约车等了大概十分钟左右没有接到，我尝试问了一下出租车什么价格，好家伙，五公里的路一个人160，我直接say good bye哈。好在我们最后运气比较好，成功叫到了一辆网约车。如果也有和我一样是这种红眼航班半夜到的，如果酒店在机场附近一定要提前联系酒店能不能接机，预约一下，或者在打车软件上提前约好。你也不想自己提着一大堆行李步行到七八公里的酒店or坐160一位的黑车吧~</p><h2 id="海棠人家椰子鸡火锅"><a href="#海棠人家椰子鸡火锅" class="headerlink" title="海棠人家椰子鸡火锅"></a>海棠人家椰子鸡火锅</h2><p>睡醒之后已经是中午了，在酒店讨论了一下行程，我们决定去租个电动车溜达溜达，我定的酒店在机场附近，大约离椰梦长廊七八公里的样子。在dy上下了个电动车租赁订单，打算直接出发海棠湾，路上找个地方吃口饭。此次电动车经历也成了我这一辈子忘不了的经历哈，从酒店到海棠湾大概五十公里，骑着骑着导航给我干隧道上了。我确认了一下实在是没有其他路了，只能骑进隧道，不过后来我发现好像这边隧道是可以走电动车的，好多电动车和摩托车都是走的隧道。这车子还挺抗骑，我们两个人来回一百多公里的路，就掉了一格半的电，租赁一天是80大洋。</p><p><img src="/images/i/2025/05/25/6832b3aa60265.jpg" alt="521eb5e6bde7123f9f28d4f4fb2b777">经过两个多小时骑行，成功抵达海棠湾，找了一个椰子鸡餐厅，也是想尝尝特色。好的不出意外被坑了，两个人点了双人餐200大洋，除了一盘鸡，一盘死的透透的海鲜和一盘牛肉，剩下的都是一些很廉价的东西，最让我感觉不舒服的是，店员让我们给好评，一般来说给好评起码都是有个礼物或者折扣之类的，这家完全没有，那个架势好像是不给好评今天就走不出去这个店了。出门在外我也是懒得和人议论，随便给了个好评就润了。味道上中规中矩吧，鸡肉挺嫩。锅底选的椰子水锅底＋骨汤锅，本来说想尝尝糟粕醋锅底，服务员说椰子水不能拼，其实不拼锅底只选椰子水锅底才是最赚的，店名海棠人家。</p><p><img src="/images/i/2025/05/25/6832b3c3159fb.jpg" alt="3069abd794102e4c8f802343ea1a352"></p><p><img src="/images/i/2025/05/25/6832b3d6df371.jpg" alt="63aa7e7e159c8209651760d03f09795"></p><h2 id="CDF免税城"><a href="#CDF免税城" class="headerlink" title="CDF免税城"></a>CDF免税城</h2><p>吃完饭又骑车十多公里去免税城逛一逛，不过这地方也就适合拍拍照，逛一逛了，里面的东西除非要出国能用到免税并且积累积分才算是比较赚，否则价格比网上贵的多。里面最便宜的可能就是霸王茶几了，不过推荐体验一下厕所，卫生做得很好，也是我唯一能消费得起的地方了哈哈。</p><p><img src="/images/i/2025/05/25/6832b3e9a80fe.jpg" alt="d8c432e1756d94ce2e04c22fed83ac6"></p><p><img src="/images/i/2025/05/25/6832b3facde60.jpg" alt="2cd7050072d7c80989a6224d6c02609"></p><p>之后又是骑了五十多公里把电动车还回去了，然后打算去海边看看。直接地图导航海韵度假酒店就可以了，路上买了两个椰子，一个八元，应该也不算特别贵，毕竟内陆的话一个要到20多吧。旁边还有个卖榴莲的，我以为这边会很便宜，非常自信的要了一个大的，称完称我问多少钱，只听到了什么xxxxxx94，我说哦哦94是吧直接扫了94块钱过去。结果摊主和我说是294。。。。吓得我直接要回了我的94块钱跑路了哈。到了海边两个人都没有拖鞋，没办法下海玩玩，直接回酒店了。到酒店发现胳膊和脸直接晒伤了，赶紧叫外卖送了个芦荟胶和防晒霜，晒伤是真的难受，如果去三亚的话一定要注意防晒！！！！！</p><p><img src="/images/i/2025/05/25/6832b427d0115.jpg" alt="d3e6aad787e305c7de2ee763d35b9e3"></p><h2 id="晚餐"><a href="#晚餐" class="headerlink" title="晚餐"></a>晚餐</h2><p>晚餐点了两份清补凉还有一些肉菜，朋友比较好奇香草鸭（路上一直看到）就点了一份，清补凉点的是阿弟哥老椰头炒冰清补凉，一份椰奶炒饼，一份芋泥冰，芋泥冰好吃。菜点的是一家叫做海南菜馆的店，我觉得味道都很不错，叉烧味道也很对，带了一盒梅子酱，价格也都可以接受，两大份炒冰43，两个肉菜和两份米饭76，其实炒冰基本上点一份就够两个人吃了，</p><p><img src="/images/i/2025/05/25/6832b43611fdf.jpg" alt="d90b095053776ee28d65f4418e4b42b"></p><p><img src="/images/i/2025/05/25/6832b446d1208.jpg" alt="cdf99b32e6d1a8c19a024f356959ed1"></p><h1 id="2025-2-17"><a href="#2025-2-17" class="headerlink" title="2025&#x2F;2.17"></a>2025&#x2F;2.17</h1><h2 id="老爸茶"><a href="#老爸茶" class="headerlink" title="老爸茶"></a>老爸茶</h2><p>第二天睡醒差不多十点多的样子，出发去吃早餐。在网上看了一下，可以去吃一下当地的老爸茶，但是美团一搜全都贵的要命。然后我发现了一个小店，在地图上搜不到，但是外卖上可以看到菜单，价格非常合适，叫爱上老爸茶。具体位置可以打开地图导航到雪姐港门粉，到了之后往前走大概10m左右就是了，茶点味道还不错，老板给推荐了抱粉，可惜我不是很能吃得惯。凉虾非常好喝，里面是冰糖打底，加了一大勺凉虾，最后两个人大概点了八九样东西，还打包了两块三明治在包里，一共消费50多。</p><p><img src="/images/i/2025/05/25/6832b45d73eca.jpg" alt="2d8b94e064c8f15c3b0b475f521207e"></p><h2 id="热带天堂森林公园"><a href="#热带天堂森林公园" class="headerlink" title="热带天堂森林公园"></a>热带天堂森林公园</h2><p>带着两块三明治当口粮，坐公交出发前往热带天堂森林公园。学生票需要在线下售票窗口购买，原价150多，学生票好像是100多来着。然后进去之后就可以坐着观光车一路向上。这个公园还有个习俗，两个观光车会车的时候要大叫，不过我比较腼腆就老老实实的闭嘴了哈哈。</p><p>抵达之后坐观光车第一站到达兰花谷，里面都是各种各样的植物，以兰花居多，我家里也有好几盆蝴蝶兰，好像长得和景区里的也都一个样子</p><p><img src="/images/i/2025/05/25/6832b47759e1d.jpg" alt="869bde7cd3118049876a956056192f3"></p><p><img src="/images/i/2025/05/25/6832b4835258c.jpg" alt="70ece7f898e799e5876374af4785266"></p><p>兰花谷逛完之后可以继续坐车向上，到达索道和玻璃栈桥。索道一个人好像是20，玻璃桥是100左右，最后我们就只去了索道。我本来是恐高不敢上去的，结果被朋友硬是忽悠上去。下面都是树林，看起来就没有那么吓人，不过索道会摇来摇去，还是会有点害怕</p><p><img src="/images/i/2025/05/25/6832b48af3bcf.jpg" alt="ecdadd75b20eef8c6b3e0b43ece5437"></p><p><img src="/images/i/2025/05/25/6832b4936b2b4.jpg" alt="f9b75f755699367551a474ac52585bd"></p><p>从索道出来之后继续坐车向上，抵达山顶公园，那边有一些游玩项目，还有一个制高点，游玩项目要么太高要么停业维修了，就溜达溜达拍拍照，还有个地方可以凭门票免费喝一杯咖啡，不过也是商家为了推销他们的咖啡豆搞得营销手段，倒是可以在那里歇歇脚</p><p><img src="/images/i/2025/05/25/6832b49bd34ac.jpg" alt="c1e2288aef39a290abefe37102fb92c"></p><h2 id="于春正宗椰奶清补凉炒冰"><a href="#于春正宗椰奶清补凉炒冰" class="headerlink" title="于春正宗椰奶清补凉炒冰"></a>于春正宗椰奶清补凉炒冰</h2><p>从公园出来之后打算找个地方等晚上夜市营业之后去夜市玩玩，在地图上找到了这家清补凉，菜单如下</p><p><img src="/images/i/2025/05/25/6832b4aa4b806.jpg" alt="dee4dae77e0475367edc67969f97712"></p><p>炒冰味道不错，确实很好吃，不是那种有冰碴子的冰沙口感，是那种比较绵密的绵绵冰</p><p><img src="/images/i/2025/05/25/6832b4b24cd46.jpg" alt="9c4bb6b157c149484b44941ac4ac79b"></p><h2 id="亿恒夜市"><a href="#亿恒夜市" class="headerlink" title="亿恒夜市"></a>亿恒夜市</h2><p>吃完炒冰之后又回酒店歇了一会，晚上大概八点钟左右出发去第一夜市，还有两三公里快到的时候，司机说这边是亿恒夜市，规模比第一夜市大一点，索性就在这里下车了。根据夜市定律，果不其然，里面的东西都比较一般，买了一份蚵仔煎和章鱼小丸子，又贵又难吃。夜市里很多开海鲜大排档的，最后能买的也就是水果摊，二十块钱五盒，建议都来尝尝这个辣椒拌青芒，我觉得很有意思，另外一位就觉得很难接受了，最后回酒店点了一份猪杂粥和猪杂粉吃，味道中规中矩。</p><p><img src="/images/i/2025/05/25/6832b4e9507d2.jpg" alt="3ee0eee2e2a4cd01bd48e7d3252c509"></p><p><img src="/images/i/2025/05/25/6832b4e004c87.jpg" alt="488776c23dcfb9bf3d8fe6652d372a7"></p><h1 id="2025-2-18"><a href="#2025-2-18" class="headerlink" title="2025&#x2F;2.18"></a>2025&#x2F;2.18</h1><h2 id="西鲨海鲜自助"><a href="#西鲨海鲜自助" class="headerlink" title="西鲨海鲜自助"></a>西鲨海鲜自助</h2><p>在三亚的最后一天，当天晚上的火车出发去广州，最后一顿饭选在了一个很好的海鲜自助，价格是400&#x2F;位，综合吃下来味道很不错，基本上属于应有尽有了。海鲜类都是在缸子里自己去捞，各种螃蟹、贝类，龙虾的话每人限半只，刺身甜虾、三文鱼、虹鳟、鳌虾、鹅肝也都有，酒水区甚至还有茅台（没仔细看真假），如果预算充足我觉得去吃一顿确实很不错，内陆很难吃到种类这么全的海鲜了。</p><p><img src="/images/i/2025/05/25/6832b53fdcd75.jpg" alt="9028a3e3b6e006091c5f4226c012f04"></p><p><img src="/images/i/2025/05/25/6832b4fa42196.jpg" alt="a5c83cfaefb54d6253abbabb15843d5"></p><p><img src="/images/i/2025/05/25/6832b52f19682.jpg" alt="c4d6f4115fc293a54a1934fb6beb500"></p><p><img src="/images/i/2025/05/25/6832b537ebe92.jpg" alt="d101e9ae675cd7f93742cd7d2c496c4"></p><h2 id="三亚-广州"><a href="#三亚-广州" class="headerlink" title="三亚-广州"></a>三亚-广州</h2><p>当天下午在三亚站坐Z502次火车出发前往广州，这趟列车比较有趣，我当时在想火车怎么从海南岛到内陆，晚上的时候和朋友在车厢里溜达，正好赶上火车车厢被分离之后运上货船（还好我们没跑到太前面的车厢 不然回不去自己车厢了），用货船把火车运过去是没想到的，非常interesting。</p><p><img src="/images/i/2025/05/25/6832b5652917a.jpg" alt="a7c677e1b96fb4778e0b39c62b6292d"></p><h1 id="Outro"><a href="#Outro" class="headerlink" title="Outro"></a>Outro</h1><p>综合下来，三亚算是个适合在冬天去的地方，前提是要忍受的了超高的消费水平（外地人特攻），三线城市消费水平远超一线城市&#x3D;&#x3D;</p><p>如果去的话要记得做好防晒，提前reserve接机，基本上夏装足矣。因为我们预算不是很多，就没有去玩那些水上项目，最后时间问题也没有去西岛、蜈支洲岛这些小岛，网上的那些景点什么大小洞天其实在我看来就是海边的几个石头，索性就懒得去了，在网上看了就等于我去过了。</p><p>给上面的餐厅排个名吧</p><p>西鲨海鲜自助 &gt; 阿弟哥老椰头炒冰清补凉、于春正宗椰奶清补凉炒冰 &gt; 爱上老爸茶、猪杂粥&#x2F;粉、海南菜馆 &gt; 海棠人家椰子鸡火锅</p>]]>
    </content>
    <id>https://www.zhajiangmian.top/2025/02/26/%E9%80%9B%E5%90%83%E9%80%9B%E5%96%9D%E7%AC%AC%E4%B8%89%E7%AB%99-%E4%B8%89%E4%BA%9A/</id>
    <link href="https://www.zhajiangmian.top/2025/02/26/%E9%80%9B%E5%90%83%E9%80%9B%E5%96%9D%E7%AC%AC%E4%B8%89%E7%AB%99-%E4%B8%89%E4%BA%9A/"/>
    <published>2025-02-25T18:26:55.000Z</published>
    <summary>
      <![CDATA[<h1 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h1><p>之前就听说三亚已经被东北人占领了，去了一看果不其然。出发之前另一个朋友和我说这边也是有着浓厚的人文风情哈，当时我还不信，到了果然被狠狠上课了，希望写这篇文章如果有去三亚的朋友能看到可以少被坑点。首先说一下我的教训哈，我只知道三亚很暖和，但是我不知道这么热，二月份30度的温度和我家零下二十多度完全就是两个极端，导致我直接轻敌了。去了第一天被狠狠晒伤，写这篇文章的时候已经过去十天了，胳膊上被晒伤的死皮还在脱落，去之前一定要做好防晒哈。</p>]]>
    </summary>
    <title>逛吃逛喝第三站-三亚</title>
    <updated>2026-10-30T10:17:05.617Z</updated>
  </entry>
  <entry>
    <author>
      <name>炸酱面</name>
    </author>
    <category term="旅游" scheme="https://www.zhajiangmian.top/categories/%E6%97%85%E6%B8%B8/"/>
    <category term="南昌 旅游" scheme="https://www.zhajiangmian.top/tags/%E5%8D%97%E6%98%8C-%E6%97%85%E6%B8%B8/"/>
    <content>
      <![CDATA[<h1 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h1><p>南昌这个城市真的就是历史遗迹的集合，然后由于我本人其实对这些兴趣不算特别大，再加上去的时候刚好赶上了连着的雨天，导致这趟南昌行程基本上没有什么产出，所以这篇内容可能稍微短一点，就把一些有意思的事情记录一下吧。</p><span id="more"></span><h1 id="飞行记录"><a href="#飞行记录" class="headerlink" title="飞行记录"></a>飞行记录</h1><p>这趟行程乘坐的是东航MU6397次航班，具体执飞机型是空客320，也是一架比较老的飞机了，飞的是早上的航班，飞机餐给的是一份面，机上提供一些饮料。虽然味道一般般但是在飞机上很饿的之后还真挺好吃的。座位间距不算很大，对我来说（183cm）正正好好能把腿放进去，机上乘务服务也很到位，算是一次还不错的经历。</p><p><img src="/images/i/2025/05/25/6832b21065ad0.jpg" alt="3c37a28cf1d4bb78be23fed8585a7f7"></p><h1 id="大士院街"><a href="#大士院街" class="headerlink" title="大士院街"></a>大士院街</h1><p>到了南昌之后是住在朋友家里，差不多中午到的，下午第一站看了网上推荐去了大士院街，总体来说就是比较一般般，如果比较好奇的话可以尝尝南昌那边的特色，白糖糕之类的，不过其实味道大概也能想象出来，我在整个步行街入口甜水点点了一份双皮奶，一份姜撞奶，还有一份小甜点，在写这篇文章的时候我人已经在广州了，相较于顺德的奶制品来说确实是差了那么一点点意思</p><p><img src="/images/i/2025/05/25/6832b2257f7fa.jpg" alt="ff9d1da1bb0f89d3bffce3574323bb4"></p><h1 id="季季红火锅"><a href="#季季红火锅" class="headerlink" title="季季红火锅"></a>季季红火锅</h1><p>味道还不错，我以为江西这边的本地火锅会做的非常辣，但红油锅真的完全没辣度哈，看起来江西的话还是炒菜比较刺激。大家都说季季红是海底捞平替，不过其实大部分人去吃海底捞主要吃的也是一个附加服务吧，季季红这边的菜新鲜度还可以，但是价格主打就是一个便宜，便宜，非常便宜，基本上可以对标海底捞五折的价格，我们两个人算是比较能吃了，最后也是吃了大概一个人100左右，海底捞的话可能一个人就要吃到200了。然后朋友推荐点一份热的白糖糕和八宝尝尝，其实吃之前脑子里已经差不多想象出来大概的味道了，基本上就是个炸粽子，稍微撒点白糖。炸八宝的话就是炸豆干，但是口味会稍微重一些，第二张图右上角一圈一圈的那个就是白糖糕，他下面的就是炸八宝。</p><p><img src="/images/i/2025/05/25/6832b23a43c7d.jpg" alt="5712d60f3dd95054930d15dfe4ae03b"></p><p><img src="/images/i/2025/05/25/6832b2427861b.jpg" alt="2ff86279001e49ecfe07d3f76b4f2ba"></p><h1 id="八一广场"><a href="#八一广场" class="headerlink" title="八一广场"></a>八一广场</h1><p>就是一个很大的广场，有一个很大的纪念塔可以拍个照。不过里面很多搞推销的年轻人，也算是个风土人情了，我和朋友刚进去，我的手机被下单了个玩偶，朋友手机多了各种dy、dy极速版等各种软件。站在一个旅客的角度来说，这种情况确实比较难顶，后来我们从八一广场出去之后找了个地方坐了一会，又被搭讪让帮个忙。我们说刚才已经下载过了，然后还是被磨叽了好半天，甚至直接上手来拿手机，给我搞的也是一脑门子黑线哈，希望当地能稍微管制一下这种情况哈，然后在朋友被疯狂缠着下软件的时候我脱身找了个地方坐着，拍了几张照片</p><p><img src="/images/i/2025/05/25/6832b2516ab34.jpg" alt="17b992ce757de1b5287cafccdaee6ea"></p><p><img src="/images/i/2025/05/25/6832b25b59240.jpg" alt="644f8401b5f679a915d81787b932057"></p><p><img src="/images/i/2025/05/25/6832b26bbcd2d.jpg" alt="402d71f126f729b3e3f80cb1552e7d9"></p><h1 id="洪都大拇指"><a href="#洪都大拇指" class="headerlink" title="洪都大拇指"></a>洪都大拇指</h1><p>之后和朋友去某个二次元商场逛了一下（没想到这项活动会多次出现），然后正好看到旁边有个这个奶茶店，上网上看了一下还是个南昌本地的奶茶店，网上说是茶颜悦色平替，不过茶颜我也就在非常非常久之前喝过一杯芊芊马卡龙。这个的还原度其实还可以，茶汤比霸王茶姬能稍微甜一点点，奶油顶的话感觉大家都一个货色，不过里面有点坚果可以加分，整体来说还不错</p><p><img src="/images/i/2025/05/25/6832b275d4341.jpg" alt="d66f23da5257fcbc5b31718b7d2a19b"></p><h1 id="滕王阁"><a href="#滕王阁" class="headerlink" title="滕王阁"></a>滕王阁</h1><p>后面赶着两天下雨最后临出发去下一站的最后一天，想着都来南昌了，总要去个景点看看吧，最有代表的应该还是滕王阁了，直接半价学生票去参观了一下，进入景点之后在阁楼前方会有一些表演的</p><p><img src="/images/i/2025/05/25/6832b28fb8672.jpg" alt="5ba746348210bfb999c2591d5f3ff26"></p><p><img src="/images/i/2025/05/25/6832b29898e60.jpg" alt="2a847b04256ceb75f4077b8ec5d91bc"></p><p>然后就可以进到阁楼里一路向上了，不过阁楼里没有洗手间，在上面还可以俯视一下江景</p><p><img src="/images/i/2025/05/25/6832b2a637566.jpg" alt="eaff8731bca05ed4aa0855e17cddabb"></p><p><img src="/images/i/2025/05/25/6832b2addd346.jpg" alt="131107b1fe814aee2d088199a24b249"></p><p><img src="/images/i/2025/05/25/6832b2c35336a.jpg" alt="6530f22ae41df89ad42bfe411782e15"></p><p>楼里面的话就是一些雕塑之类的，然后走到最顶楼还可以花钱买个祈福的那种牌子，写完之后挂到栏杆上，整个阁楼的顶拍起来还是挺好看的</p><p><img src="/images/i/2025/05/25/6832b2e208df1.jpg" alt="d68784abede2a39c03d600187690fe9"></p><h1 id="江西菜"><a href="#江西菜" class="headerlink" title="江西菜"></a>江西菜</h1><p>之后从滕王阁出来之后，发现这里离那个大士院街非常近，我们在附近找了一家卖粉和瓦罐汤的店，点了一份汤喝，因为一会要去吃正餐了就没有点粉。瓦罐汤的话价格不贵，我点了一份皮蛋肉丸，没记错好像是六七元的样子，味道的话就是鸡精味很鲜，皮蛋也挺好吃的，肉丸是那种掺了粉的肉圆</p><p><img src="/images/i/2025/05/25/6832b2f5746e4.jpg" alt="426ff0216dcf0fa5da7f49ac98a8ae2"></p><p>然后准备去吃正餐，正餐去了一家叫熊庆彬小锅小灶的，幸亏我们去得早，我们吃完之后出来外面队伍都排到街上去了。中途路过大士院街，买了一份小吃，忘记具体叫什么名字了，长这样子，味道的话就是没什么味道的糯米团子</p><p><img src="/images/i/2025/05/25/6832b306d317c.jpg" alt="98ae82c85484fea5ea5108d05b9460d"></p><p>这家江西菜在网上风评还可以，我们点了三样，一份粉蒸肉，一份黄豆鸡脚，一份牛仔骨，只有黄豆鸡脚是辣的，只能说幸好没有全点辣的，江西这边的辣不同重庆那边，是单纯用大量小米辣叠加辣度，两个人花了一百多，感觉还可以，粉蒸肉的话朋友说粉调的还可以，但是肉差点意思</p><p><img src="/images/i/2025/05/25/6832b318c4fb9.jpg" alt="b01ebf7e159cd894d4b18170b18f115"></p><p><img src="/images/i/2025/05/25/6832b320ad7e1.jpg" alt="f547386dca3da1cb3245b9ad8028dc4"></p><h1 id="Outro"><a href="#Outro" class="headerlink" title="Outro"></a>Outro</h1><p>吃完正餐就准备出发去机场了，因为这次在朋友家里寄宿，去吃的饭店不是很多，总体来说还都值得一试。南昌这边的话景点确实都是一些人文景点，如果感兴趣的话可以来看看。那个南昌之星大摩天轮本来想着晚上去坐一下，结果因为下雨我们在旁边的商超里看了一下，没机会尝试了哈哈</p>]]>
    </content>
    <id>https://www.zhajiangmian.top/2025/02/20/%E9%80%9B%E5%90%83%E9%80%9B%E5%96%9D%E7%AC%AC%E4%BA%8C%E7%AB%99-%E5%8D%97%E6%98%8C/</id>
    <link href="https://www.zhajiangmian.top/2025/02/20/%E9%80%9B%E5%90%83%E9%80%9B%E5%96%9D%E7%AC%AC%E4%BA%8C%E7%AB%99-%E5%8D%97%E6%98%8C/"/>
    <published>2025-02-19T19:41:18.000Z</published>
    <summary>
      <![CDATA[<h1 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h1><p>南昌这个城市真的就是历史遗迹的集合，然后由于我本人其实对这些兴趣不算特别大，再加上去的时候刚好赶上了连着的雨天，导致这趟南昌行程基本上没有什么产出，所以这篇内容可能稍微短一点，就把一些有意思的事情记录一下吧。</p>]]>
    </summary>
    <title>逛吃逛喝第二站-南昌</title>
    <updated>2026-10-30T10:17:05.612Z</updated>
  </entry>
  <entry>
    <author>
      <name>炸酱面</name>
    </author>
    <category term="旅游" scheme="https://www.zhajiangmian.top/categories/%E6%97%85%E6%B8%B8/"/>
    <category term="沈阳 旅游" scheme="https://www.zhajiangmian.top/tags/%E6%B2%88%E9%98%B3-%E6%97%85%E6%B8%B8/"/>
    <content>
      <![CDATA[<h1 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h1><p>也是快临近毕业了，这次寒假正好想出去玩玩，以前总是在沈阳坐飞机或者下飞机，路过这么多次也没好好玩过，索性这一次提前走两天，正月十六直接从沈阳起飞，至于目的地是哪，下一篇blog再更新~</p><p>这次也是带着新玩具（索尼A5100）的首次出征，算是初次玩摄影的尝试吧，不过镜头只配了一个55-210的，基本上拍不到广角就很难受，很多广角照片也是用手机拍了一下。</p><span id="more"></span><h1 id="2025-2-9"><a href="#2025-2-9" class="headerlink" title="2025&#x2F;2.9"></a>2025&#x2F;2.9</h1><h2 id="Step-1-–-老北市"><a href="#Step-1-–-老北市" class="headerlink" title="Step 1 – 老北市"></a>Step 1 – 老北市</h2><p>从家里到沈阳，到的时候已经是晚上六点多了，这个时间点索性去北市场夜市逛一逛，人挺多，也挺热闹，但是里面的小吃啥的也就是那些商业化的东西，实质上比较一般般，价格也是景区价格，如果特意去的话可以冲着表演去，我是赶上了晚上八点的凤凰表演，如果我没记错的话六点半也是有一场的。</p><p>我这边住宿实在故宫附近的一个青旅，位置在中街很不错，到哪都挺近，但是如果预算较为充足的话建议还是选择酒店哈，在酒店放置好东西之后直接打车去了老北市，沈阳这边的话街景灯光布置的确实都很不错，夜拍的话比较出片。</p><p><img src="/images/i/2025/05/25/6832ac2d371cc.jpg" alt="372f143a33b081edfccc634796deadd"></p><p>然后往里走的话就是各种小摊，就这种商业化小吃街其实都大差不差（点名青岛台东夜市），有啥想吃的稍微买一两样感觉就差不多了</p><p><img src="/images/i/2025/05/25/6832acc0de349.jpg" alt="372f143a33b081edfccc634796deadd"></p><p>然后赶上八点半的表演了，剩十分钟的时候广播会喊表演要开始了，之后八整点表演开始，大家都用手机对着天上要拍凤凰，实则有一段大概四五分钟的舞蹈表演，才会有鸟飞出来，这个时间气温大概有个零下二十多度，也是辛苦演员们了，然后我这边离的很远，拍的也不算特别清楚</p><p><img src="/images/i/2025/05/25/6832acdb8209d.jpg" alt="79834c397572152a8bd0ebc34375be9"></p><p>舞蹈完事就是凤凰的表演，先是一只红凤凰，后面会出来一只蓝凤凰，蓝凤凰翅膀会有焰火，like this：</p><p><img src="/images/i/2025/05/25/6832acec3b125.jpg" alt="f325096a929987bdff6ed61384320ca"></p><p><img src="/images/i/2025/05/25/6832acec3c2b6.jpg" alt="96b1925f6e2a6ae0c950def8ca5d907"></p><p>然后这边还会有室内的一个商业街，如果在外面冷了可以进去暖和暖和：</p><p><img src="/images/i/2025/05/25/6832ad55c5516.jpg" alt="73aa19ecbd8d04522593e351374d6cc"></p><p><img src="/images/i/2025/05/25/6832ad954b6ba.jpg" alt="71ce12bc6ad740a54c9e46f8aa2462b"></p><p>室内还会卖一些小东西，比如</p><p><img src="/images/i/2025/05/25/6832adb025676.jpg" alt="375726ed1f70f53f4da1e43d92a14e0"></p><h1 id="2025-2-10"><a href="#2025-2-10" class="headerlink" title="2025&#x2F;2.10"></a>2025&#x2F;2.10</h1><h2 id="Step-2-–-小河沿早市"><a href="#Step-2-–-小河沿早市" class="headerlink" title="Step 2 – 小河沿早市"></a>Step 2 – 小河沿早市</h2><p>大概是早上八点左右睡醒的，然后直接去早市溜一圈，我以为这个时间点应该没什么人，事实上永远不要低估中国人民的行动力。</p><p>入口和出口都有这样的一个门框（实在是不知道叫啥了）</p><p><img src="/images/i/2025/05/25/6832adbf819dd.jpg" alt="492c35bee852c1e0c66a7e2d4d0583e"></p><p>打车到了进去没多远买了两个酸牛奶，由于天气太冷了，酸奶直接半冻住了，根本吸不上来。再接着往前是一个打着网红招牌的牛肉火烧店，离着好远都能看见排的大长队，口味差不多和山科门口的衢州烤饼差不多，有点油油的，买完之后找了个早餐店坐着吃，因为实在是太冷了。然后早餐店里好多都是江浙沪的朋友来旅游的，然后早餐店卖的小生煎，实则没有东北人吃哈，估摸着都是被南方朋友们买走了，最后要了一份豆腐脑一份煎饼盒子，就着火烧吃掉了，反正早餐这东西其实味道都差不多哈，自行想象一下</p><p><img src="/images/i/2025/05/25/6832add190a1c.jpg" alt="243b10a18972f451b598dddb82e3b5f"></p><p>放一张全景图，自行感受一下人数</p><p><img src="/images/i/2025/05/25/6832ae3327767.jpg" alt="d2da7a0c61816c5d23648ab65a5fd71"></p><p>有一家卖米酒的味道不错，酸甜的，十块钱一瓶，我买的是茉莉花（记不清啥花了）的，适合当个伴手礼哈</p><p><img src="/images/i/2025/05/25/6832b003c54f3.jpg" alt="8f21f784d3f5da6770728e5f659d7bfd_"></p><h2 id="Step-3-–-故宫"><a href="#Step-3-–-故宫" class="headerlink" title="Step 3 – 故宫"></a>Step 3 – 故宫</h2><p><img src="/images/i/2025/05/25/6832ae5651321.jpg" alt="c5953c0cb6a35052b1efc731eb2023a"></p><p>这故宫实在是太大了，然后如果有想跟团听讲解的可以在里面直接嫖旅游团的讲解，跟在后面哈哈哈（just kidding），故宫外面的话就是各种古建筑风的店面，果然瑞幸哪里都有哈，然后我住宿的地方离故宫就一百米，进故宫的话可以在美团上直接买学生票，然后一定要记着带着身份证去，刷身份证就可以了</p><p><img src="/images/i/2025/05/25/6832ae62a7ca4.jpg" alt="bff14ecec85ede0d43733f033627666"></p><p><img src="/images/i/2025/05/25/6832ae6a026af.jpg" alt="5f5b23db2f909ef716ca4df0beb0b4a"></p><p>然后里面就是各种宫殿什么的，可以直接在门口扫个小程序或者直接搜公众号云上故宫</p><p><img src="/images/i/2025/05/25/6832ae73cef61.jpg" alt="c77a4fbf199095c13ce8bb80d09b743"></p><p><img src="/images/i/2025/05/25/6832ae79edc54.jpg" alt="11a0d90ddfae9dc1102ab755dd600ed"></p><p>还有一些文创商店，除了冰箱贴还是冰箱贴，要么就是纪念币，毫无购买欲望</p><p><img src="/images/i/2025/05/25/6832ae9e6a902.jpg" alt="9d220c5b26363f30315c3f50e03ae41"></p><p>这是个书屋，里面还有一些文物类的东西</p><p><img src="/images/i/2025/05/25/6832ae8d3f79a.jpg" alt="f94130d8d093c7b898e5781b9d61623"></p><p><img src="/images/i/2025/05/25/6832ae935a872.jpg" alt="d5b2ce2a7fc44e3e03cde7bc11e3ad9"></p><p>还有一些随便拍的人和景，谁说这A5100老的，这A5100可太棒了</p><p><img src="/images/i/2025/05/25/6832aeb04ef6b.jpg" alt="b9f5812ce7c77f1a4ee2a2550676b15"></p><p><img src="/images/i/2025/05/25/6832aeb63a37a.jpg" alt="8d3c85fd531a790dcb6028ae29788db"></p><p><img src="/images/i/2025/05/25/6832aebb394c0.jpg" alt="c5d830d40e0801da773e3b397d3a134"></p><p><img src="/images/i/2025/05/25/6832aec14c387.jpg" alt="6bb103f131ca78c8a91c408be63c95d"></p><p>最后出了故宫，在一个叫做”故宫莊啡“的店点了一点东西当歇歇脚了，建议是非拍摄需要不要点哈，这份慕斯49，茶水好像是16，纯提供观赏价值，味道中规中矩</p><p><img src="/images/i/2025/05/25/6832aece841fb.jpg" alt="fa2d8b48613d0fcf5e619c79c747f0e"></p><p>本来是打算下午去张学良故居，结果吃的太杂了直接犯胃病了，在旅馆躺了一下午，shift。</p><h2 id="Step-4-–-中街大悦城"><a href="#Step-4-–-中街大悦城" class="headerlink" title="Step 4 – 中街大悦城"></a>Step 4 – 中街大悦城</h2><p>就一个字，大，太大了，非常大，比青岛吾悦大的多的多的多，整个中街到大悦城感觉给我一整天都逛不完，没怎么拍照，主要是去吃了顿饭，到达那一天在车上正好刷到了一个肉饼自助，叫朴安胖，大悦城店，dy团购是花了37，味道算是中规中矩对得起价格，免费的就是牛肉饼、奥尔良鸡肉、西兰花、一些炸的食物，饮料水果和小蛋糕，如果预算不足的话是可以去试试的，在中街懒得掏相机了，随便拍了两张</p><p>ps5一番赏，第一次见</p><p><img src="/images/i/2025/05/25/6832aee5f1bc4.jpg" alt="0f92fed6dc33645d266477f0565786e"></p><p>不管在哪都很大的天才吧</p><p><img src="/images/i/2025/05/25/6832aefc6be6f.jpg" alt="cb110775cea4547f1ab399e50cfc746"></p><p>还有一个红烧什么玩意这是？（红色郭德纲）</p><p><img src="/images/i/2025/05/25/6832af129a794.jpg" alt="401ce4979d4b56ea879ebf93c724589"></p><h1 id="2025-2-11"><a href="#2025-2-11" class="headerlink" title="2025&#x2F;2.11"></a>2025&#x2F;2.11</h1><h2 id="Step-5-–-满记甜品"><a href="#Step-5-–-满记甜品" class="headerlink" title="Step 5 – 满记甜品"></a>Step 5 – 满记甜品</h2><p>这一天起床起的很晚哈，然后前一天晚上正好发现沈阳这边有满记甜品，离得还很近，直接约了个中午十一点半的满记冰室自助，之前一直很想去，可惜青岛店离学校太远了，菜单是这个样子的，左边的港式点心四选三，右边得榴莲忘返和榴莲双皮奶二选一，这些东西都是只能点一次，其他餐品不限。第一轮点餐可以一次性点四份，之后只能一次性点两份，上菜速度简直就是慢的恐怖，我中间加了一份咖喱鱼蛋和牛肉丸花了将近二十分钟才上，搞得我只能菜刚上就赶紧去点下一波，不然时间有点来不及</p><p><img src="/images/i/2025/05/25/6832af3093804.jpg" alt="76a2981da93852e2dac533cc1350cad"></p><p>菜品的话也是比较抽象哈，首先就是茶点种类真的很少，点心四选三我选的是排骨，凤爪和烧卖，第一次吃到凉的虾仁烧麦，然后排骨和凤爪味道中规中矩，分量和广式茶楼的差不多，这个四味虾饺根据我的经验来看，看起来就不是很好吃哈，然后又点了一份榴莲忘返，正好凑齐了第一波的四个菜。后面还点了杨枝甘露和双皮奶，杨枝甘露里面的芒果和柚粒真的令人难以下咽，比我列表里最涩的那位还要涩哈，适合孕妇来吃。双皮奶倒是不错，点了一份西贡双皮奶，一份琥珀果仁那个，好像西贡双皮奶就是没有料的双皮奶？也有可能成分不太一样，后面吃的有点饱了没吃出来味道上有啥区别，那个琥珀果仁的还不错，果仁在上面很加分。</p><p><img src="/images/i/2025/05/25/6832b003c54f3.jpg"></p><h2 id="Step-6-–-张学良故居"><a href="#Step-6-–-张学良故居" class="headerlink" title="Step 6 – 张学良故居"></a>Step 6 – 张学良故居</h2><p>吃完之后直接去张学良故居溜达，然后张学良故居和赵一荻故居、金融博物馆是挨在一起的，这三个地方在网上购票之后拿着身份证就可以通用了。景点相较于故宫来说不算很大，加上我也不是很喜欢这种地方，索性直接速通了，然后景区里洗手间面积很小，里面还开了热风，味道真的很大。。。放几张照片意思意思</p><p><img src="/images/i/2025/05/25/6832b018f00e9.jpg" alt="9be9e8f7d25adc740a779bdf6b561070_"></p><p><img src="/images/i/2025/05/25/6832b020912ef.jpg" alt="554493abf6e3ba4e633ec6d0da1afaf6_"></p><p><img src="/images/i/2025/05/25/6832b0264c2e2.jpg" alt="c42af1c25a0349684c7a174bce409ac3_"></p><p><img src="/images/i/2025/05/25/6832b02b9e296.jpg" alt="3685b7f7f164da89306a4f2f3a4d9f61_"></p><p>这里本来是想拍那个小姑娘的，结果不小心把后面仨哥们给拍上了，也是很有节目效果了，动作非常整齐奥，不知道的还以为在这摇呢</p><p><img src="/images/i/2025/05/25/6832b0326e323.jpg" alt="9897ae28aaa8c0e968212bc4dce05cc6_"></p><h2 id="Step-7-–-天主教大教堂"><a href="#Step-7-–-天主教大教堂" class="headerlink" title="Step 7 – 天主教大教堂"></a>Step 7 – 天主教大教堂</h2><p>适合拍照打卡，进不去教堂里面，忘了青岛那个大教堂长什么样了，不过从外面看起来还是很恢弘的，也没啥玩的拍了几张照片就润了</p><p><img src="/images/i/2025/05/25/6832b04056912.jpg" alt="d7dc1f2421c74593742c659107ead2e0_"></p><p><img src="/images/i/2025/05/25/6832b04777e4f.jpg" alt="04b102892a54f2e50da026d162262c16_"></p><h2 id="番外篇"><a href="#番外篇" class="headerlink" title="番外篇"></a>番外篇</h2><p>逛完教堂之后也是累了，直接回旅店睡到了晚上六点多，醒来之后不知道吃啥，点了烤鸡架和一家taco（名字叫独吞塔可），不得不说沈阳这烤鸡架味道真不错哈，就是我收到的时候有点凉了，但是还是很好吃。我点的一份焦糖，一份酸甜（本人比较喜欢吃甜的），酸甜的会带点辣，焦糖的没有特别甜，是那种带着香味的甜。然后这家taco我点了个单人餐，套餐里面是一个汉堡，一个金枪鱼的小塔可，一袋果汁，和一份辣椒拌的菠萝＋芒果（辣椒特别辣，然后这种吃法也没怎么试过，吃了两口就放弃了）。这汉堡里面的炸鸡腿真的特别大一个，比我手都大哈，参考下图，如果想点外卖的可以试试，单人餐37rmb好像是，性价比还可以哈</p><p><img src="/images/i/2025/05/25/6832b051f00fb.jpg" alt="5125eca4c2c79cda8befd06b00e92ff5_"></p><p>然后我发现旅店里面有好多ps5游戏哈，我刚想着商家放在这不怕被偷吗，打开一看里面都是空的。看了一下公告，玩ps5的话说是要找前台说一下哈，这些是其中的一部分，游戏还都很不错，如果看到这里你想问我是哪家旅店的话，尝试自己社工一下吧，毕竟在这个位置的青旅好像也就这一家。</p><p><img src="/images/i/2025/05/25/6832b051f00fb.jpg"></p><h1 id="2025-2-12"><a href="#2025-2-12" class="headerlink" title="2025&#x2F;2.12"></a>2025&#x2F;2.12</h1><h2 id="Step-8-–-二次进攻大悦城（偶遇精神小伙）"><a href="#Step-8-–-二次进攻大悦城（偶遇精神小伙）" class="headerlink" title="Step 8 – 二次进攻大悦城（偶遇精神小伙）"></a>Step 8 – 二次进攻大悦城（偶遇精神小伙）</h2><p>本来计划是今天在洗浴中心呆一天的，但是感觉非常无聊遂放弃，直接二次进攻大悦城哈，因为上一次去就是去吃了个饭，没有好好逛。</p><p>大悦城里面遍布二次元谷子店，不过ip都是那些互联网上比较热门的哈（实则暗示没有全职猎人哈）</p><p><img src="/images/i/2025/05/25/6832b095f18ec.jpg" alt="323e3d88590bc06c45a07ab2f1a428a"></p><p><img src="/images/i/2025/05/25/6832b09f28b7f.jpg" alt="6cecefb1f42f25bd925779a1706a536"></p><p>大悦城这边三楼是有通道，可以在ABCDE馆来回横跳的，就不用在外面绕了，然后正当哥们走累了找了个地方坐一会的时候出现了两位有志青年哈，我在坐着刷短视频，然后两个人上来直接跟我搭话，我刚开始还以为是哪个朋友我没认出来，这么巧，结果其中一个哥们开口来了一句我们是跑业务的。。。。我瞬间警戒值拉满，然后这个哥们就拿出了一块布，一根笔，我一看就是要推销干洗剂。。后面发生了什么看图说话，如果各位在外面遇到这种事情我的建议是不要理会，直接走人哈，尤其是不要动手，如果动手了不光是耽不耽误事情，说不准还要被讹一笔钱。</p><p><img src="/images/i/2025/05/25/6832b1377fe52.png" alt="image-20250525135710673"></p><p>之后我就继续溜达，最后在一个电玩城门口坐下了，本来是想歇一会，but电玩城里有打舞萌的朋友，（当然这里不是说对这个东西有任何偏见哈包括接下来我说的话都是在特定时间特定地点发生的特殊情况不代表任何主观意见因为我以前也玩过这东西而且我觉得会打舞萌的人很厉害）我在旁边脑子有点乱乱的，然后被砰砰砰的敲击声音搞得实在是不堪重负了，索性直接去吃午饭了。</p><p>午饭选在了一家泰餐馆，叫泰味太，点了一份椰汁鸡汤，一份猪蹄鸡爪煲，一份椰汁紫米饭，这也是我第一次吃泰餐，酸酸的很符合我的胃口，味道满分十分可以给到八分，就是量不算很多，三个菜加一起花了120，性价比自行斟酌吧</p><p><img src="/images/i/2025/05/25/6832b145ef4e1.jpg" alt="a27f62b5e8318becca0b65f87045cb4a_"></p><p><img src="/images/i/2025/05/25/6832b1556f910.jpg" alt="aa1d3ce75e7f1dbeef334889fd27b2d3_"></p><p><img src="/images/i/2025/05/25/6832b16364ac2.jpg" alt="f2791f0cef065ca287a0dc8cb4247429_"></p><p>最后在大悦城挑了点伴手礼就润去机场酒店了，到了酒店发现这位置怎么这么偏啊，离机场大概两三公里，但是周边点歌外卖都没有，直接又打了个车去的浑南天街吃饭，最后看了半天不知道吃啥，去吃的争鲜旋转寿司，之前吃过其他家旋转寿司，起码我吃过的这两家寿司都很一般哈，米饭就是纯大米饭，没有加寿司醋，凉的发硬，不过争鲜家刺身还不错，价格比较合适，新鲜度也还行（能吃出来也是冷藏过的不过没有很不新鲜）。最后是吃了九盘寿司，一碗拉面，一盘刺身，一共花了116</p><p><img src="/images/i/2025/05/25/6832b16b672b5.jpg" alt="62e34d6357c2c4e0aaa0bd444601ba7c_"></p><h1 id="Outro"><a href="#Outro" class="headerlink" title="Outro"></a>Outro</h1><p>All in all，如果没来过沈阳的话确实还是值得一试哈，最终来个排名吧。</p><p>景点排名：</p><p>故宫 &gt; 小河沿早市 &gt; 中街大悦城 &gt; 老北市 &gt; 张学良故居 &gt; 大教堂</p><p>上述出现的食物及餐厅排名（仅针对食物味道 不对性价比做比较）：</p><p>独吞塔可 ≥ 烤鸡架 &gt; 小河沿米酒 &gt; 泰味太 ≥ 朴安胖肉饼自助（这个是带着性价比做的评价）、小河沿牛肉火烧及酸奶 &gt; 满记甜品 &gt; 争鲜 ≥ 故宫莊啡、老北市</p>]]>
    </content>
    <id>https://www.zhajiangmian.top/2025/02/10/%E9%80%9B%E5%90%83%E9%80%9B%E5%96%9D%E7%AC%AC%E4%B8%80%E7%AB%99-%E6%B2%88%E9%98%B3/</id>
    <link href="https://www.zhajiangmian.top/2025/02/10/%E9%80%9B%E5%90%83%E9%80%9B%E5%96%9D%E7%AC%AC%E4%B8%80%E7%AB%99-%E6%B2%88%E9%98%B3/"/>
    <published>2025-02-10T01:28:23.000Z</published>
    <summary>
      <![CDATA[<h1 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h1><p>也是快临近毕业了，这次寒假正好想出去玩玩，以前总是在沈阳坐飞机或者下飞机，路过这么多次也没好好玩过，索性这一次提前走两天，正月十六直接从沈阳起飞，至于目的地是哪，下一篇blog再更新~</p>
<p>这次也是带着新玩具（索尼A5100）的首次出征，算是初次玩摄影的尝试吧，不过镜头只配了一个55-210的，基本上拍不到广角就很难受，很多广角照片也是用手机拍了一下。</p>]]>
    </summary>
    <title>逛吃逛喝第一站-沈阳</title>
    <updated>2026-10-30T10:17:05.610Z</updated>
  </entry>
  <entry>
    <author>
      <name>炸酱面</name>
    </author>
    <category term="电吉他" scheme="https://www.zhajiangmian.top/categories/%E7%94%B5%E5%90%89%E4%BB%96/"/>
    <category term="电吉他" scheme="https://www.zhajiangmian.top/tags/%E7%94%B5%E5%90%89%E4%BB%96/"/>
    <content>
      <![CDATA[<h1 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h1><p>​关于我为什么要学电吉他这件事，要追溯到小学的时候看过的一个真人剧说起，可能各位也都看过，叫终极一家（及其系列衍生作品），里面的战斗方式设定对当时的我来说非常酷炫，用乐器当武器，用音乐来进攻和防守，时至今日我也总是回去品味一下（其中印象最深的还是老版的终极三国），当时我深深的被其中的吉他弹奏技艺所吸引，幻想着自己有一天也能变成那样。写到这突然感觉中二气氛稍稍有点过头，再聊聊其他的~</p><span id="more"></span><p>​很小的时候家里听说学乐器对考学有帮助可以加分，所以家里给我报名了钢琴版，最终辗转来去，考到了钢琴九级，但其实对于稍微入门一些的钢琴乐手来说，这种业余的考级并不能证明一个人水平的高低，并且由于需要在一年内反反复复的练一摸一样的音阶、练习曲，还有每周回课时老师的真人快打和心灵虐待环节，我渐渐的厌倦了这一门乐器。</p><p>​再后来，乐器加分这一说法也渐渐消失了，加上当时临近中考，我的钢琴考级之路就此停留在了九级（实则是练的差不多了但是战线拖得太长没去考），上了中学之后也不怎么碰乐器了。后来高中的时候，家里添置了一台电钢琴，我当时和其他同龄人一样，喜欢流行乐，所以当时弹得曲子也主要以此为主，比如周杰伦、林俊杰、张杰等，因为一般流行乐的演奏难度不大，并且适合在人前装逼TAT。</p><p>​差不多在高二，在b站无意刷到了一名叫做Animenz的博主，当时把小小年纪的我深深震撼到了。</p><hr><p>在听音乐时我们一般会从歌词和弦律两个方面进行欣赏，能让人们觉得好听的歌一般是二者都会吸引你的歌曲，并且伴随着旋律的律动及歌词的意境，很多时候我们的脑中会浮现出各种各样的场景，这与歌曲的创作环境、背景、旋律以及个人的经历、成长环境等均有关系，</p><p>正是这种音乐与自身的共鸣，即使是从未听过的旋律，无法理解的歌词，能让人忽略重重障碍，深深陷入进去，因此我不总是推荐音乐给其他人，因为每个人所能共鸣的类型是完全不同的，在你眼中觉得不错的旋律或许在其他人眼中是完全不同的景象，这也是音乐创造能如此多样化的原因吧。</p><hr><p>​当时面对高考的压力，唯一的放松方法就是听听音乐，A叔对动漫音乐的改编让我大开眼界，我从没想到乐曲的情绪能用如此多样化的技巧表现出来，当旋律响起的那一刻，我仿佛能感受到动漫里各种角色的动作和情绪，如同将自己置身于其中，ACG＋古典乐的融合让我过于震撼，因此我又重新坐上了琴凳，选择的第一首曲子也是《Hunter X Hunter》– Departure，但后很遗憾我并没有做到善始善终。但从此我也知道了自己想演奏的是什么样的音乐，因此在上了大学后，我将《Hunter X Hunter》– 表里一体 作为了第二首尝试的曲子，不过也是一直边摸边弹，最后也忘记了多长时间才弹下来。可以说A叔的大部分曲子对一般非科班出身的乐手是充满挑战的，无论是需要日积月累的手指机能和基本功，以及先决条件（很遗憾我就是标准的九度手），但凭借着热爱与兴趣，也会有较为不错的产出。</p><p>​就在前一段时间，补了一下《孤独摇滚》，我相信很多人在看了这类番后也都产生了想学习乐器的想法，当然我也是其中的一员，再加上终极系列对我的影响，我很坚定的把电吉他作为了我的二持乐器，当时我也不知道自己是否能坚持下去，写这篇文章的时候我已经学习了15天，好在有钢琴打下的基础，所以学起来也不算很困难，下面就说一下我这一段时间的经历，从乐器购买到练习技巧，我希望这篇文章能够帮到其他想要自学吉他的人。</p><h1 id="选购器材"><a href="#选购器材" class="headerlink" title="选购器材"></a>选购器材</h1><p>对于电吉他来说，我们平日里练习大概需要两样东西：</p><ul><li><p>电吉他，很多人对电吉他和木吉他不是很能区分的清，其实它们都是靠琴弦震动来发出声音，因此两种吉他在拨动琴弦时都会发出声音。不同的是电吉他多了一个装置叫做拾音器，我们一般看到的电吉他演奏都会接一个音响，拾音器的作用就是将琴弦的震动转为电信号传给音响，因此如果不接音响我们听到的就是单纯琴弦震动发出的声音，接了音响听到的是琴弦震动以及拾音器电信号传递给音响处理后的声音。拾音器就是这几个东西，其中有两排排布的拾音器就叫做双拾音器（比如图中的第一个和第三个），反之只有一排排布的就是单拾音器。电吉他拾音器有多种搭配方案，比如我这一把就是双单双，可以通过调节右侧的挡位来控制哪几个拾音器工作，具体的我就不说了，有兴趣可以自己查一下。购买电吉他时，如果你和我一样也是一时兴起，我建议选择一把二手的，成本低一些。图中为二手收的Ibanez grg170dx，不做推荐仅供参考。或许学吉他的第一笔收入就是把吉他卖掉的那笔钱（have a joke）~</p></li><li><p>音箱&&效果器，我们刚才也提到了，音箱可以将拾音器电信号转换为声音播放出来，所以如果没有音箱的话你的演奏声音会很小，不过一般练习的话其实也够了，毕竟我们只需要自己能听见即可。我们平常听到的吉他演奏会有金属、失真过载等音色（就是你觉得很酷很摇滚的那种声音），这种音色的改变需要通过效果器来实现，不过对于初学者来说我们可以选择一个能调节音色的音箱，等进阶后再购置效果器，所以我选择的是二手的JOYO DC15，也是仅供参考不做推荐。这时候有的同学会问了，主播主播，我平时自己练不需要音箱可以吗，并且我怕接了音箱声音太大吵到其他人，The Answer is 大咩。因为在平常的某些练习中，我们也需要改变音色，不然弹出来的东西会很难听，如果怕音箱声音太大吵到其他人，可以在音箱上插一个耳机，不过琴弦振动的声音还是会有的，所以无论如何你周围的人一定是可以听到你在弹什么的~</p></li><li><p>其他的一些配件，比如拨片、调音器，以及备用琴弦，这些就仁者见仁了，调音器的话搞个七八块的完全够用，剩下的消耗品各位就自己看着来吧。</p></li></ul><h1 id="基础乐理学习"><a href="#基础乐理学习" class="headerlink" title="基础乐理学习"></a>基础乐理学习</h1><p>b站上有一个很不错的课程，我有一点点的乐理基础，所以学起来还算轻松，我自己的速度大概是两周学完了week4，主要是锻炼手指机能以及熟悉吉他。</p><p>在学习演奏之前，我的建议是一定要学会乐理，总结了一下我这两星期遇到的乐理知识：</p><ol><li><p>音程，我们从最简单的说起，不论有没有学过乐理的朋友，肯定都知道都Do Re Mi Fa So La Si，用字母表示为（C D E F G A B）可以看到除了mi和fa、si和do之间，剩下的音之间都有一个黑键，我们这些黑键插在两个白键之间，从一个任意键（黑或白都可）到相邻最近的一个键之间，我们叫做半音。比如do到do和re之间的黑键以及这个黑键到re，或者是mi到fa、si到do（因为他们之间没有黑键，所以临近的最近的一个键就是它紧邻的下一个白键），反之从do到re、re到mi就是全音关系，因为他们中有两个半音。<br>接下来介绍黑白键之间的关系，do和re之间的黑键，位于do的右侧，re的左侧，所以这个音我们可以说是升do也可以说是降re，以此类推。那么升mi是什么呢，其实就是fa了，升si、降fa、降do也是同理，我们一般都不这么说，一般就直接说mi fa si do。<br>我们有的时候会听到什么二度，三度，小三，大三，纯四，增四巴拉巴拉，这些都是什么意思呢（这里只说点皮毛不然说多了会晕)，首先我们不管前面的大小增减纯，我们先判断是几度，比如do到mi，是三度（do re mi），do到re是二度（do re），do到si是七度（do re mi fa so la si），只需要算上自己和目标音，数数中间有几个音就可以了。那么如何判断大小增减纯呢，请看下表，我说太多可能也理解不了，不如先记着，但其实规律就是根据有几组半音来决定，并且我们要记住一四五八无大小，二三六七没有纯，这里面我没有提到一度和八度，我们一般不太用这两个，比如do到do自己就是一度，do到下一个do就是八度（因为音都是从do到si循环，但音高不同，音都是一样的）。</p></li><li><p>吉他的弦由细到粗分别对应了1-6弦，琴颈上的小格子，每一个小格子我们叫做一个品。1-6弦对应的空弦音（左手不按品右手直接拨弦）分别为EBGDAE（Mi Si So Re La Mi），空弦状态下六弦音高最低，五弦其次，以此类推，六弦到五弦，五弦到四弦，四弦到三弦，二弦到一弦，同一品相差纯四度，所以可以看到六弦空弦音（0品）为mi，那么五弦空弦音就是从mi网上推纯四度，也就是A，四弦空弦音是A网上推纯四为Re，而三弦和二弦相差的就不是纯四度，而是大三度了，因此可以看到三弦空弦音为So，而二弦为So往上推大三度，为Si。左手从琴头（调节琴弦那一端）每按住一个品，音高就会升高半音，比如六弦空弦音为mi，当我按住六弦一品，此时拨琴弦则为fa，以此类推，我们就可以在琴上找到任何一个我们想要的音了。</p></li><li><p>和弦，就是我们多个音一起演奏时所形成的特殊旋律，和弦由根音及其他几个音构成，这几个音之间有规律，我们看一下是怎么个规律。这里我就不过多深入了，只初步介绍三和弦和五和弦。<br>我们如何找到一个三和弦呢，三和弦需要我们在根音上附加一个三度音和一个五度音，当根音与三度音、三度音与五度音之间的关系分别为大三度、小三度时就形成了一个大三和弦，当关系分别为小三度、大三度时则形成了一个小三和弦。当关系分别为大三度，大三度时，形成的是增三和弦，关系分别为小三度、小三度时，形成的是减三和弦。<br>  首先要确定一个根音，可以是任何一个音，我用do来举例。如果我们想形成一个大三和弦，那么我们要确保do这个根音和三度音之间是大三度关系，所以选择的三度音为mi，三度音与五度音之间要为小三度关系，所以选择的五度音为so，那么 do mi so这个和弦就是一个以do作为根音的大三和弦，可以把这个和弦记作C（意思为根音为do的大三和弦，C后的maj省略）。那么如果我们想构造一个小三和弦，这次我用mi来举例，那么我们找到mi的小三度音作为三读音，为so，再找到so的大三度音作为五毒音，为si，所以mi so si这三个音构成的和弦为以mi作为根音的小三和弦，记为Em（m指minor），其他的以此类推。</p></li><li><p>节奏，这一部分需要各位自己去了解如下知识，如各个音符的符值，以及对节奏的把控，由于我有一些钢琴基础，所以这个地方就基本上过掉了，我建议没事的时候打开在线节拍器，调整好速度，自己把各种音符的符值都打一打，刚开始比较难的可能是三连音，附点等等，但是打多了就知道大概是什么样的接走了。</p></li></ol><h1 id="演奏练习及技巧"><a href="#演奏练习及技巧" class="headerlink" title="演奏练习及技巧"></a>演奏练习及技巧</h1><p>其实这一块没什么需要说的，需要的是坚持不懈的练习以及耐心，我觉得最难的部分就是在演奏时保证演奏的干净（护弦），以及演奏的姿势，比如拨弦、按弦等等，我觉的课程里老师讲的都很不错，护弦的话我跟着b站另外一位up主学了一下，大概规律就是右手护演奏弦的上面弦，左手手指护演奏弦的下面的弦，不过还是要慢慢摸索。此外刚上手的话对琴弦之间的距离等不太熟悉，拨弦总是拨错，这个很正常，不过要尽量做到不看手去拨弦，感受琴弦之间的距离。All in all，还是要多加练习！</p><h1 id="Outro"><a href="#Outro" class="headerlink" title="Outro"></a>Outro</h1><p>文章写的都是我自己总结的内容，如果有错误欢迎各路大佬进行更正！</p>]]>
    </content>
    <id>https://www.zhajiangmian.top/2025/01/27/%E7%94%B5%E5%90%89%E4%BB%96%E8%87%AA%E5%AD%A6%E8%AE%B0%E5%BD%95/</id>
    <link href="https://www.zhajiangmian.top/2025/01/27/%E7%94%B5%E5%90%89%E4%BB%96%E8%87%AA%E5%AD%A6%E8%AE%B0%E5%BD%95/"/>
    <published>2025-01-26T22:59:07.000Z</published>
    <summary>
      <![CDATA[<h1 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h1><p>​	关于我为什么要学电吉他这件事，要追溯到小学的时候看过的一个真人剧说起，可能各位也都看过，叫终极一家（及其系列衍生作品），里面的战斗方式设定对当时的我来说非常酷炫，用乐器当武器，用音乐来进攻和防守，时至今日我也总是回去品味一下（其中印象最深的还是老版的终极三国），当时我深深的被其中的吉他弹奏技艺所吸引，幻想着自己有一天也能变成那样。写到这突然感觉中二气氛稍稍有点过头，再聊聊其他的~</p>]]>
    </summary>
    <title>电吉他自学记录</title>
    <updated>2026-10-30T10:17:05.606Z</updated>
  </entry>
  <entry>
    <author>
      <name>炸酱面</name>
    </author>
    <category term="Java安全" scheme="https://www.zhajiangmian.top/categories/Java%E5%AE%89%E5%85%A8/"/>
    <category term="Java安全 RMI" scheme="https://www.zhajiangmian.top/tags/Java%E5%AE%89%E5%85%A8-RMI/"/>
    <content>
      <![CDATA[<h1 id="RMI调用流程分析"><a href="#RMI调用流程分析" class="headerlink" title="RMI调用流程分析"></a>RMI调用流程分析</h1><h2 id="RMI"><a href="#RMI" class="headerlink" title="RMI"></a>RMI</h2><blockquote><p>以下介绍内容来自<a href="https://www.w3cschool.cn/article/30445887.html">Java的RMI介绍及使用方法详解 | w3cschool笔记</a></p></blockquote><p>现在有一个需求，我们需要在一台主机上调用另一台主机上的Java代码，这个时候就要用到RMI(Remote Method Invocation)。</p><p>使用 RMI 技术可以使一个 JVM 中的对象，调用另一个 JVM 中的对象方法并获取调用结果。这里的另一个 JVM 可以在同一台计算机也可以是远程计算机。因此，RMI 意味着需要一个 Server 端和一个 Client 端。Server 端通常会创建一个对象，并使之可以被远程访问。</p><p>这个对象被称为远程对象。Server 端需要注册这个对象可以被 Client 远程访问。</p><p>Client 端调用可以被远程访问的对象上的方法，Client 端就可以和 Server 端进行通信并相互传递信息。</p><p>说到这里，是不是发现使用 RMI 在构建一个分布式应用时十分方便，它和 RPC 一样可以实现分布式应用之间的互相通信，甚至和现在的微服务思想都十分类似。</p><span id="more"></span><hr><blockquote><p>以下内容参考自：<br><a href="https://su18.org/post/rmi-attack/">https://su18.org/post/rmi-attack/</a><br><a href="http://devgou.com/article/Java-RMI">http://devgou.com/article/Java-RMI</a></p></blockquote><p>那么我们来思考一下RMI的具体流程，首先是一个被远程调用的RMI-Server端，和一个RMI-Client端，但是有一个问题，Server端上每一个要被远程调用的对象都对应了一个RMI端口，并且这个端口是随机的，如果有很多需要被远程调用的对象，那么Client端是无法准确区分具体要调用对象的对应端口，所以我们引入RMI-Register。</p><p>RMI-Register是一个注册中心，并且Register的端口是我们可以指定的。Server端会将需要被远程调用的对象绑定到Register中，并且起一个别名。那么这个时候如果有多个需要被远程调用的对象，Client端可以通过先访问Rigister以及想要调用的远程对象的别名，此时Register会自动帮我们找到该别名对应的远程对象在RMI上的端口，之后会通过该端口找到对应的远程对象后调用其中的方法。</p><p>但其实Client端调用Server端的远程对象，它们之间并非是直接通信的，而是通过一个代理对象-存根（stub和skeleton），这样做是为了屏蔽网络通信间的复杂性。具体流程如下：</p><ol><li>Client端访问Register，拿到Stub，Stub中有调用的Server端远程对象的存根（skeleton）</li><li>Client通过调用自己的存根（stub）与服务端存根（skeleton）通信</li><li>服务端存根（skeleton）调用服务SS端远程对象的方法并将结果返回给Client端存根（stub）</li><li>Client端存根（stub）将结果返回给Client端</li></ol><p><img src="/images/i/2025/03/30/67e8ee834edb6.png" alt="image-20250330151056801"></p><p><img src="/images/i/2025/03/30/67e8ee943e10f.png" alt="image-20250330151114274"></p><hr><p>使用代码创建RMI服务端及客户端进行通信（注册中心一般在与Server放在一起）</p><p>服务端代码架构如下：</p><p><img src="/images/i/2025/03/30/67e8eeae165cf.png" alt="image-20250330151139904"></p><p>客户端代码架构如下：</p><p><img src="/images/i/2025/03/30/67e8eebf21a03.png" alt="image-20250330151156929"></p><p>1、服务端与客户端共同实现的接口RemoteInterface，该接口方法为需要远程调用的方法，并且需要继承Remote接口：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">import</span> java.rmi.Remote;</span><br><span class="line"><span class="keyword">import</span> java.rmi.RemoteException;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">RemoteInterface</span> <span class="keyword">extends</span> <span class="title class_">Remote</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="type">String</span> <span class="title function_">sayHello</span><span class="params">()</span> <span class="keyword">throws</span> RemoteException;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="type">String</span> <span class="title function_">sayHello</span><span class="params">(Object name)</span> <span class="keyword">throws</span> RemoteException;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="type">String</span> <span class="title function_">sayGoodbye</span><span class="params">()</span> <span class="keyword">throws</span> RemoteException;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>2、服务端：</p><p>要被远程调用的Server端对象RemoteObject，该远程对象需要继承UnicastRemoteObject类并且扩展我们之前写好的RemoteInterface接口，实现其中的方法。继承UnicastRemoteObject类后，客户端访问获得远程对象时，该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端，此时客户端所获得的这个拷贝称为“存根”，而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理，用于与服务器端的通信，而骨架也可认为是服务器端的一个代理，用于接收客户端的请求之后调用远程方法来响应客户端的请求。如果不想继承UnicastRemoteObject ，后面就需要主动的使用其静态方法 exportObject手动export：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">import</span> java.rmi.RemoteException;</span><br><span class="line"><span class="keyword">import</span> java.rmi.server.RMIClientSocketFactory;</span><br><span class="line"><span class="keyword">import</span> java.rmi.server.RMIServerSocketFactory;</span><br><span class="line"><span class="keyword">import</span> java.rmi.server.UnicastRemoteObject;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RemoteObject</span> <span class="keyword">extends</span> <span class="title class_">UnicastRemoteObject</span> <span class="keyword">implements</span> <span class="title class_">RemoteInterface</span> &#123;</span><br><span class="line">    <span class="type">protected</span> <span class="title function_">RemoteObject</span><span class="params">()</span> <span class="keyword">throws</span> RemoteException &#123;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">String</span> <span class="title function_">sayHello</span><span class="params">()</span> <span class="keyword">throws</span> RemoteException &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;Hello User!&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">String</span> <span class="title function_">sayHello</span><span class="params">(Object name)</span> <span class="keyword">throws</span> RemoteException &#123;</span><br><span class="line">        <span class="keyword">return</span> name.getClass().getName();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">String</span> <span class="title function_">sayGoodbye</span><span class="params">()</span> <span class="keyword">throws</span> RemoteException &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;Bye User!&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>实现Server及Registry，将上面需要被远程调用的对象绑定到Registry中，其中我们使用LocateRegistry类创建Registry，并且将Registry绑定到了12345端口上，Namning是一个可以对Registry进行操作的静态类，其中包括：查询（lookup）、绑定（bind）、重新绑定（rebind）、接触绑定（unbind）、list（列表）。Naming.bind格式为<code>//host:port/alias</code>，前面的<code>rmi:</code>加不加都可以，alias为要远程调用的类在Registry中的别名：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">import</span> java.net.MalformedURLException;</span><br><span class="line"><span class="keyword">import</span> java.rmi.AlreadyBoundException;</span><br><span class="line"><span class="keyword">import</span> java.rmi.Naming;</span><br><span class="line"><span class="keyword">import</span> java.rmi.RemoteException;</span><br><span class="line"><span class="keyword">import</span> java.rmi.registry.LocateRegistry;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RemoteServer</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> RemoteException, MalformedURLException, AlreadyBoundException &#123;</span><br><span class="line">        <span class="type">RemoteObject</span> <span class="variable">remoteObject</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RemoteObject</span>();</span><br><span class="line">        LocateRegistry.createRegistry(<span class="number">12345</span>);</span><br><span class="line">        Naming.bind(<span class="string">&quot;rmi://localhost:12345/Hello&quot;</span>,remoteObject);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>3、客户端</p><p>客户端通过获取服务端已经开放的Registry后进行调用，通过Naming.lookup()在远程对象注册表Registry中查找指定name的对象，并返回远程对象的引用（一个stub），之后可通过stub调用远程对象的方法：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">import</span> java.lang.reflect.Array;</span><br><span class="line"><span class="keyword">import</span> java.rmi.NotBoundException;</span><br><span class="line"><span class="keyword">import</span> java.rmi.RemoteException;</span><br><span class="line"><span class="keyword">import</span> java.rmi.registry.LocateRegistry;</span><br><span class="line"><span class="keyword">import</span> java.rmi.registry.Registry;</span><br><span class="line"><span class="keyword">import</span> java.util.Arrays;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RemoteClient</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> RemoteException, NotBoundException &#123;</span><br><span class="line">        <span class="type">Registry</span> <span class="variable">registry</span> <span class="operator">=</span> LocateRegistry.getRegistry(<span class="string">&quot;localhost&quot;</span>,<span class="number">12345</span>);</span><br><span class="line">        <span class="type">String</span> <span class="variable">string</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>();</span><br><span class="line">        System.out.printf(Arrays.toString(registry.list()));</span><br><span class="line">        <span class="type">RemoteInterface</span> <span class="variable">stub</span> <span class="operator">=</span> (RemoteInterface) registry.lookup(<span class="string">&quot;Hello&quot;</span>);</span><br><span class="line">        System.out.println(stub.sayHello());</span><br><span class="line">        System.out.println(stub.sayHello(string));</span><br><span class="line">        System.out.println(stub.sayGoodbye());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>这样一次简单的通信就结束了，其中还有一些需要注意的点：</p><p>可以看到在上面的实例中我们在Client端调用时向<code>sayHello</code>方法中传递了一个对象，这个对象必须是可序列化的。如果传递的该对象在Server端不存在，那么Server端就会抛出ClassNotFound 的异常，但如果设置了<code>java.rmi.server.codebase</code>，那么就可以通过<code>java.rmi.server.codebase</code>设置的另一个地址中获取该类并加载反序列化。该属性可使用<code>System.setProperty(&quot;java.rmi.server.codebase&quot;, &quot;http://127.0.0.1:7777/&quot;);</code>进行设置，也可以使用启动参数</p><p><code>-Djava.rmi.server.codebase=&quot;http://127.0.0.1:7777/&quot;</code>进行指定。</p><p>并且我们也要指定安全策略，让rmi在动态加载一个类时能够允许该类进行反序列化。</p><p>RemoteServer.java:</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">import</span> java.net.MalformedURLException;</span><br><span class="line"><span class="keyword">import</span> java.rmi.AlreadyBoundException;</span><br><span class="line"><span class="keyword">import</span> java.rmi.Naming;</span><br><span class="line"><span class="keyword">import</span> java.rmi.RemoteException;</span><br><span class="line"><span class="keyword">import</span> java.rmi.registry.LocateRegistry;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RemoteServer</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> RemoteException, MalformedURLException, AlreadyBoundException &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">policyFile</span> <span class="operator">=</span> <span class="string">&quot;/Users/y1zh3e7/web安全/Java安全/Java RMI/RMIServer/src/main/java/rmi.policy&quot;</span>;</span><br><span class="line">        System.setProperty(<span class="string">&quot;java.security.policy&quot;</span>, policyFile);</span><br><span class="line">        System.setProperty(<span class="string">&quot;java.rmi.server.codebase&quot;</span>, <span class="string">&quot;http://127.0.0.1:7777/&quot;</span>);</span><br><span class="line">        <span class="keyword">if</span> (System.getSecurityManager() == <span class="literal">null</span>) &#123;</span><br><span class="line">            System.setSecurityManager(<span class="keyword">new</span> <span class="title class_">SecurityManager</span>());</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">RemoteObject</span> <span class="variable">remoteObject</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RemoteObject</span>();</span><br><span class="line">        LocateRegistry.createRegistry(<span class="number">12345</span>);</span><br><span class="line">        Naming.bind(<span class="string">&quot;rmi://localhost:12345/Hello&quot;</span>,remoteObject);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>RemoteClient.java:</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">import</span> java.rmi.NotBoundException;</span><br><span class="line"><span class="keyword">import</span> java.rmi.RemoteException;</span><br><span class="line"><span class="keyword">import</span> java.rmi.registry.LocateRegistry;</span><br><span class="line"><span class="keyword">import</span> java.rmi.registry.Registry;</span><br><span class="line"><span class="keyword">import</span> java.util.Arrays;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RemoteClient</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> RemoteException, NotBoundException &#123;</span><br><span class="line">        <span class="type">Registry</span> <span class="variable">registry</span> <span class="operator">=</span> LocateRegistry.getRegistry(<span class="string">&quot;localhost&quot;</span>,<span class="number">12345</span>);</span><br><span class="line">        System.out.printf(Arrays.toString(registry.list()));</span><br><span class="line">        <span class="type">RemoteInterface</span> <span class="variable">stub</span> <span class="operator">=</span> (RemoteInterface) registry.lookup(<span class="string">&quot;Hello&quot;</span>);</span><br><span class="line">        System.out.println(stub.sayHello());</span><br><span class="line">        System.out.println(stub.sayHello(<span class="keyword">new</span> <span class="title class_">ClientObject</span>()));</span><br><span class="line">        System.out.println(stub.sayGoodbye());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>rmi.policy:</p><div class="p5-codebox" data-lang="tex"><figure class="highlight"><pre class="highlight-code"><code><span class="line">grant &#123;</span><br><span class="line">     permission java.net.SocketPermission &quot;localhost:12345&quot;, &quot;listen,resolve&quot;;</span><br><span class="line">     permission java.security.AllPermission;</span><br><span class="line">&#125;;</span><br></code></pre></figure></div><p><img src="/images/i/2025/03/30/67e8eed41c6d1.png" alt="image-20250330151218075"></p><h2 id="RMI-Server实现"><a href="#RMI-Server实现" class="headerlink" title="RMI-Server实现"></a>RMI-Server实现</h2><p>开始debug：</p><p><img src="/images/i/2025/03/30/67e8eee3f3839.png" alt="image-20250330151233965"></p><p>步入几次后进入到UnicastRemoteObject无参构造，并且调用另外一个构造函数，接收一个port为0，并且调用exportObject方法，继续跟进：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">protected</span> <span class="title function_">UnicastRemoteObject</span><span class="params">()</span> <span class="keyword">throws</span> RemoteException</span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">this</span>(<span class="number">0</span>);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">protected</span> <span class="title function_">UnicastRemoteObject</span><span class="params">(<span class="type">int</span> port)</span> <span class="keyword">throws</span> RemoteException</span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">this</span>.port = port;</span><br><span class="line">    exportObject((Remote) <span class="built_in">this</span>, port);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>我们发现在这个exportObject中又调用了一个重载的exportObject方法，这个方法可以将我们要远程传输的对象发布到网络上，可以发现如果继承了UnicastRemoteObject这个类就会自动调用这个方法，所以如果没有继承UnicastRemoteObject类就需要在RemoteObject中的构造方法主动调用这个静态方法，其中第二个参数传入的是一个UnicastServerRef对象，我们跟进这个对象：</p><blockquote><p>从这里开始是一系列的封装，UnicastServerRef为最外层</p></blockquote><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="type">Remote</span> <span class="title function_">exportObject</span><span class="params">(Remote obj, <span class="type">int</span> port)</span></span><br><span class="line">    <span class="keyword">throws</span> RemoteException</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">return</span> exportObject(obj, <span class="keyword">new</span> <span class="title class_">UnicastServerRef</span>(port));</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>UnicastServerRef对象构造方法会调用父类构造方法，并传入一个LiveRef对象：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">public</span> <span class="title function_">UnicastServerRef</span><span class="params">(<span class="type">int</span> port)</span> &#123;</span><br><span class="line">    <span class="built_in">super</span>(<span class="keyword">new</span> <span class="title class_">LiveRef</span>(port));</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>跟进LiveRef这个对象，它的构造方法会调用另一个重载的构造方法，我们发现这个接收两个参数的构造方法中调用了TCPEndpoint.getLocalEndpoint(port)（第一个参数就是给这个LiveRef对象分配一个标识id）：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">public</span> <span class="title function_">LiveRef</span><span class="params">(<span class="type">int</span> port)</span> &#123;</span><br><span class="line">    <span class="built_in">this</span>((<span class="keyword">new</span> <span class="title class_">ObjID</span>()), port);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">public</span> <span class="title function_">LiveRef</span><span class="params">(ObjID objID, <span class="type">int</span> port)</span> &#123;</span><br><span class="line">    <span class="built_in">this</span>(objID, TCPEndpoint.getLocalEndpoint(port), <span class="literal">true</span>);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>跟进TCPEndpoint.getLocalEndpoint(port)，其实就是在这一步中拿到了我们RMI-Server的ip地址及端口，return回来的ep里面存放了我们的ip和端口（到目前为止传递的端口一直是0）：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="comment">// 如果当前ep列表中没有东西就创建一个ep放到里面，ep里面就存放了我们RMI-Server的ip和端口 </span></span><br><span class="line"><span class="keyword">if</span> (epList == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">/*</span></span><br><span class="line"><span class="comment">             * Create new endpoint list.</span></span><br><span class="line"><span class="comment">             */</span></span><br><span class="line">            ep = <span class="keyword">new</span> <span class="title class_">TCPEndpoint</span>(localHost, port, csf, ssf);</span><br><span class="line">            epList = <span class="keyword">new</span> <span class="title class_">LinkedList</span>&lt;TCPEndpoint&gt;();</span><br><span class="line">            epList.add(ep);</span><br><span class="line">            ep.listenPort = port;</span><br><span class="line">            ep.transport = <span class="keyword">new</span> <span class="title class_">TCPTransport</span>(epList);</span><br><span class="line">            localEndpoints.put(endpointKey, epList);</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (TCPTransport.tcpLog.isLoggable(Log.BRIEF)) &#123;</span><br><span class="line">                TCPTransport.tcpLog.log(Log.BRIEF,</span><br><span class="line">                    <span class="string">&quot;created local endpoint for socket factory &quot;</span> + ssf +</span><br><span class="line">                    <span class="string">&quot; on port &quot;</span> + port);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">synchronized</span> (epList) &#123;</span><br><span class="line">                ep = epList.getLast();</span><br><span class="line">                <span class="type">String</span> <span class="variable">lastHost</span> <span class="operator">=</span> ep.host;</span><br><span class="line">                <span class="type">int</span> <span class="variable">lastPort</span> <span class="operator">=</span>  ep.port;</span><br><span class="line">                <span class="type">TCPTransport</span> <span class="variable">lastTransport</span> <span class="operator">=</span> ep.transport;</span><br><span class="line">                <span class="comment">// assert (localHost == null ^ lastHost != null)</span></span><br><span class="line">                <span class="keyword">if</span> (localHost != <span class="literal">null</span> &amp;&amp; !localHost.equals(lastHost)) &#123;</span><br><span class="line">                    <span class="comment">/*</span></span><br><span class="line"><span class="comment">                     * Hostname has been updated; add updated endpoint</span></span><br><span class="line"><span class="comment">                     * to list.</span></span><br><span class="line"><span class="comment">                     */</span></span><br><span class="line">                    <span class="keyword">if</span> (lastPort != <span class="number">0</span>) &#123;</span><br><span class="line">                        <span class="comment">/*</span></span><br><span class="line"><span class="comment">                         * Remove outdated endpoints only if the</span></span><br><span class="line"><span class="comment">                         * port has already been set on those endpoints.</span></span><br><span class="line"><span class="comment">                         */</span></span><br><span class="line">                        epList.clear();</span><br><span class="line">                    &#125;</span><br><span class="line">                    ep = <span class="keyword">new</span> <span class="title class_">TCPEndpoint</span>(localHost, lastPort, csf, ssf);</span><br><span class="line">                    ep.listenPort = port;</span><br><span class="line">                    ep.transport = lastTransport;</span><br><span class="line">                    epList.add(ep);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> ep;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p><img src="/images/i/2025/03/30/67e8eef6c6e26.png" alt="image-20250330151252708"></p><p>创建好ep后，这个ep对象会被封装到LiveRef中：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">public</span> <span class="title function_">LiveRef</span><span class="params">(ObjID objID, Endpoint endpoint, <span class="type">boolean</span> isLocal)</span> &#123;</span><br><span class="line">    ep = endpoint;</span><br><span class="line">    id = objID;</span><br><span class="line">    <span class="built_in">this</span>.isLocal = isLocal;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p><img src="/images/i/2025/03/30/67e8ef0b2d5b8.png" alt="image-20250330151313253"></p><p>经过一系列的初始化后我们又回到了UnicastRef中，会将我们刚才生成好的LiveRef对象封装到UnicastRef中：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">public</span> <span class="title function_">UnicastRef</span><span class="params">(LiveRef liveRef)</span> &#123;</span><br><span class="line">     ref = liveRef;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>我们回到了这个UnicastRemoteObject的exportObject方法上，封装结束，经过一系列封装</p><blockquote><p>UnicastServerRef内封装LiveRef，LiveRef内封装了Endpoint，EndPoint中封装的是当前要被调用的远程对象的ip和端口，UnicastServerRef从名字上就可以看出来是一个RMI-Server端的引用对象，Server端与其它RMI组件之间的通信集中于这一个类上，不需要在被调用的对象上添加任何的属性和方法。</p></blockquote><p>此时我们继续看exportObject(obj, new UnicastServerRef(port))之后的逻辑</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="type">Remote</span> <span class="title function_">exportObject</span><span class="params">(Remote obj, <span class="type">int</span> port)</span></span><br><span class="line">    <span class="keyword">throws</span> RemoteException</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">return</span> exportObject(obj, <span class="keyword">new</span> <span class="title class_">UnicastServerRef</span>(port));</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>可以看到到这里将我们封装好的UnicastServerRef再次封装到了要被远程调用的对象的ref属性上，并且调用了UnicastServerRef的exportobject，继续跟进</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="type">Remote</span> <span class="title function_">exportObject</span><span class="params">(Remote obj, UnicastServerRef sref)</span></span><br><span class="line">        <span class="keyword">throws</span> RemoteException</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// if obj extends UnicastRemoteObject, set its ref.</span></span><br><span class="line">        <span class="keyword">if</span> (obj <span class="keyword">instanceof</span> UnicastRemoteObject) &#123;</span><br><span class="line">            ((UnicastRemoteObject) obj).ref = sref;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> sref.exportObject(obj, <span class="literal">null</span>, <span class="literal">false</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>跟进后我们发现在这个exportObject方法中要进行一个动态代理的创建，这个动态代理就是我们之前提到的stub：</p><blockquote><p>这里可能会有疑问，我们服务端的代理对象不是skeleton吗，客户端才是stub，为什么会在服务端的逻辑中生成，是因为这个stub在服务端生成后需要客户端将生成好的stub拿过去使用</p></blockquote><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">Remote</span> <span class="title function_">exportObject</span><span class="params">(Remote impl, Object data,</span></span><br><span class="line"><span class="params">                           <span class="type">boolean</span> permanent)</span></span><br><span class="line">    <span class="keyword">throws</span> RemoteException</span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">Class</span>&lt;?&gt; <span class="variable">implClass</span> <span class="operator">=</span> impl.getClass();</span><br><span class="line">    Remote stub;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        stub = Util.createProxy(implClass, getClientRef(), forceStubUse);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (IllegalArgumentException e) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">ExportException</span>(</span><br><span class="line">            <span class="string">&quot;remote object implements illegal remote interface&quot;</span>, e);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (stub <span class="keyword">instanceof</span> RemoteStub) &#123;</span><br><span class="line">        setSkeleton(impl);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="type">Target</span> <span class="variable">target</span> <span class="operator">=</span></span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">Target</span>(impl, <span class="built_in">this</span>, stub, ref.getObjID(), permanent);</span><br><span class="line">    ref.exportObject(target);</span><br><span class="line">    hashToMethod_Map = hashToMethod_Maps.get(implClass);</span><br><span class="line">    <span class="keyword">return</span> stub;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>在这行代码中使用getClientRef获取客户端Ref，我们跟进查看：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    stub = Util.createProxy(implClass, getClientRef(), forceStubUse);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>可以发现实例化了一个UnicastRef，刚才我们提到了UnicastServerRef，封装的是服务端引用，那么这个UnicastRef其实封装的就是客户端引用了，传了一个ref过去，这个ref里面就是我们封装好的LiveRef，可以看到这个LiveRef和之前封装在UnicastServerRef中的LiveRef是同一个，所以说这个LiveRef是我们整个通信的核心：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">protected</span> <span class="type">RemoteRef</span> <span class="title function_">getClientRef</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">UnicastRef</span>(ref);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p><img src="/images/i/2025/03/30/67e8ef210eb0a.png" alt="image-20250330151334941"></p><p>createProxy方法内部创建好了动态代理对象，并代理了两个接口：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">final</span> <span class="type">ClassLoader</span> <span class="variable">loader</span> <span class="operator">=</span> implClass.getClassLoader();</span><br><span class="line">    <span class="keyword">final</span> <span class="type">Class</span>&lt;?&gt;[] <span class="variable">interfaces</span> <span class="operator">=</span> getRemoteInterfaces(implClass);</span><br><span class="line">    <span class="keyword">final</span> <span class="type">InvocationHandler</span> <span class="variable">handler</span> <span class="operator">=</span></span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">RemoteObjectInvocationHandler</span>(clientRef);</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* REMIND: private remote interfaces? */</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> AccessController.doPrivileged(<span class="keyword">new</span> <span class="title class_">PrivilegedAction</span>&lt;Remote&gt;() &#123;</span><br><span class="line">            <span class="keyword">public</span> <span class="type">Remote</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">                <span class="keyword">return</span> (Remote) Proxy.newProxyInstance(loader,</span><br><span class="line">                                                       interfaces,</span><br><span class="line">                                                       handler);</span><br><span class="line">            &#125;&#125;);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (IllegalArgumentException e) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">StubNotFoundException</span>(<span class="string">&quot;unable to create proxy&quot;</span>, e);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p><img src="/images/i/2025/03/30/67e8ef31159ba.png" alt="image-20250330151351028"></p><p>动态代理对象创建完成后这里又进行了一次封装，将RMI通信中所有需要的东西全部封装到这个Target对象当中，并且调用了LiveRef的exportObject方法：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">Target</span> <span class="variable">target</span> <span class="operator">=</span></span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">Target</span>(impl, <span class="built_in">this</span>, stub, ref.getObjID(), permanent);</span><br><span class="line">ref.exportObject(target);</span><br><span class="line">hashToMethod_Map = hashToMethod_Maps.get(implClass);</span><br></code></pre></figure></div><p>跟进LiveRef的exportObject方法，发现调用了TCPEndPoint的exportObject：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">exportObject</span><span class="params">(Target target)</span> <span class="keyword">throws</span> RemoteException &#123;</span><br><span class="line">    ep.exportObject(target);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>继续跟进，发现调用了TCPTransport的exportObject：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">exportObject</span><span class="params">(Target target)</span> <span class="keyword">throws</span> RemoteException &#123;</span><br><span class="line">    transport.exportObject(target);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>继续跟进，我们终于来到了最重要的一个地方，在TCPTransport的exportObject中调用了listen方法，我们跟进listen方法：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">exportObject</span><span class="params">(Target target)</span> <span class="keyword">throws</span> RemoteException &#123;</span><br><span class="line">    <span class="comment">/*</span></span><br><span class="line"><span class="comment">     * Ensure that a server socket is listening, and count this</span></span><br><span class="line"><span class="comment">     * export while synchronized to prevent the server socket from</span></span><br><span class="line"><span class="comment">     * being closed due to concurrent unexports.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">synchronized</span> (<span class="built_in">this</span>) &#123;</span><br><span class="line">        listen();</span><br><span class="line">        exportCount++;</span><br><span class="line">    &#125;</span><br></code></pre></figure></div><p>listen方法开启了一个Socket链接，并且这个tcp链接是单独开辟了一个线程运行的，与我们当前运行代码的线程是两个不同的线程，到此时我们传递的port依然是0：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">private</span> <span class="type">void</span> <span class="title function_">listen</span><span class="params">()</span> <span class="keyword">throws</span> RemoteException &#123;</span><br><span class="line">    <span class="keyword">assert</span> Thread.holdsLock(<span class="built_in">this</span>);</span><br><span class="line">    <span class="type">TCPEndpoint</span> <span class="variable">ep</span> <span class="operator">=</span> getEndpoint();</span><br><span class="line">    <span class="type">int</span> <span class="variable">port</span> <span class="operator">=</span> ep.getPort();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (server == <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span> (tcpLog.isLoggable(Log.BRIEF)) &#123;</span><br><span class="line">            tcpLog.log(Log.BRIEF,</span><br><span class="line">                <span class="string">&quot;(port &quot;</span> + port + <span class="string">&quot;) create server socket&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            server = ep.newServerSocket();</span><br><span class="line">            <span class="comment">/*</span></span><br><span class="line"><span class="comment">             * Don&#x27;t retry ServerSocket if creation fails since</span></span><br><span class="line"><span class="comment">             * &quot;port in use&quot; will cause export to hang if an</span></span><br><span class="line"><span class="comment">             * RMIFailureHandler is not installed.</span></span><br><span class="line"><span class="comment">             */</span></span><br><span class="line">            <span class="type">Thread</span> <span class="variable">t</span> <span class="operator">=</span> AccessController.doPrivileged(</span><br><span class="line">                <span class="keyword">new</span> <span class="title class_">NewThreadAction</span>(<span class="keyword">new</span> <span class="title class_">AcceptLoop</span>(server),</span><br><span class="line">                                    <span class="string">&quot;TCP Accept-&quot;</span> + port, <span class="literal">true</span>));</span><br><span class="line">            t.start();</span><br><span class="line">        &#125; <span class="keyword">catch</span> (java.net.BindException e) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">ExportException</span>(<span class="string">&quot;Port already in use: &quot;</span> + port, e);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">ExportException</span>(<span class="string">&quot;Listen failed on port: &quot;</span> + port, e);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// otherwise verify security access to existing server socket</span></span><br><span class="line">        <span class="type">SecurityManager</span> <span class="variable">sm</span> <span class="operator">=</span> System.getSecurityManager();</span><br><span class="line">        <span class="keyword">if</span> (sm != <span class="literal">null</span>) &#123;</span><br><span class="line">            sm.checkListen(port);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>在newServerSocket()中可以看到使用setDefaultPort方法为这个远程对象分配了一个随机的端口：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">ServerSocket</span> <span class="title function_">newServerSocket</span><span class="params">()</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">        <span class="keyword">if</span> (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) &#123;</span><br><span class="line">            TCPTransport.tcpLog.log(Log.VERBOSE,</span><br><span class="line">                <span class="string">&quot;creating server socket on &quot;</span> + <span class="built_in">this</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="type">RMIServerSocketFactory</span> <span class="variable">serverFactory</span> <span class="operator">=</span> ssf;</span><br><span class="line">        <span class="keyword">if</span> (serverFactory == <span class="literal">null</span>) &#123;</span><br><span class="line">            serverFactory = chooseFactory();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">ServerSocket</span> <span class="variable">server</span> <span class="operator">=</span> serverFactory.createServerSocket(listenPort);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// if we listened on an anonymous port, set the default port</span></span><br><span class="line">        <span class="comment">// (for this socket factory)</span></span><br><span class="line">        <span class="keyword">if</span> (listenPort == <span class="number">0</span>)</span><br><span class="line">            setDefaultPort(server.getLocalPort(), csf, ssf);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> server;</span><br><span class="line">    &#125;</span><br></code></pre></figure></div><p>从listen中出来后会调用super.exportObject，跟进这个exportObject：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">boolean</span> <span class="variable">ok</span> <span class="operator">=</span> <span class="literal">false</span>;</span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="built_in">super</span>.exportObject(target);</span><br><span class="line">    ok = <span class="literal">true</span>;</span><br><span class="line">&#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (!ok) &#123;</span><br><span class="line">        <span class="keyword">synchronized</span> (<span class="built_in">this</span>) &#123;</span><br><span class="line">            decrementExportCount();</span><br><span class="line">        &#125;</span><br></code></pre></figure></div><p>这里面将封装好的Target放到了ObjectTable这个表结构中：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">exportObject</span><span class="params">(Target target)</span> <span class="keyword">throws</span> RemoteException &#123;</span><br><span class="line">    target.setExportedTransport(<span class="built_in">this</span>);</span><br><span class="line">    ObjectTable.putTarget(target);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>最终对于Target的一系列操作完毕，我们回到了这里，将 stub return了出去，到这里Server端的一系列过程结束：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">Target</span> <span class="variable">target</span> <span class="operator">=</span></span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">Target</span>(impl, <span class="built_in">this</span>, stub, ref.getObjID(), permanent);</span><br><span class="line">ref.exportObject(target);</span><br><span class="line">hashToMethod_Map = hashToMethod_Maps.get(implClass);</span><br><span class="line"><span class="keyword">return</span> stub;</span><br></code></pre></figure></div><h2 id="注册中心创建及绑定"><a href="#注册中心创建及绑定" class="headerlink" title="注册中心创建及绑定"></a>注册中心创建及绑定</h2><p><img src="/images/i/2025/03/30/67e8ef417e496.png" alt="image-20250330151407299"></p><p>进入有参构造，创建RegistryImpl实例对象：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="type">Registry</span> <span class="title function_">createRegistry</span><span class="params">(<span class="type">int</span> port)</span> <span class="keyword">throws</span> RemoteException &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">RegistryImpl</span>(port);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>RegistryImpl构造方法中仍然创建了一个LiveRef实例，并传递给UnicastServerRef进行封装，再传递给setup调用，这个LiveRef中封装的就是我们Registry启动的ip和端口，并且我们再刚才远程对象的创建中也经历了这些步骤，说明Registry本质上也是一个特殊的远程对象：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">public</span> <span class="title function_">RegistryImpl</span><span class="params">(<span class="type">int</span> port)</span></span><br><span class="line">    <span class="keyword">throws</span> RemoteException</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">if</span> (port == Registry.REGISTRY_PORT &amp;&amp; System.getSecurityManager() != <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="comment">// grant permission for default port only.</span></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            AccessController.doPrivileged(<span class="keyword">new</span> <span class="title class_">PrivilegedExceptionAction</span>&lt;Void&gt;() &#123;</span><br><span class="line">                <span class="keyword">public</span> <span class="type">Void</span> <span class="title function_">run</span><span class="params">()</span> <span class="keyword">throws</span> RemoteException &#123;</span><br><span class="line">                    <span class="type">LiveRef</span> <span class="variable">lref</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">LiveRef</span>(id, port);</span><br><span class="line">                    setup(<span class="keyword">new</span> <span class="title class_">UnicastServerRef</span>(lref));</span><br><span class="line">                    <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;, <span class="literal">null</span>, <span class="keyword">new</span> <span class="title class_">SocketPermission</span>(<span class="string">&quot;localhost:&quot;</span>+port, <span class="string">&quot;listen,accept&quot;</span>));</span><br><span class="line">        &#125; <span class="keyword">catch</span> (PrivilegedActionException pae) &#123;</span><br><span class="line">            <span class="keyword">throw</span> (RemoteException)pae.getException();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="type">LiveRef</span> <span class="variable">lref</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">LiveRef</span>(id, port);</span><br><span class="line">        setup(<span class="keyword">new</span> <span class="title class_">UnicastServerRef</span>(lref));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>跟进到setup中，可以看到这里调用了UnicastServerRef的exportObject方法，和发布远程对象的逻辑相似，我们也要将这个RegistryImpl对象发布出去：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">private</span> <span class="type">void</span> <span class="title function_">setup</span><span class="params">(UnicastServerRef uref)</span></span><br><span class="line">    <span class="keyword">throws</span> RemoteException</span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">/* Server ref must be created and assigned before remote</span></span><br><span class="line"><span class="comment">     * object &#x27;this&#x27; can be exported.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    ref = uref;</span><br><span class="line">    uref.exportObject(<span class="built_in">this</span>, <span class="literal">null</span>, <span class="literal">true</span>);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>跟进之后很熟悉的地方，创建动态代理Stub，继续跟进：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">Remote</span> <span class="title function_">exportObject</span><span class="params">(Remote impl, Object data,</span></span><br><span class="line"><span class="params">                           <span class="type">boolean</span> permanent)</span></span><br><span class="line">    <span class="keyword">throws</span> RemoteException</span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">Class</span>&lt;?&gt; <span class="variable">implClass</span> <span class="operator">=</span> impl.getClass();</span><br><span class="line">    Remote stub;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        stub = Util.createProxy(implClass, getClientRef(), forceStubUse);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (IllegalArgumentException e) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">ExportException</span>(</span><br><span class="line">            <span class="string">&quot;remote object implements illegal remote interface&quot;</span>, e);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (stub <span class="keyword">instanceof</span> RemoteStub) &#123;</span><br><span class="line">        setSkeleton(impl);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="type">Target</span> <span class="variable">target</span> <span class="operator">=</span></span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">Target</span>(impl, <span class="built_in">this</span>, stub, ref.getObjID(), permanent);</span><br><span class="line">    ref.exportObject(target);</span><br><span class="line">    hashToMethod_Map = hashToMethod_Maps.get(implClass);</span><br><span class="line">    <span class="keyword">return</span> stub;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>跟进到Util.createproxy中，我们发现这里与远程对象创建Stub的方式不同，在远程对象的创建中if判断条件没有通过，所以走的是下面的Proxy.newProxyInstance(loader, interfaces,handler);使用jdk原生动态代理创建的，但在Registry这里，我们发现这个判断条件是成立的，跟进这个if判断：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    remoteClass = getRemoteClass(implClass);</span><br><span class="line">&#125; <span class="keyword">catch</span> (ClassNotFoundException ex ) &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">StubNotFoundException</span>(</span><br><span class="line">        <span class="string">&quot;object does not implement a remote interface: &quot;</span> +</span><br><span class="line">        implClass.getName());</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (forceStubUse ||</span><br><span class="line">    !(ignoreStubClasses || !stubClassExists(remoteClass)))</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">return</span> createStub(remoteClass, clientRef);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">final</span> <span class="type">ClassLoader</span> <span class="variable">loader</span> <span class="operator">=</span> implClass.getClassLoader();</span><br><span class="line"><span class="keyword">final</span> <span class="type">Class</span>&lt;?&gt;[] <span class="variable">interfaces</span> <span class="operator">=</span> getRemoteInterfaces(implClass);</span><br><span class="line"><span class="keyword">final</span> <span class="type">InvocationHandler</span> <span class="variable">handler</span> <span class="operator">=</span></span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">RemoteObjectInvocationHandler</span>(clientRef);</span><br><span class="line"></span><br><span class="line"><span class="comment">/* REMIND: private remote interfaces? */</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> AccessController.doPrivileged(<span class="keyword">new</span> <span class="title class_">PrivilegedAction</span>&lt;Remote&gt;() &#123;</span><br><span class="line">        <span class="keyword">public</span> <span class="type">Remote</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> (Remote) Proxy.newProxyInstance(loader,</span><br><span class="line">                                                   interfaces,</span><br><span class="line">                                                   handler);</span><br><span class="line">        &#125;&#125;);</span><br></code></pre></figure></div><p>我们发现这个判断条件是可以进去的，其中stubClassExists的判断条件为true，跟进去发现这个方法会使用反射寻找类名后拼接一个_Stub，如果成功找到就返回true，否则就抛出异常后返回false，此时remoteClass.getName()是我们的sun.rmi.registry.RegistryImpl，那么存不存在sun.rmi.registry.RegistryImpl_Stub这个类的，我们在jdk原生的包下是可以找到这个类的，可以发现不光有Stub也有Skel，这两个类就是JDK帮我们生成好的Registry远程对象的Stub和Skeleton：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="type">boolean</span> <span class="title function_">stubClassExists</span><span class="params">(Class&lt;?&gt; remoteClass)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (!withoutStubs.containsKey(remoteClass)) &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            Class.forName(remoteClass.getName() + <span class="string">&quot;_Stub&quot;</span>,</span><br><span class="line">                          <span class="literal">false</span>,</span><br><span class="line">                          remoteClass.getClassLoader());</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line">        &#125; <span class="keyword">catch</span> (ClassNotFoundException cnfe) &#123;</span><br><span class="line">            withoutStubs.put(remoteClass, <span class="literal">null</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p><img src="/images/i/2025/03/30/67e8ef4ecffb4.png" alt="image-20250330151420689"></p><p>回到上面的判断中，返回为true，所以走的是createStub(remoteClass, clientRef)这里，我们继续跟进：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">if</span> (forceStubUse ||</span><br><span class="line">    !(ignoreStubClasses || !stubClassExists(remoteClass)))</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">return</span> createStub(remoteClass, clientRef);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>cretaeStub中用反射为我们创建好了sun.rmi.registry.RegistryImpl_Stub这个类：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">String</span> <span class="variable">stubname</span> <span class="operator">=</span> remoteClass.getName() + <span class="string">&quot;_Stub&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Make sure to use the local stub loader for the stub classes.</span></span><br><span class="line"><span class="comment"> * When loaded by the local loader the load path can be</span></span><br><span class="line"><span class="comment"> * propagated to remote clients, by the MarshalOutputStream/InStream</span></span><br><span class="line"><span class="comment"> * pickle methods</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="type">Class</span>&lt;?&gt; <span class="variable">stubcl</span> <span class="operator">=</span></span><br><span class="line">        Class.forName(stubname, <span class="literal">false</span>, remoteClass.getClassLoader());</span><br><span class="line">    <span class="type">Constructor</span>&lt;?&gt; <span class="variable">cons</span> <span class="operator">=</span> stubcl.getConstructor(stubConsParamTypes);</span><br><span class="line">    <span class="keyword">return</span> (RemoteStub) cons.newInstance(<span class="keyword">new</span> <span class="title class_">Object</span>[] &#123; ref &#125;);</span><br></code></pre></figure></div><p>回到UnicastServerRef中，继续向下跟进，Stub已经初始化好了，接下来要对Skeleton进行初始化，跟进setSkeleton方法：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">if</span> (stub <span class="keyword">instanceof</span> RemoteStub) &#123;</span><br><span class="line">    setSkeleton(impl);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>发现调用的仍然是Utilz中的createSkeleton，继续跟进：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">setSkeleton</span><span class="params">(Remote impl)</span> <span class="keyword">throws</span> RemoteException &#123;</span><br><span class="line">    <span class="keyword">if</span> (!withoutSkeletons.containsKey(impl.getClass())) &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            skel = Util.createSkeleton(impl);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (SkeletonNotFoundException e) &#123;</span><br><span class="line">            <span class="comment">/*</span></span><br><span class="line"><span class="comment">             * Ignore exception for skeleton class not found, because a</span></span><br><span class="line"><span class="comment">             * skeleton class is not necessary with the 1.2 stub protocol.</span></span><br><span class="line"><span class="comment">             * Remember that this impl&#x27;s class does not have a skeleton</span></span><br><span class="line"><span class="comment">             * class so we don&#x27;t waste time searching for it again.</span></span><br><span class="line"><span class="comment">             */</span></span><br><span class="line">            withoutSkeletons.put(impl.getClass(), <span class="literal">null</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>和创建Stub时一样，反射加载：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">String</span> <span class="variable">skelname</span> <span class="operator">=</span> cl.getName() + <span class="string">&quot;_Skel&quot;</span>;</span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="type">Class</span>&lt;?&gt; <span class="variable">skelcl</span> <span class="operator">=</span> Class.forName(skelname, <span class="literal">false</span>, cl.getClassLoader());</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> (Skeleton)skelcl.newInstance();</span><br></code></pre></figure></div><p>Skeleton初始化好后接着走完UnicastServerRef最后一段代码，仍然是将这些要用到的东西封装到了Target对象中去，并且调用LiveRef.exportObject：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">Target</span> <span class="variable">target</span> <span class="operator">=</span></span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">Target</span>(impl, <span class="built_in">this</span>, stub, ref.getObjID(), permanent);</span><br><span class="line">ref.exportObject(target);</span><br><span class="line">hashToMethod_Map = hashToMethod_Maps.get(implClass);</span><br><span class="line"><span class="keyword">return</span> stub;</span><br></code></pre></figure></div><p>LiveRef.exportObject中逻辑和远程对象创建中基本上是一样的，不过我们可以继续跟进到Transport.exportObject中看一下现在的ObjectTable中都有哪些Target对象：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">exportObject</span><span class="params">(Target target)</span> <span class="keyword">throws</span> RemoteException &#123;</span><br><span class="line">    target.setExportedTransport(<span class="built_in">this</span>);</span><br><span class="line">    ObjectTable.putTarget(target);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>我们在ObjectTable中可以看到当前的objTable有如下三个Target对象：</p><p>第一个Target对象中的Stub为DGCImpl_Stub，这个东西我们在后续内容中会提到：<br><img src="/images/i/2025/03/30/67e8ef6200fa7.png" alt="image-20250330151439778"></p><p>第二个Target对象中的Stub为RegistryImpl_Stub，就是我们初始化好的Registry：</p><p><img src="/images/i/2025/03/30/67e8ef797f9dc.png" alt="image-20250330151503371"></p><p>第三个Target对象中的Stub为一个动态代理对象，这个Target就是我们的远程对象了</p><p><img src="/images/i/2025/03/30/67e8ef8ecf688.png" alt="image-20250330151524744"></p><p>到这里我们的Registry就初始化好了，最终拿到了一个RegistryImpl对象：</p><p><img src="/images/i/2025/03/30/67e8efa37d9f0.png" alt="image-20250330151545420"></p><p>接下来我们看一下远程对象是如何绑定到Registry上的：</p><p>我这了是使用了远程绑定的方式：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">RemoteObject</span> <span class="variable">remoteObject</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RemoteObject</span>();</span><br><span class="line"><span class="type">Registry</span> <span class="variable">r</span> <span class="operator">=</span> LocateRegistry.createRegistry(<span class="number">12345</span>);</span><br><span class="line">Naming.bind(<span class="string">&quot;rmi://localhost:12345/Hello&quot;</span>,remoteObject);</span><br></code></pre></figure></div><p>不过实际应用中我们都会直接用创建好的Registry来进行绑定，所以我们直接看这种本地的绑定：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">RemoteObject</span> <span class="variable">remoteObject</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RemoteObject</span>();</span><br><span class="line"><span class="type">Registry</span> <span class="variable">r</span> <span class="operator">=</span> LocateRegistry.createRegistry(<span class="number">12345</span>);</span><br><span class="line">r.bind(<span class="string">&quot;Hello&quot;</span>,remoteObject);</span><br></code></pre></figure></div><p>跟进到这个bind方法中其实发现就是对Registry这个哈希表结构放进去一对kv：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">bind</span><span class="params">(String name, Remote obj)</span></span><br><span class="line">    <span class="keyword">throws</span> RemoteException, AlreadyBoundException, AccessException</span><br><span class="line">&#123;</span><br><span class="line">    checkAccess(<span class="string">&quot;Registry.bind&quot;</span>);</span><br><span class="line">    <span class="keyword">synchronized</span> (bindings) &#123;</span><br><span class="line">        <span class="type">Remote</span> <span class="variable">curr</span> <span class="operator">=</span> bindings.get(name);</span><br><span class="line">        <span class="keyword">if</span> (curr != <span class="literal">null</span>)</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">AlreadyBoundException</span>(name);</span><br><span class="line">        bindings.put(name, obj);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>其实对于注册中心的其它操作也是基于对这个HashMap的一些操作。</p><h2 id="客户端请求注册中心-客户端行为"><a href="#客户端请求注册中心-客户端行为" class="headerlink" title="客户端请求注册中心-客户端行为"></a>客户端请求注册中心-客户端行为</h2><p>我们在前面已经了解到了Registry是客户端和服务端通信的核心，但是又说到Registry它本身也是一个远程对象，所以需要客户端先拿到Registry的Stub再和Registry的Skeleton进行通信，所以我们下面来看一下客户端是如何拿到Registry的Stub的：</p><p><img src="/images/i/2025/03/30/67e8efb1c9f16.png" alt="image-20250330151559616"></p><p>跟进之后调用重载的getRegistry：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="type">Registry</span> <span class="title function_">getRegistry</span><span class="params">(String host, <span class="type">int</span> port)</span></span><br><span class="line">    <span class="keyword">throws</span> RemoteException</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">return</span> getRegistry(host, port, <span class="literal">null</span>);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>跟进这个重载的方法中，可以发现客户端拿到Registry的Stub并不是通过接收Registry的一个反序列化对象实现的，而是直接本地创建了一个LiveRef，然后封装到动态代理当中：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="type">Registry</span> <span class="title function_">getRegistry</span><span class="params">(String host, <span class="type">int</span> port,</span></span><br><span class="line"><span class="params">                                   RMIClientSocketFactory csf)</span></span><br><span class="line">    <span class="keyword">throws</span> RemoteException</span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">Registry</span> <span class="variable">registry</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (port &lt;= <span class="number">0</span>)</span><br><span class="line">        port = Registry.REGISTRY_PORT;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (host == <span class="literal">null</span> || host.length() == <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="comment">// If host is blank (as returned by &quot;file:&quot; URL in 1.0.2 used in</span></span><br><span class="line">        <span class="comment">// java.rmi.Naming), try to convert to real local host name so</span></span><br><span class="line">        <span class="comment">// that the RegistryImpl&#x27;s checkAccess will not fail.</span></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            host = java.net.InetAddress.getLocalHost().getHostAddress();</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="comment">// If that failed, at least try &quot;&quot; (localhost) anyway...</span></span><br><span class="line">            host = <span class="string">&quot;&quot;</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="type">LiveRef</span> <span class="variable">liveRef</span> <span class="operator">=</span></span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">LiveRef</span>(<span class="keyword">new</span> <span class="title class_">ObjID</span>(ObjID.REGISTRY_ID),</span><br><span class="line">                    <span class="keyword">new</span> <span class="title class_">TCPEndpoint</span>(host, port, csf, <span class="literal">null</span>),</span><br><span class="line">                    <span class="literal">false</span>);</span><br><span class="line">    <span class="type">RemoteRef</span> <span class="variable">ref</span> <span class="operator">=</span></span><br><span class="line">        (csf == <span class="literal">null</span>) ? <span class="keyword">new</span> <span class="title class_">UnicastRef</span>(liveRef) : <span class="keyword">new</span> <span class="title class_">UnicastRef2</span>(liveRef);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> (Registry) Util.createProxy(RegistryImpl.class, ref, <span class="literal">false</span>);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>跟到这个createProxy中可以发现这个动态代理的创建和再Registry中创建的那个Stub过程一模一样，所以说Client端拿到Registry的Stub其实是通过在本地创建了一个一模一样的（指其中封装的LiveRef的属性是一模一样的）RegistryImpl_Stub实现的：</p><p><img src="/images/i/2025/03/30/67e8efc5d8b5a.png" alt="image-20250330151619492"></p><p><img src="/images/i/2025/03/30/67e8efda16c11.png" alt="image-20250330151639902"></p><p>拿到了Registry的Stub后，接下来我们就可以通过这个Stub来寻找其它远程对象的代理，也就是lookup操作</p><p><img src="/images/i/2025/03/30/67e8efed31dc2.png" alt="image-20250330151659001"></p><p>调进去之后我们可以看到这个RegistryImpl_Stub是一个Class文件，它是java1.1时候的东西，由于太古老没有源码文件，我们没办法在这里打断点调试，只能静态分析，可以看到这里调用了一个super.ref.newCall，实际上这里super.ref就是一个UniCastRef，进去看一下：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">Remote</span> <span class="title function_">lookup</span><span class="params">(String var1)</span> <span class="keyword">throws</span> AccessException, NotBoundException, RemoteException &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="type">RemoteCall</span> <span class="variable">var2</span> <span class="operator">=</span> <span class="built_in">super</span>.ref.newCall(<span class="built_in">this</span>, operations, <span class="number">2</span>, <span class="number">4905912898345647071L</span>);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="type">ObjectOutput</span> <span class="variable">var3</span> <span class="operator">=</span> var2.getOutputStream();</span><br><span class="line">            var3.writeObject(var1);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (IOException var18) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">MarshalException</span>(<span class="string">&quot;error marshalling arguments&quot;</span>, var18);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="built_in">super</span>.ref.invoke(var2);</span><br><span class="line"></span><br><span class="line">        Remote var23;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="type">ObjectInput</span> <span class="variable">var6</span> <span class="operator">=</span> var2.getInputStream();</span><br><span class="line">            var23 = (Remote)var6.readObject();</span><br><span class="line">        &#125; <span class="keyword">catch</span> (IOException var15) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;error unmarshalling return&quot;</span>, var15);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (ClassNotFoundException var16) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;error unmarshalling return&quot;</span>, var16);</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            <span class="built_in">super</span>.ref.done(var2);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> var23;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (RuntimeException var19) &#123;</span><br><span class="line">        <span class="keyword">throw</span> var19;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (RemoteException var20) &#123;</span><br><span class="line">        <span class="keyword">throw</span> var20;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (NotBoundException var21) &#123;</span><br><span class="line">        <span class="keyword">throw</span> var21;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Exception var22) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnexpectedException</span>(<span class="string">&quot;undeclared checked exception&quot;</span>, var22);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>这里会创建一个StreamRemoteCall对象，其中这个opnum代表的是当前的操作，比如2就代表了lookup操作：</p><p><img src="/images/i/2025/03/30/67e8f00102b97.png" alt="image-20250330151718903"></p><p>我们接着往下看，创建好这个RemoteCall之后lookup方法中会获取这个RemoteCall的输出流，并且写入一个var1，这个var1其实就是我们调用lookup时传入的name，所以其实在这里我们把相关信息以序列化流的方式要发给Registry，那么可以联想到在Registry端应该会有一个反序列化的地方将这些数据进行反序列化处理，这就可能导致一些安全问题：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="type">ObjectOutput</span> <span class="variable">var3</span> <span class="operator">=</span> var2.getOutputStream();</span><br><span class="line">    var3.writeObject(var1);</span><br><span class="line">&#125; <span class="keyword">catch</span> (IOException var18) &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">MarshalException</span>(<span class="string">&quot;error marshalling arguments&quot;</span>, var18);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>我们接着往下看，有一处地方会调用UnicastRef的invoke方法，我们继续跟进：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="built_in">super</span>.ref.invoke(var2);</span><br></code></pre></figure></div><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">invoke</span><span class="params">(RemoteCall call)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        clientRefLog.log(Log.VERBOSE, <span class="string">&quot;execute call&quot;</span>);</span><br><span class="line"></span><br><span class="line">        call.executeCall();</span><br></code></pre></figure></div><p>跟进call.executeCall，这个excuteCall其实就是我们客户端实现网络请求的地方了，我们主要看这里的一个异常处理，如果是这个类型的异常就会对一个输入流进行一个反序列化，所以如果这里客户端从Registry接收到的流是一个恶意的对象，并且触发了该异常，那么就会在客户端本地进行反序列化，导致客户端被攻击，并且我们说这里是通过调用excuteClaa方法进入的，而excuteCall方法又是通过调用了一个invoke触发的，那么其实这样就非常隐蔽，并且由于所有的Stub处理网络请求都要经过excuteCall，所以这里就会成为一个非常危险的点：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">case</span> TransportConstants.ExceptionalReturn:</span><br><span class="line">    Object ex;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        ex = in.readObject();</span><br></code></pre></figure></div><p>我们再回到RegistryImpl_Stub的lookup中，下面还有一个反序列化的点，所以其实很容易看的出来这里存在反序列化漏洞的问题，但在bind方法中虽然没有显式的调用readObject，但是会调用了invoke，所以还是会隐式的存在反序列化漏洞的问题：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="type">ObjectInput</span> <span class="variable">var6</span> <span class="operator">=</span> var2.getInputStream();</span><br><span class="line">    var23 = (Remote)var6.readObject();</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>那么做完这些动作后客户端就拿到了这个远程对象的引用代理，里面就存储了这个远程对象所在的端口：</p><p><img src="/images/i/2025/03/30/67e8f01060de4.png" alt="image-20250330151734282"></p><h2 id="客户端请求服务端-客户端行为"><a href="#客户端请求服务端-客户端行为" class="headerlink" title="客户端请求服务端-客户端行为"></a>客户端请求服务端-客户端行为</h2><p>刚才我们通过让客户端访问注册中心，拿到了远程对象所在的端口，那么接下来客户端就要通过这个端口来访问及调用远程对象的方法了，我们继续Debug：</p><p><img src="/images/i/2025/03/30/67e8f01f72b38.png" alt="image-20250330151749418"></p><p>步入之后走到了远程对象Stub的InvocationHandler的invoke中：</p><p><img src="/images/i/2025/03/30/67e8f03ba0702.png" alt="image-20250330151817102"></p><p>这里又调了一次UnicastRef的invoke方法，不过这个不是我们刚才说的那个invoke，而是重载的另外一个invoke方法：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">return</span> ref.invoke((Remote) proxy, method, args,</span><br><span class="line">                  getMethodHash(method));</span><br></code></pre></figure></div><p>然后我们继续调，发现又走到了这个executeCall方法中，所以我们上面说了不管是jdk给我们创建好的Stub（RegistryImpl_Stub）还是客户端自己接收到的远程对象的Stub，在通过Stub调用网络请求时都会走到这个方法中：</p><p><img src="/images/i/2025/03/30/67e8f051f0bc3.png" alt="image-20250330151839958"></p><p>经过这个方法后我们的远程方法就被调用了，然后可以看到下面有一个判断，如果该方法调用的返回值为void，那么直接return出去，但是我们的这个方法会返回一个值，所以会触发这个unmarshalValue方法：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">Class</span>&lt;?&gt; <span class="variable">rtype</span> <span class="operator">=</span> method.getReturnType();</span><br><span class="line"><span class="keyword">if</span> (rtype == <span class="keyword">void</span>.class)</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"><span class="type">ObjectInput</span> <span class="variable">in</span> <span class="operator">=</span> call.getInputStream();</span><br><span class="line"></span><br><span class="line"><span class="comment">/* StreamRemoteCall.done() does not actually make use</span></span><br><span class="line"><span class="comment"> * of conn, therefore it is safe to reuse this</span></span><br><span class="line"><span class="comment"> * connection before the dirty call is sent for</span></span><br><span class="line"><span class="comment"> * registered refs.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="type">Object</span> <span class="variable">returnValue</span> <span class="operator">=</span> unmarshalValue(rtype, in);</span><br></code></pre></figure></div><p>跟进到这个方法了里面可以看到会判断返回值的类型，然后通过对应的方式拿到返回值，可以发现如果不是基本类型就会通过反序列化拿到其中的值，那么这里又是一个客户端存在反序列化攻击的点：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">protected</span> <span class="keyword">static</span> <span class="type">Object</span> <span class="title function_">unmarshalValue</span><span class="params">(Class&lt;?&gt; type, ObjectInput in)</span></span><br><span class="line">    <span class="keyword">throws</span> IOException, ClassNotFoundException</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">if</span> (type.isPrimitive()) &#123;</span><br><span class="line">        <span class="keyword">if</span> (type == <span class="type">int</span>.class) &#123;</span><br><span class="line">            <span class="keyword">return</span> Integer.valueOf(in.readInt());</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (type == <span class="type">boolean</span>.class) &#123;</span><br><span class="line">            <span class="keyword">return</span> Boolean.valueOf(in.readBoolean());</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (type == <span class="type">byte</span>.class) &#123;</span><br><span class="line">            <span class="keyword">return</span> Byte.valueOf(in.readByte());</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (type == <span class="type">char</span>.class) &#123;</span><br><span class="line">            <span class="keyword">return</span> Character.valueOf(in.readChar());</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (type == <span class="type">short</span>.class) &#123;</span><br><span class="line">            <span class="keyword">return</span> Short.valueOf(in.readShort());</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (type == <span class="type">long</span>.class) &#123;</span><br><span class="line">            <span class="keyword">return</span> Long.valueOf(in.readLong());</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (type == <span class="type">float</span>.class) &#123;</span><br><span class="line">            <span class="keyword">return</span> Float.valueOf(in.readFloat());</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (type == <span class="type">double</span>.class) &#123;</span><br><span class="line">            <span class="keyword">return</span> Double.valueOf(in.readDouble());</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&quot;Unrecognized primitive type: &quot;</span> + type);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> in.readObject();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p><img src="/images/i/2025/03/30/67e8f0631bbb1.png" alt="image-20250330151856933"></p><p><img src="/images/i/2025/03/30/67e8f076d4c45.png" alt="image-20250330151916852"></p><p>程序结束，我们成功的将远程方法的返回值打印了出来：</p><p><img src="/images/i/2025/03/30/67e8f08ded9c8.png" alt="image-20250330151939545"></p><h2 id="客户端请求注册中心-注册中心行为"><a href="#客户端请求注册中心-注册中心行为" class="headerlink" title="客户端请求注册中心-注册中心行为"></a>客户端请求注册中心-注册中心行为</h2><p>接下来我们看客户端在请求注册中心时注册中心都干了哪些事情，我们在上面提到了客户端是通过拿到Registry的Stub后通过Stub与Registry的Skeleton进行通信，那么我们先来看一下在创建注册中心时走到Skeleton之前注册中心都干了什么，我们回到RegistryImpl的setup方法这里，一直跟进exportObject中，经过好几层exportObjec我们回到了这个最核心的listen方法：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">private</span> <span class="type">void</span> <span class="title function_">setup</span><span class="params">(UnicastServerRef uref)</span></span><br><span class="line">    <span class="keyword">throws</span> RemoteException</span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">/* Server ref must be created and assigned before remote</span></span><br><span class="line"><span class="comment">     * object &#x27;this&#x27; can be exported.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    ref = uref;</span><br><span class="line">    uref.exportObject(<span class="built_in">this</span>, <span class="literal">null</span>, <span class="literal">true</span>);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p><img src="/images/i/2025/03/30/67e8f0a1e932a.png" alt="image-20250330151959641"></p><p>在listen中我们之前提到了会开启一个新线程将我们的网络监听放进去，所以我们跟进到这个进程的创建中(AcceptLoop)：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">Thread</span> <span class="variable">t</span> <span class="operator">=</span> AccessController.doPrivileged(</span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">NewThreadAction</span>(<span class="keyword">new</span> <span class="title class_">AcceptLoop</span>(server),</span><br><span class="line">                        <span class="string">&quot;TCP Accept-&quot;</span> + port, <span class="literal">true</span>));</span><br></code></pre></figure></div><p>我们来看这个线程对象的run方法，其中调用了executeAcceptLoop，继续跟进：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        executeAcceptLoop();</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">/*</span></span><br><span class="line"><span class="comment">             * Only one accept loop is started per server</span></span><br><span class="line"><span class="comment">             * socket, so after no more connections will be</span></span><br><span class="line"><span class="comment">             * accepted, ensure that the server socket is no</span></span><br><span class="line"><span class="comment">             * longer listening.</span></span><br><span class="line"><span class="comment">             */</span></span><br><span class="line">            serverSocket.close();</span><br><span class="line">        &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>可以看到这里又创建了一个线程池并执行，所以我们继续跟进，查看这个线程的run方法：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">                  connectionThreadPool.execute(</span><br><span class="line">                      <span class="keyword">new</span> <span class="title class_">ConnectionHandler</span>(socket, clientHost));</span><br><span class="line">              &#125; <span class="keyword">catch</span> (RejectedExecutionException e) &#123;</span><br><span class="line">                  closeSocket(socket);</span><br><span class="line">                  tcpLog.log(Log.BRIEF,</span><br><span class="line">                             <span class="string">&quot;rejected connection from &quot;</span> + clientHost);</span><br><span class="line">              &#125;</span><br></code></pre></figure></div><p>这个run方法中调用了run0，跟进run0:</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">Thread</span> <span class="variable">t</span> <span class="operator">=</span> Thread.currentThread();</span><br><span class="line">    <span class="type">String</span> <span class="variable">name</span> <span class="operator">=</span> t.getName();</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        t.setName(<span class="string">&quot;RMI TCP Connection(&quot;</span> +</span><br><span class="line">                  connectionCount.incrementAndGet() +</span><br><span class="line">                  <span class="string">&quot;)-&quot;</span> + remoteHost);</span><br><span class="line">        AccessController.doPrivileged((PrivilegedAction&lt;Void&gt;)() -&gt; &#123;</span><br><span class="line">            run0();</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">        &#125;, NOPERMS_ACC);</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        t.setName(name);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>在run0中有一行主要代码handleMessage，这个方法会读取连接传入的信息，我们跟进：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">handleMessages(conn, <span class="literal">false</span>);</span><br></code></pre></figure></div><p>可以看到在这个方法中会对接收到的字段进行处理，比如op代表了要执行什么样的操作，这里可以看到第一种case是当请求到来时的默认操作，其中调用了ServiceCall，我们跟入ServiceCall方法看一下：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">void</span> <span class="title function_">handleMessages</span><span class="params">(Connection conn, <span class="type">boolean</span> persistent)</span> &#123;</span><br><span class="line">    <span class="type">int</span> <span class="variable">port</span> <span class="operator">=</span> getEndpoint().getPort();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="type">DataInputStream</span> <span class="variable">in</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DataInputStream</span>(conn.getInputStream());</span><br><span class="line">        <span class="keyword">do</span> &#123;</span><br><span class="line">            <span class="type">int</span> <span class="variable">op</span> <span class="operator">=</span> in.read();     <span class="comment">// transport op</span></span><br><span class="line">            <span class="keyword">if</span> (op == -<span class="number">1</span>) &#123;</span><br><span class="line">                <span class="keyword">if</span> (tcpLog.isLoggable(Log.BRIEF)) &#123;</span><br><span class="line">                    tcpLog.log(Log.BRIEF, <span class="string">&quot;(port &quot;</span> +</span><br><span class="line">                        port + <span class="string">&quot;) connection closed&quot;</span>);</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (tcpLog.isLoggable(Log.BRIEF)) &#123;</span><br><span class="line">                tcpLog.log(Log.BRIEF, <span class="string">&quot;(port &quot;</span> + port +</span><br><span class="line">                    <span class="string">&quot;) op = &quot;</span> + op);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">switch</span> (op) &#123;</span><br><span class="line">            <span class="keyword">case</span> TransportConstants.Call:</span><br><span class="line">                <span class="comment">// service incoming RMI call</span></span><br><span class="line">                <span class="type">RemoteCall</span> <span class="variable">call</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StreamRemoteCall</span>(conn);</span><br><span class="line">                <span class="keyword">if</span> (serviceCall(call) == <span class="literal">false</span>)</span><br><span class="line">                    <span class="keyword">return</span>;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">case</span> TransportConstants.Ping:</span><br><span class="line">                <span class="comment">// send ack for ping</span></span><br><span class="line">                <span class="type">DataOutputStream</span> <span class="variable">out</span> <span class="operator">=</span></span><br><span class="line">                    <span class="keyword">new</span> <span class="title class_">DataOutputStream</span>(conn.getOutputStream());</span><br><span class="line">                out.writeByte(TransportConstants.PingAck);</span><br><span class="line">                conn.releaseOutputStream();</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">case</span> TransportConstants.DGCAck:</span><br><span class="line">                DGCAckHandler.received(UID.read(in));</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">default</span>:</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IOException</span>(<span class="string">&quot;unknown transport op &quot;</span> + op);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">while</span> (persistent);</span><br><span class="line"></span><br><span class="line">    &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">        <span class="comment">// exception during processing causes connection to close (below)</span></span><br><span class="line">        <span class="keyword">if</span> (tcpLog.isLoggable(Log.BRIEF)) &#123;</span><br><span class="line">            tcpLog.log(Log.BRIEF, <span class="string">&quot;(port &quot;</span> + port +</span><br><span class="line">                <span class="string">&quot;) exception: &quot;</span>, e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            conn.close();</span><br><span class="line">        &#125; <span class="keyword">catch</span> (IOException ex) &#123;</span><br><span class="line">            <span class="comment">// eat exception</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>ServiceCall中会有对Target表取出的操作，我们之前在分析服务端创建时说过最后会讲通信用到的所有东西（尤其是Stub）放到这个Target对象中：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">Target</span> <span class="variable">target</span> <span class="operator">=</span></span><br><span class="line">    ObjectTable.getTarget(<span class="keyword">new</span> <span class="title class_">ObjectEndpoint</span>(id, transport));</span><br></code></pre></figure></div><p>那么我们此时在这里打断点，开启调试，客户端请求，成功的断到了这个地方，说明我们之前静态分析的过程都是正确的，接下来查看客户端在对注册中心发起一系列操作时注册中心的回应：</p><p><img src="/images/i/2025/03/30/67e8f0b0e1620.png" alt="image-20250330152014507"></p><p>我们接着往下看，拿到这个Target后会对这个Target调用getDispatcher，这个方法的返回值disp其实就是我们的UnicastServerRef：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">final</span> <span class="type">Dispatcher</span> <span class="variable">disp</span> <span class="operator">=</span> target.getDispatcher();</span><br></code></pre></figure></div><p><img src="/images/i/2025/03/30/67e8f0c0b72cd.png" alt="image-20250330152030596"></p><p>我们接着往下走，发现对这个disp调用了dispatch方法：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    java.security.AccessController.doPrivileged(</span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">java</span>.security.PrivilegedExceptionAction&lt;Void&gt;() &#123;</span><br><span class="line">        <span class="keyword">public</span> <span class="type">Void</span> <span class="title function_">run</span><span class="params">()</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">            checkAcceptPermission(acc);</span><br><span class="line">            disp.dispatch(impl, call);</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;, acc);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>跟进发现有一个判断条件，如果skel不为null就会调oldDispatch，我们此时的这个UniCastServer中skel是不为null的，所以会调用这个方法，继续跟进：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">                in = call.getInputStream();</span><br><span class="line">                num = in.readInt();</span><br><span class="line">                <span class="keyword">if</span> (num &gt;= <span class="number">0</span>) &#123;</span><br><span class="line">                    <span class="keyword">if</span> (skel != <span class="literal">null</span>) &#123;</span><br><span class="line">                        oldDispatch(obj, call, num);</span><br><span class="line">                        <span class="keyword">return</span>;</span><br><span class="line">                    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(</span><br><span class="line">                            <span class="string">&quot;skeleton class not found but required &quot;</span> +</span><br><span class="line">                            <span class="string">&quot;for client version&quot;</span>);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br></code></pre></figure></div><p>在oldDispatch方法中会调用到skel.dispatch这个方法：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">skel.dispatch(obj, call, op, hash);</span><br></code></pre></figure></div><p>那么这个时候我们又回到了RegistryImpl_Skel这个类中了，这个类还是不能调试，所以我们静态分析，这个var3其实就是我们在客户端提到的opnum，比如我们现在为case2，代表的就是要执行lookup操作：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">dispatch</span><span class="params">(Remote var1, RemoteCall var2, <span class="type">int</span> var3, <span class="type">long</span> var4)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">    <span class="keyword">if</span> (var4 != <span class="number">4905912898345647071L</span>) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">SkeletonMismatchException</span>(<span class="string">&quot;interface hash mismatch&quot;</span>);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="type">RegistryImpl</span> <span class="variable">var6</span> <span class="operator">=</span> (RegistryImpl)var1;</span><br><span class="line">        String var7;</span><br><span class="line">        Remote var8;</span><br><span class="line">        ObjectInput var10;</span><br><span class="line">        ObjectInput var11;</span><br><span class="line">        <span class="keyword">switch</span> (var3) &#123;</span><br><span class="line">            <span class="keyword">case</span> <span class="number">0</span>:</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    var11 = var2.getInputStream();</span><br><span class="line">                    var7 = (String)var11.readObject();</span><br><span class="line">                    var8 = (Remote)var11.readObject();</span><br><span class="line">                &#125; <span class="keyword">catch</span> (IOException var94) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;error unmarshalling arguments&quot;</span>, var94);</span><br><span class="line">                &#125; <span class="keyword">catch</span> (ClassNotFoundException var95) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;error unmarshalling arguments&quot;</span>, var95);</span><br><span class="line">                &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                    var2.releaseInputStream();</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                var6.bind(var7, var8);</span><br><span class="line"></span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    var2.getResultStream(<span class="literal">true</span>);</span><br><span class="line">                    <span class="keyword">break</span>;</span><br><span class="line">                &#125; <span class="keyword">catch</span> (IOException var93) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">MarshalException</span>(<span class="string">&quot;error marshalling return&quot;</span>, var93);</span><br><span class="line">                &#125;</span><br><span class="line">            <span class="keyword">case</span> <span class="number">1</span>:</span><br><span class="line">                var2.releaseInputStream();</span><br><span class="line">                <span class="type">String</span>[] <span class="variable">var97</span> <span class="operator">=</span> var6.list();</span><br><span class="line"></span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="type">ObjectOutput</span> <span class="variable">var98</span> <span class="operator">=</span> var2.getResultStream(<span class="literal">true</span>);</span><br><span class="line">                    var98.writeObject(var97);</span><br><span class="line">                    <span class="keyword">break</span>;</span><br><span class="line">                &#125; <span class="keyword">catch</span> (IOException var92) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">MarshalException</span>(<span class="string">&quot;error marshalling return&quot;</span>, var92);</span><br><span class="line">                &#125;</span><br><span class="line">            <span class="keyword">case</span> <span class="number">2</span>:</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    var10 = var2.getInputStream();</span><br><span class="line">                    var7 = (String)var10.readObject();</span><br><span class="line">                &#125; <span class="keyword">catch</span> (IOException var89) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;error unmarshalling arguments&quot;</span>, var89);</span><br><span class="line">                &#125; <span class="keyword">catch</span> (ClassNotFoundException var90) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;error unmarshalling arguments&quot;</span>, var90);</span><br><span class="line">                &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                    var2.releaseInputStream();</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                var8 = var6.lookup(var7);</span><br><span class="line"></span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="type">ObjectOutput</span> <span class="variable">var9</span> <span class="operator">=</span> var2.getResultStream(<span class="literal">true</span>);</span><br><span class="line">                    var9.writeObject(var8);</span><br><span class="line">                    <span class="keyword">break</span>;</span><br><span class="line">                &#125; <span class="keyword">catch</span> (IOException var88) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">MarshalException</span>(<span class="string">&quot;error marshalling return&quot;</span>, var88);</span><br><span class="line">                &#125;</span><br><span class="line">            <span class="keyword">case</span> <span class="number">3</span>:</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    var11 = var2.getInputStream();</span><br><span class="line">                    var7 = (String)var11.readObject();</span><br><span class="line">                    var8 = (Remote)var11.readObject();</span><br><span class="line">                &#125; <span class="keyword">catch</span> (IOException var85) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;error unmarshalling arguments&quot;</span>, var85);</span><br><span class="line">                &#125; <span class="keyword">catch</span> (ClassNotFoundException var86) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;error unmarshalling arguments&quot;</span>, var86);</span><br><span class="line">                &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                    var2.releaseInputStream();</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                var6.rebind(var7, var8);</span><br><span class="line"></span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    var2.getResultStream(<span class="literal">true</span>);</span><br><span class="line">                    <span class="keyword">break</span>;</span><br><span class="line">                &#125; <span class="keyword">catch</span> (IOException var84) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">MarshalException</span>(<span class="string">&quot;error marshalling return&quot;</span>, var84);</span><br><span class="line">                &#125;</span><br><span class="line">            <span class="keyword">case</span> <span class="number">4</span>:</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    var10 = var2.getInputStream();</span><br><span class="line">                    var7 = (String)var10.readObject();</span><br><span class="line">                &#125; <span class="keyword">catch</span> (IOException var81) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;error unmarshalling arguments&quot;</span>, var81);</span><br><span class="line">                &#125; <span class="keyword">catch</span> (ClassNotFoundException var82) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;error unmarshalling arguments&quot;</span>, var82);</span><br><span class="line">                &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                    var2.releaseInputStream();</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                var6.unbind(var7);</span><br><span class="line"></span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    var2.getResultStream(<span class="literal">true</span>);</span><br><span class="line">                    <span class="keyword">break</span>;</span><br><span class="line">                &#125; <span class="keyword">catch</span> (IOException var80) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">MarshalException</span>(<span class="string">&quot;error marshalling return&quot;</span>, var80);</span><br><span class="line">                &#125;</span><br><span class="line">            <span class="keyword">default</span>:</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;invalid method number&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>并且可以看到在case2中，会对我们要lookup的远程对象直接进行一次反序列化，从这次反序列化中就可以读到要lookup的对象的名称以及其它等信息，所以这里就是注册中心可能会受到攻击的一个点，在其他的case操作中可以发现也会存在这种readObject的操作，所以客户端攻击注册中心的方法也非常多，那么到这了调用了RegistryImpl的lookup方法后就结束了：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">var10 = var2.getInputStream();</span><br><span class="line">    var7 = (String)var10.readObject();</span><br><span class="line">&#125; <span class="keyword">catch</span> (IOException var89) &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;error unmarshalling arguments&quot;</span>, var89);</span><br><span class="line">&#125; <span class="keyword">catch</span> (ClassNotFoundException var90) &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;error unmarshalling arguments&quot;</span>, var90);</span><br><span class="line">&#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    var2.releaseInputStream();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">var8 = var6.lookup(var7);</span><br></code></pre></figure></div><h2 id="客户端请求服务端-服务端行为"><a href="#客户端请求服务端-服务端行为" class="headerlink" title="客户端请求服务端-服务端行为"></a>客户端请求服务端-服务端行为</h2><p>我们把断点断在这里：</p><p><img src="/images/i/2025/03/30/67e8f0cd17f05.png" alt="image-20250330152042929"></p><p>开始调试，和注册中心一样，首先会走到ServiceCall当中：</p><p><img src="/images/i/2025/03/30/67e8f0e087701.png" alt="image-20250330152102168"></p><p>我们看一下当前的target是什么，可以发现里面的Stub是一个动态代理，这个Target就是存放了我们远程对象相关的Target了：</p><p><img src="/images/i/2025/03/30/67e8f0f2729e2.png" alt="image-20250330152120333"></p><p>继续往下调，还是走到了dispatch这里跟进：</p><p><img src="/images/i/2025/03/30/67e8f100abdfe.png" alt="image-20250330152134405"></p><p>与注册中心不同的是这里target中的skel为空，所以没有调用oldDispatch，走到了下面，这里就获取到了我们要调用的远程方法：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">Method</span> <span class="variable">method</span> <span class="operator">=</span> hashToMethod_Map.get(op);</span><br><span class="line"><span class="keyword">if</span> (method == <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;unrecognized method hash: &quot;</span> +</span><br><span class="line">        <span class="string">&quot;method not supported by remote object&quot;</span>);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>之后走到这里，会对要远程调用的方法内的参数进行检测，我们调用的是无参方法，所以不会进入到这里面：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    unmarshalCustomCallData(in);</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; types.length; i++) &#123;</span><br><span class="line">        params[i] = unmarshalValue(types[i], in);</span><br><span class="line">    &#125;</span><br><span class="line">&#125; <span class="keyword">catch</span> (java.io.IOException e) &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(</span><br><span class="line">        <span class="string">&quot;error unmarshalling arguments&quot;</span>, e);</span><br><span class="line">&#125; <span class="keyword">catch</span> (ClassNotFoundException e) &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(</span><br><span class="line">        <span class="string">&quot;error unmarshalling arguments&quot;</span>, e);</span><br><span class="line">&#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    call.releaseInputStream();</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>之后走到这里，就执行到了真正的方法调用，可以发现是通过反射来调用的：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    result = method.invoke(obj, params);</span><br><span class="line">&#125; <span class="keyword">catch</span> (InvocationTargetException e) &#123;</span><br><span class="line">    <span class="keyword">throw</span> e.getTargetException();</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>后面又是走到了这里，不过不同的是这次是将方法的值进行序列化，那么客户端就可以接收到这次序列化后的返回值在进行反序列化后使用了：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">  <span class="keyword">if</span> (rtype != <span class="keyword">void</span>.class) &#123;</span><br><span class="line">        marshalValue(rtype, result, out);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p><img src="/images/i/2025/03/30/67e8f10ec9ae1.png" alt="image-20250330152148459"></p><p>服务端响应结束：</p><p><img src="/images/i/2025/03/30/67e8f121ecc83.png" alt="image-20250330152208272"></p><h2 id="客户端请求服务端-DGC"><a href="#客户端请求服务端-DGC" class="headerlink" title="客户端请求服务端-DGC"></a>客户端请求服务端-DGC</h2><p>在之前我们查看远程对象的创建时，会发现最终创建好的远程对象会封装到一个Target当中，再通过putTarget方法放入到一张静态表里，并且在调试期间，我们会发现最先生成的一个Target对象，其中的dist是一个叫做DGCImpl的Target，这个DGC其实就是RMI在使用过程中创建出来的分布式垃圾回收，也就是说在RMI中，我们最终的对象回收都是由RMI来实现的。</p><p>那么我们就看一下DGC创建时的方式，在调用到putTarget方法时，可以发现这里会走到一个DGCImpl.dgcLog上，这个dgcLog是DGCImpl中的一个静态属性，在Java中当调用一个类的静态属性时会触发该类的初始化，也就是说会调用这个类的静态代码块：</p><p><img src="/images/i/2025/03/30/67e8f1348edbd.png" alt="image-20250330152226206"></p><p>我们跟进到DGCImpl的静态代码块中，发现其中会创建一个DGCImpl的实例，所以其实在上面的if判断条件后，DGCImpl的实例就被创建出来了，并且可以发现静态代码块中也调用了createProxy，说明DGC也是通过创建一个代理对象来给客户端进行使用：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line">AccessController.doPrivileged(<span class="keyword">new</span> <span class="title class_">PrivilegedAction</span>&lt;Void&gt;() &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="type">Void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">ClassLoader</span> <span class="variable">savedCcl</span> <span class="operator">=</span></span><br><span class="line">            Thread.currentThread().getContextClassLoader();</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            Thread.currentThread().setContextClassLoader(</span><br><span class="line">                ClassLoader.getSystemClassLoader());</span><br><span class="line"></span><br><span class="line">            <span class="comment">/*</span></span><br><span class="line"><span class="comment">             * Put remote collector object in table by hand to prevent</span></span><br><span class="line"><span class="comment">             * listen on port.  (UnicastServerRef.exportObject would</span></span><br><span class="line"><span class="comment">             * cause transport to listen.)</span></span><br><span class="line"><span class="comment">             */</span></span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                dgc = <span class="keyword">new</span> <span class="title class_">DGCImpl</span>();</span><br><span class="line">                <span class="type">ObjID</span> <span class="variable">dgcID</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ObjID</span>(ObjID.DGC_ID);</span><br><span class="line">                <span class="type">LiveRef</span> <span class="variable">ref</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">LiveRef</span>(dgcID, <span class="number">0</span>);</span><br><span class="line">                <span class="type">UnicastServerRef</span> <span class="variable">disp</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">UnicastServerRef</span>(ref);</span><br><span class="line">                <span class="type">Remote</span> <span class="variable">stub</span> <span class="operator">=</span></span><br><span class="line">                    Util.createProxy(DGCImpl.class,</span><br><span class="line">                                     <span class="keyword">new</span> <span class="title class_">UnicastRef</span>(ref), <span class="literal">true</span>);</span><br><span class="line">                disp.setSkeleton(dgc);</span><br><span class="line"></span><br><span class="line">                <span class="type">Permissions</span> <span class="variable">perms</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Permissions</span>();</span><br><span class="line">                perms.add(<span class="keyword">new</span> <span class="title class_">SocketPermission</span>(<span class="string">&quot;*&quot;</span>, <span class="string">&quot;accept,resolve&quot;</span>));</span><br><span class="line">                <span class="type">ProtectionDomain</span>[] <span class="variable">pd</span> <span class="operator">=</span> &#123; <span class="keyword">new</span> <span class="title class_">ProtectionDomain</span>(<span class="literal">null</span>, perms) &#125;;</span><br><span class="line">                <span class="type">AccessControlContext</span> <span class="variable">acceptAcc</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">AccessControlContext</span>(pd);</span><br><span class="line"></span><br><span class="line">                <span class="type">Target</span> <span class="variable">target</span> <span class="operator">=</span> AccessController.doPrivileged(</span><br><span class="line">                    <span class="keyword">new</span> <span class="title class_">PrivilegedAction</span>&lt;Target&gt;() &#123;</span><br><span class="line">                        <span class="keyword">public</span> <span class="type">Target</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">                            <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Target</span>(dgc, disp, stub, dgcID, <span class="literal">true</span>);</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;, acceptAcc);</span><br><span class="line"></span><br><span class="line">                ObjectTable.putTarget(target);</span><br><span class="line">            &#125;</span><br></code></pre></figure></div><p>我们跟进到这个createProxy当中查看，可以发现还是一样的逻辑，继续跟进createStub：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="keyword">if</span> (forceStubUse ||</span><br><span class="line">    !(ignoreStubClasses || !stubClassExists(remoteClass)))</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">return</span> createStub(remoteClass, clientRef);</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>还是判断有没有DGCImpl_Stub类，发现是存在的，在class sun.rmi.transport.DGCImpl，所以直接通过反射实例化出该Stub实例，逻辑上和远程对象创建基本一样，到这里DGC的创建就结束了：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">String</span> <span class="variable">stubname</span> <span class="operator">=</span> remoteClass.getName() + <span class="string">&quot;_Stub&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Make sure to use the local stub loader for the stub classes.</span></span><br><span class="line"><span class="comment"> * When loaded by the local loader the load path can be</span></span><br><span class="line"><span class="comment"> * propagated to remote clients, by the MarshalOutputStream/InStream</span></span><br><span class="line"><span class="comment"> * pickle methods</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="type">Class</span>&lt;?&gt; <span class="variable">stubcl</span> <span class="operator">=</span></span><br><span class="line">        Class.forName(stubname, <span class="literal">false</span>, remoteClass.getClassLoader());</span><br><span class="line">    <span class="type">Constructor</span>&lt;?&gt; <span class="variable">cons</span> <span class="operator">=</span> stubcl.getConstructor(stubConsParamTypes);</span><br><span class="line">    <span class="keyword">return</span> (RemoteStub) cons.newInstance(<span class="keyword">new</span> <span class="title class_">Object</span>[] &#123; ref &#125;);</span><br></code></pre></figure></div><blockquote><p>当客户机创建（序列化）远程引用时，会在服务器端 DGC 上调用 dirty()。当客户机完成远程引用后，它会调用对应的clean() 方法。<br>针对远程对象的引用由持有该引用的客户机租用一段时间。租期从收到 dirty()调用开始。在此类租约到期之前，客户机必须通过对远程引用额外调用 dirty()    来更新租约。如果客户机不在租约到期前进行续签，那么分布式垃圾收集器会假设客户机不再引用远程对象。</p></blockquote><p>那么我们接着来看一下DGCImpl_Stub中的代码逻辑，发现有一个clean方法和一个dirty方法：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="type">public</span> <span class="title function_">DGCImpl_Stub</span><span class="params">(RemoteRef var1)</span> &#123;</span><br><span class="line">    <span class="built_in">super</span>(var1);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="title function_">clean</span><span class="params">(ObjID[] var1, <span class="type">long</span> var2, VMID var4, <span class="type">boolean</span> var5)</span> <span class="keyword">throws</span> RemoteException &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="type">RemoteCall</span> <span class="variable">var6</span> <span class="operator">=</span> <span class="built_in">super</span>.ref.newCall(<span class="built_in">this</span>, operations, <span class="number">0</span>, -<span class="number">669196253586618813L</span>);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="type">ObjectOutput</span> <span class="variable">var7</span> <span class="operator">=</span> var6.getOutputStream();</span><br><span class="line">            var7.writeObject(var1);</span><br><span class="line">            var7.writeLong(var2);</span><br><span class="line">            var7.writeObject(var4);</span><br><span class="line">            var7.writeBoolean(var5);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (IOException var8) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">MarshalException</span>(<span class="string">&quot;error marshalling arguments&quot;</span>, var8);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="built_in">super</span>.ref.invoke(var6);</span><br><span class="line">        <span class="built_in">super</span>.ref.done(var6);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (RuntimeException var9) &#123;</span><br><span class="line">        <span class="keyword">throw</span> var9;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (RemoteException var10) &#123;</span><br><span class="line">        <span class="keyword">throw</span> var10;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Exception var11) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnexpectedException</span>(<span class="string">&quot;undeclared checked exception&quot;</span>, var11);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="type">Lease</span> <span class="title function_">dirty</span><span class="params">(ObjID[] var1, <span class="type">long</span> var2, Lease var4)</span> <span class="keyword">throws</span> RemoteException &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="type">RemoteCall</span> <span class="variable">var5</span> <span class="operator">=</span> <span class="built_in">super</span>.ref.newCall(<span class="built_in">this</span>, operations, <span class="number">1</span>, -<span class="number">669196253586618813L</span>);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="type">ObjectOutput</span> <span class="variable">var6</span> <span class="operator">=</span> var5.getOutputStream();</span><br><span class="line">            var6.writeObject(var1);</span><br><span class="line">            var6.writeLong(var2);</span><br><span class="line">            var6.writeObject(var4);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (IOException var20) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">MarshalException</span>(<span class="string">&quot;error marshalling arguments&quot;</span>, var20);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="built_in">super</span>.ref.invoke(var5);</span><br><span class="line"></span><br><span class="line">        Lease var24;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="type">ObjectInput</span> <span class="variable">var9</span> <span class="operator">=</span> var5.getInputStream();</span><br><span class="line">            var24 = (Lease)var9.readObject();</span><br><span class="line">        &#125; <span class="keyword">catch</span> (IOException var17) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;error unmarshalling return&quot;</span>, var17);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (ClassNotFoundException var18) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;error unmarshalling return&quot;</span>, var18);</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            <span class="built_in">super</span>.ref.done(var5);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> var24;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (RuntimeException var21) &#123;</span><br><span class="line">        <span class="keyword">throw</span> var21;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (RemoteException var22) &#123;</span><br><span class="line">        <span class="keyword">throw</span> var22;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Exception var23) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnexpectedException</span>(<span class="string">&quot;undeclared checked exception&quot;</span>, var23);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div><p>再看一下DGCImpl_Skel，其中有两个case，其实代表的就是Stub传递过来的clean操作还是dirty操作，可以发现不管是在stub还是skel中都存在很多的反序列化点，所以DGC也是很容易被攻击的一个地方：</p><div class="p5-codebox" data-lang="java"><figure class="highlight"><pre class="highlight-code"><code><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// Source code recreated from a .class file by IntelliJ IDEA</span></span><br><span class="line"><span class="comment">// (powered by FernFlower decompiler)</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">package</span> sun.rmi.transport;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.io.IOException;</span><br><span class="line"><span class="keyword">import</span> java.io.ObjectInput;</span><br><span class="line"><span class="keyword">import</span> java.io.ObjectOutput;</span><br><span class="line"><span class="keyword">import</span> java.rmi.MarshalException;</span><br><span class="line"><span class="keyword">import</span> java.rmi.Remote;</span><br><span class="line"><span class="keyword">import</span> java.rmi.UnmarshalException;</span><br><span class="line"><span class="keyword">import</span> java.rmi.dgc.Lease;</span><br><span class="line"><span class="keyword">import</span> java.rmi.dgc.VMID;</span><br><span class="line"><span class="keyword">import</span> java.rmi.server.ObjID;</span><br><span class="line"><span class="keyword">import</span> java.rmi.server.Operation;</span><br><span class="line"><span class="keyword">import</span> java.rmi.server.RemoteCall;</span><br><span class="line"><span class="keyword">import</span> java.rmi.server.Skeleton;</span><br><span class="line"><span class="keyword">import</span> java.rmi.server.SkeletonMismatchException;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">DGCImpl_Skel</span> <span class="keyword">implements</span> <span class="title class_">Skeleton</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Operation</span>[] <span class="variable">operations</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Operation</span>[]&#123;<span class="keyword">new</span> <span class="title class_">Operation</span>(<span class="string">&quot;void clean(java.rmi.server.ObjID[], long, java.rmi.dgc.VMID, boolean)&quot;</span>), <span class="keyword">new</span> <span class="title class_">Operation</span>(<span class="string">&quot;java.rmi.dgc.Lease dirty(java.rmi.server.ObjID[], long, java.rmi.dgc.Lease)&quot;</span>)&#125;;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">long</span> <span class="variable">interfaceHash</span> <span class="operator">=</span> -<span class="number">669196253586618813L</span>;</span><br><span class="line"></span><br><span class="line">    <span class="type">public</span> <span class="title function_">DGCImpl_Skel</span><span class="params">()</span> &#123;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="type">void</span> <span class="title function_">dispatch</span><span class="params">(Remote var1, RemoteCall var2, <span class="type">int</span> var3, <span class="type">long</span> var4)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="keyword">if</span> (var4 != -<span class="number">669196253586618813L</span>) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">SkeletonMismatchException</span>(<span class="string">&quot;interface hash mismatch&quot;</span>);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="type">DGCImpl</span> <span class="variable">var6</span> <span class="operator">=</span> (DGCImpl)var1;</span><br><span class="line">            ObjID[] var7;</span><br><span class="line">            <span class="type">long</span> var8;</span><br><span class="line">            <span class="keyword">switch</span> (var3) &#123;</span><br><span class="line">                <span class="keyword">case</span> <span class="number">0</span>:</span><br><span class="line">                    VMID var39;</span><br><span class="line">                    <span class="type">boolean</span> var40;</span><br><span class="line">                    <span class="keyword">try</span> &#123;</span><br><span class="line">                        <span class="type">ObjectInput</span> <span class="variable">var14</span> <span class="operator">=</span> var2.getInputStream();</span><br><span class="line">                        var7 = (ObjID[])var14.readObject();</span><br><span class="line">                        var8 = var14.readLong();</span><br><span class="line">                        var39 = (VMID)var14.readObject();</span><br><span class="line">                        var40 = var14.readBoolean();</span><br><span class="line">                    &#125; <span class="keyword">catch</span> (IOException var36) &#123;</span><br><span class="line">                        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;error unmarshalling arguments&quot;</span>, var36);</span><br><span class="line">                    &#125; <span class="keyword">catch</span> (ClassNotFoundException var37) &#123;</span><br><span class="line">                        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;error unmarshalling arguments&quot;</span>, var37);</span><br><span class="line">                    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                        var2.releaseInputStream();</span><br><span class="line">                    &#125;</span><br><span class="line"></span><br><span class="line">                    var6.clean(var7, var8, var39, var40);</span><br><span class="line"></span><br><span class="line">                    <span class="keyword">try</span> &#123;</span><br><span class="line">                        var2.getResultStream(<span class="literal">true</span>);</span><br><span class="line">                        <span class="keyword">break</span>;</span><br><span class="line">                    &#125; <span class="keyword">catch</span> (IOException var35) &#123;</span><br><span class="line">                        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">MarshalException</span>(<span class="string">&quot;error marshalling return&quot;</span>, var35);</span><br><span class="line">                    &#125;</span><br><span class="line">                <span class="keyword">case</span> <span class="number">1</span>:</span><br><span class="line">                    Lease var10;</span><br><span class="line">                    <span class="keyword">try</span> &#123;</span><br><span class="line">                        <span class="type">ObjectInput</span> <span class="variable">var13</span> <span class="operator">=</span> var2.getInputStream();</span><br><span class="line">                        var7 = (ObjID[])var13.readObject();</span><br><span class="line">                        var8 = var13.readLong();</span><br><span class="line">                        var10 = (Lease)var13.readObject();</span><br><span class="line">                    &#125; <span class="keyword">catch</span> (IOException var32) &#123;</span><br><span class="line">                        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;error unmarshalling arguments&quot;</span>, var32);</span><br><span class="line">                    &#125; <span class="keyword">catch</span> (ClassNotFoundException var33) &#123;</span><br><span class="line">                        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;error unmarshalling arguments&quot;</span>, var33);</span><br><span class="line">                    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                        var2.releaseInputStream();</span><br><span class="line">                    &#125;</span><br><span class="line"></span><br><span class="line">                    <span class="type">Lease</span> <span class="variable">var11</span> <span class="operator">=</span> var6.dirty(var7, var8, var10);</span><br><span class="line"></span><br><span class="line">                    <span class="keyword">try</span> &#123;</span><br><span class="line">                        <span class="type">ObjectOutput</span> <span class="variable">var12</span> <span class="operator">=</span> var2.getResultStream(<span class="literal">true</span>);</span><br><span class="line">                        var12.writeObject(var11);</span><br><span class="line">                        <span class="keyword">break</span>;</span><br><span class="line">                    &#125; <span class="keyword">catch</span> (IOException var31) &#123;</span><br><span class="line">                        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">MarshalException</span>(<span class="string">&quot;error marshalling return&quot;</span>, var31);</span><br><span class="line">                    &#125;</span><br><span class="line">                <span class="keyword">default</span>:</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnmarshalException</span>(<span class="string">&quot;invalid method number&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="type">Operation</span>[] <span class="title function_">getOperations</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> (Operation[])operations.clone();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></code></pre></figure></div>]]>
    </content>
    <id>https://www.zhajiangmian.top/2024/02/26/RMI%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90/</id>
    <link href="https://www.zhajiangmian.top/2024/02/26/RMI%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90/"/>
    <published>2024-02-26T04:42:21.000Z</published>
    <summary>
      <![CDATA[<h1 id="RMI调用流程分析"><a href="#RMI调用流程分析" class="headerlink" title="RMI调用流程分析"></a>RMI调用流程分析</h1><h2 id="RMI"><a href="#RMI" class="headerlink" title="RMI"></a>RMI</h2><blockquote>
<p>以下介绍内容来自<a href="https://www.w3cschool.cn/article/30445887.html">Java的RMI介绍及使用方法详解 | w3cschool笔记</a></p>
</blockquote>
<p>现在有一个需求，我们需要在一台主机上调用另一台主机上的Java代码，这个时候就要用到RMI(Remote Method Invocation)。</p>
<p>使用 RMI 技术可以使一个 JVM 中的对象，调用另一个 JVM 中的对象方法并获取调用结果。这里的另一个 JVM 可以在同一台计算机也可以是远程计算机。因此，RMI 意味着需要一个 Server 端和一个 Client 端。Server 端通常会创建一个对象，并使之可以被远程访问。</p>
<p>这个对象被称为远程对象。Server 端需要注册这个对象可以被 Client 远程访问。</p>
<p>Client 端调用可以被远程访问的对象上的方法，Client 端就可以和 Server 端进行通信并相互传递信息。</p>
<p>说到这里，是不是发现使用 RMI 在构建一个分布式应用时十分方便，它和 RPC 一样可以实现分布式应用之间的互相通信，甚至和现在的微服务思想都十分类似。</p>]]>
    </summary>
    <title>RMI调用流程分析</title>
    <updated>2026-10-30T10:17:05.604Z</updated>
  </entry>
</feed>
