tar.xz 深潜:格式、随机访问与导航解压提速实测

八月 10, 2026 更新: 八月 11, 2026 [Linux 与系统]

背景

排查过程中遇到两个真实问题:

  1. 两个 .tar.xz 包解压后,一个多出一个 . 目录、所有内容都在里面;另一个直接散开。
  2. 经常要从一个很大的 .tar.xz 里读取固定的 wfuzz-package.json,但不想每次全量解压。

这两个问题分别指向 tar 的路径/成员顺序语义 和 xz 容器的块结构是否支持导航(随机访问)。本文把两条线都讲透,并给出实测数据。

更新说明:本文经过多轮实测反复修订。两处关键纠错:(1) 原"纯 tar 末尾成员必须解压整个流"——实际 GNU tar 在可 seek 普通文件上用 lseek 跳过数据,纯 tar 找末尾成员几乎免费(见 1.4 节);(2) 原"tar.xz 中文件在开头,tar -xOf 流式天然很快"——实际 tar -xOf 在压缩流上从不提前退出,无论位置/块数都解压整个流(见 1.4 节),头部提速要靠自定义早停读取器(见 1.5 节)。另补充:成员顺序实测(子目录乱序)、tar 头校验和验证、硬链接判重(dev+inode)、wfuzz-db"目标不在最后一块"反例、json 最前打包方案等。


一、tar 格式:顺序磁带归档,无索引

1.1 存储结构

tar(Tape Archive)设计之初就是给磁带用的——纯顺序写入,从头到尾只有一种元素:512B 头 + 数据:

+---------+--------+---------+--------+---------+--------+------+
| 512B头  | 数据A  | 512B头  | 数据B  | 512B头  | 数据C  | 2*512B 全零块
+---------+--------+---------+--------+---------+--------+------+

每个头里记录:文件名(100 字节)、大小(八进制,偏移 124 起的 12 字节)、权限、mtime、类型(普通文件/目录/软链…)、校验和(偏移 148-155 的 8 字节,见 3.2 节)。

关键特性:

1.2 路径前缀与成员顺序:.目录 之谜的根源

归档里的成员名是打包时写入的原始路径,解压方基本原样还原:

打包命令存储的成员名备注
cd dir && tar -cf a.tar *file1、sub/、sub/f3无 ./ 前缀
tar -cf a.tar -C dir ../、./file1、./sub/…带 ./ 前缀,且多了 . 条目

同一个 GNU tar 解压两者结果完全一致(实测 diff -r 无差异),因为 GNU tar 解压时会剥掉开头的 ./。你看到".目录"通常是因为解压工具不剥离 ./(如 bsdtar/libarchive、7-Zip、WinRAR 等按原始路径还原)。

更隐蔽的是成员顺序。实测(创建序 zz→aa,与字母序、哈希序分别对比):

实测对比(同一份数据):

顶层(glob字母序):      afile bfile cfile sub/
sub/ 内(tar 实际序):   xx.txt zz.txt aa.txt bb.txt mm.txt yy.txt
字母序应为:            aa.txt bb.txt mm.txt xx.txt yy.txt zz.txt
创建序应为:            zz.txt yy.txt xx.txt mm.txt aa.txt bb.txt

结论:方法 B(readdir 序)不是随机,而是"实现相关、不可预测、不可依赖";方法 A 只有顶层字母序。想让包括子目录在内全部有序,唯一方式是 tar --sort=name(GNU tar 1.28+)。

1.3 重复归档与硬链接:tar 按 dev+inode 判重

显式参数 + 通配/目录遍历重复指定同一文件时(如 tar -cf x.tar wfuzz-package.json *、tar -cf x.tar -C d wfuzz-package.json .),tar 不会报错、不会重复存数据,而是把第二次出现存成硬链接指向第一次的名字:

-rw-  wfuzz-package.json
...   ./data.txt ...
hrw-  ./wfuzz-package.json 连接到 wfuzz-package.json   ← 硬链接自引用

判重依据是 dev + inode,不是名字。两个对照实验证明:

注意硬链接条目本身 size=0 不带数据,内容在 linkname 字段(头偏移 157-256)指向的成员里——这影响块导航读取(见 3.3 节)。

1.4 tar 支持文件导航吗?

格式层面不支持:没有文件名→偏移的索引,只能顺序解析头。但分两种场景:

验证方法(追加垃圾字节):把多块包复制一份并 printf 'GARBAGE' >> 追加垃圾,然后 tar -xOf x.tar.xz wfuzz-package.json。实测无论 json 在头部还是尾部、单块还是多块,都报 xz:压缩数据已损坏——说明 tar 读到了流末尾。tar 面对的是 xz 吐出的连续字节流,它不认识 xz 块、也没有"匹配到目标就停"的逻辑,会一直读到 EOF 零块确认归档结束。xz 解压多少 = tar 读多少。

所以 tar.xz 下"文件在开头就快"是不成立的——标准 tar 会把整个流解压完。能早停的只有自定义读取器(见 1.5 节)或块级跳读(见第三章)。

1.5 早停流式读取:头部文件的正确读法

既然 tar 不早停,就自己写一个"边解压边解析 tar 头,命中即断管道"的读取器:

import subprocess
p = subprocess.Popen(['xz','-dc',archive], stdout=subprocess.PIPE, bufsize=0)
buf, off = b'', 0
while True:
    chunk = p.stdout.read(65536)
    if not chunk: break
    buf += chunk
    while len(buf)-off >= 512:
        h = buf[off:off+512]
        if h == b'\x00'*512: raise SystemExit('未命中')
        name = h[0:100].split(b'\x00')[0]
        if name == b'wfuzz-package.json':
            size = int(h[124:136].split(b'\x00')[0].strip(), 8)
            sys.stdout.buffer.write(buf[off+512:off+512+size])
            raise SystemExit
        size = int(h[124:136].split(b'\x00')[0].strip() or b'0', 8)
        off += 512 + ((size+511)//512)*512

实测(50MB 单块包,json 在头部):早停读取器只解压 712 字节就命中退出,而 tar -xOf 解压完整 52,439,040 字节。头部位置 + 早停机制对单块包同样有效——这是单块包唯一能大幅提速的路径。

块级增量解压:上面的早停是在 xz -dc 层面(进程级管道)。其实单个 xz 块也不需要完整解压——LZMA2 是顺序 chunk 流,解码器是流式状态机,可以用 lzma.LZMADecompressor.decompress(data, max_length) 增量解压、命中即停。实测对 wfuzz-python 最后一块(压缩 2.38MB / 解压 17.6MB):只要 512 字节输出,只消费了 256 字节压缩数据(占整块 0.01%)就停。

但收益有上限:增量解压省的是"目标之前的解压量"。目标在块内越靠前省得越多;目标在块尾(如 wfuzz-python 的 json 在块内偏移 17.6MB/17.6MB)就得解压几乎整块。所以:

节省量 ≈ 目标在流/块中的偏移(前面的部分不用解)
目标在头部 → 只解几百字节
目标在中间 → 解到它为止
目标在末尾 → 解压几乎整块

1.6 tar.xz 的耗时构成

实测 wfuzz-python(184.8 MiB / 8 块):

操作耗时
纯解压 xz -dc > /dev/null0.295s
解压 + tar 提取末尾文件0.299s
解压 + tar 全量列表0.302s

三者几乎相同 → tar.xz 的耗时 ≈ 100% 来自 xz 解压,tar 的流式头解析/提取开销可忽略。要优化 tar.xz 读取,只能减少解压量。


二、xz 格式:可随机访问的块容器

2.1 容器结构

.xz 是容器格式,内部是"一个或多个 Block + 尾部 Index":

+------------+--------+--------+-----+---------+-------+--------------+
| 流头(12B)  | 块1    | 块2    | ... | 块N     | Index | 流尾(12B)    |
+------------+--------+--------+-----+---------+-------+--------------+
部件内容
流头魔数 + 校验类型(CRC32/CRC64/SHA-256)
块块头(压缩设置、字典大小、可选压缩前/后大小)+ 独立 LZMA2 数据 + 填充 + 校验
Index每块的 Unpadded Size(块头+数据+校验)和 Uncompressed Size
流尾指向 Index 的 Backward Size + 校验 + 魔数

2.2 为什么能导航

关键在两点:

  1. Index 在文件末尾,记录了每块的压缩大小和解压大小 → 从文件尾往前读几字节,就能算出每个块在文件里的精确偏移(xz -lv 就能看到"压缩偏移量/解压偏移量"两列)。
  2. 每个块是独立的 LZMA2 流。只要块与块之间做了"字典重置"(多块压缩时 xz 默认如此),任意一块都能独立解码,不需要前面块的数据。

这就是 .xz 官方规范里 Index 的用途之一:"quickly access the beginning of any Block (random access)"。

2.3 块数 ≠ 线程数

实测同一份 150MiB 数据的块数:

参数块数
-T11
-T4(不指定块大小)7
-T4 --block-size=24MiB7
-T4 --block-size=1MiB151
-T2 --block-size=24MiB7
-T8 --block-size=24MiB7

规律很清晰:

顺带:1GB 数据压缩耗时 -T1 4 分钟 vs -T4 1 分钟(≈4x),块大小不伤压缩比。

--block-size 是压缩前(输入)的字节数。实测 1MiB 随机 + 1MiB 全零、--block-size=1MiB:两块未压缩大小都恰为 1MiB,压缩后分别 1,048,656 B 和 252 B。块边界按输入字节切,与压缩率无关(xz 手册原话:"start a new .xz block after every SIZE bytes of input")。

多线程压缩不影响成员排序。xz 只把 tar 传来的字节流按顺序切成块,从不重排。实测同一 tar 用 -T1 和 -T4 --block-size=1MiB 压缩,tar -tf 成员顺序 diff 完全一致。成员顺序 100% 由 tar 决定,xz 只决定块怎么切。

--block-size 的版本兼容性:该选项从 XZ Utils 5.2.0(2014) 就有(与多线程压缩 -T 一起引入),解析层面几乎无兼容问题。真正的坑在 -T0 语义:

xz 版本-T0 含义-T0 --block-size 是否切块
5.2.0~5.3.x禁用多线程单线程下不切块 → 单块(静默失效!)
5.4.0+(2022)用满所有核生效 → 多块 ✓

想跨版本行为确定,用显式线程数:XZ_OPT='-T2 --block-size=24MiB'(5.2.0 起任何版本都稳定切块),并可用 xz -l 事后验证块数。另外 XZ_OPT 是很老的环境变量(5.0 时代就有),XZ_DEFAULTS 则是 5.4.0 才加。

2.4 其他压缩格式:早停通用,跳块需索引

把"早停"和"跳块"拆成两个能力,结论不同:

格式独立块?块索引?可跳块随机访问?
xz✓✓(尾部 Index)✓
zip✓✓(central directory)✓
zstd(seekable 模式)✓✓(专用格式)✓
7z✓✓(头部目录)✓
gzip✗ 单个 DEFLATE 流✗✗
bzip2✓(900KB 块)✗ 无索引✗(找不到块边界)
zstd(普通模式)帧结构✗✗
tar✗✗✗

关键:流式解码是解码器的天性(所有格式都有);"跳块"是容器格式的设计——必须有类似 xz 的 Index、zip 的 central directory 来记录每块偏移。目标在头部时用早停就够(任何格式);目标在末尾时,只有带索引的格式(xz/zip/zstd-seekable/7z)能跳过前面的大段解压。


三、tar.xz 组合:按块导航 + 校验和验证解析

3.1 能直接跳到任意文件吗?

不能直接跳,但可以"间接"跳:

业界现成方案:ratarmount(FUSE 挂载建索引)、pixz(打包时建 .tpxz 索引)、自研脚本(见附录)。

3.2 块边界不在 tar 成员边界:用校验和定位,不用字符串匹配

常见疑问:xz 块边界可能切在某个成员的数据中间(实测 wfuzz-python 最后一块从 python/share/terminfo/w/... 中间开始,块内包含 3442 个真实 tar 头),那怎么知道块内哪个位置是真正的头?

答案:tar 头自带 8 字节校验和字段(偏移 148-155),值 = "把校验和字段当作空格后,全 512 字节求和":

valid ⟺  checksum字段是合法八进制 且
          int(字段) == sum(h[0:148]) + sum(h[156:512]) + 8*0x20

两个对齐性质保证可枚举:

  1. tar 头永远 512 字节对齐;
  2. xz 块大小是 512 的倍数(24MiB = 512×49152)→ 块起始偏移也是 512 的倍数。

所以块内候选头位置就是 块长/512 个 512 网格点,逐个做校验和验证即可。随机数据块碰巧通过校验和的概率约 1/2^19(实测 34396 个候选位中只筛出真实头 3442 个),再用 size 字段向前走、与结尾的 EOF 零块衔接做二次校验,基本是确定性解析。

字符串匹配只能当快速预筛选(find(b'wfuzz-package.json')),不可靠——文件名可能恰好出现在某个文件的数据区里。最终解析必须走校验和 + size 字段。

3.3 探测策略:先试最后一块,未命中回退全量

由于清单类文件(wfuzz-package.json)在多数包里排在末尾,实用的启发式:

多块包 → 从最后一块向前探测(512 网格 + 校验和验证)
         ├─ 命中:只解压了目标块,秒级
         └─ 未命中:回退全量解压扫描
单块包 → 无 Index 可定位,直接全量

注意反例:位置完全取决于打包机的 readdir 顺序,不保证在最后。见 4.3 节的 wfuzz-db。

链接条目处理:硬链接(typeflag='1'/'h')和软链接('2'/'l')自身不带数据,内容在 linkname 字段指向的成员里。若目标条目是链接:

两种快速读取路径对比:

策略适用优点局限
早停流式(1.5 节)json 在头部单块/多块都行,只解压几百字节目标在尾部时退化为全量
块级跳读(本章)json 在末尾、多块包跳过无关块,解压单个块单块包无效;json 在中间时探测落空

四、实测提速数据

4.1 wfuzz-python(184.8 MiB / 8 块,json 在最后一块)

方式耗时
全量解压 + 提取1.23s
按块跳读(只解最后一块 2.38MB)0.13s(≈10x)

4.2 自造 1GiB 大包(1,036.7 MiB / 44 块,json 用 --sort=name 保证最后)

方式耗时说明
全量解压 + 提取1.17s22 核并行解压
按块跳读(朴素版,读全文件再定位)0.315s瓶颈在读完 700MB 压缩文件
按块跳读(seek 版,只读尾部 2.3KB)≈0.005s≈230x

注:全量解压才 1.17s,是因为 22 核并行 + 大量不可压缩随机数据走 LZMA2 无压缩快通道。真实高压缩比数据单核解压更慢,导航优势更明显。

4.3 反例:wfuzz-db(json 不在最后一块)

wfuzz-db-1.10.10(161 成员 / 2.7GB / 115 块):打包机 readdir 顺序把大 bin/ 目录排在了 wfuzz-package.json 后面(lib/ → json → bin/),导致 json 在成员 122/161、字节偏移仅 8.1%、位于第 10/115 块。

方式耗时
探测最后一块 → 落空 → 全量解压 2.7GB14.0s
知道它在第 10 块,直接跳读该块0.21s(≈66x)

教训:"目标在最后一块"只是 readdir 运气,不是设计。探测策略的收益 = 命中率 × 加速比,未命中就回到全量路径。

4.4 单块包:无法导航(对照)

同一份数据用 -T1 压成单块包,跳读无从谈起,只能全量解压。单块 = 放弃了随机访问能力,这是压缩时就要做的取舍。


五、真实案例:wfuzz 系列包为什么 json 位置飘忽

5.1 打包方式根因

wfuzz 系列的打包在 CMake 里(cmake/package.cmake):

set(ENV{XZ_DEFAULTS} "-T0")
execute_process(COMMAND ${CMAKE_COMMAND} -E chdir "${WFUZZ_TOOL_INSTALL_DIR}"
                tar cvfJ ".../xxx.tar.xz" .)

即 tar .(方法 B)+ XZ_DEFAULTS=-T0。成员顺序 = 打包机器上安装目录的 readdir 序,与 wfuzz-package.json 相对大目录(bin/lib)的哈希位置有关。

5.2 实测各包位置

包wfuzz-package.json 位置xz 块数
wfuzz-os-0.1.349/49(最后,100%)1
wfuzz-agent-0.1.447587/7587(最后)12
wfuzz-vscode-server12879/12879(最后)1
wfuzz-python-v0.8.17354/7354(最后)8
wfuzz-db-1.10.10122/161(8.1%!)115

5.3 修复建议

两个方向,都能保证 json 位置确定且无硬链接(已实测验证)。

方案一(推荐):json 放最前 + 无硬链接,配合早停流式读取(单块/多块都有效):

# 写法1: 无 ./ 前缀
cd wfuzz-pkg && \
tar -cJf ../wfuzz-pkg.tar.xz wfuzz-package.json --exclude=wfuzz-package.json *

# 写法2: 保留 ./ 前缀
tar -cJf wfuzz-pkg.tar.xz -C wfuzz-pkg wfuzz-package.json --exclude=wfuzz-package.json .

方案二:json 放最后,配合块级跳读(仅多块包有效):

cd "${WFUZZ_TOOL_INSTALL_DIR}" && tar --sort=name -cf - . | xz -T0 --block-size=24MiB -c > out.tar.xz

--sort=name 让 w 开头的 json 排在 bin/、lib/ 之后(最后)。

注意:两种方案的关键都是让 json 只归档一次。tar -C dir wfuzz-package.json . 这类显式+遍历重复的写法会产生硬链接自引用条目(见 1.4 节),虽然无害但多余。

关于多块:只读头部 json(早停读取)不需要多块,-cJf 即可。若还需保证多块可导航/并行解压,两种方式任选:

  • 方式1:XZ_OPT 环境变量(保持 -cJf 单命令,推荐):
    XZ_OPT='-T0 --block-size=24MiB' tar -cJf out.tar.xz wfuzz-package.json --exclude=wfuzz-package.json *
    
  • 方式2:管道:
    tar -cf - wfuzz-package.json --exclude=wfuzz-package.json * | xz -T0 --block-size=24MiB -c > out.tar.xz
    

注:-cJf 是否多块取决于机器 xz 版本(xz 5.8 默认 -T0 已多块,旧版是单块),所以要想稳定多块需显式指定。


六、实践建议

针对"反复读大包里固定某个文件":

  1. 先想清楚目标在包的哪个位置——tar -xOf 在压缩流上从不提前退出(读完整流),位置对标准 tar 无影响。
  2. 文件在开头:用早停流式读取器(1.5 节),只解压几百字节,单块/多块都行。
  3. 文件在末尾且包是多块的:按块跳读最优(见附录工具);或装 ratarmount 挂载后直接读。
  4. 文件在末尾且包是单块的:无法跳块,只能全解压一次后缓存目标文件。
  5. 文件在中间(readdir 序导致):块跳读探测落空 → 一次全量扫描缓存偏移,之后直接跳该块;或从源头改打包(5.3 节方案)。
  6. 自己打包含固定目标时:首选"json 放最前 + xz --block-size"(5.3 方案一),配合早停读取最通用;不要用会重复归档的写法(会产生硬链接)。

七、附录:按块探测工具(校验和验证版)

#!/usr/bin/env python3
"""xz_read.py — 从 tar.xz 中按块导航读取单个成员,只解压目标所在块。

原理:
  1. 解析 .xz 流尾 Index,得到每块的压缩区间与解压区间
  2. 多块包:从最后一块向前探测目标成员(tar 头用校验和验证,非字符串匹配)
  3. 命中成员若是硬链接/软链接,解析 linkname 字段追踪到真实数据
  4. 未命中、或真实数据不在已解压块内:回退全量解压扫描

用法:
  python3 xz_read.py <archive.tar.xz> <member>            # 输出到 stdout
  python3 xz_read.py <archive.tar.xz> <member> -o out     # 写入文件
"""
import sys, lzma, struct, time

REGULAR = b'\x00\x30'      # 普通文件 typeflag: '\0' 或 '0'
HARDLINK = b'\x31\x68'     # 硬链接: '1' 或 'h'
SYMLINK = b'\x32\x6c'      # 软链接: '2' 或 'l'


def read_vli(b, o):
    v = 0
    for i in range(9):
        x = b[o + i]
        v |= (x & 0x7F) << (7 * i)
        if not x & 0x80:
            return v, o + i + 1
    raise ValueError("bad VLI")


def xz_blocks(path):
    data = open(path, 'rb').read()
    sz = len(data)
    footer = data[sz - 12:]
    idx_real = (struct.unpack('<I', footer[4:8])[0] + 1) * 4
    idx = data[sz - 12 - idx_real:sz - 12]
    n, o = read_vli(idx, 1)
    recs = []
    for _ in range(n):
        up, o = read_vli(idx, o)
        uc, o = read_vli(idx, o)
        recs.append((up, uc))
    start = 12
    blocks = []
    for up, uc in recs:
        padded = (up + 3) & ~3
        blocks.append((start, start + padded, up, uc))
        start += padded
    return data, blocks


def decompress_block(data, blk):
    so, eo, up, uc = blk
    b = data[so:so + up]
    hsize = ((b[0] & 0x3F) + 1) * 4
    flags = b[1]
    o = 2
    if flags & 0x40:
        _, o = read_vli(b, o)
    if flags & 0x80:
        _, o = read_vli(b, o)
    nf = (flags & 0x03) + 1
    filters = []
    for _ in range(nf):
        fid, o = read_vli(b, o)
        plen, o = read_vli(b, o)
        props = b[o:o + plen]
        o += plen
        if fid == 0x21:
            bits = props[0] & 0x3F
            dsize = (2 | (bits & 1)) << (bits // 2 + 11) if bits < 40 else 0xFFFFFFFF
            filters.append({'id': lzma.FILTER_LZMA2, 'dict_size': dsize})
    d = lzma.LZMADecompressor(format=lzma.FORMAT_RAW, filters=filters)
    return d.decompress(b[hsize:])


def tar_checksum_ok(h):
    field = h[148:156].split(b'\x00')[0].strip()
    if not field or not all(c in b'01234567' for c in field):
        return False
    stored = int(field, 8)
    return stored == sum(h[:148]) + sum(h[156:]) + 8 * 0x20


def find_member(raw, member):
    """512 网格 + 校验和扫描,返回匹配成员的头 (hdr, off) 或 None。"""
    target = member.encode()
    for p in range(0, len(raw) - 512, 512):
        h = raw[p:p + 512]
        if not tar_checksum_ok(h):
            continue
        name = h[0:100].split(b'\x00')[0]
        if name.lstrip(b'./') != target.lstrip(b'./'):
            continue
        return h, p
    return None


def extract(raw, member, seen):
    """从已解压数据中提取 member 内容,支持解析硬链接/软链接。"""
    if member.lstrip('./') in seen:
        return None, "链接成环"
    hit = find_member(raw, member)
    if hit is None:
        return None, "成员或其链接目标不在已解压数据中"
    h, p = hit
    tflag = h[156:157]
    size = int(h[124:136].split(b'\x00')[0].strip() or b'0', 8)
    if tflag in REGULAR:
        return raw[p + 512:p + 512 + size], None
    if tflag in HARDLINK or tflag in SYMLINK:
        linkname = h[157:257].split(b'\x00')[0].decode(errors='replace')
        return extract(raw, linkname, seen | {member.lstrip('./')})
    return None, f"不支持的成员类型 typeflag={tflag!r}"


def main():
    if len(sys.argv) < 3:
        sys.exit(__doc__)
    path, member = sys.argv[1], sys.argv[2]
    out = None
    if '-o' in sys.argv:
        out = sys.argv[sys.argv.index('-o') + 1]

    data, blocks = xz_blocks(path)
    t0 = time.time()

    for blk in reversed(blocks):
        if len(blocks) == 1:
            break
        raw = decompress_block(data, blk)
        content, err = extract(raw, member, set())
        if content is not None:
            print(f"[probe] 命中! 跳过 {len(blocks)-1} 个块, "
                  f"仅解压 {blk[3]//1024}KB ({time.time()-t0:.3f}s)", file=sys.stderr)
            if out:
                open(out, 'wb').write(content)
            else:
                sys.stdout.buffer.write(content)
            return
        if err and '链接目标' in err and blk is blocks[0]:
            # 链接目标在更早的块里, 交回全量扫描解决
            break

    print(f"[fallback] 全量解压扫描 ({time.time()-t0:.3f}s)", file=sys.stderr)
    raw = b''.join(decompress_block(data, b) for b in blocks)
    content, err = extract(raw, member, set())
    if content is None:
        sys.exit(f"!! 无法获取 {member}: {err}")
    if out:
        open(out, 'wb').write(content)
    else:
        sys.stdout.buffer.write(content)


if __name__ == '__main__':
    main()

实测该脚本输出与 tar -xOf 全量提取逐字节一致;wfuzz-python 命中时只解压 17MB/184MB。

关于链接条目:tar 里的硬链接(typeflag='1'/'h')和软链接(typeflag='2'/'l')自身不带数据,内容存于 linkname 字段(头偏移 157-256)指向的成员。若目标条目恰好是链接:


总结