Pull to refresh
2675.87
RUVDS.com
VDS/VPS-хостинг. Скидка 15% по коду HABR15

Диск — это просто куча битов

Level of difficultyMedium
Reading time11 min
Views15K
Original author: Dmitry Mazin

Доводилось ли вам слышать утверждение, что диск или память — это «просто куча битов?»
Не знаю точно, откуда эта идея пошла, но она вполне разумна и в некоторой степени рассеивает таинственный ореол вокруг компьютеров. Например, она опровергает теорию о том, что внутри моего ПК живёт очень плоский эльф.

Оказывается нет, в нём находятся биты, закодированные в электрических компонентах.

И всё же компьютеры по-прежнему хранят в себе загадочность. Что это за биты? Что они означают? Можем ли мы с ними поиграться, спарсить их, понять?

Далее я покажу вам, что всё это определённо возможно! Ради вашего развлечения я засуну руку в свой ПК, вытащу оттуда кучку битов, и мы их с вами изучим.

Но какие конкретно биты будет лучше изучить? Для этой задачи мы разберём способ представления файла на диске.

Предположим, у нас есть файл /data/example.txt:

$ cat /data/example.txt

Hello, world!

И здесь возникает большой вопрос: А где находится «Hello, world!»?

Помимо прочего, вы наверняка знаете, что у файлов есть разрешения (например, файл может быть исполняемым), владелец, временная метка создания и так далее. Где же хранятся эти метаданные?

Я имею в виду буквально, где находятся фактические биты, хранящие эту информацию? Давайте их отыщем и попытаемся спарсить.

Но сначала немного теории.

▍ Как работают файлы?


Описанное далее относится к файловой системе ext4, типично используемой в Linux (по факту вся статья относится именно к ext4). Хотя эти принципы применимы к большинству файловых систем.

Что вообще такое /data/example.txt? Это так называемая запись каталога, которая представляет собой просто имя — example.txt.

Записи каталогов хранятся на диске, но ничего особо интересного в себе не несут, так как просто являются именами.

Но ведь имя что-то именует, не так ли? Что же именует example.txt? Именуемый им элемент называется индексный дескриптор (inode, инод).

Вот индексные дескрипторы уже интересны. Когда мы говорим: «Файл находится на диске», то подразумеваем, что «На диске находится индексный дескриптор». Это расположенная на диске коллекция битов, описывающих файл.

В дескрипторе хранится почти вся информация о файле, например, упомянутые ранее метаданные.

На этом с теорией мы почти закончили. Вам следует знать, что индексные дескрипторы, файлы и записи каталогов — все являются элементами файловой системы. Файловая система — это программное обеспечение, которое преобразует биты на диске в знакомые нам файлы и каталоги.

Вот теперь можно приступать к практике.

▍ Разбор индексных дескрипторов


Начнём с вывода метаданных индексного дескриптора с помощью команды stat.

$ stat /data/example.txt

  File: /data/example.txt
  Size: 14              Blocks: 8          IO Block: 4096   regular file
Device: 831h/2097d      Inode: 11          Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/  dmitry)   Gid: ( 1000/  dmitry)
Access: 2023-07-18 13:53:20.808536879 +0100
Modify: 2023-07-10 15:18:48.199691583 +0100
Change: 2023-07-18 14:52:26.349625767 +0100
 Birth: 2023-07-10 15:18:48.199691583 +0100

Не стремитесь детально понять этот вывод. Просто имейте в виду, что перед вами метаданные, такие как размер файла, имя его владельца и временные метки. Вся эта информация поступила из индексного дескриптора (кроме имени, которое было взято из записи каталога, а также номера самого дескриптора).

▍ Анализ содержимого индексного дескриптора


Но мы хотим увидеть именно биты рассматриваемого дескриптора. Что для этого нужно сделать?
Опытный разработчик ядра Ted TS’o обслуживает набор инструментов отладки для файловых систем под названием e2fsprogs. С помощью одного из этих инструментов, debugfs, мы можем поиграться с индексным дескриптором.

В debugfs есть мощная команда, которая выдаст необработанное двоичное представление дескриптора. Выдержка из мануала:

inode_dump filespec
Выводит содержимое индексного дескриптора в шестнадцатеричном и ASCII форматах.

Итак, ниже я привожу обещанное двоичное представление, правда, не в виде нулей и единиц вроде 0011000, поскольку двоичные данные гораздо проще читать, когда они представлены в шестнадцатеричном виде.

$ sudo debugfs /dev/sdd1

debugfs:  inode_dump example.txt
0000  b481 e803 0e00 0000 408b b664 1a99 b664  ........@..d...d
0020  4813 ac64 0000 0000 e803 0100 0800 0000  H..d............
0040  0000 0800 0100 0000 0af3 0100 0400 0000  ................
0060  0000 0000 0000 0000 0100 0000 0082 0000  ................
0100  0000 0000 0000 0000 0000 0000 0000 0000  ................
*
0140  0000 0000 9933 e68b 0000 0000 0000 0000  .....3..........
0160  0000 0000 0000 0000 0000 0000 2349 0000  ............#I..
0200  2000 5e0c 9c76 5b53 fc34 9c2f bc2c c5c0   .^..v[S.4./.,..
0220  4813 ac64 fc34 9c2f 0000 0000 0000 0000  H..d.4./........
0240  0000 0000 0000 0000 0000 0000 0000 0000  ................
*

Всё понятно? Класс — благодарю за внимание!

Шучу, конечно. Видеть фактические сырые данные индексного дескриптора действительно неплохо, но мы по-прежнему не знаем, где на диске он находится, и что эти биты означают.

▍ Где же на диске находится дескриптор?


Итак, займёмся поиском дескриптора на диске. Здесь нам снова поможет debugfs:

imap filespec
Выводит расположение индексного дескриптора (в таблице индексных дескрипторов) по его filespec.

Теперь определим его местоположение.

debugfs:  imap example.txt                                                                  
Inode 11 is part of block group 0
        located at block 73, offset 0x0a00

Поясню: файловая система разбита на блоки. В моём случае размер блока составляет 4096 байтов (размер по умолчанию для многих дистрибутивов Linux). То есть этот вывод сообщает, что: «От начала файловой системы нужно пройти 73 блока, то есть 73*4096 байтов». Это в определённом смысле говорит нам, на какой улице находится искомый индексный дескриптор. При этом номером его дома будет смещение: 0x0a00 байтов. В десятичном формате это 2560 байтов (Почему 2560?).

Итак, чтобы найти наш дескриптор, нужно от начала раздела диска (которое также является началом файловой системы) пропустить 4096 * 73 + 2560 = 301 568 байтов.

Так и сделаем. Давайте вытащим сырые биты с диска и посмотрим, совпадут ли они с выводом debugfs inode_dump.

$ sudo dd if=/dev/sdd1 bs=1 skip=301568 count=256 2>/dev/null | hexdump -C

00000000  b4 81 e8 03 0e 00 00 00  40 8b b6 64 1a 99 b6 64  |........@..d...d|
00000010  48 13 ac 64 00 00 00 00  e8 03 01 00 08 00 00 00  |H..d............|
00000020  00 00 08 00 01 00 00 00  0a f3 01 00 04 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  01 00 00 00 00 82 00 00  |................|
00000040  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000060  00 00 00 00 99 33 e6 8b  00 00 00 00 00 00 00 00  |.....3..........|
00000070  00 00 00 00 00 00 00 00  00 00 00 00 23 49 00 00  |............#I..|
00000080  20 00 5e 0c 9c 76 5b 53  fc 34 9c 2f bc 2c c5 c0  | .^..v[S.4./.,..|
00000090  48 13 ac 64 fc 34 9c 2f  00 00 00 00 00 00 00 00  |H..d.4./........|
000000a0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000100

Вот где нам пригодилось ASCII-представление: визуально мы видим, что содержимое выглядит идентичным выводу debugfs inode_dump!

Мы выяснили, где на диске находится индексный дескриптор!

Это уже гораздо круче, нежели просто вывод inode_dump. В том случае мы попросили программу, написанную разработчиком ядра, сообщить нам, как выглядит дескриптор. Здесь же мы нашли эту информацию прямо на диске сами.

Но нам по-прежнему неизвестно, что эти биты означают. Можно ли их спарсить?

▍ Парсинг голых битов


Я просидел над этим вопросом пару недель. Как заставить компьютер превратить кучу битов в индексный дескриптор?

В итоге до меня дошло: «Ведь именно для этого используется структура (struct)!»

Вы могли встречаться со структурами. Это такие своеобразные объекты из динамических языков программирования, но только с ходу не совсем понятные.

Сейчас я представляю их себе так: предположим, вы рассматриваете кучу битов. В данном случае структура — это просто спецификация, поясняющая значение этих битов.

Значит, ядро Linux должно где-то определять структуру для дескриптора, верно? Так и есть!

/*
 * Структура индексного дескриптора на диске
 */
struct ext4_inode {
	__le16	i_mode;		/* File mode */
	__le16	i_uid;		/* Low 16 bits of Owner Uid */
	__le32	i_size_lo;	/* Size in bytes */
    /* ... */
}

Дополнительно повторюсь: именно благодаря структуре файловая система ext4 знает, как парсить биты, которые мы видели ранее. Это такой своеобразный универсальный ключ.

По сути, в ней говорится: «Первые 16 бит — это разрешения файла, следующие 16 бит — это его владелец, очередные 32 бита — это размер файла и так далее» (правда, всё немного усложняется дополнением).

Давайте используем эту структуру для парсинга ранее выведенных битов.

Мы напишем на Си небольшую программу, которая будет делать следующее:

  1. Просить компьютер выделить в памяти 256 байтов (поскольку именно таков размер структуры ext4_inode).
  2. Просить его скопировать в выделенную память 256 байтов из /dev/sdd1/ по адресу 301568.
  3. Пояснять ему, как нужно спарсить эти байты, используя структуру ext4_inode.

Вот описанная программа на Си (сокращённая до основных моментов):

// открываем файл раздела
int fd = open("/dev/sdd1/", O_RDONLY);

// перемещаем головку привода к местоположению дескриптора
lseek(fd, 301568, SEEK_SET);

// инициализируем структуру и копируем 256 байтов с диска в память
struct ext4_inode candidate_inode;
read(fd, &candidate_inode, sizeof(struct ext4_inode));

// теперь можно обращаться к полям дескриптора!
printf("User:  %u", inode->i_uid);

Вот вся программа с проверкой ошибок. Можете собрать её и опробовать на собственном ПК.
Теперь давайте её выполним! Впечатлены?! Если всё работает исправно, значит мы успешно разобрали структуру битов.

$ sudo ./parse /dev/sdd1 301568

Inode: 11   Mode:  0664
User:  1000   Group:  1000   Size: 14
Links: 1   Blockcount: 8
Inode checksum: 0x0c5e4923

Ура! Мы заполучили информацию об индексном дескрипторе!

Чтобы убедиться в верности полученной информации, мы взглянем на вывод debugfs stat example.txt. Смотрите, все общие поля, а главное — поле контрольной суммы, совпадают.

debugfs: stat example.txt

Inode: 11   Type: regular    Mode:  0664   Flags: 0x80000
Generation: 2347119513    Version: 0x00000000:00000001
User:  1000   Group:  1000   Project:     0   Size: 14
File ACL: 0
Links: 1   Blockcount: 8
Fragment:  Address: 0    Number: 0    Size: 0
 ctime: 0x64b6991a:535b769c -- Tue Jul 18 14:52:26 2023
 atime: 0x64b68b40:c0c52cbc -- Tue Jul 18 13:53:20 2023
 mtime: 0x64ac1348:2f9c34fc -- Mon Jul 10 15:18:48 2023
crtime: 0x64ac1348:2f9c34fc -- Mon Jul 10 15:18:48 2023
Size of extra inode fields: 32
Inode checksum: 0x0c5e4923
EXTENTS:
(0):33280

Я считаю, что это супер круто. Мы решили отыскать биты индексного дескриптора на диске, нашли их и затем выяснили их значение.

▍ Память — это тоже куча битов


Но на этом ещё не всё.

В начале статьи я сказал, что диски и память представляют собой кучу битов. Наша программа копирует биты индексного дескриптора в память, ведь так?

Значит, у нас должна быть возможность найти эти биты в памяти и убедиться, что это те же биты, которые поступили в неё с диска. (примечание о дополнении структуры).

Для этого мы выполним программу в отладчике gdb (подобен pdb в Python). С помощью него мы будем приостанавливать процесс программы и прослеживать его в памяти.

$ sudo gdb parse
(gdb) break 167
(gdb) run /dev/sdd1 301568
(gdb) x/160xb &candidate_inode
0x7fffffffe410: 0xb4    0x81    0xe8    0x03    0x0e    0x00    0x00    0x00
0x7fffffffe418: 0x40    0x8b    0xb6    0x64    0x1a    0x99    0xb6    0x64
0x7fffffffe420: 0x48    0x13    0xac    0x64    0x00    0x00    0x00    0x00
0x7fffffffe428: 0xe8    0x03    0x01    0x00    0x08    0x00    0x00    0x00
0x7fffffffe430: 0x00    0x00    0x08    0x00    0x01    0x00    0x00    0x00
0x7fffffffe438: 0x0a    0xf3    0x01    0x00    0x04    0x00    0x00    0x00
0x7fffffffe440: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7fffffffe448: 0x01    0x00    0x00    0x00    0x00    0x82    0x00    0x00
0x7fffffffe450: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7fffffffe458: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7fffffffe460: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7fffffffe468: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7fffffffe470: 0x00    0x00    0x00    0x00    0x99    0x33    0xe6    0x8b
0x7fffffffe478: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7fffffffe480: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7fffffffe488: 0x00    0x00    0x00    0x00    0x23    0x49    0x00    0x00
0x7fffffffe490: 0x20    0x00    0x5e    0x0c    0x9c    0x76    0x5b    0x53
0x7fffffffe498: 0xfc    0x34    0x9c    0x2f    0xbc    0x2c    0xc5    0xc0
0x7fffffffe4a0: 0x48    0x13    0xac    0x64    0xfc    0x34    0x9c    0x2f
0x7fffffffe4a8: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00

Читать такое непросто, поэтому я использовал скрипт, который сделал вывод gdb больше похожим на вывод hexdump -C:

b4 81 e8 03 0e 00 00 00 40 8b b6 64 1a 99 b6 64
48 13 ac 64 00 00 00 00 e8 03 01 00 08 00 00 00
00 00 08 00 01 00 00 00 0a f3 01 00 04 00 00 00
00 00 00 00 00 00 00 00 01 00 00 00 00 82 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 99 33 e6 8b 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 23 49 00 00
20 00 5e 0c 9c 76 5b 53 fc 34 9c 2f bc 2c c5 c0
48 13 ac 64 fc 34 9c 2f 00 00 00 00 00 00 00 00

Сравним его с битами на диске:

b4 81 e8 03 0e 00 00 00  40 8b b6 64 1a 99 b6 64
48 13 ac 64 00 00 00 00  e8 03 01 00 08 00 00 00
00 00 08 00 01 00 00 00  0a f3 01 00 04 00 00 00
00 00 00 00 00 00 00 00  01 00 00 00 00 82 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
*
00 00 00 00 99 33 e6 8b  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 23 49 00 00
20 00 5e 0c 9c 76 5b 53  fc 34 9c 2f bc 2c c5 c0
48 13 ac 64 fc 34 9c 2f  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00

Всё совпадает!

Звёздочка просто означает, что строка была заполнена всеми нулями. Наблюдательный читатель также заметит, что последние 16 байтов нулей в выводе gdb отсутствуют — думаю, это следствие выполненной компилятором оптимизации.

Здесь мы видим, что биты на диске и в памяти совпадают. Оглядываясь назад, это может показаться очевидным, но мы пронаблюдали это собственными глазами.

▍ А где же содержимое файла?


Знаю, вы могли подумать: «Мы же не видели фактического содержимого файла».

Верно. Индексный дескриптор не хранит в себе эту информацию. Она находится где-то в другом месте.

Вкратце объясню, почему. Вы можете рассмотреть файловую систему как состоящую из двух компонентов: множества ящиков, куда складывается содержимое файлов, и базы данных, управляющей этими ящиками. Это своеобразная распределённая система, в которой вы храните записи в базе данных (все метаданные), но фактическое содержимое файлов кладёте в хранилища вроде S3 или на диск.

Так что в дескрипторе нет содержимого файла, он лишь на него указывает.

Давайте спарсим эту информацию из нашего дескриптора. Выдержка из мануала:

blocks filespec
Выводит в stdout блоки, используемые спецификацией индексного дескриптора.

debugfs:  blocks example.txt
33280

Здесь говорится, что содержимое файла находится в 33,280 блоке от начала файловой системы (размер блока равен 4 KiB). (А можно было получить это расположение напрямую из структуры дескриптора?)

Сделаем дамп соответствующей области диска.

$ sudo dd if=/dev/sdd1 skip=33280 bs=4096 count=1 2>/dev/null | hexdump -C
00000000  48 65 6c 6c 6f 2c 20 77  6f 72 6c 64 21 0a 00 00  |Hello, world!...|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00001000

Вот он! Наш «Hello, world!» прямиком с диска.

▍ Чему мы в итоге научились?


Итак, чему мы научились и что проделали?

Мы начали с распространённого утверждения, что диск и память — это просто куча битов.
Далее мы поставили задачу познакомиться с этими битами, в частности с теми, которые кодируют дисковые файлы: индексными дескрипторами.

В итоге наше знакомство оказалось очень тесным: мы нашли их на диске, спарсили при помощи программы, которая загрузила их в память и применила к ним структуру, после чего заглянули в соответствующую область памяти и увидели там в точности те же биты, что были на диске.

Параллельно с этим мы немного узнали о файловой системе ext4 (и файловых системах в целом).

Лично для меня этот эксперимент стал одним из самых полезных компьютерных откровений за весь мой опыт. После него загадочность вычислительной сферы для меня немного рассеялась, надеюсь, и для вас тоже.

▍ Сноски


А также номера самого дескриптора. Номер самого дескриптора (в данном случае 11) в нём тоже не хранится. Вместо этого номера в нём указывается позиция в таблице индексных дескрипторов.

Почему 2560? Напомню, что номер дескриптора — 11. Это значит, что на диске ему предшествует 10 других дескрипторов. Каждый дескриптор имеет размер 256 байтов, то есть все они занимают 2560 байтов.

Всё немного усложняется дополнением. Технически компьютер дополняет структуру, то есть вставляет в неё пустое пространство. В результате порядок битов в структуре определяется неточно. Тем не менее, учитывая, что дескриптор был сгенерирован на том же компьютере, на котором будет прочитан, это означает, что структура фактически является универсальным ключом к кажущимся случайными битам.

Примечание о дополнении структуры. Ранее я сказал, что компилятор дополняет структуру, вставляя байты между полями. В результате представление данных в памяти сложно сравнить с их представлением на диске. В связи с этим я максимально сократил дополнение, добавив в определение структуры __attribute__((__packed__)). Именно поэтому в показанном мной дампе памяти оказалось всего 160 байтов — это sizeof(struct ext4_inode), когда дополнение отключено.

А можно было получить расположение прямо из индексного дескриптора? Мы также могли спарсить расположение из дескриптора, который загрузили в память, используя поле i_block. Но содержимое представляет собой непонятный массив, для расшифровки которого пришлось бы использовать дополнительный код. Было проще обратиться к debugfs, который сделал это всё за нас. Если кому любопытно, то выглядит этот массив так:

(gdb) p candidate_inode->i_block
$1 = {127754, 4, 0, 0, 1, 33280, 0, 0, 0, 0, 0, 0, 0, 0, 0}

Здесь присутствует значение 33280, которое мы видели в выводе debugfs.

Telegram-канал с розыгрышами призов, новостями IT и постами о ретроиграх 🕹️
Tags:
Hubs:
Total votes 41: ↑40 and ↓1+55
Comments38

Articles

Information

Website
ruvds.com
Registered
Founded
Employees
11–30 employees
Location
Россия
Representative
ruvds