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

发布时间:2026/9/11 20:00:15

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/9/11 4:46:10

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

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

2026/9/12 4:29:47

PyMC 采样与推断方法实战指南:MCMC、变分推断与诊断调优

PyMC 采样与推断方法实战指南:MCMC、变分推断与诊断调优 【免费下载链接】scientific-agent-skills Turn any AI agent into an AI Scientist. The #1 Agent Skills library for science, used by 190,000 scientists worldwide. 165 ready-to-use validated skills…

2026/9/12 4:29:47

bd recall 命令深度指南:用 Beads 按 key 检索持久记忆

bd recall 命令深度指南&#xff1a;用 Beads 按 key 检索持久记忆 【免费下载链接】beads Beads - A memory upgrade for your coding agent 项目地址: https://gitcode.com/GitHub_Trending/beads1/beads 导读 bd recall <key> 是 Beads 持久记忆体系&#xff…

2026/9/12 4:29:46

ToF相机全链路解析:从光子到点云的硬件-驱动-应用协同

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

2026/9/12 4:29:46

风光储并网系统Simulink建模与协同控制策略

1. 项目背景与核心价值风光储并网系统作为新能源电力领域的重要研究方向&#xff0c;其仿真建模对实际工程应用具有关键指导意义。这个Simulink模型研究项目聚焦永磁风机、光伏阵列与储能系统的协同运行机制&#xff0c;正是当前微电网和智能电网技术发展的前沿课题。在实际工程…

2026/9/12 2:05:33

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊&#xff01;#雷神 #复联”这类调侃式短标题&#xff0c;第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里&#xff0c;但细想一下就能发现&#xff0c;它真正碰到的根本不是…

2026/9/12 3:55:12

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊&#xff0c;可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”&#xff0c;你会发现&#xff0c;这场比较本质上是两个不同 IP 策略的长期结果对比&#xff1a;超人赢在定义了整个超级英雄题材…

2026/9/9 16:31:09

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介&#xff1a;本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案&#xff0c;聚焦调制信号自动检测与识别这一典型无线通信任务&#xff0c;解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件&#xff08;10.73MB&#xff09;&…

2026/9/12 0:04:17

MATLAB仿生优化框架:长鼻浣熊算法多策略融合实现

简介&#xff1a;本资源是一份面向智能优化算法研究者与MATLAB初学者的仿生智能算法实践代码包&#xff0c;聚焦于长鼻浣熊优化算法&#xff08;COA&#xff09;的多策略改进与性能验证。针对传统COA易陷局部最优、收敛精度不足等问题&#xff0c;作者融合Circle映射初始化提升…

2026/9/12 0:04:17

【JAVA毕设源码分享】基于 JavaWeb 的校园一卡通管理系统的设计与实现 基于 JavaWeb 的校园卡业务管理系统(程序+文档+代码讲解+一条龙定制)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

2026/9/12 0:04:17

【JAVA毕设源码分享】基于 Java 的图书馆借阅管理平台的搭建与实现 基于 Java 的图书馆综合管理系统(程序+文档+代码讲解+一条龙定制)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

2026/9/10 12:32:02

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行&#xff0c;Type-C接口算是典型的“看着简单&#xff0c;做起来全坑”的东西。光引脚就24个&#xff0c;高低速信号、电源、控制线全部塞在一个小小的连接器里&#xff0c;如果PCB布局不做规划&#xff0c;打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/10 15:19:50

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/10 15:49:53

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

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

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

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