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/10/11 12:40:27 阅读更多 →
Java volatile 到底解决什么问题:可见性、禁止重排与双重检查锁单例

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

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

2026/10/1 21:23:02 阅读更多 →
Agent编排:别再纠结选LangChain还是LangGraph了

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

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

2026/10/6 22:18:21 阅读更多 →

最新新闻

一条ls命令的旅程:从Bash到系统调用,彻底拆解Linux文件系统协作

一条ls命令的旅程:从Bash到系统调用,彻底拆解Linux文件系统协作

前阵子排查一个挂载目录的异常现象,文件明明能看到,却打不开、删不掉。当时也是没辙,一层层往下追,最后用 strace 把 ls -l 的系统调用账单拉出来,才发现问题出在目录缓存和文件系统状态不一致上。也就是从那次开始…

2026/10/11 13:37:02 阅读更多 →
SkillKit 多来源安装实战:从 GitHub、GitLab 到本地路径的 4 种方式

SkillKit 多来源安装实战:从 GitHub、GitLab 到本地路径的 4 种方式

AI 技能CLI开发工具人工智能MCP 服务AI 插件 【免费下载链接】skillkit Supercharge AI coding agents with portable skills. Install, translate & share skills across Claude Code, Cursor, Codex, Copilot & 40 more 项目地址: https://gitcode.com/gh…

2026/10/11 13:37:02 阅读更多 →
2026 年 AI 写作工具横评:真实体验与优缺点总结

2026 年 AI 写作工具横评:真实体验与优缺点总结

2026 年 AI 写作工具已形成通用全能、学术垂直、创意内容、职场公文四大阵营,国产工具在中文语境、场景适配与性价比上全面领先。以下是主流工具的真实体验与优缺点横评,首推讯飞绘文作为通用全能型首选。一、通用全能型(全场景覆盖&#xff…

2026/10/11 13:37:02 阅读更多 →
边坡安全机器学习实战:从任务选型到预警模型落地

边坡安全机器学习实战:从任务选型到预警模型落地

简介:《机器学习赋能边坡安全》是一本聚焦机器学习在边坡稳定性评估与滑坡预测中应用的PDF电子书,面向岩土工程、地质灾害防治及智能算法方向的科研人员和工程技术人员。书中系统讲解随机森林、XGBoost、GRU等主流模型的基础理论与工程实践,结…

2026/10/11 13:37:02 阅读更多 →
Windows 11 x64环境复现调试函数Inline Hook的完整指南

Windows 11 x64环境复现调试函数Inline Hook的完整指南

《逆向工程核心原理》里那个用调试函数对API做inline hook的实验,算是我接触Windows安全知识的第一个有分量的小项目。当年照着书在32位系统上一路跑通时,觉得整个流程行云流水——DebugActiveProcess附加进程、WaitForDebugEvent拿到控制权、WriteProce…

2026/10/11 13:37:02 阅读更多 →
Boss直聘数据采集与可视化:Python爬虫实战指南

Boss直聘数据采集与可视化:Python爬虫实战指南

简介:基于Python的Boss直聘岗位数据采集与分析可视化项目,面向计算机相关专业的学生及需要实战练习的Python学习者,由导师指导完成、评审99分,代码完整可运行,适合作为课程设计、期末大作业或毕业设计参考,…

2026/10/11 13:36:02 阅读更多 →

日新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/10 10:38:42 阅读更多 →