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

发布时间:2026/7/23 0:19:56

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:56

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

2026 Cloudflare 5秒盾解决指南:无限验证码、403拦截与Bot检测优化方法

2026 Cloudflare 5秒盾解决指南:无限验证码、403拦截与Bot检测优化方法

2026/7/23 0:19:56

在2026年的企业数字化运营中,公开数据采集、自动化测试、SEO监控已成为核心基础设施。然而,随着 Cloudflare 将其防护机制全面升级为以 Turnstile(无感验证) 和 AI 驱动的行为生物识别(Behavioral Biometrics&#xff…

Redis何时会成为“拖油瓶“?深度解析Redis拖垮应用程序的十大致命场景

Redis何时会成为“拖油瓶“?深度解析Redis拖垮应用程序的十大致命场景

2026/7/23 0:09:56

引言:Redis的双刃剑特性 在现代应用架构中,Redis几乎已经成为标配。它以其卓越的性能、丰富的数据结构和简单易用的API,成为了缓存、会话存储、消息队列等场景的首选。然而,正是这种"好用"的特性,让很多开发…

C++格式化输出全解析:从printf到iostream,打造专业数据展示

C++格式化输出全解析:从printf到iostream,打造专业数据展示

2026/7/23 4:30:12

1. 项目概述:为什么C格式化输出值得深究?在C的日常开发中,尤其是调试、日志记录、数据展示或者开发命令行工具时,我们几乎无时无刻不在和输出打交道。很多初学者,甚至一些有经验的开发者,往往满足于用std::…

为什么你的AI图片总被客户拒稿?揭秘商业摄影效果的4个硬性指标(分辨率/光影逻辑/材质真实度/品牌一致性)

为什么你的AI图片总被客户拒稿?揭秘商业摄影效果的4个硬性指标(分辨率/光影逻辑/材质真实度/品牌一致性)

2026/7/23 4:30:12

更多请点击: https://intelliparadigm.com 第一章:为什么你的AI图片总被客户拒稿? AI生成图像正迅速融入商业设计流程,但大量设计师反馈:客户反复拒稿,理由模糊如“不够真实”“风格不统一”“细节失真”。…

防火墙保护服务器

防火墙保护服务器

2026/7/23 4:30:12

DOS攻击 DOS攻击介绍: Dos攻击:是一种拒绝服务攻击,常用来使服务器或者网络瘫痪(一对一) DDos攻击:分布式拒绝服务攻击(群殴) DOS攻击目的: 1、消耗带宽 2、消耗服务器性…

AI模型选型与优化:核心痛点与实战指南

AI模型选型与优化:核心痛点与实战指南

2026/7/23 4:30:12

1. AI模型选型的核心痛点解析"AI选不对,调试全白费"这句话在业内流传已久,我从业十年间见过太多团队在模型调试环节投入80%精力,最终效果却不如换用合适的基础模型。2023年某电商推荐系统案例中,某团队耗时三个月优化XG…

基于Qt与C++从零构建HTTP代理服务器:原理、实现与实战

基于Qt与C++从零构建HTTP代理服务器:原理、实现与实战

2026/7/23 4:30:11

1. 项目概述:为什么我们需要自己动手写一个HTTP代理服务器? 最近在调试一个网络应用时,我遇到了一个典型的“502 Bad Gateway”错误。这个错误提示指向了一个本地代理端口,让我意识到,很多开发者和测试人员其实每天都在…

基于LLM的自然语言到结构化查询框架设计与实践

基于LLM的自然语言到结构化查询框架设计与实践

2026/7/23 4:20:11

这次我们来看一个专门解决自然语言访问领域特定元数据问题的框架项目。这个框架的核心价值在于提供了一套可复用的方法论,让开发者能够快速构建基于大语言模型的查询生成系统,将自然语言问题自动转换为结构化查询。对于需要处理复杂元数据查询的场景&…

微服务进阶:服务网格与Istio

微服务进阶:服务网格与Istio

2026/7/23 3:40:08

541|微服务进阶:服务网格与Istio 上篇文章我们聊了微服务的基本概念和拆分方法。 但微服务多了,问题也多了: 服务之间怎么通信? 怎么监控每个服务的调用链路? 熔断、限流、重试怎么做? 安全认证怎么统一? 以前这些都靠SDK库(比如Hystrix、Feign),每个服务都要集成…

零售超级终端全域协同:ShareKit 碰一碰商品流转业务落地案例

零售超级终端全域协同:ShareKit 碰一碰商品流转业务落地案例

2026/7/21 9:56:14

一、零售门店全域协同业务背景与行业痛点 1.1 门店超级终端设备矩阵(连锁便利店/商超标准配置) 自助收银Kiosk一体机:顾客结算、自助核销优惠券、商品素材预览;运营折叠平板:店长后台商品上新、图片录入、活动配置、…

噗叽短视频界面分析

噗叽短视频界面分析

2026/7/23 1:54:13

1 和小红书类似,可以采用类似判断方法------------其实他比小红书好判断,因为他没有图片,控件位置几乎是固定的,都不用判断------------2 因为他没有点赞按钮------------而且几乎所有控件位置都是完全一样的,所以我就…

企业级AI搜索落地选型实战手册(含LLM+RAG+Hybrid架构对比矩阵与ROI测算模板)

企业级AI搜索落地选型实战手册(含LLM+RAG+Hybrid架构对比矩阵与ROI测算模板)

2026/7/23 0:09:56

更多请点击: https://kaifayun.com 第一章:企业级AI搜索落地选型实战手册(含LLMRAGHybrid架构对比矩阵与ROI测算模板) 企业级AI搜索系统落地成败,核心在于技术选型与业务价值的精准对齐。盲目堆砌大模型能力或过度依赖…

TM4C129LNCZAD外设实战:LCD、比较器与PWM寄存器配置详解

TM4C129LNCZAD外设实战:LCD、比较器与PWM寄存器配置详解

2026/7/23 0:09:56

1. 项目概述与核心价值在嵌入式系统开发,尤其是基于ARM Cortex-M内核的微控制器项目中,深入理解并熟练配置芯片的片上外设,是从“点亮LED”迈向“实现复杂系统功能”的关键一步。Tiva™ TM4C129LNCZAD作为TI公司Cortex-M4F家族中的高性能成员…

AtomCode `fmt_dur` 争议溯源:两个函数、三段演进、四个事实

AtomCode `fmt_dur` 争议溯源:两个函数、三段演进、四个事实

2026/7/23 0:09:56

一、快速声明与争议背景本文是对 AtomCode 终端 spinner 时长显示 fmt_dur 相关说法的事实性核验。2026 年 7 月 CSDN 上出现两篇互相矛盾的博文,近期又有 AI 在对话中输出格式描述 XhYm / YmZs / Zs。本文基于 AtomCode 仓库 main4677ddfa 及全分支 Git 历史给出可…