ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

NTFS元文件解析:16个固定元文件逐个拆解与实战

NTFS元文件解析:16个固定元文件逐个拆解与实战 NTFS这个文件系统里最容易被忽视、又最值得花时间啃的一块就是它的元文件metadata files。你可能已经能熟练地用各种工具读出普通文件、能看懂数据运行、甚至能手工恢复误删的文件但如果对元文件这套东西没有概念遇到分区容量对不上文件莫名变成乱码名硬盘插上后挂载报错这类问题时就只能靠猜。这篇是 NTFS 系列的第三篇专门做 NTFS 元文件解析把十六个固定元文件逐个拆开讲清楚它们分别管什么、放在哪、结构长什么样、怎么用脚本或现成工具把它们读出来。不管你是做数据恢复、做取证、写文件系统驱动还是单纯想搞明白我的硬盘空间去哪了这套内容都用得上。我会尽量少堆术语多用账本目录页备份页这种生活化的类比同时给出能直接跑的代码和命令。前两篇里讲过的引导扇区、MFT 记录结构这里只做必要回顾重点放在元文件本身。1. 元文件到底是什么先理清这套自描述的记账体系1.1 从一个空间对不上的经典场景说起我先抛一个几乎每个做存储的人都遇到过的场景。一块 500GB 的 NTFS 分区资源管理器显示已用 360GB但你用工具把所有可见文件、隐藏文件、系统文件的大小加一遍只统计出 320GB中间差了 40GB。这 40GB 既不是回收站也不是页面文件更不是休眠文件。答案通常就藏在元文件里。NTFS 不像 FAT 那样把文件系统的管理信息塞在一个固定的小区域FAT 表和根目录区而是把管理文件系统的那些数据本身也当成了文件来管理。MFT 是一张表位图是一份文件日志是一份文件安全描述符数据库也是一份文件。它们有名字、有 MFT 记录号、有属性、有数据流只是名字都以$开头被系统标记为隐藏和系统文件普通浏览看不到。这种设计的好处很直接文件系统的元数据和用户数据走同一套寻址、缓存、日志机制不用为管理结构单独发明一套访问路径。代价是理解成本上去了——你必须先知道有哪几个元文件、它们各自的 MFT 记录号是几才能往下走。1.2 元文件和普通文件的三个关键差别第一个差别在命名。所有元文件的名字都以$符号开头比如$MFT、$Bitmap。这个符号是保留的你在正常途径下没法创建一个以$开头的用户文件API 层面会被拒绝。第二个差别在记录号。NTFS 把 MFT 记录 0 到 15 这一段固定分配给系统元文件这些编号是硬编码的任何 NTFS 实现都不能改。所以你在任何一块 NTFS 卷上读第 0 号记录读到的永远是$MFT。第三个差别在可见性。元文件会被同时打上隐藏属性和系统属性用dir /a在 Windows 下能勉强看到一部分但像$Extend目录下的$UsnJrnl这类东西正常列目录是看不到的得用底层工具或者直接读 MFT 才能发现。注意$开头的名字在 NTFS 命名空间里是保留的尝试用CreateFile创建$xxx文件会被系统拒绝。这不是权限问题是命名规则层面的限制。1.3 十六个固定元文件的全景清单先把账本摊开。这十六个编号在 NTFS 里是写死的任何版本都一样。记录号元文件名类型核心作用0$MFT文件主文件表所有文件记录的存放处1$MFTMirr文件$MFT 前几条记录的镜像备份2$LogFile文件事务日志崩溃后一致性恢复用3$Volume文件卷信息、卷名、NTFS 版本、脏标志4$AttrDef文件属性定义表描述每种属性的规则5.目录根目录6$Bitmap文件簇分配位图标记哪些簇已用7$Boot文件引导扇区及引导代码8$BadClus文件坏簇记录稀疏文件9$Secure文件安全描述符数据库10$UpCase文件Unicode 大写转换表11$Extend目录扩展元文件的容器目录12–15保留—预留给将来使用$Extend目录本身又是个容器里面装着四个更现代的元文件$ObjId对象 ID 索引、$Quota磁盘配额、$Reparse重解析点索引、$UsnJrnlUSN 变更日志。这四个是按名索引的不是固定编号所以它们的 MFT 记录号在不同卷上可能不一样。理解这张表的价值在于当你做任何底层解析时前 16 条记录都应该优先读出来做交叉验证。如果第 0 号记录签名不是FILE、或者第 7 号$Boot的内容和引导扇区对不上那基本可以判定这个卷有过结构性损坏。2. 逐个拆解十六个元文件各自管什么2.1 $MFT 与 $MFTMirr整个文件系统的总账本和备份页$MFT是整个 NTFS 的心脏。它本身是一个文件数据流由一条条 1KB也可能是 4KB取决于簇大小的 MFT 记录组成每条记录描述一个文件或目录。卷上有多少个文件和目录$MFT就至少有多少条记录再加上为将来增长预留的空记录。$MFT自己的第 0 号记录有点特别它描述的正是$MFT本身。也就是说你从引导扇区拿到$MFT的起始簇号读出第 0 条记录这条记录里的$DATA属性又会告诉你$MFT占用了哪些簇。这是一个自举bootstrap式的结构读起来有点绕但逻辑闭环。$MFTMirr是$MFT的保险。它只备份$MFT最前面的几条记录——具体是备份 4 条记录还是前 4 个簇取决于$MFT记录大小是否为 1KB。规则是如果每条记录是 1KB就备份前 4 条记录如果记录大于 1KB比如 4KB就备份前 4 个簇的完整内容。这样设计的原因很实际$MFT前几条记录里包含了$MFT自己、$MFTMirr、$LogFile这些最核心元文件的定位信息一旦$MFT开头被破坏靠$MFTMirr就能把这几条关键记录找回来进而重建整个卷的寻址能力。实操心得做数据恢复时如果发现 MFT 开头一片乱码先别急着宣告卷报废。去读引导扇区偏移0x38拿$MFTMirr的起始簇号把那份镜像读出来往往能救回前面几条关键记录。2.2 $LogFile 与 $Volume日志和卷自我介绍$LogFile是 NTFS 的事务日志对应的是日志文件服务LFS那一套机制。NTFS 做任何元数据修改时会先在日志里记一笔我要改了我改完了然后再真正落盘。如果系统在两步之间断电重启后系统会重放日志把没完成的操作做完或者回滚。这就是为什么 NTFS 卷很少需要长时间chkdsk大部分情况下几秒钟的日志回放就搞定了。在 NTFS 3.1也就是 Windows XP 之后普遍使用的版本里日志的粒度从整个元数据事务变成了单条记录日志文件的作用范围有所收窄但它依然存在依然是崩溃恢复的第一道防线。$Volume是卷的自我介绍它不存数据只存属性。三个关键属性$VOLUME_INFORMATION记录 NTFS 的主次版本号和脏标志$VOLUME_NAME是卷标就是你在资源管理器里看到的那串名字$VOLUME_DATA是保留的正常是空的。$VOLUME_INFORMATION里的脏标志位很值得关注。这个位被置 1 时说明卷上一次卸载得不够干净系统会认为它处于可能不一致状态。你在 Linux 下用ntfs-3g挂载时遇到volume is dirty的提示根源就在这里。2.3 $AttrDef 与 $Bitmap属性规则表和簇分配地图$AttrDef是一张属性定义表。NTFS 的属性有固定类型码比如0x10是标准信息0x30是文件名0x80是数据。但光有类型码不够还得知道每种属性的名字、是否允许常驻、最小最大长度、是否可索引等等。这些规则全部写在$AttrDef里每条记录大约 160 字节具体长度随名字变化。这张表的存在意义是让文件系统具备一定程度的自描述能力。如果哪天 NTFS 要新增一种属性类型只要往$AttrDef里加一条记录老版本的驱动至少能识别出这是个我不知道的属性并跳过它而不是直接崩溃。$Bitmap是簇分配位图。整个卷有多少个簇位图就有多少位每一位对应一个簇1 表示已分配0 表示空闲。它本身以字节为单位存储所以一个簇对应一位8 个簇对应一个字节。这张位图是判断空间去哪了的关键工具。你统计所有文件的数据流大小得到的是逻辑占用而位图反映的是物理分配。两者不一致时的差额往往来自稀疏文件比如$BadClus、压缩流、或者被标记占用但已无文件引用的孤儿簇。做取证分析时比对一个簇在位图上标记为已用、但没有任何文件的$DATA指向它这类簇就是重点怀疑对象。2.4 $Boot、$BadClus、$Secure、$UpCase$Boot覆盖了卷的前 8KB16 个 512 字节扇区。第一个扇区就是引导扇区VBR后面是引导代码。注意这里有个容易混淆的点引导扇区在物理上位于卷起始位置而$Boot作为文件又占了 MFT 第 7 号记录两处描述的是同一块区域。$BadClus是个稀疏文件。它的$DATA属性覆盖整个卷的所有簇但只有检测到坏道的那部分才真正被分配。也就是说如果某个簇被标记为坏簇$BadClus的数据运行里就会包含这一段。这种设计的巧妙之处在于坏簇被一个文件占着分配器就不会再把它分给别的文件。$Secure是安全描述符数据库内含$SDS、$SII、$SDH三个流。$SDS存实际的安全描述符内容$SII是按安全 ID 排序的索引$SDH是按哈希排序的索引。为什么要搞这么复杂因为 Windows 里成千上万个文件可能共享同一套权限设置如果每个文件都存一份完整的安全描述符浪费巨大。用集中存储加索引引用能省下大量空间。$UpCase是一张固定的 128KB 大写转换表。NTFS 在做文件名比较时不区分大小写靠的就是这张表。它把每个 Unicode 字符映射到对应的大写形式比较时双方都查表转换再比。这张表的内容在所有 NTFS 卷上都是一样的属于常量数据但仍然按元文件的方式存着方便统一管理。2.5 $Extend 里的四个扩展元文件$Extend是个目录本身没有数据流它存在的意义就是把四个较晚引入的元文件装在一起保持顶层编号区域的整洁。$ObjId存对象 ID。给文件分配一个 GUID即使文件被改名、移动到别的目录通过这个 GUID 依然能追踪到它。Windows 的分布式链接跟踪功能就靠它。$Quota是磁盘配额数据。里面的$O流记录每个用户的配额使用情况$Q流记录配额限制。企业环境里管理员给用户设最多用 5GB时数据就落在这里。$Reparse存重解析点。符号链接、目录联接junction、挂载点这些东西本质上都是重解析点它们的索引信息集中在这个元文件里。$UsnJrnl是 USN 变更日志最值得单独说一下。它有两个流$Max记录日志的元信息$J是实际的变更流水。每当卷上有文件被创建、删除、改名、写数据系统都往$J里追加一条记录包含 USN 号、文件名、变更原因、时间戳。取证分析里这个流是还原某段时间内文件发生了什么的核心证据源。3. 动手解析从原始字节读出 MFT 记录3.1 准备工作与工具选型要动手解析先得选定在哪个环境做。我的建议是 Linux配合ntfs-3g这一套工具因为它是开源的、可以直接读源码、命令行工具也齐全。Windows 下可以用 WinHex 或 010 Editor 配合 NTFS 模板做可视化分析但自动化能力差一些。需要用到的现成工具大致有这些ntfsinfo用来打印卷和 MFT 的摘要信息ntfscluster用来做簇级别的定位ntfsls列目录ntfscat按路径读文件内容包括元文件。这几个都来自ntfs-3g包apt install ntfs-3g就能装上。如果你更喜欢自己写代码Python 加struct模块足够了不需要额外依赖。下面我就用 Python 从零开始走一遍。提示操作物理磁盘前先做好镜像。dd if/dev/sdb ofdisk.img bs1M statusprogress拷一份出来之后所有实验都在镜像上做避免误操作损坏原始数据。3.2 第一步解析 $Boot 拿到卷的关键参数所有解析都得从引导扇区开始因为簇大小、MFT 起始位置这些参数全在这里。import struct def parse_boot(buf): assert buf[3:11] bNTFS , 不是 NTFS 引导扇区 bytes_per_sector struct.unpack_from(H, buf, 0x0B)[0] sectors_per_cluster struct.unpack_from(B, buf, 0x0D)[0] total_sectors struct.unpack_from(Q, buf, 0x28)[0] mft_lcn struct.unpack_from(Q, buf, 0x30)[0] mftmirr_lcn struct.unpack_from(Q, buf, 0x38)[0] clusters_per_mft_record struct.unpack_from(b, buf, 0x40)[0] cluster_size bytes_per_sector * sectors_per_cluster if clusters_per_mft_record 0: mft_record_size 1 (-clusters_per_mft_record) else: mft_record_size clusters_per_mft_record * cluster_size return { bytes_per_sector: bytes_per_sector, cluster_size: cluster_size, total_sectors: total_sectors, volume_size: total_sectors * bytes_per_sector, mft_lcn: mft_lcn, mft_offset: mft_lcn * cluster_size, mftmirr_lcn: mftmirr_lcn, mft_record_size: mft_record_size, }这里有个细节值得讲。偏移0x40那个字段是有符号字节。它记的是每条 MFT 记录占几个簇。如果值是正数记录大小就是簇大小乘以它如果是负数说明记录比簇还小记录大小等于 2 的负值次幂。比如值为-10记录大小就是1 10 1024字节。绝大多数卷上记录都是 1KB簇是 4KB所以这个字段常见值是-10。3.3 第二步处理更新序列数组fixup在读 MFT 记录内容之前必须先做 fixup 还原。这是 NTFS 保证多扇区写入原子性的手段每条记录的最后一个扇区末尾两个字节不是真实数据而是更新序列号USN。系统每次写记录时会把这个序列号复制到记录里所有扇区的末尾如果写一半断电校验时发现某处对不上就知道记录被撕裂了可以丢弃。所以读记录时要反过来做拿序列号去替换掉那些被占用的字节恢复出真实数据。def apply_fixup(record, bytes_per_sector): usa_offset struct.unpack_from(H, record, 0x04)[0] usa_count struct.unpack_from(H, record, 0x06)[0] usn record[usa_offset:usa_offset 2] for i in range(1, usa_count): pos i * bytes_per_sector - 2 if record[pos:pos 2] ! usn: raise ValueError(fixup 校验失败记录可能已损坏) record[pos:pos 2] record[usa_offset i * 2: usa_offset i * 2 2] return recordusa_count等于扇区数加一。第一个序列号是模板从第二个开始依次对应各个扇区末尾。这段校验逻辑别省它是最便宜的一致性检查。3.4 第三步遍历属性解析数据运行记录头之后就是一连串属性直到遇到0xFFFFFFFF结束标记。每个属性有自己的头常驻和非常驻的结构不一样。def iter_attributes(record): off struct.unpack_from(H, record, 0x14)[0] while off 8 len(record): attr_type struct.unpack_from(I, record, off)[0] if attr_type 0xFFFFFFFF: break attr_len struct.unpack_from(I, record, off 4)[0] non_resident record[off 8] name_len record[off 9] name_off struct.unpack_from(H, record, off 0x0A)[0] name record[off name_off: off name_off name_len * 2].decode(utf-16-le) if name_len else yield { type: attr_type, offset: off, length: attr_len, non_resident: bool(non_resident), name: name, } off attr_len数据类型属性的运行列表data runs用变长编码第一个字节的低四位表示长度字段占几个字节高四位表示偏移字段占几个字节。偏移是相对上一个运行的起始簇号所以解读时要累加。def parse_runs(record, run_off): runs, lcn, p [], 0, run_off while record[p] ! 0: header record[p] len_size header 0x0F off_size (header 4) 0x0F p 1 run_len int.from_bytes(record[p:p len_size], little) p len_size if off_size 0: runs.append((run_len, None)) # 稀疏段 else: delta int.from_bytes(record[p:p off_size], little, signedTrue) p off_size lcn delta runs.append((run_len, lcn)) return runsoff_size为 0 的情况必须单独处理那是稀疏段逻辑上全零但不占物理簇。$BadClus、$UsnJrnl:$J里都会出现这种段写解析器时漏掉它就会算错长度。3.5 第四步把元文件读出来做交叉验证有了上面这些函数读所有元文件就是循环加偏移的事。$MFT起始偏移是mft_lcn * cluster_size第 N 条记录的偏移就是起始偏移加上N * mft_record_size。def read_mft_record(f, mft_offset, record_size, index, bytes_per_sector): f.seek(mft_offset index * record_size) rec bytearray(f.read(record_size)) if rec[:4] ! bFILE: return None return apply_fixup(rec, bytes_per_sector)读出来后先验证签名再看标志位偏移0x160x01表示记录在用0x02表示这是个目录。然后遍历属性找0x30文件名属性就能拿到元文件自己的名字找0x80数据属性就能拿到它的数据运行。用这套流程去读第 0 到 11 号记录你会依次得到$MFT、$MFTMirr、$LogFile一直到$Extend的全部信息。这就是最朴素的元文件解析器。如果是做取证或者恢复工具这个骨架可以直接扩展成完整的文件系统浏览器。实操心得写解析器时建议先把所有数据运行算出来的逻辑长度和属性头里记的实际大小对一遍对不上说明解析逻辑有 bug。这个自检花不了几行代码能省掉大量调试时间。4. 实战里的坑挂载、识别与元文件损坏排查4.1 Ubuntu 识别不了 NTFS 的几种典型原因很多人在 Linux 下第一次挂 NTFS 移动盘就卡住最常见的原因其实不是驱动问题而是 Windows 那边没放手。Windows 8 之后默认开启快速启动关机时并不真正关机而是把会话状态写进休眠文件。这种情况下 NTFS 卷上的$LogFile和$Volume里都留着未干净卸载的痕迹ntfs-3g检测到脏标志就会拒绝以读写方式挂载报错里通常带 volume is dirty 或者 unclean 字样。解决办法有两个方向。一是回到 Windows关掉快速启动和休眠执行一次完整的关机让卷状态变干净。二是在 Linux 侧强制挂载并丢弃休眠文件命令是sudo mount -t ntfs-3g -o remove_hiberfile /dev/sdb1 /mnt/usb1。要清楚这是在放弃 Windows 的休眠数据如果那边有未保存的东西会被丢掉。第二个常见原因是设备名搞错了。插上 U 盘后/dev/sdb1和/dev/sdc1是动态的尤其是同时插了多个存储设备时。用lsblk -f看一眼文件系统类型和 UUID比凭感觉猜设备名靠谱得多。第三个原因是分区表本身有问题。有些盘在 Windows 下能用是因为 Windows 对分区表的一些不规范写法容忍度高而 Linux 的内核分区解析更严格。这种情况用fdisk -l看不到分区但parted或者testdisk能查出来异常。4.2 mount -t ntfs 与 FUSE 挂载的本质区别这里要澄清一个长期存在的误解。Linux 里mount -t ntfs用的是内核自带的 NTFS 驱动这个驱动功能有限基本只支持读对写入的支持很不完整而且长期没人维护。而mount -t ntfs-3g走的是 FUSE用户态文件系统机制功能完整支持读写、支持日志、支持权限映射。日常使用应该选后者。FUSE 的架构决定了它有个副作用挂载点是由一个用户态进程在支撑的。这个进程一旦退出挂载点就会变成僵尸你cd进去会报奇怪的错误ls也会失败。这就引出了下一个高频问题。4.3 transport endpoint is not connected 到底怎么回事这个报错我见过太多次了典型场景是udisks2自动挂载的移动盘正在拷贝文件时被人拔了线或者后台的mount.ntfs-3g进程因为某种原因崩了。挂载点目录还在但支撑它的 FUSE 会话已经断掉于是内核在访问时返回这个错误。排查顺序是这样的。先用mount | grep usb1看挂载点是否还登记在案再用ps aux | grep ntfs-3g看有没有残留进程。如果挂载点还在但进程没了就是经典的僵尸挂载。处理办法是强制卸载挂载点。优先试sudo umount /mnt/usb1不成功就上sudo umount -l /mnt/usb1懒卸载先断开引用或者sudo fusermount -u /mnt/usb1。还不行的话找到残留进程kill掉再卸。卸载干净之后重新插拔设备用lsblk -f确认设备节点再挂一次。注意遇到这个问题千万别直接rm -rf挂载点目录里的内容。在僵尸挂载状态下删除操作的行为是不可预期的有可能误删到别的东西也可能只是返回错误但把挂载点目录本身搞坏。4.4 元文件损坏的识别与 $LogFile 回放判断一个卷的元文件是否损坏最直接的信号是引导扇区里$MFT的起始簇号指向的位置读不出FILE签名或者读出来的记录 fixup 校验大面积失败。这时候按这个顺序排查会比较高效。第一步读$MFTMirr引导扇区偏移0x38看它里面有没有完好的前几条记录如果有说明只是$MFT开头坏了可以靠镜像重建。第二步用ntfsinfo -m /dev/sdb1打印 MFT 摘要看它能识别出多少条记录、有没有报错。第三步如果卷还能挂载但显示脏跑一次ntfsfix /dev/sdb1它会尝试重放$LogFile并清理脏标志。注意ntfsfix不是chkdsk的替代品它做的是比较轻量的修复复杂的索引损坏它处理不了。日志回放这件事你可以粗略理解为$LogFile里记着第 12345 号 MFT 记录要改成这样回放就是把没落地的改动补上。NTFS 3.1 里日志粒度细化到单条记录之后回放速度很快正常几秒内完成。5. 常见问题速查与实操经验5.1 元文件相关故障速查表把前面几节的问题整理成一张表遇到症状可以直接对号。症状可能涉及的元文件排查动作已用空间远大于文件总和$Bitmap、$BadClus比对位图与实际引用找孤儿簇挂载提示卷脏、拒绝读写$Volume、$LogFile关闭快速启动或强制移除休眠文件挂载点报 transport endpoint$MFT索引、FUSE 进程卸载残留挂载点重启挂载服务文件名乱码或显示异常$UpCase、$MFT文件名属性检查$UpCase表是否完整权限设置丢失$Secure检查$SDS、$SII索引一致性文件被改名后追踪不到$Extend:$ObjId读对象 GUID 反查恢复某时段的文件操作记录$Extend:$UsnJrnl:$J解析 USN 流水按时间过滤重解析点失效$Extend:$Reparse检查索引与目标路径是否匹配这张表不是万能的但它能帮你在遇到问题时快速把范围缩小到某个元文件避免漫无目的地翻数据。5.2 几个只有踩过才知道的细节第一个是关于$UsnJrnl:$J的大小。这个流会一直增长系统只在达到上限时才裁剪最老的部分。做取证时如果发现它只有几 MB说明裁剪很激进能追溯的时间窗口很短如果发现有几百 MB那就是金矿。判断方法是读$UsnJrnl的$Max流里面记着当前的最大 USN 和分配大小。第二个是关于$Bitmap的一个坑。它反映的是当前分配状态但不一定和实际文件一一对应。如果卷曾经经历过非正常断电可能留下一些已分配但无人引用的簇同时有些文件的记录指向了错误的簇。所以用位图判断空间占用时要意识到它有滞后性。第三个是关于$AttrDef的可玩性。你可以手工解析它来列出这个卷支持的所有属性类型输出一张对照表。这在写通用解析器时特别有用因为不同 NTFS 版本支持的属性集合略有差异硬编码属性表容易漏。第四个是关于$Extend下四个元文件的定位。它们没有固定编号必须通过遍历$Extend目录的索引来找到各自的 MFT 记录号。具体做法是读$Extend的$INDEX_ROOT和$INDEX_ALLOCATION属性按$I30索引解析得到名字到记录号的映射。这一步比读固定编号麻烦但绕不过去。5.3 解析器的自检清单如果你正在写自己的解析器收尾阶段建议对着这几条过一遍。$MFT第 0 号记录里的$DATA运行能覆盖的簇数是否与位图标记的已用簇数大致吻合$MFTMirr的内容是否和$MFT前几条记录逐字节一致$Boot文件的数据流是否和卷起始 8KB 完全一致$UpCase的长度是否刚好 131072 字节$BadClus的数据运行里有没有出现稀疏段。这几条都过了基本可以认定解析器的骨架是正确的。剩下的就是处理各种边界情况比如属性名带 Unicode、数据运行分段超过一屏、记录跨簇边界之类。我自己在这一块栽得最惨的一次是早期写解析器时忘了做 fixup读出来的文件名属性偶尔会少两个字符排查了半天才发现是记录末尾的序列号没还原。那次之后就养成了习惯任何从磁盘读出来的 MFT 记录第一件事就是 fixup校验不过直接标记为可疑。这个习惯后来在分析损坏卷时救过好几次场因为 fixup 失败本身就是一条极有价值的损坏信号。如果后面还要继续深入$Extend里那四个元文件各自都能单独写一篇$UsnJrnl的时间线重建、$Secure的权限去重机制、$Reparse和符号链接的对应关系、$ObjId在链接跟踪里的应用每一个展开都有不少可以实操的内容。
返回列表