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年最新英语写作批改平台挑选指南 附实用避坑技巧

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

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

2026/7/23 0:19:29 阅读更多 →
Java volatile 到底解决什么问题:可见性、禁止重排与双重检查锁单例

Java volatile 到底解决什么问题:可见性、禁止重排与双重检查锁单例

Java volatile 到底解决什么问题:可见性、禁止重排与双重检查锁单例 面试问 volatile,十有八九会答「保证可见性、不保证原子性」。这话没错,但真到写代码时就懵了:什么时候该加 volatile?为什么单例的双重检查锁一定要加它?加了 volatile 的 count 为什么还是会错?这篇把这…

2026/7/23 0:18:29 阅读更多 →
Agent编排:别再纠结选LangChain还是LangGraph了

Agent编排:别再纠结选LangChain还是LangGraph了

内容速览章节核心内容一、编排到底是什么Workflow vs Agent、编排在架构中的位置二、七种编排模式Prompt Chaining / Routing / Parallelization / ReAct / Plan-and-Execute / Orchestrator-workers / Evaluator-Optimizer三、生产环境绕不开的问题会话管理、并发控制、错误分…

2026/7/23 0:18:29 阅读更多 →

最新新闻

非接触式便携微型生理变异性监测设备的技术现状

非接触式便携微型生理变异性监测设备的技术现状

1. 执行摘要 本报告旨在系统性地回答一个核心问题:在当前的消费级与医疗级市场中,是否存在已通过权威认证、能够准确可靠地检测心率、呼吸频率及其变异性(HRV/RRV)的非接触式、便携式、微型化生理监测设备。若不存在,本…

2026/7/23 0:54:40 阅读更多 →
YOLOX 自定义目标检测训练实战:VOC 数据、训练评估与预测展示

YOLOX 自定义目标检测训练实战:VOC 数据、训练评估与预测展示

YOLOX 自定义目标检测训练实战:VOC 数据、训练评估与预测展示 这篇教程根据我复现 YOLOX 目标检测流程时整理,重点演示依赖安装、VOC 数据接入、类别配置、训练评估和单图预测可视化。 本文整理自我的学习和项目复现过程,尽量按实操顺序保留…

2026/7/23 0:53:40 阅读更多 →
Java 继承②

Java 继承②

目录1. super 关键字2. super 和 this3. this和super访问时的内存布局1. super 关键字 在父类成员变量和子类成员变量名字相同的情况下,为了在子类中访问父类的成员。 super关键字的用法 : super.父类成员变量 class Person{String name;int age 10;public void …

2026/7/23 0:53:40 阅读更多 →
Kafka与Zookeeper集群部署实战指南

Kafka与Zookeeper集群部署实战指南

1. Kafka与Zookeeper集群部署核心解析Kafka作为分布式消息系统的标杆,其高吞吐、低延迟的特性使其成为现代大数据架构的核心组件。而Zookeeper作为Kafka的"中枢神经系统",负责维护集群元数据、选举控制器节点以及监控Broker状态。这套组合在金…

2026/7/23 0:53:40 阅读更多 →
LangChain Memory 记忆系统:让 AI 记住对话的魔法

LangChain Memory 记忆系统:让 AI 记住对话的魔法

引言:为什么 AI 需要“记忆”? 在与 ChatGPT 等大模型对话时,你是否曾感到一丝“挫败”?你刚刚在上一轮对话中详细说明了需求,下一轮它却仿佛失忆般问道:“您能再详细描述一下吗?” 这种“金鱼式…

2026/7/23 0:52:40 阅读更多 →
含多类型V2G电动汽车的微网日前-日内两阶段协同优化调度模型研究(Matlab代码实现)

含多类型V2G电动汽车的微网日前-日内两阶段协同优化调度模型研究(Matlab代码实现)

💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 &#x1f381…

2026/7/23 0:52:40 阅读更多 →

日新闻

从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表)

从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表)

更多请点击: https://intelliparadigm.com 第一章:从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表) 当AI副业主理人不再仅满足于单次服务交付,而是主动构建可复用、可裂变、可…

2026/7/23 0:00:25 阅读更多 →
AI写作开头钩子设计:为什么你的AI文案完读率不足18%?——基于2,346篇A/B测试报告的归因分析

AI写作开头钩子设计:为什么你的AI文案完读率不足18%?——基于2,346篇A/B测试报告的归因分析

更多请点击: https://codechina.net 第一章:AI写作开头钩子设计:为什么你的AI文案完读率不足18%?——基于2,346篇A/B测试报告的归因分析 在对2,346篇跨行业AI生成文案的A/B测试数据进行聚类分析后,我们发现&#xff1…

2026/7/23 0:01:26 阅读更多 →
Chitchatter完整指南:免费开源的终极点对点安全聊天工具

Chitchatter完整指南:免费开源的终极点对点安全聊天工具

Chitchatter完整指南:免费开源的终极点对点安全聊天工具 【免费下载链接】chitchatter Secure peer-to-peer chat that is serverless, decentralized, and ephemeral 项目地址: https://gitcode.com/gh_mirrors/ch/chitchatter Chitchatter是一款革命性的安…

2026/7/23 0:01:26 阅读更多 →

周新闻

Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/22 8:58:19 阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/22 19:43:43 阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/22 12:54:44 阅读更多 →

月新闻