到目前为止,我们已经看到了通过不同编程语言读取文件的多种方法,我们学习了系统调用,如何从汇编中进行这些调用, 然后我们学习了内存映射、虚拟地址空间和通常是用户态和内核交互的一些机制。
但在我们的探索中,我们总是或多或少地把内核看作一个“黑匣子”。是时候改变这一点了。
在野兽的肚子里 在第2部分中,我们使用gdb逐步执行我们的用户态程序readfile。这使我们能够在每条指令之后检查CPU
寄存器的状态。但我们无法看到内核中发生了什么。
我们还使用strace来跟踪系统调用。它比 gdb 噪音小, 因为它只展示在我们的用户层程序与内核通信时的信息。
现在是时候加强并使用ftrace了。这是一个集成在linux内核中的追踪设施。多亏了它,我们能够追踪各种内核函数和事件。
酷熊的热心贴士
Julia Evans 的博客上有一篇精彩的ftrace简介(https://jvns.ca/blog/2017/03/19/getting-started-with-ftrace/)。
说到 Julia,请查看她的精彩杂志!(https://wizardzines.com/)
我们最简单的入门方法是使用trace-cmd。在 Manjaro/ArchLinux 上, 有一个 AUR 包,所以我们可以抓住它:
Shell session
在 Ubuntu 上,也有一个包:
Shell session
1 $ sudo apt install trace-cmd
该计划是:
启用对所有事件的跟踪ext4 事件(稍后详细介绍)
运行我们的readfile程序(汇编程序)
查看报告并尝试拼凑出发生了什么
我的第一次跟踪没有成功 - 它只包含与 zsh(我的 shell)和 git(由我的 shell 提示调用以显示指示器)。
事实证明,存在多级缓存,并且/etc/hosts 已经缓存到最大,所以读取它甚至没有注册。
幸运的是,我们可以清除所有这些缓存(在系统运行时!) 以 root 身份运行以下命令:
Bash
1 2 3 4 $ sync $ echo 3 > /proc/sys/vm/drop_caches
然后,我们可以开始跟踪(再次以 root 身份):
Bash
1 $ trace-cmd record -e ext4
然后启动我们的程序,点击Ctrl-C停止trace-cmd,然后再次启动它 trace-cmd 将收集的数据格式化为人类可读的格式:
Bash
1 $ trace-cmd report trace.dat > trace.txt
我们终于得到它了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 readfile-2056669 [002] 112770.475550: ext4_es_lookup_extent_enter: dev 8,3 ino 16252929 lblk 0 readfile-2056669 [002] 112770.475551: ext4_es_lookup_extent_exit: dev 8,3 ino 16252929 found 1 [0/1) 65019936 WR readfile-2056669 [002] 112770.475553: ext4_es_lookup_extent_enter: dev 8,3 ino 16252929 lblk 1 readfile-2056669 [002] 112770.475553: ext4_es_lookup_extent_exit: dev 8,3 ino 16252929 found 1 [1/2) 65020464 WR readfile-2056669 [002] 112770.475565: ext4_es_lookup_extent_enter: dev 8,3 ino 16253092 lblk 0 readfile-2056669 [002] 112770.475565: ext4_es_lookup_extent_exit: dev 8,3 ino 16253092 found 0 [0/0) 0 readfile-2056669 [002] 112770.475565: ext4_ext_map_blocks_enter: dev 8,3 ino 16253092 lblk 0 len 1 flags readfile-2056669 [002] 112770.475566: ext4_es_cache_extent: dev 8,3 ino 16253092 es [0/1) mapped 65050624 status W readfile-2056669 [002] 112770.475566: ext4_ext_show_extent: dev 8,3 ino 16253092 lblk 0 pblk 65050624 len 1 readfile-2056669 [002] 112770.475566: ext4_ext_map_blocks_exit: dev 8,3 ino 16253092 flags lblk 0 pblk 65050624 len 1 mflags M ret 1 readfile-2056669 [002] 112770.475566: ext4_es_insert_extent: dev 8,3 ino 16253092 es [0/1) mapped 65050624 status W readfile-2056669 [002] 112770.474893: ext4_es_lookup_extent_enter: dev 8,3 ino 18491454 lblk 0 readfile-2056669 [002] 112770.474893: ext4_es_lookup_extent_exit: dev 8,3 ino 18491454 found 0 [0/0) 0 readfile-2056669 [002] 112770.474893: ext4_ext_map_blocks_enter: dev 8,3 ino 18491454 lblk 0 len 4 flags readfile-2056669 [002] 112770.474894: ext4_es_cache_extent: dev 8,3 ino 18491454 es [0/5) mapped 93400358 status W readfile-2056669 [002] 112770.474894: ext4_ext_show_extent: dev 8,3 ino 18491454 lblk 0 pblk 93400358 len 5 readfile-2056669 [002] 112770.474894: ext4_ext_map_blocks_exit: dev 8,3 ino 18491454 flags lblk 0 pblk 93400358 len 4 mflags M ret 4 readfile-2056669 [002] 112770.474894: ext4_es_insert_extent: dev 8,3 ino 18491454 es [0/4) mapped 93400358 status W
这些列的格式如下:
process_name-pid [cpu_core] timestamp: event_name: parameters 我无法找到每个事件的精确文档,但我们可以 发现这里主要做了三件事:
查找“inode 16252929”
查找并映射“inode 16253092”
查找并映射“inode 18491454” 就是这样。
我们学到了什么? ftrace 允许跟踪内核代码内部的行为。我们无法改变它 但我们可以更好地了解正在发生的事情。
我们的路径都去哪儿了? 而strace向我们展示了/etc/hosts,即我们正在映射的文件的路径ftrace没有追踪到。相反,它向我们显示索引节点号。要了解原因,我们必须考虑一下文件系统的人体工程学。
✨ 让我们谈谈符号链接 ✨
符号链接(简称符号链接)是文件,但它们不是常规文件。 它们指向 另一个 文件,每当打开它们时,另一个文件 应该打开。有..使用符号链接的理由,我不会深入讨论, 相信我这一点。
要点是,例如,几乎所有 Linux 系统中都有一堆符号链接 在/usr/lib中:
Shell session
1 2 3 $ ll /usr/lib/x86_64-linux-gnu/libcairo.so* lrwxrwxrwx 1 root root 21 Oct 1 21:19 /usr/lib/x86_64-linux-gnu/libcairo.so.2 -> libcairo.so.2.11600.0 -rw-r--r-- 1 root root 1203976 Oct 7 2021 /usr/lib/x86_64-linux-gnu/libcairo.so.2.11600.0
此处,libcairo.so.2 指向 libcairo.so.2.11600.0。 如果我们运行像 wc 这样的命令(它计算单词数,但也计算字节数),我们会看到 它正在访问两种情况下所指向的文件:
Shell session
1 2 3 4 5 6 $ wc /usr/lib/x86_64-linux-gnu/libcairo.so.2 4419 24794 1203976 /usr/lib/x86_64-linux-gnu/libcairo.so.2 $ wc /usr/lib/x86_64-linux-gnu/libcairo.so.2.11600.0 4419 24794 1203976 /usr/lib/x86_64-linux-gnu/libcairo.so.2.11600.0
但是,它们不是副本。它们只是……符号链接。至于实现细节,我们注意到两件事:这些符号链接文件的大小正是大小 它们指向的路径的不同,并且它们的模式略有不同:
所以符号链接是带有特殊模式标志的文件,其内容是他们指向的路径。这意味着它们可以悬垂(指向空路径)!
酷熊的热心贴士
一个悬垂的符号链接是指向不存在的文件的符号链接。
请参阅下面的示例:
Shell session
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 $ echo "Hello there" > regular.txt $ cat regular.txt Hello there $ ln -s regular.txt symlink.txt $ ll total 144 drwxrwxr-x 2 amos amos 4096 Mar 11 14:11 ./ drwxrwxrwt 76 root root 135168 Mar 11 14:11 ../ -rw-rw-r-- 1 amos amos 12 Mar 11 14:11 regular.txt lrwxrwxrwx 1 amos amos 11 Mar 11 14:11 symlink.txt -> regular.txt $ cat symlink.txt Hello there $ rm regular.txt $ cat symlink.txt cat: symlink.txt: No such file or directory
你可以用它制作一个文件系统(或者你可以吗?) 我们来谈谈文件夹。正如我们所见,符号链接只是文件,带有一种特殊的模式,它们的内容是一个路径。文件夹(目录)不能做同样的事情吗?
让我们构建一个文件系统。什么是文件系统?只是组织文件的一种方式 以及磁盘上的文件夹、其内容和元数据。你可以想象一个磁盘 作为一块大白板,就像这样:
酷熊的热心贴士
在本文的其余部分中,我们将使用“磁盘”一词。什么时候 我们真正的意思是“存储设备”。还更短了!
假设,只是为了好玩,我们将此磁盘划分为一系列块。让我们将它们设置为4096字节(或 4KiB)。这样,它们的大小可能和内核内存页相同。或者是后者的倍数。至少它们是二的幂。
假设在我们的文件系统中,每个文件夹只是一个名称列表:它们的子节点。我们可以很好地遍历文件系统:
/ 的子级列表中有 /etc
/etc 的子级列表中有 /etc/hosts
/etc/hosts是一个普通文件
听起来不错吧?我的意思是,我们仍然需要解决一些问题。
首先,我们需要确定文件夹和文件在磁盘上的位置。但这并不是一个非常困难的问题。我们可以只有一个路径列表和一个偏移量列表,可能是这样的:
当我们想要访问/etc/hosts
时,我们只需:1) 浏览我们的表,找到正确的条目,然后 2) 阅读它!
这看起来并没有那么糟糕。为什么会出现问题呢?
问题 1 :查找文件的时间复杂度为O(n),其中n是文件数。
我刚刚统计了我的一个 Linux 系统上的文件,有 其中 1’128’252。听起来像用线性搜索查找文件是会变得昂贵。
问题 2 :每个文件的内容需要连续。
在我们当前的布局下,如果文件太大而无法容纳在一个块中, 并且使用了下一个块,那么我们需要移动它:
问题 3 :元数据与数据混合。
假设我们要实现 tree
命令。它打印出名字 文件系统中每个文件的模式和大小,无论嵌套有多深。
根据我们的磁盘布局,我们必须读取每个块的开头 包含一个文件只是为了了解其元数据。当然,同时还有每个文件夹的内容(但这很正常)。
这尤其成问题,因为读取元数据(这种情况发生得很频繁) 通常)需要大量的寻找(Seeking)。寻找是“跳到某个位置”的过程 不同位置”读取存储设备时。
这对于光盘读取器或硬盘驱动器来说非常昂贵。对于磁带驱动器来说极其昂贵。
问题 4 :重命名操作的开销非常大。
如果我们想将 /etc 重命名为 /config,我们有一堆东西要重写:
这意味着在整个磁盘的许多不同位置,阅读一堆条目来找出我们应该重写的位置,然后写入。 还记得搜素(Seeking)昂贵吗?忘掉它吧。
目录中的子文件夹和文件越多,情况就会变得更糟。
问题 5 :每个文件操作都需要解析整个路径。
根据我们的方案,如果我们想从/etc/hosts
到 /etc
,有 没有直接链接!我们必须将 /etc/hosts
解析为 [“etc”, “hosts”], 删除最后一个以获取 [“etc”] 并从中构建 /etc 路径。
然后我们必须在一开始就使用我们的查找表来查找 /etc 到底是什么。
总而言之,这是一个非常糟糕的磁盘布局 。从技术上讲,它适用于一个玩具操作系统,但我不想将其用作日常驱动程序。
我们学到了什么?
文件系统的磁盘布局必须仔细设计。
枚举文件、向下或向上遍历等操作, 重命名文件夹,应该可以通过尽可能少的操作来实现,最小化寻找。
输入索引节点 我们从第一次尝试文件系统磁盘中学到的教训之一:我们应该将元数据和数据分开。我们也应该寻找另一种方式来引用文件而不是完全限定名称。
无论我们的文件是否位于/etc/hosts
我们的/config/hosts
- 它是同一个文件! 我们的设计应该反映这一点。
因此,让我们首先尽可能早的把所有元数据存储在一起。我们需要保留一大块空间,大到足够描述磁盘上的每个文件。
这将是我们的索引,每个元素都是一个“索引节点”或“inode”。
每个 inode 都将包含通用的内容,而且还将包含我们可以在其中找到其数据的块的编号 :
至于文件夹 - 简单!在数据块中,我们存储目录列表 条目。每个条目都引用另一个inode,并为其指定一个名称。
让我们回顾一下。我们的问题真的解决了吗?首先,如何我们要在这个新方案中查找文件吗?
假设我们要打开/etc/hosts。首先我们读取 /
的 inode。为了方便起见,让我们决定,因为它是文件系统根(最顶层)节点,它将始终位于 inode 1 中:
然后我们读取其目录条目,查找“etc”。然后我们读 “etc”的索引节点。然后我们读取其整个目录,查找“hosts”。 最后,我们读取“hosts”的数据。
我制作了一个快速图表,以便更容易理解:
好吧,这个流程看起来并不简单。
但是与我们之前的方案相比,有一个优点。我们多次搜索更小的目录条目集,而不是通过绝对路径在文件系统中的所有文件中进行线性搜索:
酷熊的热心贴士
上图并未按比例绘制:我们只是减少了最坏的情况 110 万次字符串比较到 228 次字符串比较。
新的最坏情况是旧情况的 0.02%。
当然,这取决于每个目录有多少个文件!
重命名操作怎么样 ?。您可能已经注意到,在我们的新方案中,inode不包含路径信息-没有name字段。
这些名称实际上包含在目录条目中:
如果我们想重命名/etc,我们只需要更改目录即可 条目 - 我们不需要触及任何子节点的索引节点, 无论子目录层次结构有多深:
如果我们想将 /etc/hosts 移动到 /hosts,我们只需删除 /etc的 inode 中的目录条目,并向 /的 inode 加一。
同样,无论有多少个目录,操作次数都是固定的 以及我们目录中的文件。这要好得多。
现在,向下遍历非常高效(比以前更高),但是 向上遍历(例如,从 /etc 到 /)仍然需要 操纵路径,我们不想这样做。
我们有个主意:如果我们向每个目录添加一个条目会怎么样? 给它的父级?我们可以称其为“..”。对于根,我们只需制作 它指向自身:
解决剩余问题 我不了解你的情况,但到目前为止我对我们的磁盘布局非常满意。
然而,(至少)有两件事我们可以做得更好。
现在,每个目录条目都以不特定的顺序存储在列表中。 为了找到特定的条目,我们必须进行线性搜索,即。每个条目一个接一个做比较,只有当我们找到我们所找到的内容时才停止查找。
仅当目录中的文件不超过几千个时,这才实用。如果我们遇到几万、几十万的情况,那么会开始变得非常缓慢。
一个潜在的解决方案是散列(hash)所有条目的名称,并使用 自平衡二叉搜索树。
我不会在这里详细介绍,但基本上,它允许插入(添加 一条记录)、删除(删除一条记录)、对数时间 搜索, 这意味着它会比线性数据结构快得多。
熊的热心贴士
《数据结构》是一个丰富而复杂的领域,这需要 很多很多文章。
我们今天不去那里。
第二个问题是我们的文件仍然必须连续。如果 它们长大了,我们可能不得不将它们移到另一组块(block)中。我们可能 甚至发现没有足够大的连续块(block)来容纳文件!
为了解决这个问题,我们可以使用范围(extents)。
对于每个文件,我们可以存储一系列(start, length)对:
这带来了一个新问题:以前,如果我们想读取只是文件的后半部分,我们可以简单地计算出文件的地址第一个块:
但是现在,文件的中间可能位于任何范围内。这是 不再是简单的算术。为了解决这个问题,我们还可以使用树形数据结构。
如果我们想从头开始读取文件,那么我们可以简单地 按顺序走树。如果我们正在寻找其他地方,我们只需 在树中进行搜索,直到找到正确的范围,然后然后 这么简单的算术。
现在来体验一下现实世界 玩够了!是时候看看真正的实时文件系统了。
在我们开始考虑磁盘布局之前,我们正在跟踪 打开和映射时的内核事件/etc/hosts。
我们得到的跟踪包含如下行:
1 2 3 4 ext4_ext_map_blocks_enter: dev 8,3 ino 18491454 lblk 0 len 4 flags ext4_ext_map_blocks_enter: dev 8,3 ino 16253092 lblk 0 len 1 flags
现在这更有意义了。
第一行引用 inode“18491454” (我们现在知道这意味着什么)。这 ext 事件名称的一部分可能指的是范围,因为有一个 “镜头4” (长度为4)。
可能是/etc/hosts吗?
酷熊的热心贴士
在 Linux 上,我们可以将 -i 标志传递给 ls 命令,使其显示 inode 编号。
Shell session
1 2 $ ls -i -l /etc/hosts 16253092 -rw-r--r-- 1 root root 220 Oct 1 21:21 /etc/hosts
不! /etc/hosts 是 16253092。这是第二行。
那么第一个是什么?
好吧,还记得我说过二进制文件在执行时会被映射吗?
Shell session
1 2 $ ls -i ./readfile 18491454 ./readfile
答对了。
据我们所知,真实的 Linux 系统使用的文件系统 也在使用inode。还有范围!
我们可以使用 df 命令找出它是什么文件系统(具体来说, -T 标志):
Shell session
1 2 3 4 $ df -Th / Filesystem Type Size Used Avail Use% Mounted on /dev/sda3 ext4 548G 96G 425G 19% /
所以,显然是ext4。它似乎是最新的文档 ext4 位于 kernel.org。
不幸的是,这个文档似乎不是由ext4作者编写的,而是由其他人在Linux中阅读ext4的代码时写的。哦,好吧,这就是生活。
但首先:这是什么/dev/sda3
?
好吧,还记得我说过内核是老大吗?并且它控制了用户态进程所能看到的现实?对于文件来说,这更是如此。
并非所有可以 open()’d 的东西都是传统意义上的文件。
有些只是..资源。
例如,/proc/cpuinfo
“包含”有关已安装 CPU 的信息:
Shell session
1 2 3 4 5 6 $ head -5 /proc/cpuinfo processor : 0 vendor_id : AuthenticAMD cpu family : 25 model : 33 model name : AMD Ryzen 9 5950X 16-Core Processor
实际上并不在任何存储设备上。这只是内核的一种方式 通过标准系统调用为用户态进程提供一些信息 例如 open、read、write 和 close。
/proc/self/
是一个包含当前信息的目录 运行过程。如果我们像这样修改 Go 程序:
Go code
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport ( "fmt" "strings" "io/ioutil" ) func main () { payload, _ := ioutil.ReadFile("/proc/self/cmdline" ) fmt.Printf("%#v\n" , strings.Split(string (payload), "\x00" )) }
…然后像这样运行它,它会打印它的参数:
Shell session
1 2 3 4 5 $ go build ./main.go $ ./main "foo" "argument 2" "bar" []string{"./main", "foo", "argument 2", "bar", ""}
事实上,在《Unix 哲学》中,一切皆文件。
酷熊的热心贴士
这让很多人感到愤怒(https://en.wikipedia.org/wiki/The_UNIX-HATERS_Handbook)。
因此,/dev/sda3
只是一个“文件”。我们的根ext4分区所包含的全部内容。
如果我们想要整个磁盘,我们会打开/dev/sda。那我们就不得不学习分区表了,这个最好留待以后。
让我们读取整个分区 现在我们已经完成了介绍性材料,让我们直接进入正题。
我们将在下一部分中使用 Rust。
Shell session
1 2 3 4 $ cargo new read-raw-ext4 Created binary (application) `read-raw-ext4` package $ cd read-raw-ext4/
让我们添加我们需要的板条箱之一:
Shell session
1 2 3 4 5 $ cargo add [email protected] Updating crates.io index Adding hex-slice v0.1.4 to dependencies. Updating crates.io index
Rust code // in src/main.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 use hex_slice::AsHex;use std::{fs::OpenOptions, io::Read};fn main () -> Result <(), std::io::Error> { let mut file = OpenOptions::new ().read (true ).open ("/dev/sda3" )?; let mut buf = vec! [0u8 ; 128 ]; file.read_exact (&mut buf)?; println! ("{:x}" , buf.as_hex ()); Ok (()) }
这可以愉快地构建cargo build,但它不会运行:
Shell session
1 2 3 4 5 6 7 8 $ cargo build Compiling hex-slice v0.1.4 Compiling read-raw-ext4 v0.1.0 (/home/amos/bearcove/read-raw-ext4) Finished dev [unoptimized + debuginfo] target(s) in 0.37s $ ./target/debug/read-raw-ext4 Error: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }
看,我们当前正在以普通用户身份运行该程序。如果任何用户都可以随意访问任何分区,然后他们就可以绕过文件权限。
那会很糟糕。
所以,让我们假装我们热衷于这一分钟,并且:
Shell session
1 2 3 $ sudo ./target/debug/read-raw-ext4 [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 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 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 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
啊。它充满了零。我们可能需要文档(https://www.kernel.org/doc/html/latest/filesystems/ext4/overview.html)。
这是它要说的:
对于块组0的特殊情况,前1024字节未使用,以允许安装x86引导扇区和其他怪事。超级块将从偏移量 1024 字节开始,无论是哪个块(通常是 0)。
好吧,让我们从字节 1024 开始读取。我们将使用 positioned-io crate (https://crates.io/crates/positioned-io)为此。
Shell session
1 2 3 4 5 6 7 $ cargo add [email protected] Updating crates.io index Adding positioned-io v0.3 to dependencies. Features as of v0.3.0: + byteorder Updating crates.io index
Rust code // in src/main.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 use hex_slice::AsHex;use positioned_io::ReadAt;use std::fs::OpenOptions;fn main () -> Result <(), std::io::Error> { let file = OpenOptions::new ().read (true ).open ("/dev/sda3" )?; let mut buf = vec! [0u8 ; 128 ]; file.read_exact_at (1024 , &mut buf)?; println! ("{:x}" , buf.as_hex ()); Ok (()) }
Shell session
1 2 3 4 5 6 7 8 $ cargo b && sudo ./target/debug/read-raw-ext4 Compiling libc v0.2.140 Compiling byteorder v1.4.3 Compiling positioned-io v0.3.1 Compiling read-raw-ext4 v0.1.0 (/home/amos/bearcove/read-raw-ext4) Finished dev [unoptimized + debuginfo] target(s) in 0.84s [0 e0 2d 2 0 3b b7 8 c0 8f 6f 0 3b 9 d 7 ac 7e 10 2 0 0 0 0 2 0 0 0 2 0 0 0 0 80 0 0 0 80 0 0 0 20 0 0 c5 b4 4 64 c4 b4 4 64 43 0 ff ff 53 ef 1 0 1 0 0 0 98 92 38 63 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 b 0 0 0 0 1 0 0 3c 0 0 0 c6 2 0 0 6b 4 0 0 f7 6a 4c a7 98 2f 4d 8b bb cb ae 71 15 ed d6 90 0 0 0 0 0 0 0 0]
现在我们开始做饭了!文档提到了“超级块”,如果我们阅读 它的结构,它说我们应该找到幻数0xEF53 偏移量 0x38。它还说它是一个小端 16 位整数。
嗯,有一个板条箱可以解决这个问题:byteorder。
Shell session
1 2 3 4 5 6 7 $ cargo add [email protected] Updating crates.io index Adding byteorder v1.4 to dependencies. Features as of v1.4.0: + std - i128
让我们再添加一个用于错误处理:
Shell session
1 2 3 4 5 6 7 8 9 10 11 12 $ cargo add [email protected] Updating crates.io index Adding color-eyre v0.6 to dependencies. Features as of v0.6.0: + capture-spantrace + color-spantrace + tracing-error + track-caller - issue-url - url Updating crates.io index
由于我们将阅读很多内容,因此让我们创建一个辅助结构。 我们还需要一些 use 指令:
Rust code // this will let us read integers of various width
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 use byteorder::{LittleEndian, ReadBytesExt};use positioned_io::{ReadAt, Cursor};use color_eyre::Result ;struct Reader <IO> { inner: IO, } impl <IO: ReadAt> Reader<IO> { fn new (inner: IO) -> Self { Self { inner } } fn u16 (&self , offset: u64 ) -> Result <u16 > { let mut cursor = Cursor::new_pos (&self .inner, offset); Ok (cursor.read_u16::<LittleEndian>()?) } }
现在,任何时候我们想从分区中读取某些内容,我们都会首先创建一个 positioned_io::Slice,将其传递给 Reader,并使用它。
酷熊的热心贴士
请注意,File、Slice 或 Reader 都不需要是可变的,因为它们都不在其中维护位置信息。
它们甚至可以同时使用!
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 use positioned_io::Slice;fn main () -> Result <()> { let file = OpenOptions::new ().read (true ).open ("/dev/sda3" )?; let r = Reader::new (Slice::new (file, 1024 , None )); let magic = r.u16 (0x38 )?; println! ("magic = {magic:x}" ); Ok (()) }
Shell session
1 2 $ cargo b -q && sudo ./target/debug/read-raw-ext4 magic = ef53
嘿,这就是文档给我们的价值!我们在正确的轨道。
块组(Block Group) 在我们的玩具磁盘布局中,我们将磁盘划分为块。ext4
确实如此 只是同样的事情!在超级块的偏移量0x18处,我们发现块的大小。
好吧,我们找到n,其中块大小为2 ^ (10 + n)。
我们需要向 Reader 添加一个u32 方法:
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 impl <IO: ReadAt> Reader<IO> { fn new (inner: IO) -> Self { Self { inner } } fn u16 (&self , offset: u64 ) -> Result <u16 > { let mut cursor = Cursor::new_pos (&self .inner, offset); Ok (cursor.read_u16::<LittleEndian>()?) } fn u32 (&self , offset: u64 ) -> Result <u32 > { let mut cursor = Cursor::new_pos (&self .inner, offset); Ok (cursor.read_u32::<LittleEndian>()?) } }
在 main 中,做一些数学运算:
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 fn main () -> Result <()> { let file = OpenOptions::new ().read (true ).open ("/dev/sda3" )?; let r = Reader::new (Slice::new (file, 1024 , None )); let magic = r.u16 (0x38 )?; println! ("magic = {magic:x}" ); let n = r.u32 (0x18 )?; let block_size = 1 << (10 + n); println! ("block_size = {block_size}" ); Ok (()) }
Shell session
1 2 3 4 $ cargo b -q && sudo ./target/debug/read-raw-ext4 magic = ef53 block_size = 4096
多么幸运啊!块大小为 4KB,就像我们的玩具磁盘布局一样。
但是,如果我们查看文档,我们会注意到这些块是分组的 (分成..块组)。
ext4文件系统分为一系列块组。为了减少碎片造成的性能困难,块分配器尽力将每个文件的块保留在同一组中,从而减少寻道时间。块组的大小在`sb.s_blocks_per_group`中指定,也可以`8 * block_size_in_bytes` 来计算。在默认块大小为4kb的情况下,每组包含32768个块。块组的数量是设备的大小,除以块组的大小。 -- 内核文档。
这些在…组描述符中进行了描述(GDT 代表 “组描述符表”):
标准块组的布局大致如下(每个字段将在下面的单独部分中讨论):
Group 0 Padding
ext4 Super Block
Group Descriptors
Reserved GDT Blocks
Data Block Bitmap
inode Bitmap
inode Table
Data Blocks
1024 bytes
1 block
many blocks
many blocks
1 block
1 block
many blocks
many more blocks
每组的块数存储在超级块中, 在偏移量 0x20 处:
Rust code
1 2 3 let bpg = r.u32 (0x20 )?;println! ("blocks per group = {bpg}" );
Shell session
1 2 3 4 5 $ cargo b -q && sudo ./target/debug/read-raw-ext4 magic = ef53 block_size = 4096 blocks per group = 32768
最后,每组的inodes数量存储在偏移量0x28处;
Rust code
1 2 3 4 let ipg = r.u32 (0x28 )?;println! ("inodes per group = {ipg}" );
Shell session
1 2 3 4 5 6 $ cargo b -q && sudo ./target/debug/read-raw-ext4 magic = ef53 block_size = 4096 blocks per group = 32768 inodes per group = 8192
好消息是所有这些值看起来都是合法的。坏消息 是我们还远未读取索引节点。
读取ext4中的inode 让我们回顾一下。ext4分区以1024字节的填充开始,然后是一个超级块,其中包含各种设置,例如大小块的数量、每组块的数量、每组inode的数量。
然后是块组描述符。这些包含的偏移量 有几件事,但我们感兴趣的是索引节点表:
让我们从头开始 - 查找 “/“ 的inode。
ext4 中有一些特殊的 inode:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 inode号 目的 0 不存在;没有索引节点 0。 1 有缺陷的块列表。 2 根目录。 3 用户配额。 4 团体名额。 5 引导加载程序。 6 取消删除目录。 7 保留的组描述符 inode。 (“调整索引节点大小”) 8 日志索引节点。 9 “排除”是指“排除”。 inode,用于快照(?) 10 副本 inode,用于非上游功能? 11 传统的第一个非保留 inode。通常这是丢失+找到的目录。请参阅超级块中的 s_first_ino。 — 内核文档
文档看起来和我们一样有信心。那些问题 那里有标记..非常棒。
无论如何,根(“/“)的 inode始终是2
。 我们如何找到它? 同样,文档有帮助:
4.1.2.寻找索引节点 每个块组包含 sb->s_inodes_per_group inode。由于 inode 0 被定义为不存在,因此可以使用以下公式查找 inode 所在的块组:`bg = (inode_num - 1) / sb->s_inodes_per_group`。可以在块组的索引节点表中找到特定的索引节点,位置为 index = (inode_num - 1) % sb->s_inodes_per_group。要获取 inode 表中的字节地址,请使用 offset = index * sb->s_inode_size。
— 内核文档
这是有道理的!
既然我们使用的是一种好的编程语言,那么让我们开始构建一些抽象。
Shell session
1 2 3 4 $ cargo add [email protected] Updating crates.io index Adding custom_debug v0.5 to dependencies.
首先是超级块:
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 use custom_debug::{Debug as CustomDebug};#[derive(CustomDebug)] struct Superblock { #[debug(format = "{:x}" )] magic: u16 , block_size: u64 , blocks_per_group: u64 , inodes_per_group: u64 , inode_size: u64 , } impl Superblock { fn new (dev: &dyn ReadAt) -> Result <Self > { let r = Reader::new (Slice::new (dev, 1024 , None )); Ok (Self { magic: r.u16 (0x38 )?, block_size: (2u32 .pow (10 + r.u32 (0x18 )?)) as u64 , blocks_per_group: r.u32 (0x20 )? as u64 , inodes_per_group: r.u32 (0x28 )? as u64 , inode_size: r.u16 (0x58 )? as u64 , }) } }
让我们来试一下:
Rust code
1 2 3 4 5 6 7 8 9 10 11 fn main () -> Result <()> { color_eyre::install ()?; let file = OpenOptions::new ().read (true ).open ("/dev/sda3" )?; let sb = Superblock::new (&file)?; println! ("{sb:#?}" ); Ok (()) }
Shell session
1 2 3 4 5 6 7 8 $ cargo b -q && sudo ./target/debug/read-raw-ext4 Superblock { magic: ef53, block_size: 4096, blocks_per_group: 32768, inodes_per_group: 8192, inode_size: 256, }
看起来不错!
现在让我们为块组描述符创建一个类型。我们要去 将其用作常量的容器:块组的大小 描述符:
Rust code
1 2 3 4 5 6 struct BlockGroupDescriptor {}impl BlockGroupDescriptor { const SIZE: u64 = 64 ; }
接下来,让我们为块组编号创建一个类型。当然,这只是 一个数字 - 但我们将在其上添加一个方法来获取描述符的一部分。
酷熊的热心贴士 具有单个字段的元组或结构称为newtype。
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #[derive(Debug, Clone, Copy)] struct BlockGroupNumber (u64 );impl BlockGroupNumber { fn desc_slice <T>(self , sb: &Superblock, dev: T) -> Slice<T> where T: ReadAt, { assert! (sb.block_size != 1024 , "1024 block size not supported" ); let gdt_start = sb.block_size; let offset = gdt_start + self .0 * BlockGroupDescriptor::SIZE; Slice::new (dev, offset, None ) } }
接下来,让我们为 inode 编号创建一个类型!我们将添加一个方法 它告诉我们特定 inode 位于哪个块组中。
Rust code
1 2 3 4 5 6 7 8 9 #[derive(Debug, Clone, Copy)] struct InodeNumber(u64); impl InodeNumber { fn blockgroup_number(self, sb: &Superblock) -> BlockGroupNumber { let n = (self.0 - 1) / sb.inodes_per_group; BlockGroupNumber(n) } }
让我们尝试一下这一切。我们正在寻找 inode 2:
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 fn main () -> Result <()> { color_eyre::install ()?; let file = OpenOptions::new ().read (true ).open ("/dev/sda3" )?; let sb = Superblock::new (&file)?; println! ("{sb:#?}" ); let root_bg = InodeNumber (2 ).blockgroup_number (&sb); dbg!(&root_bg); let mut buf = vec! [0u8 ; 64 ]; root_bg.desc_slice (&sb, &file).read_at (0 , &mut buf)?; println! ("{:x}" , buf.as_hex ()); Ok (()) }
Shell session
1 2 3 4 5 6 7 8 9 10 11 12 13 $ cargo b -q && sudo ./target/debug/read-raw-ext4 Superblock { magic: ef53, block_size: 4096, blocks_per_group: 32768, inodes_per_group: 8192, inode_size: 256, } [src/main.rs:92] &root_bg = BlockGroupNumber( 0, ) [47 4 0 0 57 4 0 0 67 4 0 0 a6 4d ee 1f 2 0 4 0 0 0 0 0 2b 7e e7 d ed 1f a4 83 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 a8 e1 c3 9c 0 0 0 0]
正如预期的那样,inode 2
位于块组 0 中,并且描述符好吧,切片并不全为零,所以这是一个开始。
块描述符中我们唯一关心的是 inode 表的偏移量。与大多数其他地点一样, 这是一个块号。
这个有点烦人,因为低 32 位是 存储在0x8,高32位存储在0x28。
我们可以直接在BlockGroupDescriptor中进行一些操作, 但让我们向 Reader 添加一个方便的方法:
Rust code
1 2 3 4 5 6 // in `impl Reader {` fn u64_lohi(&self, lo: u64, hi: u64) -> Result<u64> { Ok(self.u32(lo)? as u64 + ((self.u32(hi)? as u64) << 32)) }
让我们填写BlockGroupDescriptor:
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #[derive(Debug)] struct BlockGroupDescriptor { inode_table: u64 , } impl BlockGroupDescriptor { const SIZE: u64 = 64 ; fn new (slice: &dyn ReadAt) -> Result <Self > { let r = Reader::new (slice); Ok (Self { inode_table: r.u64_lohi (0x8 , 0x28 )?, }) } }
现在,我们可以添加一个方法来直接抓取块组 BlockGroupNumber 的描述符:
Rust code
1 2 3 4 5 fn desc (self , sb: &Superblock, dev: &dyn ReadAt) -> Result <BlockGroupDescriptor> { let slice = self .desc_slice (sb, dev); BlockGroupDescriptor::new (&slice) }
再次,让我们尝试一下:
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 fn main () -> Result <()> { color_eyre::install ()?; let file = OpenOptions::new ().read (true ).open ("/dev/sda3" )?; let sb = Superblock::new (&file)?; println! ("{sb:#?}" ); let bgd = InodeNumber (2 ).blockgroup_number (&sb).desc (&sb, &file)?; println! ("{bgd:#?}" ); Ok (()) }
Shell session
1 2 3 4 5 6 7 8 9 10 11 $ cargo b -q && sudo ./target/debug/read-raw-ext4 Superblock { magic: ef53, block_size: 4096, blocks_per_group: 32768, inodes_per_group: 8192, inode_size: 256, } BlockGroupDescriptor { inode_table: 1127, }
嘿,1127 看起来是一个相当合理的 inode 数字 第一个块组的表。
✨我们正在进步✨ 现在,inode表是一系列固定大小的inode。
第一个字段是一个 16 位(little-endian,仍然)整数,其中包含文件模式。稍后,我们找到索引节点的大小,再次划分为低32位(位于 0x4)和高 32位(位于 0x6C)。
在0x28处,我们找到“块映射或范围树”。它是一个 60 字节的块。我 有一种感觉我们也需要它!
让我们为 reader 添加一个方便的方法,这样我们就可以轻松地获取该块:
Rust code // in impl Reader
1 2 3 4 5 fn vec(&self, offset: u64, len: usize) -> Result<Vec<u8>> { let mut v = vec![0u8; len]; self.inner.read_exact_at(offset, &mut v)?; Ok(v) }
接下来,我们添加一个方法来获取特定 inode 编号的切片。
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 fn inode_slice <T>(self , sb: &Superblock, dev: T) -> Result <Slice<T>>where T: ReadAt, { let desc = self .blockgroup_number (sb).desc (sb, &dev)?; let table_off = desc.inode_table * sb.block_size; let idx_in_table = (self .0 - 1 ) % sb.inodes_per_group; let inode_off = table_off + sb.inode_size * idx_in_table; Ok (Slice::new (dev, inode_off, Some (sb.inode_size))) }
现在,让我们创建一个 Inode 类型,其中包含我们想要的所有数据:模式、大小、 和 60 字节块。我们将使用我们最近添加的 Reader::vec:
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #[derive(CustomDebug)] struct Inode { #[debug(format = "{:o}" )] mode: u16 , size: u64 , #[debug(skip)] block: Vec <u8 >, } impl Inode { fn new (slice: &dyn ReadAt) -> Result <Self > { let r = Reader::new (slice); Ok (Self { mode: r.u16 (0x0 )?, size: r.u64_lohi (0x4 , 0x6C )?, block: r.vec (0x28 , 60 )?, }) } }
最后,让我们向 InodeNumber 添加一个方法,直接读取 索引节点:
Rust code
1 2 3 4 fn inode (self , sb: &Superblock, dev: &dyn ReadAt) -> Result <Inode> { let slice = self .inode_slice (sb, dev)?; Inode::new (&slice) }
// in impl InodeNumber
我们准备好了。让我们尝试一下:
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 fn main () -> Result <()> { color_eyre::install ()?; let file = OpenOptions::new ().read (true ).open ("/dev/sda3" )?; let sb = Superblock::new (&file)?; println! ("{sb:#?}" ); let root_inode = InodeNumber (2 ).inode (&sb, &file)?; println! ("{root_inode:#?}" ); Ok (()) }
Shell session
1 2 3 4 5 6 7 8 9 10 11 12 13 $ cargo b -q && sudo ./target/debug/read-raw-ext4 Superblock { magic: ef53, block_size: 4096 , blocks_per_group: 32768 , inodes_per_group: 8192 , inode_size: 256 , } Inode { mode: 40755 , size: 4096 , }
万岁!这些值与 stat 命令报告的内容匹配:
Shell session
1 2 3 4 5 6 7 8 9 $ stat / File: / Size: 4096 Blocks: 8 IO Block: 4096 directory Device: 803h/2051d Inode: 2 Links: 23 Access: (0755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2023-03-11 12:40:08.175908741 +0100 Modify: 2022-12-26 15:42:11.483999585 +0100 Change: 2022-12-26 15:42:11.483999585 +0100 Birth: 2022-10-01 21:18:48.000000000 +0200
我确信有很多目录具有相同的权限和大小,所以我们 可能读取了错误的索引节点,但至少我们可能没有读取, 比如说,一个随机数据块。
让我们快速添加一个 Filetype 枚举,这样我们就可以确保它是一个目录。 文件类型包含在模式中:
这些是互斥的文件类型
1 2 3 4 5 6 7 8 0x1000 S_IFIFO(先进先出) 0x2000 S_IFCHR(字符设备) 0x4000 S_IFDIR(目录) 0x6000 S_IFBLK(块设备) 0x8000 S_IFREG(常规文件) 0xA000 S_IFLNK(符号链接) 0xC000 S_IFSOCK(套接字) — 内核文档
我们将在此处使用num_enum crate:
Shell session
1 2 3 4 5 6 7 8 9 $ cargo add [email protected] Updating crates.io index Adding num_enum v0.5 to dependencies. Features as of v0.5.0: + std - complex-expressions - external_doc Updating crates.io index
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 use num_enum::*;use std::convert::TryFrom;#[derive(Debug, TryFromPrimitive)] #[repr(u16)] enum Filetype { Fifo = 0x1000 , CharacterDevice = 0x2000 , Directory = 0x4000 , BlockDevice = 0x6000 , Regular = 0x8000 , SymbolicLink = 0xA000 , Socket = 0xC000 , }
并添加 getter Inode:
Rust code
1 2 3 4 fn filetype (&self ) -> Filetype { Filetype::try_from (self .mode & 0xF000 ).unwrap () }
酷熊的热心贴士
为了保持示例代码简洁,我们在这里使用unwrap()。
如果我们以某种方式从错误的片中读取了一个 inode,或者如果该 inode 如果模式无效,我们的程序就会出现恐慌。
这在实际的文件系统实现中是不可取的,但是 在这里它可以作为一个很好的健全性检查。
让我们试一试:
Rust code
1 2 3 4 let root_inode = InodeNumber (2 ).inode (&sb, &file)?;let root_inode_type = root_inode.filetype ();println! ("({root_inode_type:?}) {root_inode:#?}" );
Shell session
1 2 3 4 5 6 7 8 9 10 11 12 $ cargo b -q && sudo ./target/debug/read-raw-ext4 Superblock { magic: ef53, block_size: 4096, blocks_per_group: 32768, inodes_per_group: 8192, inode_size: 256, } (Directory) Inode { mode: 40755, size: 4096, }
一切看起来都很好!
现在我们确定我们正在处理一个目录,我们可以开始 阅读其条目。
但首先 - 记得范围吗?这些就在我们刚刚读到的区块中!
内核文档可以为您提供帮助。
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #[derive(Debug)] struct ExtentHeader { entries: u64 , depth: u64 , } impl ExtentHeader { fn new (slice: &dyn ReadAt) -> Result <Self > { let r = Reader::new (slice); let magic = r.u16 (0x0 )?; assert_eq! (magic, 0xF30A ); Ok (Self { entries: r.u16 (0x2 )? as u64 , depth: r.u16 (0x6 )? as u64 , }) } }
我们来读一篇。
Rust code
1 2 3 4 let ext_header = ExtentHeader::new (&Slice::new (&root_inode.block, 0 , Some (12 )))?;println! ("{ext_header:#?}" );
Shell session
1 2 3 4 5 6 $ cargo b -q && sudo ./target/debug/read-raw-ext4 (cut) ExtentHeader { entries: 1 , depth: 0 , }
嗯,虽然不多,但同样,这些值是有意义的。已被关注 通过单个条目,因此我们知道 / 的数据存储在单个范围中 连续的块。它的深度为0,这意味着它是一个叶节点。
酷熊的热心贴士
在我们的例子中,我们假设 /、/etc 和 /etc/hosts 都只有 一个范围节点。
这是有道理的,因为/只有 24 个孩子,只有 204 个孩子 对于 /etc,并且 /etc/hosts 比块 (4096) 短 (511)。
但是,我们将使用 assert! 验证我们的假设。
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #[derive(Debug)] struct Extent { len: u64 , start: u64 , } impl Extent { fn new (slice: &dyn ReadAt) -> Result <Self > { let r = Reader::new (slice); Ok (Self { len: r.u16 (0x4 )? as u64 , start: ((r.u16 (0x6 )? as u64 ) << 32 ) + r.u32 (0x8 )? as u64 , }) } }
我们完整的范围读取代码变为:
Rust code
1 2 3 4 5 6 7 8 9 let ext_header = ExtentHeader::new (&Slice::new (&root_inode.block, 0 , Some (12 )))?;println! ("{ext_header:#?}" );assert_eq! (ext_header.depth, 0 );assert_eq! (ext_header.entries, 1 );let ext = Extent::new (&Slice::new (&root_inode.block, 12 , Some (12 )))?;println! ("{:#?}" , ext);
Shell session
1 2 3 4 5 6 7 8 9 10 11 12 13 $ cargo b -q && sudo ./target/debug/read-raw-ext4 (Directory) Inode { mode: 40755 , size: 4096 , } ExtentHeader { entries: 1 , depth: 0 , } Extent { len: 1 , start: 9319 , }
嘿,这些仍然是合理的值!
据此可以找到/区块9319的数据。
让我们花点时间向 Inode 类型添加一个便捷方法:
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fn data <T>(&self , sb: &Superblock, dev: T) -> Result <Slice<T>>where T: ReadAt, { let ext_header = ExtentHeader::new (&Slice::new (&self .block, 0 , Some (12 )))?; assert_eq! (ext_header.depth, 0 ); assert_eq! (ext_header.entries, 1 ); let ext = Extent::new (&Slice::new (&self .block, 12 , Some (12 )))?; assert_eq! (ext.len, 1 ); let offset = ext.start * sb.block_size; let len = ext.len * sb.block_size; Ok (Slice::new (dev, offset, Some (len))) }
然后转储/数据的前 128 个字节,就好像它是一个字符串一样:
Rust code
1 2 3 4 5 6 7 8 9 let root_inode = InodeNumber (2 ).inode (&sb, &file)?;let root_inode_type = root_inode.filetype ();println! ("({root_inode_type:?}) {root_inode:#?}" );let root_data = root_inode.data (&sb, &file)?;let data_start = Reader::new (&root_data).vec (0 , 128 )?;println! ("{}" , String ::from_utf8_lossy (&data_start));
Shell session
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ $ cargo b -q && sudo ./target/debug/read-raw-ext4 (Directory) Inode { mode: 40755 , size: 4096 , } . .. lost+found ( boot swapfile� etc�media� var bin@
现在,我不了解你,但这看起来非常令我兴奋。
我们可以在其中看到一些顶级目录名称:lost+found、boot、 swapfile — 甚至 . 和 ..! (当前目录和父目录, 分别)。
让我们正确地阅读它们:目录项格式实际上很漂亮 直截了当。
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #[derive(CustomDebug)] struct DirectoryEntry { #[debug(skip)] len: u64 , inode: InodeNumber, name: String , } impl DirectoryEntry { fn new (slice: &dyn ReadAt) -> Result <Self > { let r = Reader::new (slice); let name_len = r.u8 (0x6 )? as usize ; Ok (Self { inode: InodeNumber (r.u32 (0x0 )? as u64 ), len: r.u16 (0x4 )? as u64 , name: String ::from_utf8_lossy (&r.vec (0x8 , name_len)?).into (), }) } }
我们只需向 Inode 添加一个方法,该方法返回 DirectoryEntry 的 vec。 这并不适合所有用例,但它适合我们的用例!
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fn dir_entries (&self , sb: &Superblock, dev: &dyn ReadAt) -> Result <Vec <DirectoryEntry>> { let data = self .data (sb, dev)?; let mut entries = Vec ::new (); let mut offset : u64 = 0 ; loop { let entry = DirectoryEntry::new (&Slice::new (&data, offset, None ))?; if entry.inode.0 == 0 { break ; } offset += entry.len; entries.push (entry); } Ok (entries) }
现在到了关键时刻:
Rust code
1 2 3 4 5 6 7 8 let root_inode = InodeNumber (2 ).inode (&sb, &file)?;let root_inode_type = root_inode.filetype ();println! ("({root_inode_type:?}) {root_inode:#?}" );let root_entries = root_inode.dir_entries (&sb, &file)?;println! ("{root_entries:#?}" );
Shell session
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 $ cargo b -q && sudo ./target/debug/read-raw-ext4 (Directory) Inode { mode: 40755, size: 4096, } [ DirectoryEntry { inode: InodeNumber( 2, ), name: ".", }, DirectoryEntry { inode: InodeNumber( 2, ), name: "..", }, DirectoryEntry { inode: InodeNumber( 11, ), name: "lost+found", }, DirectoryEntry { inode: InodeNumber( 19398657, ), name: "boot", }, DirectoryEntry { inode: InodeNumber( 12, ), name: "swapfile", }, DirectoryEntry { inode: InodeNumber( 16252929, ), name: "etc", }, (cut) 🎉🎉🎉
一切似乎都井然有序。
让我们添加一个方便的方法来查找特定的孩子:
Rust code
1 2 3 4 5 6 7 8 9 10 fn child (&self , name: &str , sb: &Superblock, dev: &dyn ReadAt) -> Result <Option <InodeNumber>> { let entries = self .dir_entries (sb, dev)?; Ok (entries .into_iter () .filter (|x| x.name == name) .map (|x| x.inode) .next ()) }
酷熊的热心贴士
为什么返回类型是Result<Option<T>>?
嗯,从分区读取可能会失败 - 并且会返回Err(e)。
从分区读取可能会成功,但可能没有子分区 用这个名字。这将返回 Ok(None)。
最后,阅读可能会成功,我们可能会找到这样一个孩子,并且 这将返回 Ok(Some(n))。
让我们使用它:
Rust code
1 2 3 4 5 6 let root_inode = InodeNumber (2 ).inode (&sb, &file)?;println! ("({:?}) {:#?}" , root_inode.filetype (), root_inode);let etc_inode = root_inode.child ("etc" , &sb, &file)?;println! ("{etc_inode:?}" );
Shell session
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ cargo b -q && sudo ./target/debug/read-raw-ext4 (Directory) Inode { mode: 40755, size: 4096, } Some(InodeNumber(16252929)) $ stat /etc File: /etc Size: 12288 Blocks: 24 IO Block: 4096 directory Device: 803h/2051d Inode: 16252929 Links: 146 Access: (0755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2023-03-11 19:51:34.661133912 +0100 Modify: 2023-03-11 19:47:58.858792961 +0100 Change: 2023-03-11 19:47:58.858792961 +0100 Birth: 2022-10-01 21:18:52.867725869 +0200
一切都检查完毕。
让我们更进一步,读取该索引节点:
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 let root_inode = InodeNumber (2 ).inode (&sb, &file)?;let root_inode_type = root_inode.filetype ();println! ("({root_inode_type:?}) {root_inode:#?}" );let etc_inode = root_inode .child ("etc" , &sb, &file)? .expect ("/etc should exist" ) .inode (&sb, &file)?; let etc_inode_type = etc_inode.filetype ();println! ("({etc_inode_type:?}) {etc_inode:#?}" );
Shell session
1 2 3 4 5 6 7 8 9 10 $ cargo b -q && sudo ./target/debug/read-raw-ext4 (Directory) Inode { mode: 40755, size: 4096, } (Directory) Inode { mode: 40755, size: 12288, }
还是不错的。我们能在那里找到hosts吗?
Rust code
1 2 3 4 5 6 7 8 let hosts_inode = etc_inode .child ("hosts" , &sb, &file)? .expect ("/etc/hosts should exist" ) .inode (&sb, &file)?; let hosts_inode_type = hosts_inode.filetype ();println! ("({hosts_inode_type:?}) {hosts_inode:#?}" );
Shell session
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ cargo b -q && sudo ./target/debug/read-raw-ext4 (Directory) Inode { mode: 40755, size: 4096, } (Directory) Inode { mode: 40755, size: 12288, } thread 'main' panicked at 'assertion failed: `(left == right)` left: `2`, right: `1`', src/main.rs:110:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
啊!
看,当我最初写这个系列时,早在 2019 年,就在安装在Lenovo X200上的ArchLinux系统(我知道对吧?),这个曾经可以工作。
但现在不再这样了,因为我们的范围标头中有两个条目, 不只是一个。
所以,没关系,我们可以稍微调整一下我们的代码!
首先,我们添加一个 Inode::data_count 返回数据数量的方法 与 inode 关联的块:
Rust code
1 2 3 4 5 6 7 8 fn data_count (&self ) -> Result <u64 > { let ext_header = ExtentHeader::new (&Slice::new (&self .block, 0 , Some (12 )))?; assert_eq! (ext_header.depth, 0 ); Ok (ext_header.entries) }
我们将更改 data 函数以获取 index,这样它就可以检索任何 数据块。
Rust code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 fn data <T>(&self , sb: &Superblock, dev: T, index: u64 ) -> Result <Slice<T>>where T: ReadAt, { let ext_header = ExtentHeader::new (&Slice::new (&self .block, 0 , Some (12 )))?; assert_eq! (ext_header.depth, 0 ); assert! (index < ext_header.entries); let offset = 12 * (index + 1 ); let ext = Extent::new (&Slice::new (&self .block, offset, Some (12 )))?; assert_eq! (ext.len, 1 ); let offset = ext.start * sb.block_size; let len = ext.len * sb.block_size; Ok (Slice::new (dev, offset, Some (len))) }
现在,在 dir_entries 中,我们调用 self.data,我们实际上必须传递一个 指数!另外,我们的结束条件(找到 inode 编号为 0 的条目)仅有效 对于最后数据块。对于之前的那些,我们必须在到达时停下来 范围的末尾。
Rust code // in impl Inode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 fn dir_entries (&self , sb: &Superblock, dev: &dyn ReadAt) -> Result <Vec <DirectoryEntry>> { let mut entries = Vec ::new (); for block_index in 0 ..self .data_count ()? { let data = self .data (sb, dev, block_index)?; let data_size = data.size ()?.unwrap (); let mut offset : u64 = 0 ; while offset < data_size { let entry = DirectoryEntry::new (&Slice::new (&data, offset, None ))?; if entry.inode.0 == 0 { break ; } offset += entry.len; entries.push (entry); } } Ok (entries) }
现在…
Shell session
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ cargo b -q && sudo ./target/debug/read-raw-ext4 (Directory) Inode { mode: 40755, size: 4096, } (Directory) Inode { mode: 40755, size: 12288, } (Regular) Inode { mode: 100644, size: 220, }
找到/etc/hosts!
兴奋加剧
我们可以将它作为字符串读取吗?
Rust code // in main
1 2 3 4 5 6 let hosts_data = hosts_inode.data (&sb, &file, 0 )?;let hosts_data = Reader::new (&hosts_data).vec (0 , hosts_inode.size as usize )?;let hosts_data = String ::from_utf8_lossy (&hosts_data);println! ("---------------------------------------------" );println! ("{hosts_data}" );
Shell session
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 $ cargo b -q && sudo ./target/debug/read-raw-ext4 (Directory) Inode { mode: 40755, size: 4096, } (Directory) Inode { mode: 40755, size: 12288, } (Regular) Inode { mode: 100644, size: 220, } --------------------------------------------- 127.0.0.1 localhost 127.0.1.1 sonic # The following lines are desirable for IPv6 capable hosts ::1 ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters
我们做到了!