简介:面向数据恢复、系统调试与安全分析初学者及需要补强磁盘底层认知的IT从业者,这份PPT课件围绕WinHex定位文件首个扇区展开,帮助读者建立从物理磁盘到分区、文件系统再到具体文件的定位思路。压缩包仅含1个pptx文件,大小约1.1MB,以图示化页面串联MBR与分区表、DBR、FAT32的FAT表、根目录区、32字节目录项以及起始簇号换算等关键概念,并给出分区起始扇区叠加、根目录扇区计算和文件首扇区确认的完整案例。已有79人学习,适合课堂演示、自学梳理或实验复盘时对照使用,可据此理解扇区号、簇号与分区偏移之间的关系,形成可迁移的磁盘分析框架。
1. 一个文件“在哪”:为什么答案不是一个数字,而是三层地址的叠加
很多人以为,问“这个文件在磁盘的哪个扇区”,答案应该是一个整数。真去操作系统课上讲的磁盘结构里翻一遍就会发现,文件系统从来不直接记录物理扇区号。文件里存的是一个簇号,簇号是分区内部的相对编号;分区自己又是整块盘里的一个区间。想知道某个 txt 文件的字节究竟躺在哪一格磁介质上,得把三层的地址一段段叠起来:MBR 里的分区表给出分区起点,DBR 里的 BPB 给出 FAT 表和数据区布局,目录项里给出起始簇号,最后才换算出物理扇区。这套链条在数据恢复、磁盘取证、坏盘抢救里是唯一的坐标系,搞不清它,工具再强也只是个黑匣子。
2. MBR 与分区表:0 号扇区里那 64 个字节是怎么切盘的
2.1 446 加 64 加 2:一个扇区被切成三段
整块磁盘的第一个扇区,也就是 0 号扇区,在传统 MBR 分区方案里承担着“目录页”的角色。它的 512 个字节并不是随意堆放的,而是严格分成三段:前 446 字节是引导代码,接着 64 字节是分区表,最后 2 字节是固定签名 0x55AA。三段各有各的职责,也各有各的历史包袱。
| 区域 | 偏移 | 长度 | 作用 |
|---|---|---|---|
| 引导代码 | 0x000 - 0x1BD | 446 字节 | 早期用于跳转并加载活动分区的引导记录 |
| 分区表 | 0x1BE - 0x1FD | 64 字节 | 4 个分区表项,每项 16 字节 |
| 结束标识 | 0x1FE - 0x1FF | 2 字节 | 0x55AA,供 BIOS 判断该扇区是否可引导 |
最容易忽略的是最后那 2 个字节。0x55AA 不是校验和,也不是版本号,它只是“这个扇区的最后两个字节恰好是这两个值”的一个标志。BIOS 在开机自检阶段读入 0 号扇区后会检查这个签名,不匹配就直接判定为不可引导设备。反过来说,做磁盘分析时它也是极好用的锚点:你在 WinHex 里跳到 0 号扇区,看到最后两个字节不是 55 AA,那基本可以断定你读的位置不对,或者这块盘用的是 GPT 而保护 MBR 被改写过。
提示:GPT 磁盘的 0 号扇区通常是一个类型为 0xEE 的“保护 MBR”,整块盘被描述成一个覆盖全盘的分区,目的是让不认识 GPT 的老工具不要误以为这是一块空盘而重新分区。
2.2 16 字节分区表项:起始扇区藏在偏移 8
64 字节分成 4 份,每份 16 字节,对应一个主分区。这 16 个字节的内部结构从 1980 年代沿用至今,字段安排相当紧凑:
| 偏移(项内) | 长度 | 含义 |
|---|---|---|
| 0x00 | 1 字节 | 状态:0x80 表示活动分区,0x00 表示非活动 |
| 0x01 | 3 字节 | 起始 CHS 地址(柱面/磁头/扇区) |
| 0x04 | 1 字节 | 分区类型:0x0B/0x0C 为 FAT32,0x07 为 NTFS,0xEE 为 GPT 保护 |
| 0x05 | 3 字节 | 结束 CHS 地址 |
| 0x08 | 4 字节 | 分区起始扇区号(LBA),小端序 |
| 0x0C | 4 字节 | 分区总扇区数,小端序 |
真正有用的是偏移 8 开始的 4 字节。前面那两组 CHS 字段在超过 8GB 的盘上早就溢出了——CHS 用 10 位表示柱面,最大只能编到 1023,按当时的几何换算大概 7.8GB 封顶。所以后来的工具和系统基本都只看 LBA 字段,CHS 字段留着只是为了老 BIOS 能在过渡年代启动系统。
在 WinHex 里看这 16 个字节,最直观的方式是打开“视图 → 显示 → 数据解释器”,把光标停在偏移 0x1C6(也就是 0x1BE + 8)上,解释器会直接把 4 个字节按小端解释成十进制数。分区起始扇区是 2048,对应的十六进制就是 00 08 00 00,反过来读就是 0x00000800。
用代码验证一遍更踏实:
import struct DISK = r"\\.\PhysicalDrive0" # Linux 下换成 /dev/sda with open(DISK, "rb") as fp: mbr = fp.read(512) if mbr[510:512] != b"\x55\xAA": raise SystemExit("签名不匹配,读到的不一定是 MBR") for i in range(4): ent = mbr[446 + i * 16 : 446 + (i + 1) * 16] ptype = ent[4] if ptype == 0x00: continue # 空表项直接跳过 lba = struct.unpack("<I", ent[8:12])[0] # 分区起始扇区(LBA) count = struct.unpack("<I", ent[12:16])[0] # 分区总扇区数 print(f"分区{i+1} 类型=0x{ptype:02X} 起始={lba} 扇区数={count}")这段代码里有两个细节值得说明。struct.unpack("<I", ...)中的<表示小端序,磁盘上的多字节整数几乎都是小端存放,如果你按大端读,2048 会被读成 0x00080000,一个完全无意义的巨大数字。另外我用ptype == 0x00来跳过空表项,而不是用“起始扇区为 0”来判断,因为理论上存在起始扇区写 0 的畸形分区表,按类型判断更稳。
2.3 为什么第一个分区偏偏从 2048 开始
拆开一块盘,你会发现绝大多数磁盘的第一个分区起始 LBA 都是 2048,这个数字稳定得像约定俗成。2048 × 512 字节 = 1MB,也就是说 0 号扇区到 2047 号扇区这 1MB 空间被完整空了出来。
这不是浪费。早期机械盘时代,分区起始扇区按“柱面对齐”来放,第一个分区常常从 63 号扇区开始,因为一条磁道是 63 个扇区,这样分区边界正好落在柱面开头。到了固态盘和大容量机械盘时代,物理扇区从 512 字节变成 4096 字节,闪存的擦除块更大,如果一个 4KB 的逻辑页跨越了两个物理页,每次写入都要先读-改-写两次,性能直接腰斩。1MB 是 512、4096 乃至更大擦除块的公倍数,从 2048 起步天然满足所有对齐要求。
空出来的这 1MB 还顺便安置了引导代码、GPT 头、以及一些引导管理器的 stage 1.5。所以你在分析时看到第一个分区从 2048 起,不要以为是巧合,这是几十年踩坑之后形成的工程惯例。
3. DBR 与 BPB:分区自己的“第 0 扇区”里写了什么
3.1 两个坐标系:分区内偏移与物理偏移
拿到 2048 之后,第一个容易翻车的地方就来了。2048 是物理磁盘上的绝对扇区号,而从这一刻起,DBR 里记录的所有数字都是“分区内相对扇区号”。两套坐标系同时存在,是从这里开始贯穿全文的主线。
用 WinHex 的操作来说:在物理磁盘视图下跳到 2048,看到的是这个分区的第一个扇区,也就是 DBR(DOS Boot Record)。它等价于分区内的 0 号扇区。DBR 里有一个字段叫“隐藏扇区数”,记录的值正好就是 2048,它就是连接两套坐标系的桥。有些分析工具懒得读 MBR,直接从这个字段反推分区起点,就是这个道理。
DBR 本身还有备份。FAT32 规定在分区的第 6 号扇区保存一份 DBR 副本,第 7 号扇区保存 FSInfo 的副本。主 DBR 被病毒或误操作写坏时,把第 6 扇区的内容拷回 0 号扇区往往能让分区重新被识别——这是数据恢复里成本最低的一招。
3.2 BPB 关键字段:决定整条地址链的几个数字
DBR 的前 90 个字节左右是 BPB(BIOS Parameter Block),文件系统的几何参数全在这里。FAT32 的几个关键字段:
| 偏移 | 长度 | 字段 | 本例取值 | 作用 |
|---|---|---|---|---|
| 0x0B | 2 | 每扇区字节数 | 512 | 扇区换算的基准 |
| 0x0D | 1 | 每簇扇区数 | 16 | 簇到扇区的乘数 |
| 0x0E | 2 | 保留扇区数 | 3234 | DBR 到第一个 FAT 表之间的间隔 |
| 0x10 | 1 | FAT 表个数 | 2 | 通常双份,互为备份 |
| 0x1C | 4 | 隐藏扇区数 | 2048 | 分区起始 LBA,坐标系的桥 |
| 0x20 | 4 | 总扇区数(32 位) | — | 分区总容量 |
| 0x24 | 4 | 每个 FAT 表扇区数 | 14767 | 第二个 FAT 表和数据区的偏移来源 |
| 0x2C | 4 | 根目录起始簇 | 2 | FAT32 固定为 2,不是固定区域 |
| 0x30 | 2 | FSInfo 扇区号 | 1 | 记录空闲簇数,仅供参考 |
保留扇区数 3234 这个值偏大,正常 FAT32 格式化一般只保留 32 个扇区。这里之所以是 3234,通常是格式化工具按 FAT 表容量做了对齐,或者这块分区是从别的布局调整过来的。不管来源如何,后续所有计算都必须用 BPB 里的实测值,不能套“一般是 32”这种经验值——这正是后面避坑章要展开的一条。
每簇扇区数为 16,意味着一个簇是 8KB。簇越大,FAT 表项越少、表越小,但文件末尾浪费的空间越多。一个 1 字节的文件也要占满 8KB,这就是内部碎片。FAT32 在大容量盘上往往被迫用大簇,是它作为过渡方案的一个结构性缺陷。
3.3 根目录公式是怎么推出来的
FAT32 最反直觉的一点是:根目录不再是一块固定大小的区域。FAT12/16 时代,BPB 里有个“根目录项数”字段,根目录被固定安排在 FAT 表之后,占 512 项、每项 32 字节,合计 32 个扇区,位置永远不变。FAT32 把根目录降级成了普通簇链,起始簇固定为 2,BPB 里的根目录项数字段直接写 0。这个改动让根目录可以像普通目录一样动态增长,但也让“根目录在哪”从一个常量变成了一个算式。
算式本身很朴素,就是把分区内的布局从左到右依次加起来:
分区内布局 = 保留扇区 → FAT 表 1 → FAT 表 2 → 数据区
数据区的第一个扇区,也就是簇 2 的起始位置,等于保留扇区数加上 FAT 表个数乘以每个 FAT 表的扇区数。代入本例:
32768 = 3234 + 2 × 147673234 是保留扇区,两个 FAT 表各占 14767 个扇区。这也顺便解释了为什么根目录簇号是 2 而不是 0 或 1:簇号 0 和 1 被 FAT 表自身占用了,0 号表项是介质描述符,1 号表项存的是脏标志,真正的数据从 2 开始编号。簇 2 就是数据区的第一个簇,正好落在 32768。
用代码把 BPB 完整读一遍:
import struct PART_START = 2048 # 来自 MBR 分区表 with open(DISK, "rb") as fp: fp.seek(PART_START * 512) # 物理偏移 = 分区起点 × 扇区大小 dbr = fp.read(512) bps = struct.unpack("<H", dbr[0x0B:0x0D])[0] # 每扇区字节数 spc = dbr[0x0D] # 每簇扇区数 reserved = struct.unpack("<H", dbr[0x0E:0x10])[0] # 保留扇区数 nfat = dbr[0x10] # FAT 表个数 hidden = struct.unpack("<I", dbr[0x1C:0x20])[0] # 应等于 PART_START fat_size = struct.unpack("<I", dbr[0x24:0x28])[0] # 每个 FAT 表扇区数 root_clus= struct.unpack("<I", dbr[0x2C:0x30])[0] # 根目录起始簇 data_start = reserved + nfat * fat_size # 数据区起始(分区内) print(f"分区起点={hidden} 保留={reserved} FAT数={nfat} " f"FAT大小={fat_size} 数据区起={data_start} 根簇={root_clus}")这段的输出应该是分区起点=2048 保留=3234 FAT数=2 FAT大小=14767 数据区起=32768 根簇=2。注意hidden和PART_START应当一致,如果不一致,说明你从 MBR 读到的分区表和 DBR 里自述的分区位置矛盾,这块盘的结构被动过,后面所有计算都得重新核对。
4. 目录项与簇号:32 个字节如何扛起一个文件的全部身份
4.1 32 字节字段表:文件名的位置比想象中更紧
FAT32 把目录也当成文件来存,目录的内容就是一串 32 字节的目录项,顺序排列,直到遇到首字节为 0x00 的项表示结束。每个目录项描述一个文件或子目录:
| 偏移 | 长度 | 含义 |
|---|---|---|
| 0x00 | 8 | 主文件名,8.3 格式,不足补空格 |
| 0x08 | 3 | 扩展名 |
| 0x0B | 1 | 属性:0x10 目录、0x20 归档、0x0F 长文件名项 |
| 0x0C | 1 | NT 保留,一般置 0 |
| 0x0D | 1 | 创建时间的 10 毫秒位 |
| 0x0E | 2 | 创建时间 |
| 0x10 | 2 | 创建日期 |
| 0x12 | 2 | 最后访问日期 |
| 0x14 | 2 | 起始簇号高 16 位 |
| 0x16 | 2 | 最后修改时间 |
| 0x18 | 2 | 最后修改日期 |
| 0x1A | 2 | 起始簇号低 16 位 |
| 0x1C | 4 | 文件大小(字节) |
在 WinHex 里把光标放到根目录区,每 32 字节就是一个整齐的分组。首字节为 0xE5 表示这个目录项已被删除,文件名首字节为 0x00 表示后面没有更多项了。子目录里的 “.” 和 “..” 也是真实的目录项,分别指向自己和父目录,别当成普通文件名处理。
本例中,根目录区里能看到一个卷标项、一个自动创建的 docs 目录项,以及后续在 docs 目录里找到的 note.txt 目录项。卷标项的属性字节是 0x08,它占一个目录项位置但不代表任何文件,遍历时按属性过滤掉即可。
4.2 起始簇号为什么被劈成两半
FAT32 的簇号是 32 位整数,但目录项里只给了两个 16 位的槽:0x14 处 2 字节存高位,0x1A 处 2 字节存低位。中间隔着修改时间、修改日期这些字段,看起来毫无章法。原因是这套目录项格式定型的年代,32 位簇号还远没有普及,设计师需要留出位置给时间戳,后来扩到 32 位簇号时只能在原有结构里找空隙塞。
拼装公式是:
起始簇号 = 低 16 位 + 高 16 位 × 6553665536 就是 2 的 16 次方,高位的权重。本例中 docs 目录的簇号是 6,note.txt 的簇号是 7,两个值都很小,高位字段全是 0,所以只看低 2 字节也能算对。这是最阴险的一个陷阱:在小分区上练习时一切正常,换到用了几百 GB 的盘上,高位不为零,只读低位就会算出一个完全错误的簇号,然后指向一片毫无意义的数据。
def parse_entry(raw): if raw[0] in (0x00, 0xE5): return None # 结束标记或已删除 if raw[0x0B] == 0x0F: return None # 长文件名项,单独处理 name = raw[0x00:0x08].decode("ascii", "replace").rstrip() ext = raw[0x08:0x0B].decode("ascii", "replace").rstrip() attr = raw[0x0B] hi = struct.unpack("<H", raw[0x14:0x16])[0] # 簇号高 16 位 lo = struct.unpack("<H", raw[0x1A:0x1C])[0] # 簇号低 16 位 size = struct.unpack("<I", raw[0x1C:0x20])[0] return {"name": name + "." + ext, "attr": attr, "cluster": lo + hi * 65536, "size": size}返回的cluster就是后面一切计算的起点。attr保留下来是为了区分目录(0x10)和普通文件(0x20),两者在后续遍历时的处理逻辑不同:目录的内容是更多目录项,文件的内容才是真正的数据。
4.3 从簇号到物理扇区:三次加法,缺一不可
现在手上有了簇号,要算出物理扇区,还得做两步换算。第一步把簇号换成数据区内的偏移,第二步把分区内偏移换成磁盘绝对位置。
簇号到数据区偏移要减 2,因为簇 0 和簇 1 不存在于数据区。簇 N 的数据区偏移等于(N - 2) × 每簇扇区数。docs 目录簇号 6,(6 - 2) × 16 = 64,数据区起始 32768,所以分区内扇区号是 32832。note.txt 簇号 7,(7 - 2) × 16 = 80,分区内扇区号 32848。
分区内到物理要加分区起点 2048。docs 目录的物理扇区是32832 + 2048 = 34880,note.txt 的物理扇区是32848 + 2048 = 34896。
def cluster_to_abs(part_start, data_start, cluster, spc): """part_start: MBR 给出的分区起始 LBA data_start: BPB 算出的数据区起始(分区内) cluster: 目录项里的 32 位簇号 spc: 每簇扇区数""" return part_start + data_start + (cluster - 2) * spc print(cluster_to_abs(2048, 32768, 6, 16)) # 34880,docs 目录 print(cluster_to_abs(2048, 32768, 7, 16)) # 34896,note.txt 文件两个结果和 WinHex 里跳转看到的完全一致。注意 note.txt 也可以用另一种算法验证:docs 目录在 34880,1 簇等于 16 个扇区,如果两个簇在 FAT 表里是连续的,下一个簇的起点就是34880 + 16 = 34896。两种算法一致,说明 FAT 链在这里是顺序分配的。如果两者不一致,说明文件被碎片化了,FAT 表里记录的下一簇号跳到了别处,这时候必须沿着 FAT 链一跳一跳地走。
4.4 全链条复算:把六个关键数字串成一条线
| 步骤 | 数据来源 | 数值 |
|---|---|---|
| 分区起始扇区 | MBR 分区表偏移 8 | 2048 |
| 保留扇区数 | DBR BPB 偏移 0x0E | 3234 |
| FAT 表个数 | DBR BPB 偏移 0x10 | 2 |
| 每个 FAT 表扇区数 | DBR BPB 偏移 0x24 | 14767 |
| 数据区起始 | 3234 + 2 × 14767 | 32768 |
| 每簇扇区数 | DBR BPB 偏移 0x0D | 16 |
| docs 目录簇号 | 目录项偏移 0x1A | 6 |
| docs 物理扇区 | 2048 + 32768 + (6-2)×16 | 34880 |
| note.txt 簇号 | docs 目录区内目录项 | 7 |
| note.txt 物理扇区 | 2048 + 32768 + (7-2)×16 | 34896 |
这张表就是整篇文章的核心。每一个数字都能在 WinHex 的十六进制视图里指出来,每一步运算都能用代码复现。如果你能从任意一块 FAT32 盘出发,把这张表重新填一遍并和工具跳转的结果对上,这条地址链就算真正掌握了。
5. 避坑:这五处最容易翻车
5.1 直接跳 32768 看到一片空白
现象:按公式算出根目录在 32768,在 WinHex 里跳过去,看到的是一堆零或者不相干的数据,完全找不到目录项。
原因:32768 是分区内扇区号,不是物理扇区号。WinHex 在物理磁盘视图下跳转是按绝对 LBA 解释的,它不知道你心里的坐标系是分区内的。
解决:加上分区起始扇区 2048,跳 34816 才能看到根目录的起始。判断自己有没有踩这个坑有个简单办法:跳过去之后看首个目录项的位置,如果整片区域全是 0x00,十有八九是偏移错了,因为真实的根目录不可能从第一字节开始就是空的。
5.2 拿簇号当扇区号直接用
现象:从目录项里读到簇号 6,直接在 WinHex 里跳到 6 号扇区,看到的是 MBR 里的引导代码,根本不是目标目录。
原因:簇号和扇区号是两个量纲。簇号是数据区内的逻辑编号,从 2 起算;扇区号是物理地址,从 0 起算。中间的换算要经过“减 2 乘以每簇扇区数”和“加数据区起始”两道手续。
解决:永远走完整公式,不要图省事。临时用手算时,先把data_start和spc两个值写在纸上,再代入簇号,能避免大部分心算错误。
5.3 只读簇号低 2 字节
现象:在小分区上演练时一切正常,换到一块用得很满的大盘上,算出来的簇号跳过去是别人的数据,或者干脆落在文件系统管理区里。
原因:目录项里簇号低 2 字节在偏移 0x1A,高 2 字节在偏移 0x14,中间隔着好几个时间字段。很多手写脚本图快只读 0x1A,在小盘上高位恰好是 0,看不出问题;簇号超过 65535 之后高位非零,结果就错了。
解决:拼装公式写死,lo + hi * 65536,不要有任何“高位一般是 0”的假设。写完脚本可以拿一个大文件试一下,看算出的簇号是否大于 65535,能覆盖到高位就说明代码没问题。
5.4 把 FAT32 的根目录当成固定区域
现象:照着 FAT16 的资料,把根目录位置算成“FAT 表之后固定 32 个扇区”,结果遍历出来的目录项错乱,文件名读出来是一堆乱码。
原因:FAT32 的根目录已经改成普通簇链,BPB 里的根目录项数字段是 0,根目录起始簇号写在偏移 0x2C,通常是 2。套用 FAT12/16 的固定区域模型必然出错。
解决:先看 BPB 偏移 0x52 处的文件系统类型字符串是不是 “FAT32 ”,确认之后再按簇链方式读根目录。用簇链方式读还有个好处:根目录项数超过一个簇时能自动沿着 FAT 表继续往下读,不会读一半就断掉。
5.5 默认每扇区 512 字节
现象:在一块较新的盘上,所有偏移量都算错,跳转位置系统性地偏移了一截,而且偏移量随扇区号增大而线性放大。
原因:部分磁盘的物理扇区是 4096 字节,虽然逻辑扇区对外仍报告 512,但如果你的工具或脚本按 4096 去乘偏移,或者反过来在 4K 原生盘上按 512 乘,都会整体错位。
解决:偏移计算里的扇区大小一律从 BPB 的 0x0B 字段读,不要硬编码 512。读取物理磁盘时也要确认工具报告的逻辑扇区大小,WinHex 的状态栏会显示当前扇区尺寸,跳转前扫一眼能省掉大量排查时间。
6. 设计取舍:为什么文件系统不肯直接记住物理扇区
把整条链路走完,一个自然的疑问会浮上来:既然每次访问文件都要做这么多次加法和一次 FAT 链遍历,为什么不干脆在目录项里直接存物理扇区号,一步到位?
答案藏在“分区是可以整体搬家的”这件事里。一块盘做镜像克隆到另一块盘,分区起始 LBA 可能从 2048 变成 4096;用工具调整分区大小,数据区整体后移;把分区做成镜像文件挂载,物理位置根本不存在。如果目录项里存的是物理扇区号,上述每一个操作都要把整盘所有目录项重写一遍,成本高到不可接受。存簇号则完全解耦:簇号是分区内的相对编号,分区怎么平移,簇号都不变,只要改一下 MBR 里的一个 4 字节字段就行。
代价也很明确。每次文件访问都要经过“目录项 → 簇号 → FAT 链 → 数据区偏移 → 分区偏移 → 物理扇区”这条链,链上任何一环出错,指向的就是完全错误的数据,而且错误往往不会报错,只会静默地读到别人的内容。这就是为什么数据恢复这行当里,交叉验证是铁律:BPB 算出的数据区起始、FAT 表里记录的簇链、目录项里的簇号,三处必须能互相印证。
另一个取舍是 FAT 链本身的形式。FAT32 用链表串起文件的各个簇,优点是实现简单、删除文件只需改一个表项;缺点是随机访问要先走一遍链,大文件读到尾部时前面的表项全被扫过。后来的文件系统改用索引结构,把数据块地址直接放在 inode 里,多级指针一跳到位,本质上是拿空间换随机访问性能。FAT32 单文件 4GB 的上限也是同一批取舍的产物:文件大小字段只有 4 字节,目录项格式一旦定下来,再改就要破坏兼容性。
我一般会这么理解这套机制:它不是设计得不好,而是在 1980 年代的硬件约束下做出了当时最优的解,然后被兼容性一路绑到了今天。也正因为如此,理解它的每一层偏移,比记住任何工具的操作步骤都值钱。
从那以后我每次分析一块盘,都强制先把三个锚点写下来对着看:分区起始 LBA、数据区起始扇区、根目录簇号。三个数字对不上,就说明这块盘的结构被动过,绝不往下算。希望帮到你。
本文还有配套的精品资源,点击获取