Hi, I'm zorro.
版权声明:
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址,以便读者询问问题和甄误反馈,共同进步。
博客: https://zorrozou.github.io/
微信公众号:Linux系统技术
如果你的数据被不小心误删除了,那么对文件系统结构的深入理解可以帮助你找到数据恢复的途径。我们先从一个数据恢复的例子开始,对ext4的文件系统结构做个介绍。
废话少说,下面我们直接上手进行数据恢复的实例。先格式化一个ext4文件系统。
mkfs.ext4 /dev/sdf1
mke2fs 1.42.9 (28-Dec-2013)
Filesystem label=
OS type: Linux
Block size=4096 (log=2)
Fragment size=4096 (log=2)
Stride=0 blocks, Stripe width=0 blocks
122101760 inodes, 488378390 blocks
24418919 blocks (5.00%) reserved for the super user
First data block=0
Maximum filesystem blocks=2636120064
14905 block groups
32768 blocks per group, 32768 fragments per group
8192 inodes per group
Superblock backups stored on blocks:
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968,
102400000, 214990848
Allocating group tables: done
Writing inode tables: done
Creating journal (32768 blocks): done
Writing superblocks and filesystem accounting information: done
挂载到测试目录:
mount /dev/sdf1 /test
df -h
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 20G 8.3G 11G 45% /
devtmpfs 32G 0 32G 0% /dev
tmpfs 32G 0 32G 0% /dev/shm
tmpfs 32G 259M 31G 1% /run
tmpfs 32G 0 32G 0% /sys/fs/cgroup
/dev/sda3 20G 2.2G 17G 12% /usr/local
tmpfs 6.3G 0 6.3G 0% /run/user/0
/dev/sdf1 1.8T 77M 1.7T 1% /test
/test是我们这次做测试的目录。我们给这个目录下创建一些文件,为了文件内容比较好分辨,我们使用/etc/下的文件作为测试文件。因为都是普通文本文件,肉眼比较方便区分文件内容。
cp -rf /etc/* /test/
ls /test/
DIR_COLORS hosts.allow quotatab
DIR_COLORS.256color hosts.deny rc.d
DIR_COLORS.lightbgcolor idmapd.conf rc.local
GREP_COLORS infiniband rc0.d
GeoIP.conf init.d rc1.d
GeoIP.conf.default inittab rc2.d
HOSTNAME inputrc rc3.d
NetworkManager iproute2 rc4.d
X11 issue rc5.d
acpi issue.net rc6.d
......
为了恢复测试方便,我们使用passwd文件的内容,手工做一个比较大的文本文件:
count=0;while [ $count -lt 1048578 ];do dd if=/test/passwd of=/test/bigfile bs=1K count=2 seek=$[$count*2] ; ((count ++));done
du -sh /test/bigfile
2.0G /test/bigfile
这样我们就有了一个2G左右的文件,等下我们就删除这个文件,再来恢复它的数据。 再删除他之前,我们先记录一些文件的信息,以方便我们后续针对测试进行数据对比,先学习使用一个命令debugfs来查看ext4文件系统的相关信息:
debugfs /dev/sdf1
debugfs 1.42.9 (28-Dec-2013)
debugfs: ls
2 (12) . 2 (4084) .. 11 (20) lost+found
13 (28) DIR_COLORS.256color 95158273 (24) NetworkManager
97779713 (12) X11 19 (16) adjtime 26738689 (20) alternatives
21 (20) anacrontab 23 (16) at.deny 45613057 (16) avahi
24 (32) bash-command-not-found 25 (16) bashrc
26 (24) bg_rsyncd.conf 77332481 (28) bonobo-activation
......
可以显示出文件的inode编号对应文件名的信息。在其中找到我们的bugfile:189 (1420) bigfile,看到它的inode编号为189。然后使用这个编号来查看文件相关其他信息:
debugfs: stat <189>
Inode: 189 Type: regular Mode: 0644 Flags: 0x80000
Generation: 1657554558 Version: 0x00000000:00000001
User: 0 Group: 0 Size: 2147471360
File ACL: 0 Directory ACL: 0
Links: 1 Blockcount: 4194288
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x5e5879f8:6d0be5e0 -- Fri Feb 28 10:24:56 2020
atime: 0x5e5879fc:91493d68 -- Fri Feb 28 10:25:00 2020
mtime: 0x5e587866:18a0fec4 -- Fri Feb 28 10:18:14 2020
crtime: 0x5e5876d3:34a7eb94 -- Fri Feb 28 10:11:31 2020
Size of extra inode fields: 28
EXTENTS:
(ETB0):34816, (0-32767):184320-217087, (32768-45055):217088-229375, (45056-77823):231424-264191, (77824-108543):26
4192-294911, (108544-141311):296960-329727, (141312-174079):329728-362495, (174080-206847):362496-395263, (206848-
239615):395264-428031, (239616-272383):428032-460799, (272384-305151):460800-493567, (305152-335871):493568-524287
, (335872-368639):557056-589823, (368640-401407):589824-622591, (401408-434175):622592-655359, (434176-466943):655
360-688127, (466944-499711):688128-720895, (499712-524284):720896-745468
stat命令可以查看文件inode信息,最后面的EXTENTS标记的内容就是这个文件目前索引的block编号。
debugfs: ex <189>
Level Entries Logical Physical Length Flags
0/ 1 1/ 1 0 - 524284 34816 524285
1/ 1 1/ 17 0 - 32767 184320 - 217087 32768
1/ 1 2/ 17 32768 - 45055 217088 - 229375 12288
1/ 1 3/ 17 45056 - 77823 231424 - 264191 32768
1/ 1 4/ 17 77824 - 108543 264192 - 294911 30720
1/ 1 5/ 17 108544 - 141311 296960 - 329727 32768
1/ 1 6/ 17 141312 - 174079 329728 - 362495 32768
1/ 1 7/ 17 174080 - 206847 362496 - 395263 32768
1/ 1 8/ 17 206848 - 239615 395264 - 428031 32768
1/ 1 9/ 17 239616 - 272383 428032 - 460799 32768
1/ 1 10/ 17 272384 - 305151 460800 - 493567 32768
1/ 1 11/ 17 305152 - 335871 493568 - 524287 30720
1/ 1 12/ 17 335872 - 368639 557056 - 589823 32768
1/ 1 13/ 17 368640 - 401407 589824 - 622591 32768
1/ 1 14/ 17 401408 - 434175 622592 - 655359 32768
1/ 1 15/ 17 434176 - 466943 655360 - 688127 32768
1/ 1 16/ 17 466944 - 499711 688128 - 720895 32768
1/ 1 17/ 17 499712 - 524284 720896 - 745468 24573
ex命令可以查看更详细的EXTENTS的映射关系。在此我们先不对EXTENTS信息作详细解释,后续恢复数据的时候我们再说。
debugfs: imap <189>
Inode 189 is part of block group 0
located at block 1301, offset 0x0c00
imap命令可以查看一个inode所在的block位置和其偏移量,就是说,利用这个信息我们就可以找到这个inode在当前磁盘的什么位置,比如这个例子就是:189号inode在当前磁盘的1301个块上再偏移0x0c00字节。那么当前分区一个block多大?一个inode多大呢?
debugfs: stats
Filesystem volume name: <none>
Last mounted on: /test
Filesystem UUID: 29434ffa-1987-4379-9ca8-a0cc5d35e2cc
Filesystem magic number: 0xEF53
Filesystem revision #: 1 (dynamic)
Filesystem features: has_journal ext_attr resize_inode dir_index filetype needs_recovery extent 64bit flex_bg
sparse_super large_file huge_file uninit_bg dir_nlink extra_isize
Filesystem flags: signed_directory_hash
......
Block size: 4096
......
Inode size: 256
......
通过stats命令我们可以查看文件系统的superblock,其中记录了文件系统的属性信息。当前文件系统的block size为4096,inode size为256。 根据以上信息,我们可以将189号inode的二进制数据dump出来:
printf %d 0x0c00
3072
dd if=/dev/sdf1 of=$[1301*4096+3072] bs=1 count=256 skip=$[1301*4096+3072]
256+0 records in
256+0 records out
256 bytes (256 B) copied, 0.000271671 s, 942 kB/s\
ls
5331968
产生的这个文件就是189号inode的磁盘二进制数据。当然这是未删除文件前的,我们等下再删除文件后建一个新的,以对比删除前后两个inode的变化。 我们回到debugfs命令,然后看一下文件的block内容,以确认文件内容:
debugfs: stat <189>
Inode: 189 Type: regular Mode: 0644 Flags: 0x80000
Generation: 1657554558 Version: 0x00000000:00000001
User: 0 Group: 0 Size: 2147471360
File ACL: 0 Directory ACL: 0
Links: 1 Blockcount: 4194288
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x5e5879f8:6d0be5e0 -- Fri Feb 28 10:24:56 2020
atime: 0x5e5879fc:91493d68 -- Fri Feb 28 10:25:00 2020
mtime: 0x5e587866:18a0fec4 -- Fri Feb 28 10:18:14 2020
crtime: 0x5e5876d3:34a7eb94 -- Fri Feb 28 10:11:31 2020
Size of extra inode fields: 28
EXTENTS:
(ETB0):34816, (0-32767):184320-217087, (32768-45055):217088-229375, (45056-77823):231424-264191, (77824-108543):26
4192-294911, (108544-141311):296960-329727, (141312-174079):329728-362495, (174080-206847):362496-395263, (206848-
239615):395264-428031, (239616-272383):428032-460799, (272384-305151):460800-493567, (305152-335871):493568-524287
, (335872-368639):557056-589823, (368640-401407):589824-622591, (401408-434175):622592-655359, (434176-466943):655
360-688127, (466944-499711):688128-720895, (499712-524284):720896-745468
debugfs: block_dump 745468
0000 726f 6f74 3a78 3a30 3a30 3a72 6f6f 743a root:x:0:0:root:
0020 2f72 6f6f 743a 2f62 696e 2f62 6173 680a /root:/bin/bash.
0040 6269 6e3a 783a 313a 313a 6269 6e3a 2f62 bin:x:1:1:bin:/b
0060 696e 3a2f 7362 696e 2f6e 6f6c 6f67 696e in:/sbin/nologin
0100 0a64 6165 6d6f 6e3a 783a 323a 323a 6461 .daemon:x:2:2:da
0120 656d 6f6e 3a2f 7362 696e 3a2f 7362 696e emon:/sbin:/sbin
0140 2f6e 6f6c 6f67 696e 0a61 646d 3a78 3a33 /nologin.adm:x:3
......
可以看到,文件对应的block信息确实是passwd的相关数据。 之后,我们可以删除文件了,然后使用debugfs再观察文件信息:
rm /test/bigfile
umount /test/ #保护文件系统不受后续磁盘操作的影响
debugfs /dev/sdf1
debugfs 1.42.9 (28-Dec-2013)
debugfs: ls -d
2 (12) . 2 (4084) .. <2> (20) DIR_COLORS
<13> (28) DIR_COLORS.256color <14> (32) DIR_COLORS.lightbgcolor
......
<189> (1420) bigfile
......
这时要使用ls -d参数,显示包括已删除文件在内的所有inode信息,其中所有带有<>标示的inode编号都是已经被删除的文件,但此时,仍然可以用debugfs看到对应inode信息。 然后我们继续在debugfs中使用其他命令查看188号inode的信息:
debugfs: stat <189>
Inode: 189 Type: regular Mode: 0644 Flags: 0x80000
Generation: 1657554558 Version: 0x00000000:00000001
User: 0 Group: 0 Size: 0
File ACL: 0 Directory ACL: 0
Links: 0 Blockcount: 0
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x5e587c0e:ed3f4c64 -- Fri Feb 28 10:33:50 2020
atime: 0x5e5879fc:91493d68 -- Fri Feb 28 10:25:00 2020
mtime: 0x5e587c0e:ed3f4c64 -- Fri Feb 28 10:33:50 2020
crtime: 0x5e5876d3:34a7eb94 -- Fri Feb 28 10:11:31 2020
dtime: 0x5e587c0e -- Fri Feb 28 10:33:50 2020
Size of extra inode fields: 28
EXTENTS:
debugfs: ex <189>
Level Entries Logical Physical Length Flags
debugfs: imap <189>
Inode 189 is part of block group 0
located at block 1301, offset 0x0c00
我们发现,此时inode中数据除了EXTENTS信息没了以外,其他相关信息还能找到。理想情况下,我们刚删除一个文件,在较短时间进行恢复的话,就应该是这样一个状态。于是,我们开始尝试恢复数据,仍然是根据imap信息,先dump出文件的inode进行查看:
dd if=/dev/sdf1 of=$[1301*4096+3072].rm bs=1 count=256 skip=$[1301*4096+3072]
256+0 records in
256+0 records out
256 bytes (256 B) copied, 0.000554775 s, 461 kB/s
ls
5331968 5331968.rm
此时我们有两个inode数据,一个是文件删除前的,一个是文件删除后的。为了后续我们查看inode内容方便,我们先要补充inode数据结构的相关知识。ext4 inode结构可以在内核源代码目录下的 fs/ext4/ext4.h 文件中找到。我们来看一下:
/*
* Structure of an inode on the disk
*/
struct ext4_inode {
__le16 i_mode; /* File mode */
__le16 i_uid; /* Low 16 bits of Owner Uid */
__le32 i_size_lo; /* Size in bytes */
__le32 i_atime; /* Access time */
__le32 i_ctime; /* Inode Change time */
__le32 i_mtime; /* Modification time */
__le32 i_dtime; /* Deletion Time */
__le16 i_gid; /* Low 16 bits of Group Id */
__le16 i_links_count; /* Links count */
__le32 i_blocks_lo; /* Blocks count */
__le32 i_flags; /* File flags */
union {
struct {
__le32 l_i_version;
} linux1;
struct {
__u32 h_i_translator;
} hurd1;
struct {
__u32 m_i_reserved1;
} masix1;
} osd1; /* OS dependent 1 */
__le32 i_block[EXT4_N_BLOCKS];/* Pointers to blocks */
__le32 i_generation; /* File version (for NFS) */
__le32 i_file_acl_lo; /* File ACL */
__le32 i_size_high;
__le32 i_obso_faddr; /* Obsoleted fragment address */
union {
struct {
......
inode结构信息并未显示完整,我们只选取我们当前关注的信息进行学习。在这个结构体中我们可以看到,文件的相关属性信息都在inode中,这里对于数据恢复最重要的是i_block数组,这个数组中有15个元素,记录的是此文件指向的存储文件数据的对应block。在ext2/3文件系统上,这个数组的前12的元素是直接指向存储数据的block,恢复数据可以直接读对应编号的block即可。而13、14、15三个分别为一级、二级、三级间接索引指向,相关概念不在此详述,但是相对比较容易找到对应的block查看相关数据。 而我们目前面对的是ext4,在数据block索引方法上相对ext3有很大变化,主要就是引入了extent机制。我们在此不详述为啥要引入extent,仅从数据恢复的角度来学习extent的结构。那么针对当前这个2G左右的文件,其extent在inode上是怎么布局的呢?可以参考下图:
此图引用自: https://zhuanlan.zhihu.com/p/52052278 更细节内容可以查看原文。
在ext4_inode结构中,存储extents相关数据结构的位置实际就是i_block数组所在的位置,因为ext4是使用extent索引磁盘block的,所以直接复用i_block空间即可。extents相关数据结构有三种,所有数据结构原型声明都在内核源代码目录下fs/ext4/ext4_extents.h 文件中,我们来看一下:
/*
* This is the extent on-disk structure.
* It's used at the bottom of the tree.
*/
struct ext4_extent {
__le32 ee_block; /* first logical block extent covers */
__le16 ee_len; /* number of blocks covered by extent */
__le16 ee_start_hi; /* high 16 bits of physical block */
__le32 ee_start_lo; /* low 32 bits of physical block */
};
/*
* This is index on-disk structure.
* It's used at all the levels except the bottom.
*/
struct ext4_extent_idx {
__le32 ei_block; /* index covers logical blocks from 'block' */
__le32 ei_leaf_lo; /* pointer to the physical block of the next *
* level. leaf or next index could be there */
__le16 ei_leaf_hi; /* high 16 bits of physical block */
__u16 ei_unused;
};
/*
* Each block (leaves and indexes), even inode-stored has header.
*/
struct ext4_extent_header {
__le16 eh_magic; /* probably will support different formats */
__le16 eh_entries; /* number of valid entries */
__le16 eh_max; /* capacity of store in entries */
__le16 eh_depth; /* has tree real underlying blocks? */
__le32 eh_generation; /* generation of the tree */
};
根据途中的索引关系我们可以知道,实际索引block信息的是 ext4_extent 数据结构。因为ext4支持48位块,所以在这个结构中用三个记录了指向的块,其中 ee_start_hi 和 ee_start_lo 两个组合起来记录了48位的物理第一块编号,ee_len 记录了从第一个块之后多少块都是属于这个extent连续数据块。以此可知,因为 ee_len 只有16bit,而且其首个bit被用来标记次extent是否被初始化,所以单独一个ext4_extent最多可以索引的连续块长度为2^15 * 4096(block 长度) = 128M空间。 我们根据inode的内容再来看一下其他数据结构的含义:
hexdump -e '"%4_ad |" 8/4 "%12d " "\n"' 5331968
0 | 33188 2147471360 1582856700 1582856696 1582856294 0 65536 4194288
32 | 524288 1 127754 65540 0 0 34816 131072
64 | 32768 12288 217088 45056 32768 231424 77824 30720
96 | 264192 1657554558 0 0 0 0 0 0
128 | 28 1829496288 413204164 -1857471128 1582855891 883420052 0 0
160 | 0 0 0 0 0 0 0 0
*
hexdump -e '"%4_ad |" 8/4 "%12d " "\n"' 5331968.rm
0 | 33188 0 1582856700 1582857230 1582857230 1582857230 0 0
32 | 524288 1 62218 4 0 0 34816 131072
64 | 32768 12288 217088 45056 32768 231424 77824 30720
96 | 264192 1657554558 0 0 0 0 0 0
128 | 28 -314618780 -314618780 -1857471128 1582855891 883420052 0 0
160 | 0 0 0 0 0 0 0 0
*
使用hexdump命令按照每行8个数据每个数据4字节长度来对比看一下文件删除后和删除前的inode信息。根据inode结构我们可以推算出,第40字节数据开始处是i_block存储位置。根据对应数据结构细节和布局图可知,一个ext4_extent_header占12字节,一个ext4_extent_idx占12字节。针对如上描述,可知对应位置每四字节的数据为: 删除后:
ext4_extent_header:
eh_magic + eh_entries(共4字节) = 62218
eh_max + eh_depth(共4字节) = 4
eh_generation(共4字节) = 0
ext4_extent_idx:
ei_block(共4字节)= 0
ei_leaf_lo(共4字节)= 34816
ei_leaf_hi + ei_unused = 131072
删除前:
ext4_extent_header:
eh_magic + eh_entries(共4字节) = 127754
eh_max + eh_depth(共4字节) = 65540
eh_generation(共4字节) = 0
ext4_extent_idx:
ei_block(共4字节)= 0
ei_leaf_lo(共4字节)= 34816
ei_leaf_hi + ei_unused = 131072
对比发现,此inode信息的ext4_extent_idx没有被清除,而ext4_extent_header信息在删除后有被改动。我们丢失了比较关键的eh_depth信息,这个信息记录了这个文件extents树的层级个数,这会影响后续数据恢复的过程。如果知道这个层级个数,恢复会更容易一些,不知道的话就要靠猜测了。 不过针对我们当前这个文件,因为删除前我们备份了inode信息,所以可以找出这个eh_depth层级数为1。
hexdump -e '"%4_ad |" 16/2 "%5d " "\n"' 5331968
0 |-32348 0 -12288 32767 31228 24152 31224 24152 30822 24152 0 0 0 1 -16 63
32 | 0 8 1 0 -3318 1 4 1 0 0 0 0 -30720 0 0 2
64 |-32768 0 12288 0 20480 3 -20480 0 -32768 0 -30720 3 12288 1 30720 0
96 | 2048 4 18046 25292 0 0 0 0 0 0 0 0 0 0 0 0
128 | 28 0 -6688 27915 -316 6304 15720 -28343 30419 24152 -5228 13479 0 0 0 0
160 | 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
*
hexdump -e '"%4_ad |" 16/2 "%5d " "\n"' 5331968.rm
0 |-32348 0 0 0 31228 24152 31758 24152 31758 24152 31758 24152 0 0 0 0
32 | 0 8 1 0 -3318 0 4 0 0 0 0 0 -30720 0 0 2
64 |-32768 0 12288 0 20480 3 -20480 0 -32768 0 -30720 3 12288 1 30720 0
96 | 2048 4 18046 25292 0 0 0 0 0 0 0 0 0 0 0 0
128 | 28 0 19556 -4801 19556 -4801 15720 -28343 30419 24152 -5228 13479 0 0 0 0
160 | 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
*
显示中对应第二行第八个数字为eh_depth值。可以看到这个值在删除后被清0了。 不过最关键的索引信息在ext4_extent_idx中,ei_leaf_lo的值为34816,不过因为要记录的是48bit块,所以其后面的16bit还可能记录着高16bit的block编号数据。再看hexdump -e ‘“%4_ad |” 16/2 “%5d “ “\n”’ 5331968.rm 显示中的第二行倒数第三个数,就是ei_leaf_hi,值为0。意味着34816就是下一级extent结构。所以我们dump出这个块来继续分析:
dd if=/dev/sdf1 of=34816 bs=4K count=1 skip=34816
1+0 records in
1+0 records out
4096 bytes (4.1 kB) copied, 0.0252319 s, 162 kB/s
[root@100-66-27-140 /data/home/zorrozou]# hexdump -e '"%4_ad |" 8/4 "%12d " "\n"' 34816
0 | 1176330 340 0 0 32768 184320 32768 12288
32 | 217088 45056 32768 231424 77824 30720 264192 108544
64 | 32768 296960 141312 32768 329728 174080 32768 362496
96 | 206848 32768 395264 239616 32768 428032 272384 32768
128 | 460800 305152 30720 493568 335872 32768 557056 368640
160 | 32768 589824 401408 32768 622592 434176 32768 655360
192 | 466944 32768 688128 499712 24573 720896 0 0
224 | 0 0 0 0 0 0 0 0
*
3200 | 0 0 1705826272 32736 1705826208 32736 0 0
3232 | 1706116375 32736 1706116368 32736 1706116361 32736 1706116353 32736
3264 | 0 0 1706116350 32736 0 0 1706116413 32736
3296 | 0 0 0 0 0 0 1706116342 32736
3328 | 1706116381 32736 0 0 1706116336 32736 1706116329 32736
3360 | 1706116322 32736 1706116314 32736 0 0 1706116311 32736
3392 | 0 0 1706116420 32736 0 0 0 0
3424 | 0 0 1706116303 32736 1706116389 32736 0 0
3456 | 1706116420 32736 1706116413 32736 1706116405 32736 1706116397 32736
3488 | 1706116389 32736 1706116381 32736 1708309936 32736 1 0
3520 | 868 0 1 0 884 0 14 0
3552 | 918 0 12 0 5112 0 13 0
3584 | 288544 0 25 0 2489480 0 27 0
3616 | 8 0 26 0 2489488 0 28 0
3648 | 8 0 1879047925 0 1705820656 32736 5 0
3680 | 1705822424 32736 6 0 1705820912 32736 10 0
3712 | 986 0 11 0 24 0 3 0
3744 | 1708310528 32736 2 0 672 0 20 0
3776 | 7 0 23 0 1705824600 32736 7 0
3808 | 1705823664 32736 8 0 936 0 9 0
3840 | 24 0 1879048190 0 3376 0 1879048191 0
3872 | 2 0 1879048176 0 1705823410 32736 1879048185 0
3904 | 27 0 0 0 0 0 0 0
3936 | 0 0 0 0 0 0 0 0
*
4000 | 1708310824 32736 1708310800 32736 1706196704 32736 0 0
4032 | 0 0 1706128832 32736 0 0 1708310792 32736
4064 | 1702104384 32736 0 0 0 0 0 0
因为已知这个文件extent树只有1级,所以我们可以大胆的根据下一级结构对索引的block进行估算。再上图中,这个block内容对应这个布局:
我们猜测上述blcok的hexdump内容在6-12行为有效数据,即偏移量标示为0-224字节内的所有内容。前12个字节为ext4_extent_header信息。使用 hexdump -e ‘“%4_ad |” 16/2 “%5d “ “\n”’ 34816 命令可以看到这个header中对应的eh_depth为0,所以12字节后的都是ext4_extent结构,直接指向对应block。ext4_extent中,我们主要关注ee_len、ee_start_hi、ee_start_lo,并且后两个组合在一起表示一个48位block信息。所以我们先用下面这个命令列出所有的ext4_extent中的起始block位置的低32位数字:
hexdump -e '"%4_ad |" 3/4 "%12d " "\n"' 34816 | head -20
0 | 1176330 340 0
12 | 0 32768 184320
24 | 32768 12288 217088
36 | 45056 32768 231424
48 | 77824 30720 264192
60 | 108544 32768 296960
72 | 141312 32768 329728
84 | 174080 32768 362496
96 | 206848 32768 395264
108 | 239616 32768 428032
120 | 272384 32768 460800
132 | 305152 30720 493568
144 | 335872 32768 557056
156 | 368640 32768 589824
168 | 401408 32768 622592
180 | 434176 32768 655360
192 | 466944 32768 688128
204 | 499712 24573 720896
216 | 0 0 0
*
以上除第一列偏移字节数以外的第三列数字就是起始block位置的低32位数字。然后我们再列出高16位数字:
hexdump -e '"%4_ad |" 6/2 "%12d " "\n"' 34816 | head -20
0 | -3318 17 340 0 0 0
12 | 0 0 -32768 0 -12288 2
24 | -32768 0 12288 0 20480 3
36 | -20480 0 -32768 0 -30720 3
48 | 12288 1 30720 0 2048 4
60 | -22528 1 -32768 0 -30720 4
72 | 10240 2 -32768 0 2048 5
84 | -22528 2 -32768 0 -30720 5
96 | 10240 3 -32768 0 2048 6
108 | -22528 3 -32768 0 -30720 6
120 | 10240 4 -32768 0 2048 7
132 | -22528 4 30720 0 -30720 7
144 | 8192 5 -32768 0 -32768 8
156 | -24576 5 -32768 0 0 9
168 | 8192 6 -32768 0 -32768 9
180 | -24576 6 -32768 0 0 10
192 | 8192 7 -32768 0 -32768 10
204 | -24576 7 24573 0 0 11
216 | 0 0 0 0 0 0
*
以上除第一列偏移字节数以外的第四列数字就是起始block位置的高16位数字。全是0,就是说目前所有的block编号32bit长度就可以记录了,没用到高16bit,所以我们直接使用32位数字作为每个分段的起始block位置即可。每个分段的连续长度是这里显示的第三列,但是这16bit中,最高一个bit用来标记本extent是否被占用,所以我们只看后15bit。于是碰巧,这列中的数字取绝对值就是这个分片的blocks个数。由此我们可以得出这个文件所有block在磁盘上的覆盖范围,并保存到block_list文件中:
cat block_list
184320 32768
217088 12288
231424 32768
264192 30720
296960 32768
329728 32768
362496 32768
395264 32768
428032 32768
460800 32768
493568 30720
557056 32768
589824 32768
622592 32768
655360 32768
688128 32768
720896 24573
第一列为分片起始磁盘block,第二列为这个分片连续的块个数。 使用这个文件的内容恢复文件数据:
count=0;cat block_list |while read -a block;do dd if=/dev/sdf1 of=bigfile_restore bs=4K count=${block[1]} skip=${block[0]} seek=$count ;count=$[$count+${block[1]}];done
32768+0 records in
32768+0 records out
134217728 bytes (134 MB) copied, 1.33926 s, 100 MB/s
12288+0 records in
12288+0 records out
50331648 bytes (50 MB) copied, 0.271418 s, 185 MB/s
32768+0 records in
32768+0 records out
......
head bigfile_restore
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
tail bigfile_restore
webdev:x:510:100::/data/webdev:/bin/bash
user_00:x:511:100::/home/user_00:/bin/bash
user_01:x:512:100::/home/user_01:/bin/bash
user_02:x:513:100::/home/user_02:/bin/bash
user_03:x:514:100::/home/user_03:/bin/bash
user_04:x:515:100::/home/user_04:/bin/bash
user_05:x:516:100::/home/user_05:/bin/bash
user_06:x:517:100::/home/user_06:/bin/bash
user_07:x:518:100::/home/user_07:/bin/bash
user
ls -l bigfile_restore
-rw-r--r-- 1 root root 2147471360 Feb 28 12:05 bigfile_restore
一个2G大小的文件内容恢复完成。当然,我们在这里选择恢复一个2G大小的文件是有原因的。目前我们发现,文件比较小时侯,inode中的extent可以直接指向extent块,并且在删除文件的时候,这部分extent信息会被清零,导致索引块信息丢失。而有多级索引的extent信息却可以被保留下来,虽然也丢失了部分信息,但是依然可以通过残留的部分信息对文件进行恢复。
根据上面的例子,我们已经对文件inode的结构和extent索引结构有了一定了解。接下来我们来补充一下ext4整个文件系统的相关知识。
ext4的分区结构布局跟ext3基本没什么变化,结构参见下图:
这里跟ext3有变化的是绿色标记的部分:ext4加入了flex_bg属性,这个属性让文件系统在块组结构之上又多了个flex块组结构。每个flex_bg包含连续的若干个块组,这个功能让之前分散在各个块组中管理的组描述符、块位图、inode位图和inode表等相关metadate信息放在了flex_bg的第一个块组中管理,于是其他块组中基本都是连续的块。 我们可以使用dumpe2fs命令查看ext4文件系统的结构,其中Flex block group size就是一个flex_bg中包含的块组个数:
dumpe2fs /dev/sdf1
Filesystem volume name: <none>
Last mounted on: /mnt
Filesystem UUID: 29434ffa-1987-4379-9ca8-a0cc5d35e2cc
Filesystem magic number: 0xEF53
Filesystem revision #: 1 (dynamic)
Filesystem features: has_journal ext_attr resize_inode dir_index filetype extent 64bit flex_bg spar
se_super large_file huge_file uninit_bg dir_nlink extra_isize
Filesystem flags: signed_directory_hash
Default mount options: user_xattr acl
Filesystem state: clean
Errors behavior: Continue
Filesystem OS type: Linux
Inode count: 122101760
Block count: 488378390
Reserved block count: 24418919
Free blocks: 423686492
Free inodes: 122089836
First block: 0
Block size: 4096
Fragment size: 4096
Group descriptor size: 64
Reserved GDT blocks: 1024
Blocks per group: 32768
Fragments per group: 32768
Inodes per group: 8192
Inode blocks per group: 512
Flex block group size: 16
Filesystem created: Fri Feb 28 08:12:25 2020
Last mount time: Sun Mar 1 11:57:21 2020
Last write time: Tue Mar 3 21:05:32 2020
Mount count: 5
Maximum mount count: -1
Last checked: Fri Feb 28 08:12:25 2020
Check interval: 0 (<none>)
Lifetime writes: 567 GB
Reserved blocks uid: 0 (user root)
Reserved blocks gid: 0 (group root)
First inode: 11
Inode size: 256
Required extra isize: 28
Desired extra isize: 28
Journal inode: 8
Default directory hash: half_md4
Directory Hash Seed: e0f5015a-a434-4705-9e20-d0da3d20f10f
Journal backup: inode blocks
Journal features: journal_incompat_revoke journal_64bit
Journal size: 128M
Journal length: 32768
Journal sequence: 0x00001a0d
Journal start: 0
Group 0: (Blocks 0-32767) [ITABLE_ZEROED]
Checksum 0x1bcb, unused inodes 7904
Primary superblock at 0, Group descriptors at 1-233
Reserved GDT blocks at 234-1257
Block bitmap at 1258 (+1258), Inode bitmap at 1274 (+1274)
Inode table at 1290-1801 (+1290)
23278 free blocks, 7910 free inodes, 2 directories, 7904 unused inodes
Free blocks: 9490-32767
Free inodes: 188-190, 286-8192
Group 1: (Blocks 32768-65535) [INODE_UNINIT, ITABLE_ZEROED]
Checksum 0xd88f, unused inodes 8192
Backup superblock at 32768, Group descriptors at 32769-33001
Reserved GDT blocks at 33002-34025
Block bitmap at 1259 (bg #0 + 1259), Inode bitmap at 1275 (bg #0 + 1275)
Inode table at 1802-2313 (bg #0 + 1802)
417 free blocks, 8192 free inodes, 0 directories, 8192 unused inodes
Free blocks: 34174, 34400-34815
Free inodes: 8193-16384
Group 2: (Blocks 65536-98303) [INODE_UNINIT, ITABLE_ZEROED]
Checksum 0x191f, unused inodes 8192
Block bitmap at 1260 (bg #0 + 1260), Inode bitmap at 1276 (bg #0 + 1276)
Inode table at 2314-2825 (bg #0 + 2314)
0 free blocks, 8192 free inodes, 0 directories, 8192 unused inodes
Free blocks:
Free inodes: 16385-24576
Group 3: (Blocks 98304-131071) [INODE_UNINIT, ITABLE_ZEROED]
Checksum 0x32b4, unused inodes 8192
Backup superblock at 98304, Group descriptors at 98305-98537
Reserved GDT blocks at 98538-99561
Block bitmap at 1261 (bg #0 + 1261), Inode bitmap at 1277 (bg #0 + 1277)
Inode table at 2826-3337 (bg #0 + 2826)
790 free blocks, 8192 free inodes, 0 directories, 8192 unused inodes
Free blocks: 99562-100351
Free inodes: 24577-32768
......
超级块(superblock):
就是我们在dumpefs现实中看到的前些行块组信息以外的内容。记录了整个文件系统的块大小、inode大小、块、inode个数、日志块等关键属性信息。
组描述符(Group descriptors):
存储了本块组内相关块的位置,比如块位图、inode位图、inode table等。内核中相关结构体定义如下:
/*
* Structure of a blocks group descriptor
*/
struct ext4_group_desc
{
__le32 bg_block_bitmap_lo; /* Blocks bitmap block */
__le32 bg_inode_bitmap_lo; /* Inodes bitmap block */
__le32 bg_inode_table_lo; /* Inodes table block */
__le16 bg_free_blocks_count_lo;/* Free blocks count */
__le16 bg_free_inodes_count_lo;/* Free inodes count */
__le16 bg_used_dirs_count_lo; /* Directories count */
__le16 bg_flags; /* EXT4_BG_flags (INODE_UNINIT, etc) */
__le32 bg_exclude_bitmap_lo; /* Exclude bitmap for snapshots */
__le16 bg_block_bitmap_csum_lo;/* crc32c(s_uuid+grp_num+bbitmap) LE */
__le16 bg_inode_bitmap_csum_lo;/* crc32c(s_uuid+grp_num+ibitmap) LE */
__le16 bg_itable_unused_lo; /* Unused inodes count */
__le16 bg_checksum; /* crc16(sb_uuid+group+desc) */
__le32 bg_block_bitmap_hi; /* Blocks bitmap block MSB */
__le32 bg_inode_bitmap_hi; /* Inodes bitmap block MSB */
__le32 bg_inode_table_hi; /* Inodes table block MSB */
__le16 bg_free_blocks_count_hi;/* Free blocks count MSB */
__le16 bg_free_inodes_count_hi;/* Free inodes count MSB */
__le16 bg_used_dirs_count_hi; /* Directories count MSB */
__le16 bg_itable_unused_hi; /* Unused inodes count MSB */
__le32 bg_exclude_bitmap_hi; /* Exclude bitmap block MSB */
__le16 bg_block_bitmap_csum_hi;/* crc32c(s_uuid+grp_num+bbitmap) BE */
__le16 bg_inode_bitmap_csum_hi;/* crc32c(s_uuid+grp_num+ibitmap) BE */
__u32 bg_reserved;
};
保留的全局描述符表(Reserved GDT):
这部分空间一般用来给文件系统进行空间拓展的时候使用。当空间拓展的时候,由于新空间的加入可能导致组描述变大,用这部分空间进行扩展。
块位图(Block bitmap):
用位图方式标记每一个块是否被占用。
inode位图(Inode bitmap):
用位图方式标记每一个inode是否被占用。
inode表(Inode table):
用来存放所有inode,每个inode在当前文件系统上是256字节。在超级块中的Inode size记录每一个inode大小。
在ext4文件系统上,以上数据结构在flex_bg中是集中存放在第一个块组中的。其余块组中可以不用记录相关信息,都集中存放block。在ext3上相关信息是分散在每一个块组中。
当我们mount一个ext4文件系统的时候,此文件系统的第一个inode会跟要要挂载的目录inode编号进行关联。而目录的inode中索引的block存放这个目录的目录项信息。每一个目录项纪录了下一级目录或文件的inode编号,于是就可以遍历到文件系统中所有的目录和文件。 我们可以在挂载文件系统前后来观察对应挂载目录的inode编号变化:
ls -id /mnt/
917505 /mnt/
mount /dev/sdf1 /mnt
ls -id /mnt/
2 /mnt/
挂载前,mnt目录的inode编号为917505。挂载一个分区之后,对应的inode编号变为2。对于一个ext4文件系统来说,第一个inode编号总为2。我们可以通过debugfs命令来查看其相关信息:
debugfs /dev/sdf1
debugfs 1.42.9 (28-Dec-2013)
debugfs: stat <2>
Inode: 2 Type: directory Mode: 0755 Flags: 0x81000
Generation: 0 Version: 0x00000000:00002c7b
User: 0 Group: 0 Size: 12288
File ACL: 0 Directory ACL: 0
Links: 104 Blockcount: 24
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x5e6b2df0:e11f2ff8 -- Fri Mar 13 14:53:36 2020
atime: 0x5e5e3c3d:d23c6030 -- Tue Mar 3 19:15:09 2020
mtime: 0x5e6b2df0:e11f2ff8 -- Fri Mar 13 14:53:36 2020
crtime: 0x5e585aeb:00000000 -- Fri Feb 28 08:12:27 2020
Size of extra inode fields: 28
EXTENTS:
(0):9482, (1-2):9488-9489
debugfs:
从extent信息中我们可以看到,这个inode的Flags为0x81000,表示其使用了dir_index方式存储目录项。所以我们不能再以直接查看9482块的内容方式查看其目录项。先通过htree,命令查看其index结构:
debugfs: htree <2>
Root node dump:
Reserved zero: 0
Hash Version: 1
Info length: 8
Indirect levels: 0
Flags: 0
Number of entries (count): 2
Number of entries (limit): 508
Entry #0: Hash 0x00000000, block 1
Entry #1: Hash 0x8de2fd4e, block 2
Entry #0: Hash 0x00000000, block 1
Reading directory block 1, phys 9488
11 0x20353058-89026648 (20) lost+found
14 0x5e93248e-827623e1 (32) DIR_COLORS.lightbgcolor
15 0x766fabb2-a611f5e9 (20) GREP_COLORS
16 0x3dd382c6-f2d0a8d1 (20) GeoIP.conf
17 0x775935ca-74b556d6 (28) GeoIP.conf.default
18 0x4414754a-d84f5e38 (16) HOSTNAME
90701825 0x6082fbf0-e119f11f (24) NetworkManager
19 0x7fb61d88-e242ff5d (16) adjtime
37748737 0x4c1eb84c-a6b9ab3f (20) alternatives
21 0x2b0243ee-25248d91 (20) anacrontab 23 0x354a6afa-558b7be2 (16) at.deny
20447233 0x83c6c248-aae4a677 (16) audisp
104595457 0x399cda3a-dbfc005d (16) audit
26 0x8c960154-b5fbf07f (24) bg_rsyncd.conf
74973185 0x770e82b8-0447d0a1 (16) binfmt.d
27 0x800cc36c-14ebb931 (24) centos-release
28 0x03387d80-f60cbd8b (24) cgconfig.conf
22020097 0x48562f10-d9514c4c (20) cgconfig.d
29 0x6c7c6f24-6179a0d5 (20) cgrules.conf
30 0x0a6837ca-5ce16cab (36) cgsnapshot_blacklist.conf
60555265 0x5daa8af2-850f21ea (20) chkconfig.d
31 0x4954f0ce-b4d1c6f0 (32) command-not-found.json
32 0x23c009ba-5df41673 (20) conman.conf
33 0x14e0d092-586275d7 (20) cron.deny
56623105 0x6807f248-87fac77b (20) cron.monthly
34 0x69ff30ec-7aa761fb (16) crontab 35 0x71fd63d2-dfab0e24 (16) crypttab
113246209 0x82140724-2ca1443f (20) dnsmasq.d
.......
由于其用了dir_index,其第一个block变成了index_block。第一个block是9482。我们查看这个block内容:
debugfs: block_dump 9482
0000 0200 0000 0c00 0102 2e00 0000 0200 0000 ................
0020 f40f 0202 2e2e 0000 0000 0000 0108 0000 ................
0040 fc01 0200 0100 0000 4efd e28d 0200 0000 ........N.......
.......
这个块起始于一个dx_root结构体,相关代码在内核源代码 fs/ext4/namei.c 文件中,定义为:
struct fake_dirent
{
__le32 inode;
__le16 rec_len;
u8 name_len;
u8 file_type;
};
struct dx_entry
{
__le32 hash;
__le32 block;
};
struct dx_root
{
struct fake_dirent dot;
char dot_name[4];
struct fake_dirent dotdot;
char dotdot_name[4];
struct dx_root_info
{
__le32 reserved_zero;
u8 hash_version;
u8 info_length; /* 8 */
u8 indirect_levels;
u8 unused_flags;
}
info;
struct dx_entry entries[0];
};
本目录的dx_entry管理了2个block,一共需要16个字节的内容,block内容的第三行就是dx_entry存储开始的位置:
fc01 0200 :hash值
0100 0000 :block编号
4efd e28d :hash值
0200 0000 :block编号
这个block后续内容已经废弃,所以不用继续看了。dx_root各个字段大家可以参照结构体内容分别对应研究一下,此处不在过多讲解。
这里要额外说明的是,dir_index功能会在目录索引的block超过一个之后默认开启,其目的是为了在目录下存放的文件或者子目录过多的时候以hash的方式加快block索引速度。这样要比直接线性存放目录项的索引速度要快,目录下的文件越多,效果比线性存放目录项越好。
之后,对应的下一级文件或目录名就会以hash的方法分别放在另外两个块中。我们看一下其中一个block的内容:
debugfs: block_dump 9488
0000 0b00 0000 1400 0a02 6c6f 7374 2b66 6f75 ........lost+fou
0020 6e64 0000 0e00 0000 2000 1701 4449 525f nd...... ...DIR_
0040 434f 4c4f 5253 2e6c 6967 6874 6267 636f COLORS.lightbgco
0060 6c6f 7200 0f00 0000 1400 0b01 4752 4550 lor.........GREP
0100 5f43 4f4c 4f52 5300 1000 0000 1400 0a01 _COLORS.........
0120 4765 6f49 502e 636f 6e66 0000 1100 0000 GeoIP.conf......
0140 1c00 1201 4765 6f49 502e 636f 6e66 2e64 ....GeoIP.conf.d
0160 6566 6175 6c74 0000 1200 0000 1000 0801 efault..........
0200 484f 5354 4e41 4d45 0100 6805 1800 0e02 HOSTNAME..h.....
0220 4e65 7477 6f72 6b4d 616e 6167 6572 0000 NetworkManager..
0240 1300 0000 1000 0701 6164 6a74 696d 6500 ........adjtime.
0260 0100 4002 1400 0c02 616c 7465 726e 6174 ..@.....alternat
0300 6976 6573 1500 0000 1400 0a01 616e 6163 ives........anac
......
这时block中存放的就是目录项的内容。每个单独的目录项结构如下:
/*
* The new version of the directory entry. Since EXT4 structures are
* stored in intel byte order, and the name_len field could never be
* bigger than 255 chars, it's safe to reclaim the extra byte for the
* file_type field.
*/
struct ext4_dir_entry_2 {
__le32 inode; /* Inode number */
__le16 rec_len; /* Directory entry length */
__u8 name_len; /* Name length */
__u8 file_type;
char name[EXT4_NAME_LEN]; /* File name */
};
block最开始记录了lost+found的目录项,注意其子节序为intel byte,是小端字节序。分析其内容可知:
0b00 0000:inode编号,11。
1400 :目录项长度,20字节。
0a:目录名长度,10字节。
02 :文件类型,2表示目录,1表示普通文件。
后续就是文件/目录名的字符串存放位置。此处要注意的是,每个目录项都会按照4字节对齐,所以名字字符串后面还可能有0填充对齐。
后续的文件名我们就不挨个分析了。如果没启用dir_index的话,inode索引block的内容直接线性存放的这种的结构。
我们来删除一个目录,来观察一下目录的inode信息变化。我们想要操作分区上的yum目录。其inode编号为:
ls -id /mnt/yum/
119275521 /mnt/yum/
查看其inode的结构内容:
echo "stat <119275521>" | debugfs /dev/sdf1
debugfs 1.42.9 (28-Dec-2013)
debugfs: stat <119275521>
Inode: 119275521 Type: directory Mode: 0755 Flags: 0x80000
Generation: 657829489 Version: 0x00000000:00000008
User: 0 Group: 0 Size: 4096
File ACL: 0 Directory ACL: 0
Links: 6 Blockcount: 8
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x5e6b3eae:64c1907c -- Fri Mar 13 16:05:02 2020
atime: 0x5e6c545a:278a4a0c -- Sat Mar 14 11:49:46 2020
mtime: 0x5e6b3eae:64c1907c -- Fri Mar 13 16:05:02 2020
crtime: 0x5e6b3eae:60f1007c -- Fri Mar 13 16:05:02 2020
Size of extra inode fields: 28
EXTENTS:
(0):477110304
echo "imap <119275521>" | debugfs /dev/sdf1
debugfs 1.42.9 (28-Dec-2013)
debugfs: imap <119275521>
Inode 119275521 is part of block group 14560
located at block 477102112, offset 0x0000
dd if=/dev/sdf1 of=inode_119275521 bs=1 count=256 skip=$[477102112*4096]
256+0 records in
256+0 records out
256 bytes (256 B) copied, 0.000280918 s, 911 kB/s
hexdump -e '3/4 "%12u" "\n"' inode_119275521
16877 4096 1584157786
1584086702 1584086702 0
393216 8 524288
8 127754 4
0 0 1
477110304 0 0
0 0 0
*
0 657829489 0
0 0 0
0 0 28
1690407036 1690407036 663374348
1584086702 1626407036 0
0 0 0
*
0
我们观察到,这个目录因为内容比较少,所以只占用了一个block,所以flags显示并没启用dir_index。inode中也直接记录了其索引的block编号。我们删除这个目录再来观察一下inode内容:
rm -rf /mnt/yum
[root@100-66-27-140 /data/home/zorrozou]# dd if=/dev/sdf1 of=inode_119275521.rm bs=1 count=256 skip=$[477102112*4096]
256+0 records in
256+0 records out
256 bytes (256 B) copied, 0.000267636 s, 957 kB/s
[root@100-66-27-140 /data/home/zorrozou]# hexdump -e '3/4 "%12u" "\n"' inode_119275521.rm
16877 0 1584157786
1584158264 1584158264 1584158264
0 0 524288
16 62218 4
0 0 0
*
0 657829489 0
0 0 0
0 0 28
2650872228 2650872228 663374348
1584086702 1626407036 0
0 0 0
*
0
inode中的block编号纪录已经被删除。我们创建一个内容比较多的目录,尽量让其占用很多的block,再来看看效果:
for i in `seq 1 10000`;do cp /mnt/pam.d/passwd /mnt/pam.d/zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz_$i;done
ls -id /mnt/pam.d/
87687169 /mnt/pam.d/
echo "stat <87687169>" | debugfs /dev/sdf1
debugfs 1.42.9 (28-Dec-2013)
debugfs: stat <87687169>
Inode: 87687169 Type: directory Mode: 0755 Flags: 0x81000
Generation: 657828295 Version: 0x00000000:00002731
User: 0 Group: 0 Size: 1970176
File ACL: 0 Directory ACL: 0
Links: 2 Blockcount: 3848
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x5e6c574c:bb4fe7d0 -- Sat Mar 14 12:02:20 2020
atime: 0x5e6c56b5:666f7544 -- Sat Mar 14 11:59:49 2020
mtime: 0x5e6c574c:bb4fe7d0 -- Sat Mar 14 12:02:20 2020
crtime: 0x5e6b3ead:ded39088 -- Fri Mar 13 16:05:01 2020
Size of extra inode fields: 28
EXTENTS:
(0-480):350756896-350757376
echo "imap <87687169>" | debugfs /dev/sdf1
debugfs 1.42.9 (28-Dec-2013)
debugfs: imap <87687169>
Inode 87687169 is part of block group 10704
located at block 350748704, offset 0x0000
dd if=/dev/sdf1 of=inode_87687169 bs=1 count=256 skip=$[350748704*4096]
256+0 records in
256+0 records out
256 bytes (256 B) copied, 0.000270948 s, 945 kB/s
hexdump -e '3/4 "%12u" "\n"' inode_87687169
16877 1970176 1584158389
1584158540 1584158540 0
131072 3848 528384
10033 127754 4
0 0 481
350756896 0 0
0 0 0
*
0 657828295 0
0 0 0
0 0 28
3142576080 3142576080 1718580548
1584086701 3738407048 0
0 0 0
*
0
此时目录已经启用了dir_index,观察inode信息,我们发现虽然其一共索引了480个block,但inode中只纪录了第一个block,其他block都是由第一个block中记录的hash tree进行索引的。我们删除这个目录再观察:
rm -rf /mnt/pam.d/
dd if=/dev/sdf1 of=inode_87687169.rm bs=1 count=256 skip=$[350748704*4096]
256+0 records in
256+0 records out
256 bytes (256 B) copied, 0.000257484 s, 994 kB/s
hexdump -e '3/4 "%12u" "\n"' inode_87687169.rm
16877 0 1584158890
1584158890 1584158890 1584158890
0 0 528384
20066 62218 4
0 0 0
*
0 657828295 0
0 0 0
0 0 28
2798224204 2798224204 2238224208
1584086701 3738407048 0
0 0 0
*
0
第一个block的索引已经在删除之后被清除了。从这个实验可以看出来,我们是无法通过inode相关信息恢复目录相关信息的。即使找到第一个块,也无法通过其hash tree恢复相关内容,因为hash tree里只有逻辑块编号,并没有物理块编号。
对于ext4系统来说,我们基本无法通过inode和dir_index相关索引信息恢复目录树结构。
通过目录的每一级索引,就可以遍历到文件系统上所有文件的inode编号,进而找到对应的inode信息。我们已经知道inode中索引了相关block信息,于是整个文件系统所存储的数据就这样组织起来了。
我们通过开头的实验已经大概知道inode中如何索引的block,但那是对一个2G的大文件。如果文件比较小,那么其extent的索引结构相对比较简单。我们知道inode中存放i_block索引的数组元素个数是15个,每个是一个int,所以一共有60字节的长度可以用来存放extent相关数据结构。对于小文件的情况,起索引结构如下图:
每个ext4_extent_header和ext4_extent结构都是12字节。所以这部分最多可以存4个ext4_extent索引,每个ext4_extent最多可以索引32768个连续的block。所以理论上,通过inode直接索引的文件大小最大可以达到512M长度。但是现实使用的情况下,磁盘上一般都不会有那么多连续的块分配给文件,所以大多数文件都到不了这么长,只要占用的ext4_extent结构超过4个,就会产生分级的extent进行更多的块索引。 如果是这种小文件inode直接索引block,在文件被删除的时候,inode中的extent数据结构都会被清零。所以,这种直接索引的文件,也无法通过inode中残留的信息找回相关数据。
以上是普通的文本文件的inode结构。Linux中文件类型除了目录和普通文件以外,还包括符号连接、块设备、字符设备、管道文件和socket文件。这些特殊类型文件大多数不会通过extnet索引块纪录相关信息,相关信息都直接被记录在inode中。比如符号连接,因为它只是纪录了到其指向文件的路径,所以其路径信息会直接记录在i_block数组中。我们可以通过以下命令来查看符号连接的inode内容:
ls -i /mnt/mtab
102 /mnt/mtab
echo "stat <102>" | debugfs /dev/sdf1
debugfs 1.42.9 (28-Dec-2013)
debugfs: stat <102>
Inode: 102 Type: symlink Mode: 0777 Flags: 0x0
Generation: 657828240 Version: 0x00000000:00000001
User: 0 Group: 0 Size: 17
File ACL: 0 Directory ACL: 0
Links: 1 Blockcount: 0
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x5e6b3ead:d63e4c88 -- Fri Mar 13 16:05:01 2020
atime: 0x5e6b4327:21a882e8 -- Fri Mar 13 16:24:07 2020
mtime: 0x5e6b3ead:d63e4c88 -- Fri Mar 13 16:05:01 2020
crtime: 0x5e6b3ead:d63e4c88 -- Fri Mar 13 16:05:01 2020
Size of extra inode fields: 28
Fast_link_dest: /proc/self/mounts
echo "imap <102>" | debugfs /dev/sdf1
debugfs 1.42.9 (28-Dec-2013)
debugfs: imap <102>
Inode 102 is part of block group 0
located at block 1296, offset 0x0500
dd if=/dev/sdf1 of=inode_102 bs=1 count=256 skip=$[1296*4096+1280]
256+0 records in
256+0 records out
256 bytes (256 B) copied, 0.000263532 s, 971 kB/s
hexdump -C inode_102
00000000 ff a1 00 00 11 00 00 00 27 43 6b 5e ad 3e 6b 5e |........'Ck^.>k^|
00000010 ad 3e 6b 5e 00 00 00 00 00 00 01 00 00 00 00 00 |.>k^............|
00000020 00 00 00 00 01 00 00 00 2f 70 72 6f 63 2f 73 65 |......../proc/se|
00000030 6c 66 2f 6d 6f 75 6e 74 73 00 00 00 00 00 00 00 |lf/mounts.......|
00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00000060 00 00 00 00 90 a9 35 27 00 00 00 00 00 00 00 00 |......5'........|
00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000080 1c 00 00 00 88 4c 3e d6 88 4c 3e d6 e8 82 a8 21 |.....L>..L>....!|
00000090 ad 3e 6b 5e 88 4c 3e d6 00 00 00 00 00 00 00 00 |.>k^.L>.........|
000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00000100
除非符号连接中记录的文件路径在i_block数组位置中记录不下了,才会索引一个块来记录相关内容。其他特殊文件机制类似,而这种文件在删除的时候,inode中的数据会像小文件一样被清0,所以我们也无法通过同样的思路恢复这些特殊类型文件。
至此,我们通过一个文件的数据恢复的实例对ext4文件系统的整体结构做了介绍。并介绍了通过文件系统结构和inode结构恢复数据的原理。但这种数据恢复思路依然有其局限性,比如小文件无法恢复、特殊文件无法恢复、目录树结构无法恢复。在有数据持续写入的情况下,被误删除的大文件索引的block也有可能被其他数据覆盖,所以实际在恢复数据的过程中会因这种情况导致数据恢复不完整。我们还可以通过对文件系统块进行全部扫描的方式,来通过文件头部的特征码找到部分占用1个block以内的小文件进行恢复。
数据恢复技术是一个复杂的技术,在实际的恢复过程中会遇到各种各样的问题。希望本文可以通过ext4文件系统的结构视角对理解数据恢复技术有一定帮助。
大家好,我是Zorro!
如果你喜欢本文,欢迎在微博上搜索“orroz”关注我,地址是: https://weibo.com/orroz
大家也可以在微信上搜索:Linux系统技术 关注我的公众号。
我的所有文章都会沉淀在我的个人博客上,地址是: https://zorrozou.github.io/。
欢迎使用以上各种方式一起探讨学习,共同进步。
公众号二维码: