200行代码硬核读取ext4磁盘文件

发布时间:2026/11/5 6:08:49

200行代码硬核读取ext4磁盘文件 我们可能听说过很多文件系统比如 FAT32、NTFS、EXT4在linux 下 EXT4 是一个使用比较广泛的文件系统前面的小节我们简单介绍了 EXT4 文件系统的内部结构这个小节我们用硬核方式读取 EXT4 文件系统下/etc/hosts文件以深入理解文件系统的底层原理。关于 ext4 文件系统的设计可以参考官网的介绍 www.kernel.org/doc/html/la…首先我们通过 df 或者 mount 命令查看/挂载在哪个盘以我的测试机为例根目录/挂载在/dev/sda2下df -Th / Filesystem Type Size Used Avail Use% Mounted on /dev/sda2 ext4 117G 104G 6.8G 94% /此时我们就可以使用文件 io 来读取/dev/sda2了。file, err : os.OpenFile(/dev/sda2, os.O_RDONLY, 0755)在开始读取 /dev/sda2 裸盘之前先来写一个简单的工具结构体SeekableReadertype SeekableReader struct { io Reader offset int64 }定义了下面这几个工具函数放在在特定 offset 处读取数据func (sr *SeekableReader) ReadBytes(pos int64, bytes []byte) int; func (sr *SeekableReader) ReadUint8(offset int64) uint8; func (sr *SeekableReader) ReadUint16(offset int64) uint16; func (sr *SeekableReader) OffSet(n int64); func (sr *SeekableReader) ClearOffSet();superblock第一件事情就是读取超级块 superblock它的偏移量为 1024从这里读取我们关心的 superblock 的字段type SuperBlock struct { magic uint16 // 魔数0xEF53 blockSize uint64 // 块大小偏移量为 0x18 blocksPerGroup uint64 // 每个块组中的块数偏移量为 0x20 inodesPerGroup uint64 // 每个块组中的 inode 数偏移量为 0x28 inodeSize uint64 // 每个 inode 的大小偏移量: 0x58 }superblock 布局结构如下图所示根据上图对应的 offset 和 size 来读取 superblockconst Superblock0Offset int64(1024) func NewSuperBlock(r SeekableReader) SuperBlock { r.OffSet(Superblock0Offset) magic : r.ReadUint16(0x38) // s_log_block_size n : r.ReadUint32(0x18) // Block size is 2 ^ (10 s_log_block_size). blockSize : 1 (10 n) blocksPerGroup : r.ReadUint32(0x20) inodesPerGroup : r.ReadUint32(0x28) inodeSize : r.ReadUint16(0x58) return SuperBlock{ magic: magic, blockSize: uint64(blockSize), blocksPerGroup: uint64(blocksPerGroup), inodesPerGroup: uint64(inodesPerGroup), inodeSize: uint64(inodeSize), } }运行打印 superblock 的值{magic:61267 blockSize:4096 blocksPerGroup:32768 inodesPerGroup:8192 inodeSize:256}可以看到这个 superblock 的魔数为 612670xEF53block 大小为 4kB每个 BlockGroup 中块的个数为 32768每个 Block group 中的 inode 数量为 8192 个每个 inode 大小为 256 字节。Block groupBlock group 是一组连续的块这些块被组织在一起以便于管理和分配。每个 block group 包含的数据结构包括块位图 (Block Bitmap): 用于跟踪 block group 中哪些块是已分配的哪些是空闲的。inode 位图Inode Bitmap): 用于跟踪 block group 中哪些 inode 是已分配的哪些是空闲的。inode 表 (Inode Table): 存储文件和目录的元数据数据块 (Data Blocks): 用于存储实际的文件数据和目录内容根据上面的输出我们可以知道 block 大小为 4K每个 block group 包含 32768 个 block可以计算出每个 Block group 的大小为 4K*32768 128MB。每个block group都有对应的 group descriptor,用于记录该block group的元数据,如 inode 表、block bitmap和inode bitmap的位置等。Block group descriptorBlock group descriptor 它描述了每个 block group 的元数据信息:block bitmap 的块号inode bitmap 的块号inode 表起始块号重点关注空闲块数空闲inode数磁盘布局结构如下所有 block group 的 group descriptor 信息存储在一起,形成 group descriptor table该表紧跟在 superblock 之后。接下来我们来读取 / 根目录 inode通过 ext4 的文档我们可以发现系统内置的特殊的 inode 编号根目录的 inode 号为 2。文档中还规定了如何找到 inodeEach block group contains sb-s_inodes_per_group inodes. Because inode 0 is defined not to exist, this formula can be used to find the block group that an inode lives in: bg (inode_num - 1) / sb-s_inodes_per_group. The particular inode can be found within the block groups inode table at index (inode_num - 1) % sb-s_inodes_per_group. To get the byte address within the inode table, use offset index * sb-s_inode_size.公式bg (inode_num - 1) / sb-s_inodes_per_group用于计算给定 inode 编号所在的 block group公式index (inode_num - 1) % sb-s_inodes_per_group用于计算 inode 在块组的 inode 表中的索引位置。公式offset index * sb-s_inode_size用于计算 inode 在 inode 表中的字节偏移量InodeInode 的结构如下offsetsizenamedesc0x0__le16i_modemode0x4__le32i_size_loLower 32-bits of size in bytes0x6C__le32i_size_highUpper 32-bits of file0x2860 bytesi_blockBlock map or extent tree定义 Inode 数据结构type Inode struct { mode uint16 size uint64 block []byte sb *SuperBlock }根据上面的 inode 读取公式我们来实现一个读取 inode 的方法func NewInode(r *SeekableReader, sb *SuperBlock, inodeNumber uint64) Inode { // The blockGroupNumber of the block group containing an inode // can be calculated as (inode_number - 1) / sb.s_inodes_per_group, blockGroupNumber : (inodeNumber - 1) / sb.inodesPerGroup // group descriptor table 开始地址跳过 superblock // 每个 block group descriptor 的长度为 64 gdtOffset : sb.blockSize blockGroupNumber*64 r.OffSet(int64(gdtOffset)) // 读取低 32 位地址 lo : r.ReadUint32(0x8) // 读取高 32 位地址 hi : r.ReadUint32(0x28) inodeTableOffset : uint64(lo) (uint64(hi))32 // 将 inode 表的地址乘以块大小得到 inode 表在磁盘上的偏移量 inodeTableOff : inodeTableOffset * sb.blockSize // 根据公式 index (inode_num - 1) % sb-s_inodes_per_group inodeIdxInTable : (inodeNumber - 1) % sb.inodesPerGroup // 将 inode 表的偏移量加上 inode 在表中的索引乘以 inode 大小得到 inode 在磁盘上的实际偏移量 inodeOff : inodeTableOff inodeIdxInTable*sb.inodeSize // 偏移量设置为 inode 的偏移量以便读取 inode 的信息 r.OffSet(int64(inodeOff)) // 读取 inode 的信息 mode : r.ReadUint16(0) size : r.ReadU64LoHi(0x4, 0x6C) buffer : make([]byte, 60) r.ReadBytes(0x28, buffer) return Inode{ mode: mode, size: size, block: buffer, sb: sb, } }写一个测试代码读取根目录的 inoderootInode : NewInode(r, sb, 2) fmt.Printf(rootInode: %v\n, rootInode)输出如下rootInode: {mode:16877 size:4096 block:[10 243 1 0 4 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 48 36 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] sb:0xc00007e000}Extent header不管是叶子节点还是索引节点最开始的 12 字节总是一个名为 ext4_extent_header 结构用来存储 extents 的信息extent header 的结构如下OffsetSizeNameDescription0__le16eh_magicMagic number, 0xF30A.2__le16eh_entriesNumber of valid entries following the header.4__le16eh_maxMaximum number of entries that could follow the header.6__le16eh_depthDepth of this extent node in the extent tree. 0 this extent node points to data blocks; otherwise, this extent node points to other extent nodes. The extent tree can be at most 5 levels deep: a logical block number can be at most 2^32, and the smallest n satisfies*(((blocksize-12)/12)^n)2^32is 5.8__le32eh_generationGeneration of the tree. (Used by Lustre, but not standard ext4).简化定义 ExtentHeader 如下type ExtentHeader struct { entries uint64 depth uint64 }从 Inode 中读取 extent headerfunc (i *Inode) ReadExtentHeader() ExtentHeader { r : SeekableReader{io: bytes.NewReader(i.block), offset: 0} magic : r.ReadUint16(0) // Magic number, 0xF30A. fmt.Printf(magic: 0x%x\n, magic) entries : r.ReadUint16(0x02) // Number of valid entries following the header. depth : r.ReadUint16(0x06) // Depth of this extent node in the extent tree. return ExtentHeader{ entries: uint64(entries), depth: uint64(depth), } }Extent 结构Extent 的作用是表示一个连续的文件块它包含了两个重要的信息起始位置ee_block和文件块数ee_len起始位置表示了文件的连续块在磁盘的起始位置文件块数表示了文件连续块的数量。通过 ext4_extent文件系统可以将文件内容存储为一块连续的区域减少了内存碎片的产生。Extent 结构如下OffsetSizeNameDescription0__le32ee_blockFirst file block number that this extent covers.4__le16ee_lenNumber of blocks covered by extent. If the value of this field is 32768, the extent is initialized. If the value of the field is 32768, the extent is uninitialized and the actual extent length is ee_len - 32768. Therefore, the maximum length of a initialized extent is 32768 blocks, and the maximum length of an uninitialized extent is 32767.6__le16ee_start_hiUpper 16-bits of the block number to which this extent points.8__le32ee_start_loLower 32-bits of the block number to which this extent points.简化定义 Extenttype Extent struct { len uint64 start uint64 } func (i *Inode) ReadExtent(offset int64) Extent { r : NewSeekableReader(bytes.NewReader(i.block), offset) len : r.ReadUint16(0x04) // Number of blocks covered by extent. hi : r.ReadUint16(0x06) // Upper 16-bits of the block number to which this extent points. lo : r.ReadUint32(0x08) //Lower 32-bits of the block number to which this extent points. start : uint64(hi)32 uint64(lo) return Extent{ len: uint64(len), start: start, } }Directory Entries 结构我们这里只介绍线性数组表示的目录结构它包含了目录文件名、inode 编号等重要信息。Directory Entries 结构定义如下OffsetSizeNameDescription0__le32inodeNumber of the inode that this directory entry points to.4__le16rec_lenLength of this directory entry. Must be a multiple of 4.6__le16name_lenLength of the file fileName.8charname[EXT4_NAME_LEN]File name.据此可以简化定义如下type DirEntry struct { recordLen uint64 inode uint64 fileName string } func ReadDirEntry(r *SeekableReader) DirEntry { nameLen : r.ReadUint8(0x06) nameBuf : make([]byte, nameLen) r.ReadBytes(0x08, nameBuf) name : string(nameBuf) return DirEntry{ recordLen: uint64(r.ReadUint16(0x04)), inode: uint64(r.ReadUint32(0x0)), fileName: name, } }接下来实现一个方法从某个 inode 中读取所有的 dir entry。这里简化处理认为处理的 /etc/hosts 各路径都只有一级 extent 结构。const EXTENT_HEADER_SIZE 12 func (i *Inode) dirEntries(r *SeekableReader) []DirEntry { entries : make([]DirEntry, 0) // 从 inode 中读取 extent header extentHeader : i.ReadExtentHeader() fmt.Printf(extentHeader: %v\n, extentHeader) entryCount : extentHeader.entries // 遍历所有的 extent for idx : uint64(0); idx entryCount; idx { extentOffset : EXTENT_HEADER_SIZE * (idx 1) // 读取一个 extent extent : i.ReadExtent(int64(extentOffset)) entryStart : int64(extent.start * i.sb.blockSize) totalEntryLen : int64(extent.len * i.sb.blockSize) offset : int64(0) for offset totalEntryLen { r.OffSet(entryStart offset) // 读取 dir entry entry : ReadDirEntry(r) if entry.inode 0 { break } entries append(entries, entry) offset int64(entry.recordLen) } } return entries }接下来我们在 main 函数中逐级调用函数查询 /、/etc、/etc/host 文件func main() { file, err : os.OpenFile(/dev/sda2, os.O_RDONLY, 0755) if err ! nil { panic(err) } defer file.Close() r : SeekableReader{io: file} sb : NewSuperBlock(r) fmt.Printf(%v\n, sb) // 获取 / 的 inode rootInode : NewInode(r, sb, 2) fmt.Printf(rootInode: %v\n, rootInode) // 获取 / 目录下所有的 dir rootEntries : rootInode.dirEntries(r) for i : range rootEntries { if rootEntries[i].fileName etc { // 读取 /etc 的 inode etcInode : NewInode(r, sb, rootEntries[i].inode) fmt.Printf(etcInode: %v\n, etcInode) // 获取 /etc 目录下所有的 dir etcEntries : etcInode.dirEntries(r) for j : range etcEntries { if etcEntries[j].fileName hosts { fmt.Printf(etcEntries[%d]: %v\n, j, etcEntries[j]) // 读取 /etc/hosts 的 inode hostsInode : NewInode(r, sb, etcEntries[j].inode) extentOffset : 12 // 读取 extent extent : hostsInode.ReadExtent(int64(extentOffset)) offset : int64(extent.start * sb.blockSize) len : extent.len * sb.blockSize println(offset:, offset, len:, len) buf : make([]byte, len) r.ClearOffSet() // 读取文件内容 r.ReadBytes(offset, buf) fmt.Printf(/etc/hosts :\n%s, string(buf)) } } break } } }最终的效果是可以将 /etc/hosts 文件打印出来。完整的代码我放在了小册对应的代码仓库你可以运行一下跑起来逐个分析。
延伸阅读

更多相关文章

2026/11/4 6:37:07

2026年最新英语写作批改平台挑选指南 附实用避坑技巧

文章摘要:随着英语写作批改需求快速增长,市场上涌现了大量智能批改工具,但质量参差不齐,普通用户往往难以甄别。本文旨在帮助读者厘清核心评估维度,从技术能力、场景适配、合规安全等角度系统梳理挑选标准,…

2026/11/5 7:18:05

Hadoop+Spark+Hive构建分布式租房推荐系统实践

1. 项目概述:基于HadoopSparkHive的租房推荐系统在当前的租房市场中,用户常常面临信息过载和推荐不精准的问题。传统平台依赖人工筛选或简单关键词匹配,难以满足个性化需求。作为一名长期从事大数据系统开发的工程师,我设计并实现…

2026/11/5 7:18:05

WPF餐饮管理系统:小餐馆可直接上线的半成品解决方案

简介:这是一套面向C#初学者与WPF进阶开发者的餐饮管理软件学习项目,聚焦多食法支持、触屏交互与手写识别等实际业务场景,适用于课程设计、毕业设计或中小型餐饮系统原型开发。资源包含2000个文件,主体为3231个C#源码(.…

2026/11/5 7:18:05

HBase备份恢复核心技术解析与实践指南

1. HBase备份恢复的核心价值与挑战在金融交易系统和用户行为分析平台的实际运维中,我经历过多次因硬件故障导致RegionServer宕机的情况。最严重的一次是某支付平台的核心HBase集群在业务高峰期发生磁盘阵列故障,当时如果没有完善的备份体系,将…

2026/11/5 7:18:05

RESTCONF与OpenDaylight实现高效网络设备管理

1. 为什么需要RESTCONF替代传统CLI网络设备管理领域正在经历一场静默革命。过去二十年里,网络工程师们习惯了通过CLI(命令行界面)敲入成百上千条命令来配置设备,这种工作方式就像用打字机编写长篇小说——效率低下且容易出错。我曾…

2026/11/5 7:18:04

基于AT89C51与DS18B20的多点温度检测系统设计

简介:这是一份面向电子信息、自动化及嵌入式课程设计学习者的单片机多点温度检测系统设计文档,围绕AT89C51与DS18B20组成的两路温度采集与1602LCD循环显示方案展开,可用于课程设计、毕业设计开题及单片机综合实验参考。压缩包内共1个doc文件&…

2026/11/5 7:13:04

Git误操作急救指南:从撤销到数据恢复

1. Git误操作急救手册:从惊慌到从容的完整指南作为开发者,我们都经历过这样的时刻:手指在键盘上飞舞执行git命令后,突然意识到自己犯了个致命错误——误删了重要文件、提交了错误代码,或者更糟,把混乱的历史…

2026/11/4 12:38:23

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 0:02:07

多智能体协作系统架构设计与实践优化

1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中…

2026/11/5 0:02:07

大模型系统性入门实战指南:从原理到部署微调的全链路路线

提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#…

2026/11/5 0:02:07

鸿蒙文件系统沙箱机制解析与开发实践

1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的…

2026/11/4 12:38:23

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑