Nydus bootstrap 文件解析

語言: CN / TW / HK

這是零零散散的關於 Nydus 學習筆記中的一段,主要是瞭解下 bootstrap 文件的存儲格式。我們知道 nydus 會創建兩種類型的文件:

  • bootstrap: 存儲文件系統的元數據
  • blob: 存儲數據內容

這裏我們以一個很簡單的例子,來看一下 bootstrap 都保存了哪些文件系統的信息。

環境準備

我們使用的測試例子很簡單,通過 nydus-image create 創建一個 bootstrap,這個 bootstrap 的輸入文件夾內容也很少。

$ mkdir fs
$ touch fs/aaa
$ date > fs/bbb
$ nydus-image create --fs-version 5 --bootstrap output/bootstrap --blob-dir output fs

可以看出,該文件系統一共有兩個文件:一個空文件,一個很簡單的文本文件。

$ ls -l output/bootstrap
-rw-r--r-- 1 vagrant vagrant 8832 Apr 26 06:55 output/bootstrap

bootstrap 文件解析

我們使用 xxd 命令來輸出 bootstrap 文件的 16 進制內容,具體命令如下:

$ xxd -a -e output/bootstrap

xxdhexdump 的優點是可以非常簡單的輸出 little-endian 的內容,即其 -e 選項,rafs super block 存儲的時候使用的就是 little-endian 的字節順序。 -a 選項用於去除空行(全 0x00 的行)。 -g 1 選項設置一個字節單獨一個字段,可以幫助我們計數,默認的話一個字段 4 個字節,即一個 u32 。

而且 xxd 輸出默認就是 32 位一個字段,正好是一個 u32 ,而 RafsV5SuperBlock 中很多數據就是 u32 類型的。該命令輸出結果一行對應 16 個字節,也就是 4 個 u32 。每列寬度可以通過 -g 選項控制,默認為 2,即 4 個字節。

下面輸出結果中第一列為地址,後 4 列為二進制數據,之後部分,比如 SFAR... 為 ASCII 顯示字符,在這裏沒有意義,可以忽略。

RafsV5SuperBlock

先來看看 RafsV5SuperBlock 對應的數據。

第一個 16 字節

00000000: 52414653 00000500 00002000 00100000  SFAR..... ......

前 16 個字節,對應 RafsV5SuperBlock 的以下屬性:

  • s_magic: u32 : v5 magic number 是 0x5241_4653
// rafs/src/metadata/layout/v5.rs
const RAFSV5_SUPER_MAGIC: u32 = 0x5241_4653;
  • s_fs_version: u32 : 文件系統版本,即 500
// rafs/src/metadata/layout/mod.rs
pub const RAFS_SUPER_VERSION_V5: u32 = 0x500;
s_sb_size: u32
s_block_size: u32

第二個 16 字節

00000010: 00000016 00000000 00000003 00000000  ................

對應兩個屬性:

  • s_flags: u64 : 來自 RafsSuperFlags ,這裏值為 16,具體內容為 COMPRESS_LZ4_BLOCK | DIGESTER_BLAKE3 | EXPLICIT_UID_GID ,如何算出來的如下所示:
// rafs/src/metadata/mod.rs
const COMPRESS_LZ4_BLOCK = 0x0000_0002;
const DIGESTER_BLAKE3 = 0x0000_0004;
const EXPLICIT_UID_GID = 0x0000_0010;
  • s_inodes_count: u64 : 這裏值為3,即只有3個inode,一個根文件,還有兩個普通文件。

第三個 16 字節

00000020: 00002000 00000000 00002010 00000000  . ....... ......

對應兩個屬性:

  • s_inode_table_offset: u64 : inode table 所在位置的位移,這裏為 0x2000 = 8192。注意這裏雖然是 u64 ,但是好像 00002000 00000000 的順序還是需要以 4 字節為單位,從右往左讀,而每個 4 字節則是從左往右讀。
  • s_prefetch_table_offset: u64 : prefetch table 的位移,從相對值來説,比上面的 inode table 的位置 多了 0x10,即 16 個字節。

第四個 16 字節

00000030: 00002010 00000000 00000003 00000000  . ..............

對應三個屬性:

s_blob_table_offset: u64
s_inode_table_entries: u32
s_prefetch_table_entries: u32

第5個 16 字節

00000040: 00000048 00000001 00002058 00000000  H.......X ......

對應三個屬性:

s_blob_table_size: u32
s_extended_blob_table_entries: u32
s_extended_blob_table_offset: u64

第5個 16 字節及以後

00000050: 00000000 00000000 00000000 00000000  ................

RafsV5SuperBlock 結構體的最後一個屬性如下:

  • s_reserved: [u8; RAFSV5_SUPERBLOCK_RESERVED_SIZE]

我們從這個常量的定義可以看到:

// rafs/src/metadata/layout/v5.rs
pub(crate) const RAFSV5_SUPERBLOCK_SIZE: usize = 8192;
const RAFSV5_SUPERBLOCK_RESERVED_SIZE: usize = RAFSV5_SUPERBLOCK_SIZE - 80;

Inode Table

整個 super block 佔用 8192 字節,其中前面我們看到的幾個屬性,佔用 80 個字節,其餘部分為保留區域,以供擴展時使用。

RafsV5SuperBlock 中大部分都是預留的空位置,從下面一行可以看到,地址跳到了 0x2000,也是我們前面看到的 s_inode_table_offset 的值,這也意味着,這行開始的內容是 inode table 的內容。

在看上面的內容之前,我們先看看 inode table 的結構:

// rafs/src/metadata/layout/v5.rs
pub struct RafsV5InodeTable {
    /// Inode offset array.
    pub data: Vec<u32>,
}

注意這裏 inode table 是一個 vector,裏面存的不是 inode 對象,而是 inode 對應的 offset,而 vector 的索引就是 inode 對應的數值,每個 offset 都是 32 位類型,佔用 4 個字節。而且索引的值為 inode -1 ,這樣 root inode 就保存在 0 的位置上,一點都不浪費。

00002000: 00000413 00000424 00000435 00000000  ....$...5.......

我們看到了 3 個 offset,分別為 413、424 和 432,每個 offset 之間間隔 11 。這個 offset 是 inode table 元素對應在 bootstrap 文件中的絕對位置的偏移量,比如 0x413 = 1043,而這個數據結構保存的位置都是 8 字節對齊的,所以這個值在保存的時候,是右位移 3 位的,取出來的時候再左位移 3 位,所以真正的位移值為 1043 * 8 = 8344,也就是 16 進制的 0x2098,在下面的分析中我們還會看到 inode table 的具體內容。

同理 0x424 對應的偏移為 0x2120,0x432 對應的偏移量是 0x2190。

注意: rafs v5 存儲都有對齊,為 8 字節。由於在這裏的測試中只有 3 個文件,所以存儲需要 3 個 u32,但是由於存在 8 字節對齊的需求,這裏 3 個 u32 是 12 個字節,所以需要補齊 4 個字節,所以我們看到 0x2000 那一行最後補齊的全零的 u32。

pub(crate) const RAFSV5_ALIGNMENT: usize = 8;

Blob table

從 0x2010 開始存儲的是 blob table 內容。還是先來看看 blob table 的定義:

// rafs/src/metadata/layout/v5.rs
#[derive(Clone, Debug, Default)]
pub struct RafsV5BlobTable {
    /// Base blob information array.
    pub entries: Vec<Arc<BlobInfo>>,
    /// Extended blob information array.
    pub extended: RafsV5ExtBlobTable,
}

注意,BlobInfo 結構是在 storage crate 中定義的。

// storage/src/device.rs

/// Configuration information for a metadata/data blob object.
///
/// The `BlobInfo` structure provides information for the storage subsystem to manage a blob file
/// and serve blob IO requests for clients.
#[derive(Clone, Debug, Default)]
pub struct BlobInfo {
    /// The index of blob in RAFS blob table.
    blob_index: u32,
    /// A sha256 hex string generally.
    blob_id: String,

    /// 此處省略其餘屬性
}

前面我們已經看到,s_blob_table_offset 對應的值為 0x2010 ,s_blob_table_size 的值為 0x48 = 72 字節,也就是存儲位置在 [2010 - 2058),2058 也正是 s_extended_blob_table_offset 的值。這部分數據如下:

00002010: 00000000 00000000 31343261 65373762  ........a241b77e
00002020: 38333362 32373532 63623763 38336231  b3382572c7bc1b38
00002030: 38623561 36393139 36326366 62343062  a5b89196fc26b04b
00002040: 37363666 34313962 63653062 33313137  f667b914b0ec7113
00002050: 37343061 32623835 00000001 00000000  a04758b2........
00002050: 37343061 32623835   00000001 00000000  a04758b2........

注意上面 2050 這一行我又拷貝了一遍,並在 2058 前添加了 2 個空格來方便識別位置。

這裏我們 blob 的 id 是 a241b77eb3382572c7bc1b38a5b89196fc26b04bf667b914b0ec7113a04758b2 ,可以和上面輸出最右側 ASCII 部分內容對照。

而且要注意的是上面的輸出左右對照稍微不太直觀,雖然列是從左到右,但是一列之中是從右到左的順序。比如 31343261 ,對應的內容實際是 61323431 ,即 a241

雖然上面看到的 blob table 的定義很複雜、屬性很多,但是我們的測試足夠簡單。RafsV5BlobTable 存儲到磁盤後,最開始的內容就是 BlobInfo 結構體。

但是在 RafsV5BlobTable 的 store 方法(用於序列化到磁盤的 RafsStore trait 所需)中,我們可以看到,對於每一個 blob info 對象,都會在前面寫兩個 readahead 屬性,這兩個屬性供佔用 8 個字節,然後才是 blob id 。所以我們可以在上面 bootstrap 內容中,在 8 個自己的全零(默認值即是 0 )後面,才是 blob id。

w.write_all(&u32::to_le_bytes(entry.readahead_offset() as u32))?;
w.write_all(&u32::to_le_bytes(entry.readahead_size() as u32))?;
w.write_all(entry.blob_id().as_bytes())?;

儘管 BlobInfo 對象屬性很多,但是持久化到磁盤的內容卻不多,主要就上面這 3 個,外加一些對齊的字節。

下面從 0x2058 開始是 RafsV5ExtBlobTable 的內容。其定義如下:

/// Rafs v5 on disk extended blob information table.
#[derive(Clone, Debug, Default)]
pub struct RafsV5ExtBlobTable {
    /// The vector index means blob index, every entry represents
    /// extended information of a blob.
    pub entries: Vec<Arc<RafsV5ExtBlobEntry>>,
}

/// Rafs v5 extended blob information on disk metadata.
///
/// RafsV5ExtDBlobEntry is appended to the tail of bootstrap,
/// can be used as an extended table for the original blob table.
// This disk structure is well defined and rafs aligned.
#[repr(C)]
#[derive(Clone)]
pub struct RafsV5ExtBlobEntry {
    /// Number of chunks in a blob file.
    pub chunk_count: u32,
    pub reserved1: [u8; 4],     //   --  8 Bytes
    pub uncompressed_size: u64, // -- 16 Bytes
    pub compressed_size: u64,   // -- 24 Bytes
    pub reserved2: [u8; RAFSV5_EXT_BLOB_RESERVED_SIZE],
}

其中 RAFSV5_EXT_BLOB_RESERVED_SIZE 的值為 40。

對齊進行持久化的方法如下:

w.write_all(&u32::to_le_bytes(entry.chunk_count))?;
w.write_all(&entry.reserved1)?;
w.write_all(&u64::to_le_bytes(entry.uncompressed_size))?;
w.write_all(&u64::to_le_bytes(entry.compressed_size))?;
w.write_all(&entry.reserved2)?;

看完代碼再來看一下存儲的數據內容。

00002050: 37343061 32623835 00000001 00000000  a04758b2........
00002060: 00000040 00000000 00000035 00000000  @.......5.......

注意 0x2058 是從上面第 9 個字節開始的,這裏 chunk_count 的值為 0x00000001,即只有一個 chunk。然後 uncompressed_size 屬性佔用 8 個字節,在上面兩行中實際是跨行了,包括第一行的後四個和第二行的前四個字節。其值為 0x40,即 64 字節。同理,compressed_size 的值為 0x35,即 53 字節。這也和我們在文件系統上看到的是一樣的:

-rw-r--r-- 1 vagrant vagrant   53 Apr 26 06:55 a241b77eb3382572c7bc1b38a5b89196fc26b04bf667b914b0ec7113a04758b2

0x2060 行最後的 4 個字節為保留字節。

Blob table 之後寫入是 inode 信息,這些信息是通過 RafsV5InodeWrapper 結構表示的。Inode 信息由 3 部分組成:

  • Inode 結構體的數據
  • xattrs
  • Chunk info

我們還是先來看看一些關鍵數據結構的定義。

// rafs/src/metadata/layout/v5.rs
pub struct RafsV5InodeWrapper<'a> {
    pub name: &'a OsStr,
    pub symlink: Option<&'a OsStr>,
    pub inode: &'a RafsV5Inode,
}

// 刪除了該方法的一些內容,主要保留了核心寫入到磁盤的數據
impl<'a> RafsStore for RafsV5InodeWrapper<'a> {
    fn store(&self, w: &mut dyn RafsIoWrite) -> Result<usize> {
        let mut size: usize = 0;

        // 1. 寫入 RafsV5Inode 內容,128 字節
        let inode_data = self.inode.as_ref();
        w.write_all(inode_data)?;

        // 2. 寫入文件名,可變長度
        let name = self.name.as_bytes();
        w.write_all(name)?;
    }
}

pub struct RafsV5Inode {
    /// sha256(sha256(chunk) + ...), [char; RAFS_SHA256_LENGTH]
    pub i_digest: RafsDigest, // 32
    /// parent inode number
    pub i_parent: u64,
    /// from fs stat()
    pub i_ino: u64,
    pub i_uid: u32,
    pub i_gid: u32,
    pub i_projid: u32,
    pub i_mode: u32, // 64
    pub i_size: u64,
    pub i_blocks: u64,
    pub i_flags: RafsV5InodeFlags,
    pub i_nlink: u32,
    /// for dir, child start index
    pub i_child_index: u32, // 96
    /// for dir, means child count.
    /// for regular file, means chunk info count.
    pub i_child_count: u32,
    /// file name size, [char; i_name_size]
    pub i_name_size: u16,
    /// symlink path size, [char; i_symlink_size]
    pub i_symlink_size: u16, // 104
    // inode device block number, ignored for non-special files
    pub i_rdev: u32,
    // for alignment reason, we put nsec first
    pub i_mtime_nsec: u32,
    pub i_mtime: u64,        // 120
    pub i_reserved: [u8; 8], // 128
}

我們繼續對照 dump 出來的 bootstrap 數據來看一下寫入的具體內容。

00002070: 00000000 00000000 00000000 00000000  ................
00002080: 00000000 00000000 00000000 00000000  ................
00002090: 00000000 00000000 afbe1b2a 8b68b09e  ........*.....h.
000020a0: ac7a3553 ec9df26a 5d07bafa 02097de0  S5z.j......].}..
000020b0: 4c85264b 5749c4a7 00000000 00000000  K&.L..IW........
000020c0: 00000001 00000000 000003e8 000003e8  ................
000020d0: 00000000 000041ed 00000080 00000000  .....A..........
000020e0: 00000001 00000000 00000000 00000000  ................
000020f0: 00000002 00000002 00000002 00000001  ................
00002100: 00000000 00000000 00000000 00000000  ................
00002110: 00000000 00000000 0000002f 00000000  ......../.......

首先我們肉眼可見的 128 長度的 sha256 摘要,所以很容易定位到 RafsV5Inode 結構的內容。之後的 8 字節表示 parent inode,這裏為 0。

然後到着看找到 2110 行的 2f ,這就是 ASCII 的 / ,也就是根目錄。再倒着往回數 26 個字節,到了 i_name_size 屬性,這裏值為 1,即 / 長度為 1,再往上一個屬性,即 i_child_count ,這裏值為 2。其餘屬性這裏就不再詳細介紹。

00002120: b94913af a6a1f9f5 ea4d40a0 49c9dc36  [email protected]
00002130: c925cb9b b712c1ad ca939acc 62321fe4  ..%...........2b
00002140: 00000001 00000000 00000002 00000000  ................
00002150: 000003e8 000003e8 00000000 000081a4  ................
00002160: 00000000 00000000 00000000 00000000  ................
00002170: 00000000 00000000 00000001 00000000  ................
00002180: 00000000 00000003 00000000 00000000  ................
00002190: 626767b2 00000000 00000000 00000000  .ggb............
000021a0: 00616161 00000000 b232f6e2 e21610c0  aaa.......2.....

同樣下一個文件名為 aaa ,也可以從 00616161 看到。

000021a0: 00616161 00000000 b232f6e2 e21610c0  aaa.......2.....
000021b0: efe31e11 2532c9d6 2f8e943d e7082bfe  ......2%=../.+..
000021c0: 81da0118 d1192211 00000001 00000000  ....."..........
000021d0: 00000003 00000000 000003e8 000003e8  ................
000021e0: 00000000 000081a4 00000040 00000000  ........@.......
000021f0: 00000001 00000000 00000000 00000000  ................
00002200: 00000001 00000000 00000001 00000003  ................
00002210: 00000000 00000000 62679767 00000000  ........g.gb....
00002220: 00000000 00000000 00626262 00000000  ........bbb.....

然後是最後一個文件 bbb ,也可以從 00626262 看到。它的父文件夾為 1,文件大小為 0x40,這裏應該是壓縮後的文件大小了。

最後是 chunk 信息。我們來看看它的結構:

// rafs/src/metadata/layout/v5.rs
pub struct RafsV5ChunkInfo {
    /// sha256(chunk), [char; RAFS_SHA256_LENGTH]
    pub block_id: RafsDigest, // 32
    /// blob index.
    pub blob_index: u32,
    /// chunk flags
    pub flags: BlobChunkFlags, // 40
    /// compressed size in blob
    pub compress_size: u32,
    /// uncompressed size in blob
    pub uncompress_size: u32, // 48
    /// compressed offset in blob
    pub compress_offset: u64, // 56
    /// uncompressed offset in blob
    pub uncompress_offset: u64, // 64
    /// offset in file
    pub file_offset: u64, // 72
    /// chunk index, it's allocated sequentially and starting from 0 for one blob.
    pub index: u32,
    /// reserved
    pub reserved: u32, //80
}

可以看出,它的長度是 80 個字節,對應文件的最後這部分:

00002230: ec5944de 690964ef 8274f1bf 7cf30f7c  .DY..d.i..t.|..|
00002240: 62fc5b93 d8d8c7a0 3272f74b b91db007  .[.b....K.r2....
00002250: 00000000 00000001 00000035 00000040  ........5...@...
00002260: 00000000 00000000 00000000 00000000  ................
00002270: 00000000 00000000 00000000 00000000  ................

上面只是簡單分析了下 bootstrap 文件的內容,至於 blob 數據文件,則留待以後有時間再看了。