Ext2

Eu tinha começado a escrever sobre Ext2 em um server no Discord. Depois de um tempo, percebi que a coisa começou a ficar grande, então resolvi trazer o que eu escrevi sobre o assunto para cá e transformar em um artigo.

Vou disponibilizar um arquivo chamado ext2.img de 25 MiB que foi formatado com ext2. Ele vai ser usado em alguns testes que vamos fazer. Se você montar a imagem vai ver que ela contém alguns arquivos:

ext2/
├── dagon.txt
└── dir1
    └── arquivo.txt
1 directory, 2 files

São arquivos simples. O dagon.txt tem um texto com um conto em inglês. Queria criar um arquivo que não fosse nem tão grande e nem tão pequeno, porque quero demonstrar como arquivos maiores que 4096 bytes são organizados na memória. Ele tem 12 KiB bytes de tamanho.

O resto é apenas 1 diretório chamado dir1/ e um arquivo dentro dele chamado arquivo.txt. O arquivo tem apenas 10 bytes de tamanho.

Essa imagem pode ser baixada aqui.

Você pode criar sua própria imagem Ext2 usando o dd e o mkfs.ext2:

$ dd if=/dev/zero of=ext2.img bs=25M count=1
$ mkfs.ext2 ext2.img 

Depois é só montar a imagem e colocar o que você quiser nela.

1 - Ext2

O Ext2 (Second Extended Filesystem) foi lançado, em 1993, para o Kernel Linux. Ele foi criado para substituir o Ext (Extended Filesystem). Além do Linux, existem os Sistemas Operacionais BSDs e Minix que implementam o Ext2 no kernel.

Logo depois veio o Ext3, que introduziu o journaling e, posteriormente, veio o ext4. O Ext4 é o mais usado pelas distros Linux, hoje em dia. Entretanto, mesmo o Ext2 não sendo muito utilizado, a implementação ainda continua no código do kernel.

Além disso, o conhecimento de Ext2 pode servir como base para entender Ext3 e Ext4, então não acho um perda de tempo aprender Ext2.

2 – Blocos e grupo de blocos

Basicamente, quase toda a estrutura do Ext2 é dividida em blocos. O tamanho desses blocos podem influenciar o tamanho da partição, o tamanho máximo dos arquivos, áreas importantes, etc. O tamanho pode ser 1 KiB, 2 KiB, 4 KiB e 8 KiB. Geralmente, o tamanho mais usado é o de 4 KiB que é, inclusive, o tamanho usado por nossa imagem.

Os blocos também podem ser organizados em grupos de blocos. Cada um desses grupos contém áreas importantes do Ext2. Essa organização estranha serve para evitar que o disco faça muitas buscas quando está lidando, por exemplo, com uma quantidade gigante de dados contíguos.

3 – inode

O inode é uma estrutura usada para representar arquivos e diretórios. Ele fornece informações importantes como o tamanho do arquivo, localização dele, a última vez que foi escrito, etc. A estrutura é a seguinte:

struct ext2_inode {
    __le16    i_mode;
    __le16    i_uid;
    __le32    i_size;
    __le32    i_atime;
    __le32    i_ctime;
    __le32    i_mtime;
    __le32    i_dtime;
    __le16    i_gid;
    __le16    i_links_count;
    __le32    i_blocks;
    __le32    i_flags;
    union {
        struct {
            __le32  l_i_reserved1;
        } linux1;
        struct {
            __le32  h_i_translator;
        } hurd1;
        struct {
            __le32  m_i_reserved1;
        } masix1;
    } osd1;
    __le32    i_block[EXT2_N_BLOCKS];
    __le32    i_generation;
    __le32    i_file_acl;
    __le32    i_dir_acl;
    __le32    i_faddr;
    union {
        struct {
            __u8    l_i_frag;
            __u8    l_i_fsize;
            __u16    i_pad1;
            __le16    l_i_uid_high;
            __le16    l_i_gid_high;
            __u32    l_i_reserved2;
        } linux2;
        struct {
            __u8    h_i_frag;
            __u8    h_i_fsize;
            __le16    h_i_mode_high;
            __le16    h_i_uid_high;
            __le16    h_i_gid_high;
            __le32    h_i_author;
        } hurd2;
        struct {
            __u8    m_i_frag;
            __u8    m_i_fsize;
            __u16    m_pad1;
            __u32    m_i_reserved2[2];
        } masix2;
    } osd2;
};

Essa estrutura e as próximas ficam localizada em fs/ext2/ext2.h.

Antes de continuar, preciso explicar esses tipos usados pela estrutura. Todos que são prefixados com __u (__u8, __u16 e __u32) são tipos inteiros sem sinal (unsigned). O número após ele é o total de bits, ou seja, o __u8 é um tipo inteiro sem sinal de 8 bits. Quanto aos tipos prefixados com __le (__le16, __le32 e __le64), eles são do tipo inteiro sem sinal e devem ser little endian. Existe também os tipos para big endian (__be16, __be32, __be64). Você pode achar esses tipos em tool/include/linux/types.h:

Não vou me aprofundar em cada um dos elementos dessa estrutura, se não eu ficaria muito tempo nisso. Vou falar, principalmente, do i_mode, i_size, i_blocks, i_block e i_dir_acl, pois são os mais importantes.

3.1 – i_mode

O i_mode é uma valor de 16 bits que vai dizer com que tipo de arquivo estamos lidando e as permissões dele. O linux disponibiliza algumas macros para identificar isso. Um exemplo:

// ATENÇÃO: Estamos lidando com octal aqui.
#define S_IFDIR        0040000
#define S_IFREG        0100000
#define S_ISDIR(mode)  (((mode) & S_IFMT) == S_IFDIR) // Ele verifica o bit 15 para saber se é um diretórios.
#define S_ISREG(mode)  (((mode) & S_IFMT) == S_IFREG) // Ele verifica o bit 14 para saber se é um arquivos comum.

Existe outros formatos de arquivos, mas vamos focar apenas nos diretórios e arquivos tradicionais.

Os 9 primeiros bits do i_mode são usados para definir as permissões do arquivo. Os bits 8:6 são as permissões de usuários. Se o bit 8 for definido, o usuário tem permissão de leitura. Se o bit 7 for definido, o usuário tem permissão de escrita. E, se o bit 6 for definido, o usuário tem permissão de execução.

Esse mesmo padrão de leitura, escrita e execução se repete para os bits 5:3 e 2:0. A diferença é que os bits 5:3 são permissões para o grupo, e os bits 2:0 são para as permissões de outros usuários.

3.2 – i_size e i_dir_acl

O i_size tem 32 bits bit e é a parte baixa de um valor de 64 bits que dá o tamanho do arquivo em bytes. O i_dir_acl é a parte alta desse valor. Em versões antigas do Ext2, o i_size era usado sozinho, mas 32 bits não é o suficiente, pois 2³² = 4294967296 bytes (4 GiB). Os arquivos no Ext2, como vou mostrar daqui a pouco, tem 16 GiB, no máximo, caso seja usado blocos de 1 KiB, então o tamanho do arquivo tem que ser maior que 32 bits. Claro, 64 bits é mais do que o suficiente: 2⁶⁴ = 18446744073709551616 bytes (16 EiB).

3.3 – i_blocks

O i_blocks tem 32 bits e ele dá o tamanho do arquivo em blocos de 512 bytes. Ele é mais conveniente, claro, quando estamos lidando com discos que têm setores de 512 bytes.

3.4 – i_block

Chegamos na parte mais interessante. Só lembrando, não confunda i_block com i_blocks.

A estrutura do i_block é bem complexa. Ela lembra muito paginação. Se entender ela, o resto do artigo vai ser fácil para vocês. A estrutura é mais ou menos assim:

O i_block é um array de tamanho 15. Cada uma das entradas nesse array tem 32 bits. As 12 primeiras entradas vão apontar para o que é chamado de Direct Block, que é um bloco que contém os dados do arquivo. Se você fizer um cálculo simples vai perceber que, com apenas essas 12, já é possível ter arquivos com tamanho de 12 KiB se o bloco for de 1 KiB: 1024 * 12 = 12288 (12 KiB).

A entrada 13 serve para expandir o tamanho do arquivo. Ela vai apontar para um bloco que contém entradas de 32 bits que apontam para Indirect Blocks. Com isso já é possível aumentar o tamanho dos arquivos para 268 KiB caso esteja usando blocos de 1 KiB, pois (12 + 1024 / 4) * 1024 = 274432 (268 KiB). Primeiro é calculado o total de entradas e depois é multiplicado por 1024 para descobrir o total de bytes.

A entrada 14 aponta para um bloco. Esse bloco contém entradas que apontam para outros blocos. Esses outros blocos tem entradas que apontam para Double Indirect Blocks. Se o tamanho do bloco for 1 KiB, então o tamanho do arquivo sobe para 64 MiB, porque (12 + 1024 / 4 + (1024 / 4) * (1024 / 4)) * 1024 = 67383296 (64 MiB)

Quanto à entrada 15, ela segue a mesma lógica das anteriores, ou seja, é adicionado um nível. Acho que não preciso falar aqui como essa hierarquia vai funcionar. O último bloco deste nível é chamado de Triple Idirect Block. O arquivo aumenta o tamanho com a entrada 15. Ele pode ter agora 16 GiB:

(12 + 1024 / 4 + (1024 / 4) * (1024 / 4) + (1024 / 4) * (1024 / 4) * 1024 / 4) * 1024 = 17247252480 (16 GiB)

Tentei simplificar tudo isso para ficar melhor de calcular: t = ((b/4)³ + (b/4)² + 12) * b. t é o tamanho máximo do arquivo, b é o tamanho do bloco e 12 são dos primeiros Direct Blocos. Usando todos os tamanhos de blocos:

((1024/4)³ + (1024/4)² + 12) * 1024 = 17247252480 (16 GiB)
((2048/4)³ + (2048/4)² + 12) * 2048 = 275414802432 (256 GiB)
((4096/4)³ + (4096/4)² + 12) * 4096 = 4402341527552 (4 TiB)
((8192/4)³ + (8192/4)² + 12) * 8192 = 70403104014336 (64 TiB)

Note que o blocos de 8 KiB possibilitam arquivos de até 64 TiB, entretanto o tamanho máximo de um arquivo no Ext2 pode variar de 16 GiB até 4 TiB. Acredito que o pessoal que pensou no Ext2 limitou o tamanho máximo de um arquivo para 4 TiB, porque:

1 – Arquivos com 64 TiB são muito grandes, principalmente, para os padrões da época;

2 – O tamanho máximo de uma volume com Ext2 é 32 TiB.

4 – Estrutura do Ext2

Como eu disse antes, o Ext2 é quase todo dividido em blocos e grupo de blocos. Pode existir o grupo de blocos 0, grupo de blocos 1, etc. O grupo 0 deve conter, no mínimo, 5 áreas importantes do Sistema de Arquivos. Essas áreas são: O superblock, o group descriptors, o data block bitmap, o inode bitmap, o inode table e o data Blocks.

Do grupo 1 em diante, o superblock e o group descriptors não são obrigatórios. Mas, quando essas áreas estão presentes em outros grupos, elas servem apenas como backup das que ficam no grupo 0.

Os primeiros 1 KiB do disco são uma exceção, pois ele não faz parte de nenhum grupo. Essa área é chamada de boot block e ela sempre vai ter 1 KiB de tamanho, mesmo se o tamanho do bloco for outro. Claro, como o próprio nome já diz, ela é usada pelo bootloader.

Um exemplo de como ela fica organizada na memória:

4.1 – Superblock

Vamos pular o boot block, porque não vamos falar de bootloader aqui.

O superblock é uma área de 1024 bytes que sempre fica localizada no offset 1024 (0x400) do disco. Ela tem informações importante do Sistema de Arquivos como o número de inodes, o número de blocos, o tamanho dos blocos, etc.

Você pode ver essa área no nosso ext2.img:

A estrutura do superblock, no kernel do Linux, é a seguinte:

struct ext2_super_block {
	__le32	s_inodes_count;
	__le32	s_blocks_count;
	__le32	s_r_blocks_count;
	__le32	s_free_blocks_count;
	__le32	s_free_inodes_count;
	__le32	s_first_data_block;
	__le32	s_log_block_size;
	__le32	s_log_frag_size;
	__le32	s_blocks_per_group;
	__le32	s_frags_per_group;
	__le32	s_inodes_per_group;
	__le32	s_mtime;
	__le32	s_wtime;
	__le16	s_mnt_count;
	__le16	s_max_mnt_count;
	__le16	s_magic;
	__le16	s_state;
	__le16	s_errors;
	__le16	s_minor_rev_level;
	__le32	s_lastcheck;
	__le32	s_checkinterval;
	__le32	s_creator_os;
	__le32	s_rev_level;
	__le16	s_def_resuid;
	__le16	s_def_resgid;
	__le32	s_first_ino;
	__le16   s_inode_size;
	__le16	s_block_group_nr;
	__le32	s_feature_compat;
	__le32	s_feature_incompat;
	__le32	s_feature_ro_compat;
	__u8	s_uuid[16];
	char	s_volume_name[16];
	char	s_last_mounted[64];
	__le32	s_algorithm_usage_bitmap;
	__u8	s_prealloc_blocks;
	__u8	s_prealloc_dir_blocks;
	__u16	s_padding1;
	__u8	s_journal_uuid[16];
	__u32	s_journal_inum;
	__u32	s_journal_dev;
	__u32	s_last_orphan;
	__u32	s_hash_seed[4];
	__u8	s_def_hash_version;
	__u8	s_reserved_char_pad;
	__u16	s_reserved_word_pad;
	__le32	s_default_mount_opts;
 	__le32	s_first_meta_bg;
	__u32	s_reserved[190];
};

4.1.1 – s_inodes_count

O s_inodes_count tem 32 bits e fica no offset 0 em relação ao início do superblock. Como o próprio nome já diz, ele dá o total de inodes do Sistema de Arquivos, ou seja, vai incluir todos os inodes de cada Grupo.

No caso da nossa imagem, ela tem 0x1900 (6400) inodes:

4.1.2 – s_blocks_count

O s_blocks_count tem 32 bits também e fica no offset 4. Ele contém o total de todos os blocos, independente se eles estão livres ou não. Nossa imagem tem 0x1900 (6400) blocos:

Fiz algumas contas e percebi que posso calcular o tamanho do arquivo usando o s_blocks_count: 6400 * 4096 = 26214400 (25 MiB). Multipliquei por 4096, porque é o tamanho de bloco usado pela imagem. Se vocês lembrarem, eu tinha dito no começo que o tamanho da imagem é, realmente, 25 MiB.

4.1.3 – s_free_blocks_count

s_free_blocks_count tem um valor de 32 bits e fica no offset 12. Não tem muito o que falar sobre ele. Apenas saiba que ele é o total de todos os blocos livres no Sistema de Arquivos. No nosso caso ele é 0x1763 (5987):

4.1.4 – s_free_inodes_count

s_free_inodes_count tem 32 bits e fica no offset 16. Ele é o total de todos os inodes livres no Sistema de Arquivos. A imagem tem 0x18f3 (6387) inodes livres:

4.1.5 – s_log_block_size

O s_log_block_size é um elemento bem estranho. Ele tem 32 bits de tamanho e fica no offset 24. Ele é um valor onde precisa ser feito um shift para a direita com 1024 para descobrir o tamanho do bloco. Aí eu me pergunto: Por quê simplesmente não é colocado 1024, 2048…?

Enfim, os blocos da nossa imagem tem tamanho de 4 KiB, pois 1024 << 2 = 4096:

Bom, a maioria dos nomes dos elementos na estrutura do superblock já são autoexplicativos. Ainda existem outros importantes que não falei sobre, mas conforme eu for escrevendo eu vou citar algum deles.

4.2 – Group Descriptors

O group descriptors é uma tabela de estruturas que descrevem os grupos. Ele não tem o tamanho fixo como o superblock e o boot block. Existe uma estrutura dessa para cada grupo no Sistema de Arquivos. A estrutura é a seguinte:

struct ext2_group_desc
{
    __le32    bg_block_bitmap;
    __le32    bg_inode_bitmap;
    __le32    bg_inode_table;
    __le16    bg_free_blocks_count;
    __le16    bg_free_inodes_count;
    __le16    bg_used_dirs_count;
    __le16    bg_pad;
    __le32    bg_reserved[3];
};

O group descriptors fica localizado no bloco adjacente ao bloco do superblock. Por exemplo, se o tamanho do bloco for 1 KiB, então o group descriptors ocupa o terceiro bloco, porque o boot block vai ocupar o primeiro e o superblock vai ocupar o segundo. Mas, se o tamanho do bloco for 2 KiB em diante, então o group descriptors vai ocupar o segundo bloco, porque o boot Block e superblock já ocupam o primeiro.

A primeira entrada, no nosso ext2.img, fica localizada no offset 0x1000 (4096):

O bg_block_bitmap contém o bloco que fica localizado o data block bitmap. bg_inode_bitmap contém o bloco do inode bitmap. E, bg_inode_table contém o bloco do inode table.

Só lembrando, cada desses valores se referem a área que fica no grupo descrito pela entrada.

bg_free_blocks_count contém o total de blocos livre no grupo descrito pelo entrada. bg_free_inodes_count tem o total de inodes livres no grupo descrito. E, bg_used_dirs_count tem o total de inodes alocados para os diretórios do grupo descrito.

Antes de continuar para o próximo assunto, eu preciso falar mais uma coisa. Se você perceber, o nosso ext2.img tem apenas 1 grupo. Isso acontece, porque, como eu disse antes, o total de blocos dele é 6400. No superblock existe um campo chamado s_blocks_per_group que diz o total máximo de blocos por grupo. Nossa imagem pode ter até 0x8000 (32768) blocos por grupo:

6400 blocos não chegam nem perto de 32768, então nós vamos ter apenas 1 entrada no group descriptors.

4.3 – Block Bitmap

O block bitmap vai conter o bitmap dos blocos livres e usados no grupo que ele faz parte. Ele tem o tamanho de 1 bloco. Se o bit for 1, então o bloco representado por ele está sendo usado, mas se o bit for 0, então o bloco é livre. O bit 0 do byte 0 vai representar o 1º Bloco do Grupo, o bit 1 vai representar o 2º Bloco. O bit 0 do byte 1 vai representar o 9º Bloco e assim por diante.

Ele pode ser localizado usando o bg_block_bitmap do Group Descriptors:

Como ele fica no bloco 3, então ele pode ser achado no offset 0x3000 (4 * 4096) em relação ao início da partição:

Lembra quando eu disse que o total máximo de blocos por grupo era 32768? Vou explicar o porquê. Percebi que isso estava ligada ao tamanho do bloco e ao block bitmap (Sim, o tamanho do bloco novamente. Pra você vê como apenas isso já influencia todo o Sistema de Arquivos). Acontece que o block bitmap tem o tamanho de 1 bloco (4 KiB no caso do ext2.img). Como cada bit nesse bloco representa outro bloco, e 1 byte tem 8 bits, então 4 KiB tem 32768 bits (4096 * 8 = 32768), ou seja, ele pode representar 32768 blocos. Para provar isso com outros tamanhos de blocos, eu criei uma imagem com blocos de 1 KiB:

Ele pode representar até 0x2000 (8192) blocos, pois 1024 * 8 = 8192. Para blocos de tamanho de 2 KiB, o total de blocos por grupo vai ser 16384, pois 2048 * 8 = 16384. Blocos de 8 KiB parece ser uma exceção, pois o total de blocos é 0xfff8 (65528):

Se fossemos calcular usado o mesmo métodos dos anteriores, blocos de 8 KiB permitiam 65536 blocos por grupo, porque 8192 * 8 = 65536. É uma diferença de apenas 8 blocos. O motivo disso? Eu, sinceramente, não faço ideia.

Se você quiser criar uma imagem com tamanho de bloco diferente para testar, basta apenas especificar o tamanho do bloco usando -b do mkfs.ext2. Exemplo:

$ sudo mkfs.ext2 -b 8192 ext2.img

4.3 – Inode Bitmap

O inode bitmap funciona da mesma forma que o block bitmap. A única diferença é que ela é um bitmap onde cada bit representa um inode no inode table. Ela pode ser localizada usando o bg_inode_bitmap do Group Descriptors:

Como ela fica localizada no bloco 4 em relação ao inicio da partição, então você pode achar ele no offset 0x4000:

Note que todos os bits do 1º byte (0x11 = 0b11111111), 4 bits do 2º bytes e apenas 1 bits do 3º byte estão setados, ou seja, temos 13 inodes sendo usados. Os primeiros 10 bits são reservados. Os outros 3 bits restantes são os nossos dagon.txt, dir1/ e arquivo.txt.

Você pode descobrir o total de bits reservados usando o s_first_ino do superblock. Esse s_first_ino dá índice do primeiro inode que pode ser usado, então os inodes que vierem antes são reservados. A nossa imagem tem 10 inodes reservados, pois o índice no s_first_ino é 0xb (11):

Mas, antes de você procurar o s_first_ino dentro da inode bitmap, eu preciso falar de uma coisa. Os índices dos inodes precisam ser subtraidos por 1 para achar eles no inode bitmap e no inode table. O motivo disso é porque o ext2, por algum motivo, começa a contar os inodes do 1 e não do 0. O inode 1 é representado pelo bit 0, o inode 2 pelo bit 1, etc.

Os bits para cada arquivo seriam esses:

4.3.1 – Localizar o Inode

Falei que para achar o inode dentro do inode bitmap e do inode table, você precisa subtrair o número dele por 1. No caso do inode 11 dado pelo s_first_ino da nossa imagem, ele é o representado pelo bit 10 (11 - 1) no Inode Bitmap mostrado acima. Porém, quando estamos lidando com mais de 1 grupo de blocos, vamos precisar calcular o grupo do qual o inode faz parte e o índice dele dentro do inode bitmap ou inode table.

Você pode calcular o grupo do inode assim:

group = (inode - 1) / s_inodes_per_group

Para calcular o índice:

index = (inode - 1) % s_inodes_per_group

O s_inodes_per_group contém o total de inodes por grupo. Esse valor fica no superblock. O ext2.img tem 0x1900 (6400) inodes por grupo:

O inode 11, por exemplo, fica no grupo 0 e fica no índice 10 do inode bitmap e inode table, porque:

(11 - 1) / 6400 = 0
(11 - 1) % 6400 = 10

Se existisse outro grupo na nossa imagem, então o inode 6401 sério o primeiro dentro dele:

(6401 - 1) / 6400 = 1
(6401 - 1) % 6400 = 0

4.4 – Inode Table

O inode table é uma tabela e cada entrada nessa tabela guarda uma inode. Essa tabela pode ser localizada usando bg_inode_table do group descriptors. A inode table do ext2.img fica localizada no bloco 5:

Como nossa inode table fica no bloco 5, o localização dela fica no offset 0x5000 (5 * 4096):

A inode table não tem o tamanho fixo, portanto ela pode ter vários blocos de tamanho.

Lembra quando eu disse que os 10 primeiros inodes eram reservados? O inode 2 é reservado, porque ele é usado para representar o Diretório Raiz. Para achar a localização dele, precisamos calcular, em primeiro lugar, o índice: (2 - 1) % 6400 = 1. Depois precisamos somar o índice multiplicado pelo tamanho do inode com a base do inode table (o s_inode_size, no superblock, contém o tamanho do inode): 0x5000 + 1 * 256 = 0x5100. Ou seja, podemos achar o inode do Diretório Raiz em 0x5100:

Não vamos calcular o grupo aqui, porque temos apenas 1 no ext2.img.

Vamos relembrar um pouco sobre inode aqui. Veja que os 2 bytes do início tem o valor de 0x41ed (0b0100000111101101). Ele é o elemento i_mode na estrutura do inode. Resumindo, os 9 primeiros bits são usados para definir as permissões do arquivo. O bit 15 serve para dizer se estamos lidando com um diretório. O inode acima, obviamente, representa uma diretório (Diretório Raiz). Ele permite a execução, leitura e escrita para o usuário dele (usuário root). Permissão de leitura e execução para outros grupos de usuários. E, permissão de leitura e execução para outros usuários.

Outro elemento importante do inode é o i_size. Ele nos dá o tamanho do arquivo ou diretório em bytes. O Diretório Raiz tem o tamanho de 0x1000 (4 KiB):

Agora sabemos que o Diretório Raiz precisa apenas da primeira entrada no i_block, pois ela vai apontar para um Direct Block de 4 KiB.

O offset da primeira entrada do i_block é 40. Claro, isso é relativo ao início do inode. Perceba que a primeira entrada tem o valor de 0x195 (405), ou seja, o bloco no data blocks que contém o conteúdo do nosso Diretório Raiz:

O bloco 0x195 fica no offset 0x195000 (405 * 4096) em relação ao início da partição:

Olha aí o nosso dir1/ e o dagon.txt. Vamos falar deles depois.

4.5 – Data Blocks

Não tem muito o que dizer dessa área. Apenas saiba que é nela que ficam os blocos apontados pelo i_block do inode. Apenas isso.

Claro, tem uma estrutura usada por diretórios que ficam nessa área. Vou falar dela no próximo tópico.

5 – Diretórios e Listas Encadeadas

O conteúdo dos blocos apontados pelo i_block dos diretórios são lista encadeadas. Os dados ligados por essa lista são estruturas que têm informações dos arquivos que ficam dentro do diretório. A estrutura, no linux, é a seguinte:

struct ext2_dir_entry_2 {
    __le32  inode;
    __le16  rec_len;
    __u8    name_len;
    __u8    file_type;
    char    name[];
};

Em versões antigas do Ext2, a estrutura era diferente:

struct ext2_dir_entry {
    __le32  inode;
    __le16  rec_len;
    __le16  name_len;
    char    name[];
};

A única diferença entre elas é que o name_len, da versão mais antiga, tinha 16 bits e na versão atual ele tem apenas 8 bits. O name_len dá o tamanho, em bytes, do nome do arquivo. Os outros 8 bits são usados pelo file_type.

O name_len foi diminuído, porque não tinha necessidade de 16 bits. Quem é o doido que iria criar nome de arquivos com o tamanho de 65535 bytes (2¹⁶-1), não é?

Além disso, a maioria dos Sistemas de Arquivos, na época, tinha limitado o tamanho para 255 bytes, logo faz sentido que o name_len seja 8 bits, pois: 2⁸-1 = 255.

O inode, claro, é o número do inode que representa o arquivo ou diretório. Não tem muito segredo aqui.

O rec_len tem o tamanho da entrada. O tamanho dele pode variar, porque o nome do arquivo (ou diretório) que fica contíguo aos outros dados da entrada pode ter tamanhos diferentes também. Para achar a próxima entrada (nó), o rec_len precisa ser somado com a base da entrada que ele faz parte. O rec_len da última entrada (nó) vai conter o total de bytes entre a base da entrada e o final da área reservada para o diretório.

Quanto ao file_type, ele nos diz o tipo do arquivo. Se o valor for 1, então estamos lidando com um arquivo comum, mas se for 2, é um diretório.

As primeiras entradas da lista são os famosos . e ... O inode do . tem o número do inode para o diretório pai dele. E, o .. vai ter o número do inode para diretório pai que contém o diretório do qual o .. faz parte. No entanto, caso seja a lista do Diretório Raiz, o .. precisa ter o inode igual ao do ., ou seja, ambos devem apontar para o inode do Diretório Raiz. As próximas entradas, por outro lado, são utilizadas normalmente.

Lembra quando a gente calculou a localização do conteudo do Diretório Raiz? Veja:

Note que o inode da primeira entrada é 2, ou seja, o número do inode para o Diretório Raiz:

O rec_len é 0xc (12), portanto sabemos que os primeiros 12 bytes fazem parte da primeira entrada (nó):

Como o name_len é 1, então podemos concluir que a primeira entrada é o ., já que ele precisa de apenas 1 byte para representar o caracter ‘.‘.

O file_type é 2, portanto o . é do tipo diretório:

O próximo byte, claro, é o 0x2e (caracter ‘.’):

Os bytes zerados após o 0x2e (‘.’) estão aí, porque a entrada precisa estar alinha em 4 bytes.

Para achar a próxima entrada, você precisa somar a base da entrada com o rec_len, ou seja, 0x195000 + 12 = 0x19500c. A próxima entrada é o nosso ..:

Perceba que o .., assim como o ., tem o inode igual a 2, pois estamos lidando com o Diretório Raiz.

Existe outras 2 entradas nessa lista. Uma delas é a entrada do dir1/:

A outra entrada é o dagon.txt:

Agora que sabemos o inode do dir1/, podemos achar ele dentro do inode table. Lembre-se que a base do inode table é 0x5000, o total de inodes por grupo é 6400 e o tamanho da estrutura do inode é 256. Podemos, agora, calcular a localização do inode do dir1/ dentro da tabela: 0x5000 + ((12 - 1) % 6400) * 256 = 0x5b00:

Como o dir/ ocupa apenas 4096 bytes (0x1000), então apenas a primeira entrada do i_block é necessária. A localização da lista encadeada do dir1/ fica em 0x19b000, pois 0x19b * 4096 = 0x19b000:

Note que nosso arquivo.txt está aí. Além disso, se você relembrar a estrutura de diretórios do nosso ext2.img, vai saber que o arquivo.txt fica realmente dentro do dir/:

ext2.img/
├── dagon.txt
└── dir1
    └── arquivo.txt
1 directory, 2 files

Quanto ao dagon.txt, o inode dele é o 15, logo pode podemos achar ele em 0x5e00, porque 0x5000 + ((15 - 1) % 6400) * 256 = 0x5e00:

Perceba que o dagon.txt é um caso diferente. Ele precisa de 4 blocos de 4 KiB, já que o tamanho dele é 12348 bytes: 12348 / 4096 = 3.0146484375. Arredonde esse valor para cima, pois blocos não podem ser “quebrados”. Com isso, o arquivo vai ocupar 4 blocos de 4 KiB, ou seja, vamos usar o i_block[0], i_block[1], i_block[2] e i_block[3].

Claro, se você procurar todos esses blocos, podera encontrar os dados do dagon.txt. O bloco 515 que fica em 0x203000 (515 * 4096), por exemplo, tem o seguinte conteudo:

Monte o ext2.img e olhe o dagon.txt. Ele realmente tem esse texto dentro do arquivo:

$ cat dagon.txt 
I am writing this under an appreciable mental strain, since by tonight I shall be no more...

Referências

The Second Extended File System (nongnu)

ext2 (Wikipedia)

Kernel Linux (fs/ext2)