<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Bob&#39;s Blog of Nonsense</title>
  
  
  <link href="https://blog.bobh0724.net/atom.xml" rel="self"/>
  
  <link href="https://blog.bobh0724.net/"/>
  <updated>2026-06-17T06:04:00.154Z</updated>
  <id>https://blog.bobh0724.net/</id>
  
  <author>
    <name>Bob Huang</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>记一次逆向 Wii 游戏</title>
    <link href="https://blog.bobh0724.net/2025/08/25/reverse-engineer-puyo20-wii/"/>
    <id>https://blog.bobh0724.net/2025/08/25/reverse-engineer-puyo20-wii/</id>
    <published>2025-08-25T10:21:49.000Z</published>
    <updated>2026-06-17T06:04:00.154Z</updated>
    
    <content type="html"><![CDATA[<h1 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h1><p>直接原因是我看到了今年 PSP Homebrew Developer Conference 上 Precise Museum 做的<a href="https://youtu.be/xr0QegPt7X4?t=584">报告</a>，其中提到他们刚刚发布的《噗哟噗哟20周年纪念版》PSP版本的英化补丁在制作过程中遇到的一个问题：<strong>如果一个字库文件中的非全宽字符（即宽度不等于文件头中描述的最大宽度的字符）出现在字库的第一行以外的行，就会使这个字符无法正常显示。</strong>他们后来通过缩小字体的大小、重新排列字符使非全宽字符全都出现在第一行绕开了这个问题。</p><p>他们提到这件事引起了我的兴趣，因为我在 5 年前开始研究这个游戏的 Wii 版本的汉化的时候就遇到过这个问题，不过中文字符都是方形，简单重排一下字库文件，把少数的非方形字符放到前面很容易解决这个问题。不过后来噗哟群没找到人翻译，汉化版的工作就搁置了。</p><p>我电脑上正好已经装了 Ghidra 以及用于解析 Wii 可执行文件格式的插件，于是决定试试找出这个问题的根源。我之前的逆向经验仅有两个毫无难度的 Unity 游戏（一点混淆没有，符号表全在游戏里），因此这次稍微攒点逆向经验（真能攒到吗）。</p><h1 id="背景：关于文件格式"><a href="#背景：关于文件格式" class="headerlink" title="背景：关于文件格式"></a>背景：关于文件格式</h1><p>这里需要先介绍一下这个游戏使用的字库和文本的文件格式。英文社区已经有人写了用于编辑这个游戏的大部分文件的工具（<a href="https://github.com/nickworonekin/puyotools">Puyo tools</a> 和 <a href="https://github.com/nickworonekin/puyo-text-editor">Puyo text editor</a>），从这些工具的源码中可以了解到文件格式的信息。下面我借用上文提到的报告视频中的示意图来介绍。</p><p><img src="/2025/08/25/reverse-engineer-puyo20-wii/fnt_file.png" alt="fnt file structure"></p><p>上图展示的是字库文件，即 <code>FNT</code> 文件的格式。文件头中给出了字库中所含字符的尺寸和个数，随后以列表的形式给出了每个字符的 UTF-16 编码和显示宽度。字符表后面直接接一个材质（贴图）文件（PSP版中为 <code>GIM</code>， Wii版中为 <code>GVR</code>）。值得注意的是，虽然字符表中规定了每个字符的宽度，但是这个宽度只用于显示字符串时确定下一个字的水平位置。在材质文件中，字符仍然以文件头中的尺寸整齐地排列。</p><p>每个 <code>FNT</code> 文件会配套一个 <code>MTX</code> 文件，里面存储游戏中用到的各种字符串。每个字符占 2 个字节，表示这个字符在 <code>FNT</code> 文件中的 index 。比如在图中的字库中，<code>い</code>的编码就是<code>0000</code>。</p><h1 id="正文"><a href="#正文" class="headerlink" title="正文"></a>正文</h1><h2 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h2><p>为了能解析 Wii 的可执行文件格式（<code>dol</code>），需要给 Ghidra 安装相应的插件。插件可以在<a href="https://github.com/Cuyler36/Ghidra-GameCube-Loader">此处</a>下载。随后将游戏镜像中的可执行文件 <code>main.dol</code> 提取出来（Dolphin 模拟器就可以），加载进 Ghidra 里就可以开始分析了。</p><h2 id="定位相关逻辑"><a href="#定位相关逻辑" class="headerlink" title="定位相关逻辑"></a>定位相关逻辑</h2><p>可执行文件里显然没有任何符号表和调试信息。想要直接找到处理 FNT 文件的函数就像是大海捞针。十分幸运地是，通过在程序中搜索<code>FNT</code>字符串，我发现了可执行文件中“嵌入”了一对FNT文件和MTX文件，用于在游戏刚启动时显示创建存档等提示信息和错误信息。</p><p><img src="/2025/08/25/reverse-engineer-puyo20-wii/embedded_fnt.png" alt="embedded fnt file"></p><p>直接跟随这里的XREF可以见到这样的函数（函数名是我改的）：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><span class="line">undefined4 * <span class="title function_">load_embed_text</span><span class="params">(undefined4 *param_1)</span></span><br><span class="line">&#123;</span><br><span class="line">  uint size;</span><br><span class="line">  uint size_00;</span><br><span class="line">  undefined *puVar1;</span><br><span class="line">  </span><br><span class="line">  *param_1 = <span class="number">0</span>;</span><br><span class="line">  param_1[<span class="number">4</span>] = <span class="number">0</span>;</span><br><span class="line">  param_1[<span class="number">5</span>] = <span class="number">0</span>;</span><br><span class="line">  param_1[<span class="number">6</span>] = <span class="number">0</span>;</span><br><span class="line">  param_1[<span class="number">7</span>] = <span class="number">0</span>;</span><br><span class="line">  param_1[<span class="number">8</span>] = <span class="number">0</span>;</span><br><span class="line">  param_1[<span class="number">9</span>] = <span class="number">3</span>;</span><br><span class="line">  param_1[<span class="number">10</span>] = <span class="number">0</span>;</span><br><span class="line">  param_1[<span class="number">0xb</span>] = <span class="number">0</span>;</span><br><span class="line">  param_1[<span class="number">0xc</span>] = <span class="number">0</span>;</span><br><span class="line">  param_1[<span class="number">0xd</span>] = <span class="number">0xffffffff</span>;</span><br><span class="line">  *(undefined2 *)(param_1 + <span class="number">0xe</span>) = <span class="number">0</span>;</span><br><span class="line">  puVar1 = malloc_simple(<span class="number">0x8c</span>);</span><br><span class="line">  <span class="keyword">if</span> (puVar1 != (undefined *)<span class="number">0x0</span>) &#123;</span><br><span class="line">    FUN_800305e4(puVar1,<span class="number">0</span>,<span class="number">1</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  size_00 = x402c8;</span><br><span class="line">  *param_1 = puVar1;</span><br><span class="line">  puVar1 = <span class="built_in">malloc</span>(size_00,<span class="number">0x20</span>,<span class="number">0x20</span>,<span class="number">0</span>);</span><br><span class="line">  size = DAT_803606ec;</span><br><span class="line">  param_1[<span class="number">4</span>] = puVar1;</span><br><span class="line">  puVar1 = <span class="built_in">malloc</span>(size,<span class="number">0x20</span>,<span class="number">0x20</span>,<span class="number">0</span>);</span><br><span class="line">  param_1[<span class="number">5</span>] = puVar1;</span><br><span class="line">  memmove((undefined8 *)param_1[<span class="number">4</span>],(undefined8 *)ptr2fnt,size_00);</span><br><span class="line">  memmove((undefined8 *)param_1[<span class="number">5</span>],(undefined8 *)ptr2mtx,size);</span><br><span class="line">  puVar1 = malloc_simple(<span class="number">0x34</span>);</span><br><span class="line">  <span class="keyword">if</span> (puVar1 != (undefined *)<span class="number">0x0</span>) &#123;</span><br><span class="line">    FUN_800315d8(puVar1,(undefined *)*param_1,(undefined *)param_1[<span class="number">4</span>],size_00,<span class="number">1</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  FUN_800308e4(*param_1,puVar1,<span class="number">0</span>);</span><br><span class="line">  puVar1 = malloc_simple(<span class="number">0x28</span>);</span><br><span class="line">  <span class="keyword">if</span> (puVar1 != (undefined *)<span class="number">0x0</span>) &#123;</span><br><span class="line">    FUN_80031abc(puVar1,*param_1,param_1[<span class="number">5</span>],size);</span><br><span class="line">  &#125;</span><br><span class="line">  FUN_80030a54(*param_1,puVar1);</span><br><span class="line">  FUN_80030a5c(*param_1,<span class="number">0</span>);</span><br><span class="line">  memset_((<span class="type">int</span>)(param_1 + <span class="number">1</span>),<span class="number">0</span>,<span class="number">0xc</span>);</span><br><span class="line">  puVar1 = malloc_simple(<span class="number">0x50</span>);</span><br><span class="line">  <span class="keyword">if</span> (puVar1 != (undefined *)<span class="number">0x0</span>) &#123;</span><br><span class="line">    FUN_8001d3ac(puVar1,*param_1,DAT_802a2810);</span><br><span class="line">  &#125;</span><br><span class="line">  param_1[<span class="number">1</span>] = puVar1;</span><br><span class="line">  *(undefined4 *)(puVar1 + <span class="number">0x18</span>) = <span class="number">0</span>;</span><br><span class="line">  *(undefined4 *)(puVar1 + <span class="number">0x1c</span>) = <span class="number">0</span>;</span><br><span class="line">  *(undefined *)(param_1[<span class="number">1</span>] + <span class="number">0x11</span>) = <span class="number">3</span>;</span><br><span class="line">  *(byte *)(param_1[<span class="number">1</span>] + <span class="number">0x10</span>) = *(byte *)(param_1[<span class="number">1</span>] + <span class="number">0x10</span>) | <span class="number">1</span>;</span><br><span class="line">  FUN_8002f8cc(param_1,param_1[<span class="number">7</span>]);</span><br><span class="line">  <span class="keyword">return</span> param_1;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注：这里的<code>malloc</code> <code>memmove</code> <code>memset</code>等函数并不是原来就叫这些名字，而是根据函数体内容和用法作出的推测（意外的是 LLM 能轻松地识别出这些经过优化过后的函数的功能）</p><p>这里注意到程序在把 FNT 文件复制到新分配的内存里之后对其调用了 <code>FUN_800315d8</code>：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">FUN_800315d8</span><span class="params">(undefined *param_1,undefined *param_2,undefined *param_3,uint size,<span class="type">int</span> param_5)</span></span><br><span class="line">&#123;</span><br><span class="line">  list_push_front((ListNode *)param_1,param_2);</span><br><span class="line">  *(undefined **)(param_1 + <span class="number">8</span>) = &amp;DAT_801a2944;</span><br><span class="line">  *(undefined4 *)(param_1 + <span class="number">0x20</span>) = <span class="number">0</span>;</span><br><span class="line">  *(undefined4 *)(param_1 + <span class="number">0x24</span>) = <span class="number">0</span>;</span><br><span class="line">  *(undefined4 *)(param_1 + <span class="number">0x28</span>) = <span class="number">0</span>;</span><br><span class="line">  *(undefined **)(param_1 + <span class="number">0x2c</span>) = param_2;</span><br><span class="line">  param_1[<span class="number">0x30</span>] = <span class="number">0</span>;</span><br><span class="line">  parse_fnt((uint *)param_1,(uint *)param_3,size,param_5);</span><br><span class="line">  <span class="keyword">return</span> (<span class="type">int</span>)param_1;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里面调用的 <code>parse_fnt</code> 是实际用于解析FNT文件的函数，内容相当长，下面贴出一部分：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">parse_fnt</span><span class="params">(uint *param_1,uint *fnt_base,<span class="type">int</span> param_3,<span class="type">int</span> param_4)</span></span><br><span class="line">&#123;</span><br><span class="line">  ...</span><br><span class="line">  param_1[<span class="number">4</span>] = (uint)fnt_base;</span><br><span class="line">  <span class="keyword">if</span> (fnt_base == (uint *)<span class="number">0x0</span>) &#123;</span><br><span class="line">    param_1[<span class="number">5</span>] = <span class="number">0</span>;</span><br><span class="line">    param_1[<span class="number">6</span>] = <span class="number">0</span>;</span><br><span class="line">    param_1[<span class="number">7</span>] = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  uVar11 = fnt_base[<span class="number">1</span>];</span><br><span class="line">  fnt_base[<span class="number">1</span>] = uVar11 &lt;&lt; <span class="number">0x18</span> | (uVar11 &amp; <span class="number">0xff00</span>) &lt;&lt; <span class="number">8</span> | uVar11 &gt;&gt; <span class="number">0x18</span> | uVar11 &gt;&gt; <span class="number">8</span> &amp; <span class="number">0xff00</span>;</span><br><span class="line">  uVar11 = *(uint *)(param_1[<span class="number">4</span>] + <span class="number">8</span>);</span><br><span class="line">  *(uint *)(param_1[<span class="number">4</span>] + <span class="number">8</span>) =</span><br><span class="line">       uVar11 &lt;&lt; <span class="number">0x18</span> | (uVar11 &amp; <span class="number">0xff00</span>) &lt;&lt; <span class="number">8</span> | uVar11 &gt;&gt; <span class="number">0x18</span> | uVar11 &gt;&gt; <span class="number">8</span> &amp; <span class="number">0xff00</span>;</span><br><span class="line">  uVar11 = *(uint *)(param_1[<span class="number">4</span>] + <span class="number">0xc</span>);</span><br><span class="line">  *(uint *)(param_1[<span class="number">4</span>] + <span class="number">0xc</span>) =</span><br><span class="line">       uVar11 &lt;&lt; <span class="number">0x18</span> | (uVar11 &amp; <span class="number">0xff00</span>) &lt;&lt; <span class="number">8</span> | uVar11 &gt;&gt; <span class="number">0x18</span> | uVar11 &gt;&gt; <span class="number">8</span> &amp; <span class="number">0xff00</span>;</span><br><span class="line">  param_1[<span class="number">5</span>] = <span class="number">0</span>;</span><br><span class="line">  uVar11 = param_1[<span class="number">4</span>] + <span class="number">0x10</span>;</span><br><span class="line">  param_1[<span class="number">6</span>] = uVar11;</span><br><span class="line">  param_1[<span class="number">7</span>] = <span class="number">4</span>;</span><br><span class="line">  param_1[<span class="number">8</span>] = uVar11 + *(<span class="type">int</span> *)(param_1[<span class="number">4</span>] + <span class="number">0xc</span>) * <span class="number">4</span>;</span><br><span class="line">  <span class="keyword">for</span> (uVar11 = <span class="number">0</span>; uVar11 &lt; *(uint *)(param_1[<span class="number">4</span>] + <span class="number">0xc</span>); uVar11 = uVar11 + <span class="number">1</span>) &#123;</span><br><span class="line">    puVar10 = (ushort *)(param_1[<span class="number">6</span>] + param_1[<span class="number">7</span>] * uVar11);</span><br><span class="line">    uVar2 = *(ushort *)(param_1[<span class="number">6</span>] + param_1[<span class="number">7</span>] * uVar11);</span><br><span class="line">    *puVar10 = uVar2 &gt;&gt; <span class="number">8</span> | uVar2 &lt;&lt; <span class="number">8</span>;</span><br><span class="line">    puVar10[<span class="number">1</span>] = puVar10[<span class="number">1</span>] &gt;&gt; <span class="number">8</span> | puVar10[<span class="number">1</span>] &lt;&lt; <span class="number">8</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br></pre></td></tr></table></figure><p>注意到这里只是将 FNT 文件中的各个字段从小端序转换到 PowerPC 架构使用的大端序。另外，这个 <code>parse_fnt</code> 函数被两个不同的函数调用，说明加载游戏中其他字库时大概率也会调用这个函数。</p><p>考虑到可执行文件中嵌入的那段文本只有在新存档才会显示一次，对调试来说并不方便，我想要换成使用游戏主菜单对应的文本文件进行调试。因此我在 Dolphin 模拟器中对 <code>parse_fnt</code> 函数打断点，跳过了加载嵌入文本的断点，在进入主菜单瞬间成功触发断点。</p><p>触发断点之后就可以获得加载进来的 FNT 文件的内存位置。</p><p><img src="/2025/08/25/reverse-engineer-puyo20-wii/fnt_in_memory.png" alt="fnt loaded in memory"></p><p><code>parse_fnt</code> 函数中似乎并没有切分图片中的字符的逻辑，因此我对 FNT 文件中的字符表打内存断点，看游戏什么时候会读取它们。</p><p>果然，几帧之后触发了内存断点，并且屏幕上播放文字时每出现一个字就会调用一次这个函数（在反编译结果上有一些小修改）：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">FUN_80030318</span><span class="params">(<span class="type">int</span> *param_1,undefined2 param_2,uint charindex,undefined4 param_4,</span></span><br><span class="line"><span class="params">                 undefined4 param_5,undefined4 param_6,undefined4 param_7)</span></span><br><span class="line">&#123;</span><br><span class="line">  uint uVar1;</span><br><span class="line">  <span class="type">int</span> iVar2;</span><br><span class="line">  <span class="type">int</span> *piVar3;</span><br><span class="line">  undefined2 *puVar4;</span><br><span class="line">  uint charcnt_perline;</span><br><span class="line">  <span class="type">int</span> *iVar7;</span><br><span class="line">  undefined4 local_38;</span><br><span class="line">  undefined4 local_34;</span><br><span class="line">  </span><br><span class="line">  iVar7 = (<span class="type">int</span> *)((<span class="type">int</span> *)param_1[<span class="number">2</span>])[<span class="number">4</span>];</span><br><span class="line">  iVar2 = FUN_80031784((<span class="type">int</span> *)param_1[<span class="number">2</span>],charindex);</span><br><span class="line">  piVar3 = (<span class="type">int</span> *)FUN_8001b238(param_5);</span><br><span class="line">  (**(code **)(*piVar3 + <span class="number">0x10</span>))</span><br><span class="line">            (piVar3,<span class="number">4</span>,*(undefined4 *)(param_1[<span class="number">2</span>] + <span class="number">0x28</span>),<span class="number">3</span>,iVar7[<span class="number">2</span>] &amp; <span class="number">0xffff</span>,iVar7[<span class="number">1</span>] &amp; <span class="number">0xffff</span>);</span><br><span class="line">  uVar1 = countLeadingZeros((uint)*(byte *)((<span class="type">int</span>)param_1 + <span class="number">0x12</span>));</span><br><span class="line">  (**(code **)(*piVar3 + <span class="number">0x1c</span>))(piVar3,param_4,uVar1 &gt;&gt; <span class="number">5</span>);</span><br><span class="line">  piVar3 = (<span class="type">int</span> *)FUN_80024ba8(param_1[<span class="number">1</span>],piVar3);</span><br><span class="line">  local_38 = param_6;</span><br><span class="line">  local_34 = param_7;</span><br><span class="line">  (**(code **)(piVar3[<span class="number">2</span>] + <span class="number">0x18</span>))(piVar3,&amp;local_38);</span><br><span class="line">  *(undefined *)((<span class="type">int</span>)piVar3 + <span class="number">0x11</span>) = *(undefined *)(param_1 + <span class="number">4</span>);</span><br><span class="line">  charcnt_perline = <span class="number">0x200</span> / (*(ushort *)(iVar2 + <span class="number">2</span>) + <span class="number">1</span>);   <span class="comment">//!!!!!</span></span><br><span class="line">  FUN_80028c68(piVar3,(iVar7[<span class="number">2</span>] + <span class="number">1</span>) * (charindex - (charindex / charcnt_perline) * charcnt_perline)</span><br><span class="line">                      * <span class="number">0x1000</span>,(iVar7[<span class="number">1</span>] + <span class="number">2</span>) * (charindex / charcnt_perline) * <span class="number">0x1000</span>);</span><br><span class="line">  puVar4 = (undefined2 *)malloc_simple(<span class="number">0xc</span>);</span><br><span class="line">  puVar4[<span class="number">1</span>] = *(undefined2 *)(iVar2 + <span class="number">2</span>);</span><br><span class="line">  *puVar4 = param_2;</span><br><span class="line">  *(<span class="type">int</span> *)(puVar4 + <span class="number">4</span>) = param_1[<span class="number">3</span>];</span><br><span class="line">  *(<span class="type">int</span> **)(puVar4 + <span class="number">2</span>) = piVar3;</span><br><span class="line">  param_1[<span class="number">3</span>] = (<span class="type">int</span>)puVar4;</span><br><span class="line">  <span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过查看寄存器和内存可以得知，<code>iVar7</code>指向的正是 FNT 文件的内容，在接近结尾的地方有一个非常可疑的 <code>charcnt_perline = 0x200 / (*(ushort *)(iVar2 + 2) + 1)</code> ，而正是这一行触发了内存断点，读取了某个字符的宽度值，用于计算<strong>字库中每行字符的数量</strong>。</p><p>通过多次触发断点，我发现了这个函数的第三个参数就是将要显示的字符在字库里的序号，将其命名为 <code>charindex</code>。<code>iVar7[1]</code> 和 <code>iVar7[2]</code> 分别是 FNT 文件中定义的单元格高度和宽度。可以发现，在触发断点的代码下一行，程序计算了将要从字库的哪个位置切出字符的显示图形，其中 <code>(iVar7[2] + 1) * (charindex - (charindex / charcnt_perline) * charcnt_perline)</code> 是水平位置，<code>(iVar7[1] + 2) * (charindex / charcnt_perline) * 0x1000)</code> 是垂直位置。</p><p>那么问题的原因就很明显了：由于程序员的疏忽，在计算每行字符数量的时候使用了每个字符的宽度，而不是实际上的文件头中规定的宽度，算出的每行字符数是错的，导致后面计算字符位置也算错了。第一行的非全宽字符逃过一劫，因为这些字符的 <code>(charindex / charcnt_perline)</code> 一定为0。</p><h2 id="解决问题"><a href="#解决问题" class="headerlink" title="解决问题"></a>解决问题</h2><p>找到问题的根源之后解决起来就简单了，只需要将 <code>charcnt_perline = 0x200 / (*(ushort *)(iVar2 + 2) + 1)</code> 改成 <code>charcnt_perline = 0x200 / (iVar7[2] + 1)</code> 即可。具体地，仅需将 <code>0x800303f8</code> 处的一条指令修改为 <code>80 9a 00 08</code> 即可修复此问题。手动用十六进制编辑器找到对应指令然后直接改就行了。</p><p>我制作了测试用的文本和字库（下图1），导入到游戏里，启动游戏以验证问题的解决（下图2）。</p><p><img src="/2025/08/25/reverse-engineer-puyo20-wii/fnt_used.png" alt="fnt file used"></p><p><img src="/2025/08/25/reverse-engineer-puyo20-wii/result.png" alt="result comparison"></p><p>可以看到能正常显示第二行的英文字母了。有些字母下面被切了是因为我写的用来生成字库用的脚本对字符尺寸的处理有点问题。</p><h1 id="总结-闲话"><a href="#总结-闲话" class="headerlink" title="总结 &amp; 闲话"></a>总结 &amp; 闲话</h1><p>虽然我最后解决了 Wii 版上的这个问题，但是好像价值也不是很大，因为英文版那边已经有 workaround 了，汉化版这边甚至不用 workaround <del>虽然缺翻译导致汉化版可能会无限拖下去，这个问题属于是小问题</del>。另外 PSP 版因为没有在可执行文件里嵌入字库，所以定位问题可能会更难，不过有 Wii 版的经验，应该能想办法找到类似结构的函数（？）</p><p>这应该是我第一次尝试逆向这种陌生架构的程序，意外地还挺顺利的。不过要是遇到混淆过的程序那可能就两眼一黑了。</p><p><strong>UPDATE 2025.08.30</strong> 实际上运用 Wii 版的经验稍微找一下就能找到 PSP 版本的有问题的函数在<code>0x000910a4</code>。</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;起因&quot;&gt;&lt;a href=&quot;#起因&quot; class=&quot;headerlink&quot; title=&quot;起因&quot;&gt;&lt;/a&gt;起因&lt;/h1&gt;&lt;p&gt;直接原因是我看到了今年 PSP Homebrew Developer Conference 上 Precise Museum 做的&lt;a href=&quot;https://youtu.be/xr0QegPt7X4?t=584&quot;&gt;报告&lt;/a&gt;，其中提到他们刚刚发布的《噗哟噗哟20周年纪念版》PSP版本的英化补丁在制作过程中遇到的一个问题：&lt;strong&gt;如果一个字库文件中的非全宽字符（即宽度不等于文件头中描述的最大宽度的字符）出现在字库的第一行以外的行，就会使这个字符无法正常显示。&lt;/strong&gt;他们后来通过缩小字体的大小、重新排列字符使非全宽字符全都出现在第一行绕开了这个问题。&lt;/p&gt;
&lt;p&gt;他们提到这件事引起了我的兴趣，因为我在 5 年前开始研究这个游戏的 Wii 版本的汉化的时候就遇到过这个问题，不过中文字符都是方形，简单重排一下字库文件，把少数的非方形字符放到前面很容易解决这个问题。不过后来噗哟群没找到人翻译，汉化版的工作就搁置了。&lt;/p&gt;
&lt;p&gt;我电脑上正好已经装了 Ghidra 以及用于解析 Wii 可执行文件格式的插件，于是决定试试找出这个问题的根源。我之前的逆向经验仅有两个毫无难度的 Unity 游戏（一点混淆没有，符号表全在游戏里），因此这次稍微攒点逆向经验（真能攒到吗）。&lt;/p&gt;
&lt;h1 id=&quot;背景：关于文件格式&quot;&gt;&lt;a href=&quot;#背景：关于文件格式&quot; class=&quot;headerlink&quot; title=&quot;背景：关于文件格式&quot;&gt;&lt;/a&gt;背景：关于文件格式&lt;/h1&gt;&lt;p&gt;这里需要先介绍一下这个游戏使用的字库和文本的文件格式。英文社区已经有人写了用于编辑这个游戏的大部分文件的工具（&lt;a href=&quot;https://github.com/nickworonekin/puyotools&quot;&gt;Puyo tools&lt;/a&gt; 和 &lt;a href=&quot;https://github.com/nickworonekin/puyo-text-editor&quot;&gt;Puyo text editor&lt;/a&gt;），从这些工具的源码中可以了解到文件格式的信息。下面我借用上文提到的报告视频中的示意图来介绍。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/2025/08/25/reverse-engineer-puyo20-wii/fnt_file.png&quot; alt=&quot;fnt file structure&quot;&gt;&lt;/p&gt;
&lt;p&gt;上图展示的是字库文件，即 &lt;code&gt;FNT&lt;/code&gt; 文件的格式。文件头中给出了字库中所含字符的尺寸和个数，随后以列表的形式给出了每个字符的 UTF-16 编码和显示宽度。字符表后面直接接一个材质（贴图）文件（PSP版中为 &lt;code&gt;GIM&lt;/code&gt;， Wii版中为 &lt;code&gt;GVR&lt;/code&gt;）。值得注意的是，虽然字符表中规定了每个字符的宽度，但是这个宽度只用于显示字符串时确定下一个字的水平位置。在材质文件中，字符仍然以文件头中的尺寸整齐地排列。&lt;/p&gt;
&lt;p&gt;每个 &lt;code&gt;FNT&lt;/code&gt; 文件会配套一个 &lt;code&gt;MTX&lt;/code&gt; 文件，里面存储游戏中用到的各种字符串。每个字符占 2 个字节，表示这个字符在 &lt;code&gt;FNT&lt;/code&gt; 文件中的 index 。比如在图中的字库中，&lt;code&gt;い&lt;/code&gt;的编码就是&lt;code&gt;0000&lt;/code&gt;。&lt;/p&gt;
&lt;h1 id=&quot;正文&quot;&gt;&lt;a href=&quot;#正文&quot; class=&quot;headerlink&quot; title=&quot;正文&quot;&gt;&lt;/a&gt;正文&lt;/h1&gt;</summary>
    
    
    
    <category term="逆向" scheme="https://blog.bobh0724.net/categories/%E9%80%86%E5%90%91/"/>
    
    <category term="游戏" scheme="https://blog.bobh0724.net/categories/%E9%80%86%E5%90%91/%E6%B8%B8%E6%88%8F/"/>
    
    
  </entry>
  
  <entry>
    <title>用 Meshtastic 搭建低成本的离网通信</title>
    <link href="https://blog.bobh0724.net/2025/08/09/exploring-meshtastic/"/>
    <id>https://blog.bobh0724.net/2025/08/09/exploring-meshtastic/</id>
    <published>2025-08-09T17:10:00.000Z</published>
    <updated>2026-06-17T06:04:00.096Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景（起因？）"><a href="#背景（起因？）" class="headerlink" title="背景（起因？）"></a>背景（起因？）</h1><p>事情的起因是我某天在刷 Fediverse 的时候看到了我所在的实例上有人发了这么一条帖子（现在找不到了，内容大概是）：</p><blockquote><p>Meshtastic is modern Pictochat</p></blockquote><p>这里提到的 <a href="https://en.wikipedia.org/wiki/PictoChat">Pictochat</a> 指的是老任在 NDS 掌机上推出的聊天功能，可以与附近的掌机进行无线连接，并通过触摸屏画画来聊天。我恰好有一台 3DS ，它有一个类似的功能叫 <a href="https://en.wikipedia.org/wiki/SpotPass_and_StreetPass">Streetpass</a> ，能在休眠状态下和附近的掌机联机，我觉得这个功能还挺酷的。因此我当时对这个 <a href="https://meshtastic.org/">Meshtastic</a> 起了点兴趣，简单了解了一下之后发现是一个开源的离网无线电通讯工具。不过我很快就忘记了这件事。</p><p>后来我在油管上又看到了 <a href="https://events.ccc.de/congress/2024/infos/startpage.html">38C3</a> 上有两个 talk 都介绍了 Meshtastic （<a href="https://media.ccc.de/v/38c3-building-your-first-lora-mesh-network-from-scratch">这个</a>和<a href="https://media.ccc.de/v/38c3-hacker-s-guide-to-meshtastic-off-grid-encrypted-lora-meshnets-for-cheap">这个</a>），其中提到节点的成本相当便宜（DIY成本在<strong>5-7刀</strong>，我在国内买零件应该会比他更便宜），于是就想试试。</p><p>正好 <a href="https://blog.lyc8503.net/">lyc8503</a> 也感兴趣，就决定在学校尝试一下 Meshtastic 。他已经写了<a href="https://blog.lyc8503.net/post/meshtastic-networking/">一篇博客</a>介绍总体的探索过程，我在这里仅做一些我自己的补充和想法。建议先去看他的博客再看我的。</p><h1 id="硬件选择与成本"><a href="#硬件选择与成本" class="headerlink" title="硬件选择与成本"></a>硬件选择与成本</h1><p>我当时看完 talk 就去搜了一下  Meshtastic 支持的各种成品硬件（开发板），结果成品硬件的价格都在 100 元以上，远超我的预期。这下不得不自己组装了。</p><p>一开始使用的 ESP32 + SX1278（Ra-01） 组合的成本也达到了 40 元，这还没算上天线，也没有带屏幕。虽然有更便宜的 ESP32C3 ，但是我们把固件刷进去之后无法启动，而且没有任何信息输出，抓瞎了一会儿就放弃了。后来尝试的 nRF52840 + SX1268（E22-400MM22S） 的组合相对便宜，算上小天线也只有 20 元出头，但是射频模块其实是以拿样价 6 元购买的，如果原价购买则需要 20 元，算下来总成本也要将近 40 元。而且需要手动焊接。</p><p>另外，我还购买了 U-blox NEO-6M GPS 模块，成本为 11 元，可以为节点提供位置信息，虽然精度不高。</p><p>我的目标是尽可能压低成本，这样看下来 20 元每个节点应该已经是极限了。 <del>不过Meshtastic群的群友似乎不太在意成本， 100 多的成品也买得很开心</del></p><h1 id="更多距离测试"><a href="#更多距离测试" class="headerlink" title="更多距离测试"></a>更多距离测试</h1><p>我的另一个目标就是想看看自制节点的极限传输距离。根据 <a href="https://meshtastic.org/docs/overview/range-tests/">Meshtastic 网站</a>显示，目前地面上的距离纪录高达 <strong>331km</strong> ，<a href="https://meshcn.net/">国内的 Meshtastic 社群</a>也有群友达到 <strong>10km</strong> 以上的传输距离。而在南京进行的几次实验都因为周围有很多建筑物遮挡而无法达到很远的距离（不到1km）。因此我去了乡下的一些地方尝试更远的距离。相对空旷的地形和较弱的噪声干扰应该能让传输距离有所提升。</p><h2 id="V2-硬件的距离测试"><a href="#V2-硬件的距离测试" class="headerlink" title="V2 硬件的距离测试"></a>V2 硬件的距离测试</h2><p><strong>使用硬件:</strong> <code>nRF52840 + SX1268(焊接在小板) + 转接线连接的胶棒天线</code>（即 lyc8503 文中的 硬件 Version 2）</p><p><img src="/2025/08/09/exploring-meshtastic/rangev2_pic.jpg" alt="放置在四楼阳台上的节点"></p><p>将一个节点放置在四楼阳台上之后拿着另一个节点沿着门口的直路走。最远能达到大约 <strong>800m</strong> 的距离，再远就收不到消息了。地图上的位置和最远位置的信噪比如下。</p><p><img src="/2025/08/09/exploring-meshtastic/rangev2_map.png" alt="地图上的位置"><br><img src="/2025/08/09/exploring-meshtastic/rangev2_trace.jpg" alt="信号指标"></p><p>从地图上看连线几乎没有建筑物遮挡（蹭着厂房的一角过去了，可能就是因为这个原因不能继续往外走），可惜还是没满 1km 。可能这个手搓的天线转接线的质量还是太幽默了。</p><h2 id="V3-硬件的距离测试"><a href="#V3-硬件的距离测试" class="headerlink" title="V3 硬件的距离测试"></a>V3 硬件的距离测试</h2><p><strong>使用硬件:</strong> <code>nRF52840 + SX1268 + 转接板 + 胶棒天线 + (可选的) GPS 模块</code>（即 lyc8503 文中的 硬件 Version 3）</p><p>升级了硬件之后，我去了另一个乡下地方进行测试。一个节点放置在3楼窗口，另一个节点四处移动。这个地方没有很长的直路，只能尽可能找空旷的方向走。但是我仍然没能找到理想的、完全没有建筑遮挡的长线段，最终在有少量房屋遮挡的线上测到了 <strong>1km</strong> 的距离。地图上的位置和信号测量记录如下。注意到箭头所指位置有少量房屋遮挡，但仍然能成功传输。这次实验的时候在移动的节点上尝试交换了长天线(24cm)和短天线(11cm)，但似乎两根天线的传输距离和信号质量并无明显区别。</p><p><img src="/2025/08/09/exploring-meshtastic/rangev3_map.png" alt="地图上的位置"><br><img src="/2025/08/09/exploring-meshtastic/rangev3_trace.jpg" alt="信号指标"></p><h2 id="V3-硬件的距离测试，第二次"><a href="#V3-硬件的距离测试，第二次" class="headerlink" title="V3 硬件的距离测试，第二次"></a>V3 硬件的距离测试，第二次</h2><p>去乡下还是没能找到足够平坦的地方，于是我决定再来一次测试，这次利用山的高度来绕过城市中的各种建筑物障碍。</p><p>使用硬件同上一次（只使用了短天线），把一个节点贴在家里的4楼阳台窗户上，拿着另一个节点去爬山。虽然一路上没有信号，但是到达山顶的一个建筑物之后成功收到了信号，此处距离我家 <strong>2.6km</strong> ，海拔比我家高 <strong>200m</strong> 左右 <del>在这里放地图的话就把我家盒了，还是算了。</del> </p><p><img src="/2025/08/09/exploring-meshtastic/rangev32_pic.jpg" alt="山顶的节点"></p><p>^山顶的节点。此处到我家的肉眼视线被树挡住了，但是电波能轻松穿过。</p><p><img src="/2025/08/09/exploring-meshtastic/rangev32_trace.png" alt="信号指标"></p><p>^四次 traceroute 的信号指标。甚至有一次是良好，应该还没到极限。</p><p><img src="/2025/08/09/exploring-meshtastic/rangev32_rec.png" alt="聊天记录"></p><p>^连续多条消息被成功接收、发出的消息被成功 ACK 。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>上山测完这一次之后，总算是达到了我对这玩意传输距离的预期值。不过这应该还没达到极限。也许之后还可以进一步测试，但我不会分身术，不能同时移动两个节点，也不太能在山上放置节点之后再下来到处移动（毕竟公共场所）。上一次山只能获得 1 bit 的信息。之后有空再说吧。果然高度才是王道。</p><p>从 lyc8503 五月发的博客到现在也拖了快 3 个月了。这个 project 终于结束了。稍微感受了一下在现实中做嵌入式开发的感觉。我个人感觉能用上 Meshtastic 的场景还是不太多，现在能想到的只有：去偏远没手机信号的地方旅游，以及在空旷地区使用物联网设备（比如范围较大的农田）成本上应该比使用 4G 要低一点。在城市这种环境组网只是看起来有点酷（？），并且基础设施由自己控制而已。</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;背景（起因？）&quot;&gt;&lt;a href=&quot;#背景（起因？）&quot; class=&quot;headerlink&quot; title=&quot;背景（起因？）&quot;&gt;&lt;/a&gt;背景（起因？）&lt;/h1&gt;&lt;p&gt;事情的起因是我某天在刷 Fediverse 的时候看到了我所在的实例上有人发了这么一条帖子（现在找不到了，内容大概是）：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Meshtastic is modern Pictochat&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里提到的 &lt;a href=&quot;https://en.wikipedia.org/wiki/PictoChat&quot;&gt;Pictochat&lt;/a&gt; 指的是老任在 NDS 掌机上推出的聊天功能，可以与附近的掌机进行无线连接，并通过触摸屏画画来聊天。我恰好有一台 3DS ，它有一个类似的功能叫 &lt;a href=&quot;https://en.wikipedia.org/wiki/SpotPass_and_StreetPass&quot;&gt;Streetpass&lt;/a&gt; ，能在休眠状态下和附近的掌机联机，我觉得这个功能还挺酷的。因此我当时对这个 &lt;a href=&quot;https://meshtastic.org/&quot;&gt;Meshtastic&lt;/a&gt; 起了点兴趣，简单了解了一下之后发现是一个开源的离网无线电通讯工具。不过我很快就忘记了这件事。&lt;/p&gt;
&lt;p&gt;后来我在油管上又看到了 &lt;a href=&quot;https://events.ccc.de/congress/2024/infos/startpage.html&quot;&gt;38C3&lt;/a&gt; 上有两个 talk 都介绍了 Meshtastic （&lt;a href=&quot;https://media.ccc.de/v/38c3-building-your-first-lora-mesh-network-from-scratch&quot;&gt;这个&lt;/a&gt;和&lt;a href=&quot;https://media.ccc.de/v/38c3-hacker-s-guide-to-meshtastic-off-grid-encrypted-lora-meshnets-for-cheap&quot;&gt;这个&lt;/a&gt;），其中提到节点的成本相当便宜（DIY成本在&lt;strong&gt;5-7刀&lt;/strong&gt;，我在国内买零件应该会比他更便宜），于是就想试试。&lt;/p&gt;
&lt;p&gt;正好 &lt;a href=&quot;https://blog.lyc8503.net/&quot;&gt;lyc8503&lt;/a&gt; 也感兴趣，就决定在学校尝试一下 Meshtastic 。他已经写了&lt;a href=&quot;https://blog.lyc8503.net/post/meshtastic-networking/&quot;&gt;一篇博客&lt;/a&gt;介绍总体的探索过程，我在这里仅做一些我自己的补充和想法。建议先去看他的博客再看我的。&lt;/p&gt;
&lt;h1 id=&quot;硬件选择与成本&quot;&gt;&lt;a href=&quot;#硬件选择与成本&quot; class=&quot;headerlink&quot; title=&quot;硬件选择与成本&quot;&gt;&lt;/a&gt;硬件选择与成本&lt;/h1&gt;&lt;p&gt;我当时看完 talk 就去搜了一下  Meshtastic 支持的各种成品硬件（开发板），结果成品硬件的价格都在 100 元以上，远超我的预期。这下不得不自己组装了。&lt;/p&gt;
&lt;p&gt;一开始使用的 ESP32 + SX1278（Ra-01） 组合的成本也达到了 40 元，这还没算上天线，也没有带屏幕。虽然有更便宜的 ESP32C3 ，但是我们把固件刷进去之后无法启动，而且没有任何信息输出，抓瞎了一会儿就放弃了。后来尝试的 nRF52840 + SX1268（E22-400MM22S） 的组合相对便宜，算上小天线也只有 20 元出头，但是射频模块其实是以拿样价 6 元购买的，如果原价购买则需要 20 元，算下来总成本也要将近 40 元。而且需要手动焊接。&lt;/p&gt;
&lt;p&gt;另外，我还购买了 U-blox NEO-6M GPS 模块，成本为 11 元，可以为节点提供位置信息，虽然精度不高。&lt;/p&gt;</summary>
    
    
    
    <category term="硬件" scheme="https://blog.bobh0724.net/categories/%E7%A1%AC%E4%BB%B6/"/>
    
    
  </entry>
  
  <entry>
    <title>Homelab 系统盘爆炸历险记</title>
    <link href="https://blog.bobh0724.net/2025/02/12/rescuing-homelab/"/>
    <id>https://blog.bobh0724.net/2025/02/12/rescuing-homelab/</id>
    <published>2025-02-12T20:13:23.000Z</published>
    <updated>2026-06-17T06:04:00.146Z</updated>
    
    <content type="html"><![CDATA[<div class="alert alert-warning"><i class="fa fa-bell  float-left"></i>  <p>本文章含有较多拍屏图片，因为 Live CD 不方便把截图传出来，请读者忍一下（。</p></div><p>前年的时候我把我家的一台旧电脑升级了下硬件改造成了服务器，持续运行至今没出过什么大问题，直到前两天——</p><h1 id="红温的开始"><a href="#红温的开始" class="headerlink" title="红温的开始"></a>红温的开始</h1><p>我像往常一样连着 ssh 操作服务器，然后命令行突然没反应了，随后执行什么命令都报 <code>输入/输出错误</code></p><p><img src="/2025/02/12/rescuing-homelab/io-error.png" alt="输入/输出错误"></p><p>再过一会儿， ssh 连接也没反应了，我前去查看服务器情况，插上显示器发现没有显示输出，电源也没有断，但是键盘完全无反应，只能硬重启。重启之后发现 BIOS 识别不到系统盘了。我想着可能是接触不良就把硬盘换了个 SATA 接口，启动之后识别到了系统盘并且正常启动了系统，但是没过多久就开始报满屏的 <code>I/O Error</code> 。</p><p><img src="/2025/02/12/rescuing-homelab/io-errors.jpg" alt="噔噔咚"></p><p>大事不妙。这块 SSD 很可能已经坏了。糟糕的是我没有对系统盘做任何备份，虽然上面没有什么重要数据，但是重装系统以及重新配置各种服务还是很烦的。</p><p>硬重启之后 BIOS 又识别不到硬盘了，必须断电一段时间才能重新识别到。我启动到 Live CD 试图把盘里的数据 <code>dd</code> 出来，结果读到 4GB 就读不出来了：</p><p><img src="/2025/02/12/rescuing-homelab/dd-failed.jpg" alt="dd中断"></p><p>重启之后跳过这一块继续试图  dd ，但是在 8GB 多的地方又失败了，看来坏了不止一个地方。</p><p>至少分区表和超级块还在，不过这时候已经晚上11点了，如果继续的话我不知道要干到什么时候，就先睡了 <del>（结果没睡好）</del></p><h1 id="恢复数据"><a href="#恢复数据" class="headerlink" title="恢复数据"></a>恢复数据</h1><p>第二天起来查到了一个 <code>ddrescue</code> 工具，可以自动从坏掉的硬盘里读出尽量多的数据。于是就插上 Live CD 开干了。 ddrescue 运行的时候遇到 I&#x2F;O Error 会等内核重置 SATA 链接之后跳过去继续读。花了约 1.5h 完成  Pass 1 之后已经成功读取了 240GB 中 239.897GB 的数据，看来还坏得不严重。</p><p><img src="/2025/02/12/rescuing-homelab/ddrescue-1.jpg" alt="ddrescue成功读取"></p><p>继续运行几个小时之后已经只剩 5MB 没有读出来了。我挂载上恢复出来的镜像先 <code>fsck</code> 了一遍，发现没有找到问题，看来文件系统是完好的，损坏的只有文件数据。<code>ddrescue</code> 还会输出一个 map 文件，里面有每一段读取成功&#x2F;失败的范围，应该可以通过这个范围去寻找被破坏的数据属于哪个文件。</p><p>于是找到了<a href="https://superuser.com/questions/490787/reverse-lookup-of-inode-file-from-offset-in-raw-device-on-linux-and-ext3-4">superuser上的这个问答</a>，发现可以使用 <code>debugfs</code> 命令来寻找被影响的文件，于是写了个脚本找出了所有损坏的文件。</p><p><img src="/2025/02/12/rescuing-homelab/broken-files.jpg" alt="损坏的少量文件"></p><p>万幸的是损坏的少量文件都是不重要的文件，只需要重新安装一下软件包就能恢复。</p><h1 id="复活"><a href="#复活" class="headerlink" title="复活!"></a>复活!</h1><p>接下来就简单了，我正好有张几乎是空的 HDD ，直接划个分区出来，把恢复出来的镜像 dd 回去，重新安装 grub ，然后修一下受损的文件就好了。虽然在 HDD 上的系统可能会慢一点，但是我几乎不跑什么 IO 密集型负载，就先凑合着用了。</p><h1 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h1><p>血的教训：备份很重要，所有丢失了就很烦的数据都应该备份。虽然这次运气好能完全恢复数据，但是下次可不一定。此外这块 <code>致态 SC001 Active</code> SSD竟然用了一年多就坏了， SMART 信息显示写入才 15TB ，看来长江存储也不太能信（?）</p>]]></content>
    
    
    <summary type="html">&lt;div class=&quot;alert alert-warning&quot;&gt;&lt;i class=&quot;fa fa-bell  float-left&quot;&gt;&lt;/i&gt;  &lt;p&gt;本文章含有较多拍屏图片，因为 Live CD 不方便把截图传出来，请读者忍一下（。&lt;/p&gt;
&lt;/div&gt;


&lt;p&gt;前年的时候我把我家的一台旧电脑升级了下硬件改造成了服务器，持续运行至今没出过什么大问题，直到前两天——&lt;/p&gt;
&lt;h1 id=&quot;红温的开始&quot;&gt;&lt;a href=&quot;#红温的开始&quot; class=&quot;headerlink&quot; title=&quot;红温的开始&quot;&gt;&lt;/a&gt;红温的开始&lt;/h1&gt;&lt;p&gt;我像往常一样连着 ssh 操作服务器，然后命令行突然没反应了，随后执行什么命令都报 &lt;code&gt;输入/输出错误&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/2025/02/12/rescuing-homelab/io-error.png&quot; alt=&quot;输入/输出错误&quot;&gt;&lt;/p&gt;
&lt;p&gt;再过一会儿， ssh 连接也没反应了，我前去查看服务器情况，插上显示器发现没有显示输出，电源也没有断，但是键盘完全无反应，只能硬重启。重启之后发现 BIOS 识别不到系统盘了。我想着可能是接触不良就把硬盘换了个 SATA 接口，启动之后识别到了系统盘并且正常启动了系统，但是没过多久就开始报满屏的 &lt;code&gt;I/O Error&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/2025/02/12/rescuing-homelab/io-errors.jpg&quot; alt=&quot;噔噔咚&quot;&gt;&lt;/p&gt;
&lt;p&gt;大事不妙。这块 SSD 很可能已经坏了。糟糕的是我没有对系统盘做任何备份，虽然上面没有什么重要数据，但是重装系统以及重新配置各种服务还是很烦的。&lt;/p&gt;
&lt;p&gt;硬重启之后 BIOS 又识别不到硬盘了，必须断电一段时间才能重新识别到。我启动到 Live CD 试图把盘里的数据 &lt;code&gt;dd&lt;/code&gt; 出来，结果读到 4GB 就读不出来了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/2025/02/12/rescuing-homelab/dd-failed.jpg&quot; alt=&quot;dd中断&quot;&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="homelab" scheme="https://blog.bobh0724.net/categories/homelab/"/>
    
    
  </entry>
  
  <entry>
    <title>Hello World on my Hexo Blog</title>
    <link href="https://blog.bobh0724.net/2023/10/24/hello-world/"/>
    <id>https://blog.bobh0724.net/2023/10/24/hello-world/</id>
    <published>2023-10-24T00:00:00.000Z</published>
    <updated>2026-06-17T06:04:00.142Z</updated>
    
    <content type="html"><![CDATA[<p>UPD 2023.10.27 加入了 Waline 评论系统。欢迎在评论区交换友链;-)</p><p>从高中开始想搞的博客，咕了5年终于想起来做了。如你所见这是一个 retro 风格的主题，虽然只是表面上 retro ，但是我之后应该会做一个纯<code>html</code>，没有任何<code>js</code>和<code>css</code>的分站。</p><p>现在还没想好这里要写什么，想到了再说。首先排除课程笔记。</p><p>此帖也用作本站的 Changelog.</p>]]></content>
    
    
    <summary type="html">This is a hello world.</summary>
    
    
    
    
  </entry>
  
</feed>
