Чтение сектора диска в с
В пятой части нашей серии статей мы показали, как можно использовать прерывания BIOS'а после перехода в защищенный режим, и в качестве примера определили размер оперативной памяти. Сегодня мы разовьем этот успех и реализуем полноценную поддержку работы с дисками с файловой системой FAT16 и FAT32. Работу с файлами на диске можно разбить на 2 части: работа с файловой системой и работа с диском на уровне чтения/записи секторов. Можно сказать, что для этого нам нужно написать «драйвер» файловой системы и «драйвер» диска.
Работа с диском на уровне чтения/записи секторов
Для начала научимся работать с диском.
Итак, мы можем вызывать прерывания BIOS'а. Помимо прочих возможностей, BIOS предоставляет интерфейс для работы с диском, а именно — прерывание int 0x13. Со списком сервисов, предоставляемых прерыванием, можно ознакомиться на википедии. Нас интересуют сервисы чтения и записи дисков.
Существует два способа адресации сектора на диске, с которыми работает BIOS – CHS(cylinder-head-sector) и LBA(logical block addressing). Адресация CHS основана на использовании геометрии диска, и адресом сектора является совокупность трех координат: цилиндр, головка, сектор. Способ позволяет адресовать до 8Гб. Прерывание int0x13 предоставляет возможность читать и писать на диск с использованием этой адресации.
Понятно, что 8Гб — это очень мало, и данный способ адресации является устаревшим, а все современные (и не очень) контроллеры жестких дисков поддерживают адресацию LBA. Адресация LBA абстрагируется от геометрии диска и присваивает каждому сектору собственный номер. Нумерация секторов начинается с нуля. LBA для задания номера блока использует 48 бит, что позволяет адресовать 128 ПиБ, с учетом размера сектора в 512 байт. Прерывание int0x13 предоставляет два сервиса для чтения и записи секторов на диск с использованием LBA. Их мы и будем использовать. Для чтения сектора прерывание in0x13 ожидает следующие параметры:
Прерывание возвращает следующие значения:
Один из параметров – номер диска. Нужно как-то узнать номер диска, с которым мы собрались работать. Нумерация происходит следующим образом: флоппи-диски (fdd), и все, что эмулируется как флоппи, нумеруются с нуля, а жесткие диски (hdd), и все, что эмулируется как они(usb-флешки, например), нумеруются с 0x80. Этот номер никак не связан с последовательностью загрузки в настройках BIOS’а. В нашем случае, диск, с которым мы собираемся работать, является тем диском, с которого мы загрузились.
Когда BIOS передает управление MBR, он загружает его по адресу 0000h:7C00h, а в регистре DL передает нужный нам номер загрузочного устройства. Это является частью интерфейса взаимодействия между BIOS и MBR. Таким образом, этот номер попадает в GRUB, где далее используется для работы с диском. GRUB, в свою очередь, передает этот номер ОС как часть структуры Multiboot information.
Сразу после передачи управления от GRUB’а к ОС в регистре EBX находится указатель на эту структуру. Первое поле структуры – это flags, и если в нем выставлен 2-й бит, то поле boot_device корректно. Это поле так же принадлежит структуре Multiboot information и в его старшем байте (размер поля – 4 байта) хранится нужный нам номер диска, который понимает прерывание int0x13. Таким образом, используя GRUB, мы получили недостающий параметр для чтения/записи секторов на диск.
Мы научились читать и писать сектора на диск, это, безусловно, важно. Но файловая система привязана не к целому диску, а только к его части – разделу. Для работы с файловой системой нужно найти сектор, с которого начинается раздел, на котором она располагается. Информация об имеющихся на диске секторах хранится в первом секторе диска, там же, где располагается MBR. Существует много различных форматов MBR, но для всех них верна следующая структура:
Информация о разделах хранится в таблице разделов. На диске может быть только 4 первичных раздела, с которых можно загрузиться. На запись о разделе приходится 8 байт. Первый байт — это флаги, если его значение 0x80, то раздел загрузочный. Код MBR в процессе своей работы пробегает по этим 4-м разделам в поиске загрузочного раздела. После его обнаружения, MBR копирует содержимое первого сектора этого раздела на адрес 0000h:7C00h и передает туда управление. Нас интересует LBA адрес первого сектора загрузочного раздела, так как именно на нем располагается наше ядро, и присутствует файловая система, которую мы собираемся читать. Для того чтобы получить этот адрес, нужно прочитать первый сектор диска, найти на нем таблицу разделов, в таблице разделов найти загрузочный раздел, а из его записи прочитать нужное поле.
Итак, у нас есть механизм для чтения сектора с диска и знание о расположении нужного нам раздела на диске. Осталось научиться работать с файловой системой на этом разделе.
Работа с файловой системой
Для работы с файловой системой мы будем использовать библиотеку fat_io_lib. Библиотека доступна под лицензией GPL. Она предоставляет интерфейс для работы с файлами и директориями, аналогичный имеющемуся в libc. Реализованы такие функции, как fopen(), fgets(), fputc(), fread(), fwrite() и т.д. Библиотека для своей работы требует всего лишь две функции: записать сектор и прочитать сектор, причем первая является необязательной. Функции имеют следующий прототип:
Библиотека написана на чистом С, что опять-таки нам на руку. Для использования в своей мини-ОС нам не придется менять в ней ни строчки. Библиотека ожидает, что чтение секторов происходит в рамках раздела с файловой системой.
Итак, у нас есть функции чтения/записи сектора на раздел и есть библиотека для работы с FAT16/32, которая использует эти функции. Осталось собрать все воедино и продемонстрировать результат. Но прежде чем перейти к коду, хотелось бы показать, что подход, который мы собираемся использовать, вполне применим в реальной жизни. Ниже представлена небольшая часть VBR windows 7, в которой происходит чтение сектора диска посредством прерывания int0x13. Этот код многократно вызывается в процессе загрузки системы, вплоть до момента отрисовки загрузочной анимации.
Для вызова этого кода, Windows 7, подобно тому, как это делаем мы, переходит из защищенного режима в реальный, и обратно. Это несложно проверить, запустив Windows 7 в QEMU. QEMU должен ожидать подключения отладчика. После подключения отладчика (gdb) ставим breakpoint на адрес (0x7c00 + 0x11d). Срабатывание breakpoint’а будет означать вызов этой функции. Кстати в Windows XP этот механизм отсутствует, для вызова прерываний BIOS'а там переходят в режим VM86.
! ВАЖНО! Все дальнейшие действия могут успешно осуществляться только после успешного прохождения всех шагов из пятой части нашей серии статей
Шаг 1. Изменим основную логику в kernel.c
1. Добавим в файле kernel.c следующие объявления:
Код, печатающий размер оперативной памяти
заменим на следующий код:
Память под переменные mbd и magic зарезервирована в файле loader.s, так что их можно использовать аналогично глобальным переменным из кода на С. Переменная magic содержит сигнатуру, подтверждающую, что для загрузки использовался стандарт Multiboot, эталонной реализацией которого является GRUB. Переменная mbd указывает на структуру multiboot_info_t, которая объявлена в multiboot.h. Номер загрузочного диска определяется следующим выражением — p_multiboot_info->boot_device >> 24. Функция InitBootMedia запоминает номер диска и ищет первый сектор файловой системы, чтобы затем все смещения считать от него.
Библиотека fat_io_lib для инициализации требует вызова двух функций: fl_init и fl_attach_media. Первая функция обнуляет внутренние структуры библиотеки, а вторая получает в качестве параметров функции чтения и записи секторов на диск, которые затем используются для обращения к файлам. Далее идет демонстрация работы с библиотекой: выводится список файлов в папке /boot/grub и распечатывается содержимое файла menu.lst.
2. Добавляем файл multiboot.h в папку include. Содержимое файла берем с сайта спецификации предыдущей версии.
Шаг 2. Добавим функции для работы с диском
1. В файл include\callrealmode.h добавим прототипы следующих функций:
2. В файле include\callrealmode_asm.h добавим в enum callrealmode_Func новое значение так, чтобы получилось следующее:
Добавим в union внутри структуры callrealmode_Data только что объявленную структуру callrealmode_read_disk. Должно получиться следующее:
3. В файл include\string.h добавим функции strncmp и strncpy, используемые в библиотеке fat_io_lib.
4. Добавим в файл callrealmode.c следующие объявления:
И несколько функций:
Функции ReadBootMedia и WriteBootMedia используются библиотекой fat_io_lib для чтения/записи секторов. Функция WriteBootMedia не обязательная и является заглушкой, так как в данном примере нет записи на диск. Ее реализация выглядела бы аналогично функции ReadBootMedia. Функция ReadBootMedia похожа на функцию GetRamsize из прошлой статьи с точностью до типа param.func, а вместо param.getsysmemmap используется param.readdisk. Функция InitBootMedia должна быть вызвана раньше двух остальных, так как она инициализирует значения g_BootPartitionStart и g_BootDeviceInt13Num.
5. Изменим callrealmode_asm.s. Добавим еще один тип CALLREALMODE_FUNC_READ_DISK вызываемых функций, должно получиться следующее:
Далее добавим еще одну проверку на тип функции и непосредственно код, читающий с диска. Должно получиться следующее:
Метка readdisk указывает на код, который формирует структуру DAP из структуры callrealmode_Data и вызывает int0x13. В коде после метки callrealmode_switch добавилось 2 инструкции, проверяющие, не нужно ли вызывать readdisk.
6. Добавим файл include\mbr.h, содержащий определения для работы с MBR. Его содержимое:
Структура MBRSector используется в функции InitBootMedia.
Шаг 3. Добавим библиотеку fat_io_lib и запустим
1. Скачаем архив fat_io_lib.zip и распакуем его в папку fat_io_lib в корне проекта.
2. Добавим пустые файлы assert.h и stdlib.h в папку include. Они нужны, что бы библиотека скомпилировалась.
3. Исправим Makefile. Добавим файлы из библиотеки в список целей для компиляции. Должно получиться следующее:
Теперь размер образа равен 10Mb. Это делается для того, чтобы команда mkdosfs отформатировала раздел в FAT16 вместо FAT12. FAT12 не поддерживается библиотекой fat_io_lib.
С этим дефайном библиотека не будет включать stdio.h, но будет использовать готовый прототип функции printf, который совпадает с нашим, и который уже реализован.
4. Пересоберем проект
sudo make image
Должно получиться следующее:
Как и в предыдущих частях, можно сделать dd образа hdd.img на флешку и проверить код на реальном железе, загрузившись с нее.
В результате мы реализовали работу с файловыми системами FAT16 и FAT32. Мы немного схитрили, использовав готовую библиотеку, но разбираться в устройстве FAT'а было бы менее интересно, да и вряд ли бы мы тогда уложились в 1 статью. Надеюсь, вам было интересно читать. Пишите в комментариях, если у вас возникнут проблемы в прохождении описанных шагов!
как по очереди прочитать все сектора на диске?
по идее должна использоваться функция ReadFile(hDevice, buf, 512, &dwBytesRead, NULL); но где подставить номер сектора или как перебрать все?
и как из функции DeviceIoControl вытащить количество секторов
вот сам код до которого додумался, но далее буксую на месте, надеюсь на подсказку)
Чтение секторов жёсткого в DOS
Добрый вечер! Проблема заключается именно в том, что программа должна работать под DOS, и ей не.
Чтение секторов жесткого диска
пишу драйвер, считывающий секторы жесткого диска он сохраняет в файл результат проделанной работы.
Ремап секторов диска с AF
Понадобилось сделать ремап на своём винче, много бэдов и блоков с чтением >600 ms (даже один с 56к.
Чтение секторов с CD
Как и многие программеры-извращенцы,решил написать свою ОС в учебных целях. Т.к. нынче флоппиков.
да с флешки на флешку
цикл do while если функцию writefile убрать читает без проблем
при добавление writefile на первом такте работы валетает
А так же читать вторую флешку пробовали? Нормально читается? А флешки больше 2 ГБ?
Сейчас поробую создать диски > 2 ГБ может в этом проблема.
Добавлено через 21 минуту
На виртуальной машине у меня все работает на дисках 9 ГБ. Флешек, к сожалению, сейчас нет под рукой, чтоб проверить, все ребенок перетерял. На физическом жестком диске тоже нет возможности потестить.
А так же читать вторую флешку пробовали? Нормально читается? А флешки больше 2 ГБ?
Сейчас поробую создать диски > 2 ГБ может в этом проблема.
пробовал их менять местами тот же результат, проблема видно в коде
обе флешки micro sd на 2 гига
Добавлено через 2 минуты
так как семерка наследует ядро этих систем то думаю эти правила и к ней относятся,
у вас глохнет на месте где вы пытаетесь открыть доступ к диску на запись о чем сказано в базе MS цитирую
A file system can write to a volume handle only if the following conditions are true:
Condition 1: The sectors that are being written to are boot sectors.
Note This condition exists to support programs such as antivirus programs, Setup programs, and other programs that have to update the startup code of the system volume. The system volume cannot be locked.
Condition 2: The sectors that are being written to reside outside the file system space.
Note The region between the end of the file system space and the end of the volume space is not under the control of the file system. Therefore, there is no reason to require the volume to be locked to write to it.
Condition 3: The volume has been locked implicitly by requesting exclusive write access.
Condition 4: The volume has been locked explicitly by requesting a lock request or an unmount request.
Condition 5: The write request has a SL_FORCE_DIRECT_WRITE flag that indicates that Condition 2 must be bypassed.
Добавлено через 48 минут
у меня не 7 стоит, но заблокировать диск и получить эсклюзивный доступ я смог по доке из MSDN
теперь у хэндла hdd нужно изменить доступ на запись :P
проверить то что ваша программа получила эсклюзивный доступ к диску, во время трасировке после FSCTL_DISMOUNT_VOLUME попробуйте обратится к диску из винды
Хочу записывать и читать информацию прямиком из сектора диска а не из файла. На С++. Приложите пример по максимуму простой. Или ссылку дайте. Посекторное чтение и запись я полагаю даст мне возможность делать полные посекторные копии нескольких ОС и разделов сразу. Также делать защиту программ от взлома.
вот финальный код ширения основан на ответе.Чтение первого сектора первого диска.
Визуалку надо от имени админа запускать, проект создавать не консольный а пустой enemy. Буду благодарен если кто то ещё покажет как позицию чтение установить. и записывать в заданную позицию, можно в 32 сектор писать для примера, там никогда ничего важного нет. Благодарен всем участникам.
Если это без ОС то читать int 13h, либо. низкоуровневая работа с портами контроллера + DMA + USB. Если используя windows - CreateFile/ReadFile. ОС может заблокировать запись/чтение диска, или подменить области. Если линукс юникс - как-то ещё.
Из под ОС вы не имеете права делать ввод вывод куда угодно, можно это делать с уровня драйвера. По драйверам - отдельный вопрос, и скорее всего врядли тут подскажут.
Знакомство с устройствами хранения
Зачем определять расширенный сегмент?
Причина в том, что для отображения содержимого сектора сначала мы считываем его в адрес памяти 0x9000 .
Мы вызываем макрос для считывания 2-го сектора, а затем отображаем его содержимое, после чего снова вызываем макрос для считывания 3-го сектора, также отображая его содержимое.
После вывода содержимого секторов мы переходим к бесконечному циклу, останавливая программу.
Этот раздел перехода к метке мы определили на случай возникновения сбоя, в следствии чего программа также остановится.
Мы переходим к 510-му байту сектора и добавляем сигнатуру загрузки, необходимую для определения дискеты как загрузочной. В противном случае система выбросит ошибку, сообщив о невозможности загрузки.
Здесь мы добавляем строку в начало 2-го и 3-го сектора.
На этом вторая часть серии завершается. Предлагаю самостоятельно поэкспериментировать с чтением дискеты в реальном режиме и внедрением в загрузчик различной функциональности.
В третьей части серии я коротко расскажу о файловых системах и их важности. В качестве практики мы напишем простейший загрузчик для считывания дискеты в формате FAT12, научимся выполнять чтение/запись такой дискеты, а также создадим загрузчик второй стадии и разберем его назначение.
Таким образом, адрес конкретного сектора формируется из трёх составляющих – головка (выбирает диск), цилиндр на этом диске, и сектор на поверхности выбранного цилиндра. Рисунок ниже представляет трехмерный адрес CHS в визуальной форме:
Теперь проведём арифметические расчёты, чтобы определить макс.возможную ёмкоcть дисков с разметкой CHS. Для этого достаточно найти произведение всех значений BIOS и результат умножить на размер сектора. Для накопителей HDD, сектор в 512-байт является скорее правилом, чем исключением, хотя на твёрдотелых SSD он может достигать информационного веса в 4 Кб (его подогнали под размер виртуальной страницы ОЗУ). Но сейчас разговор об HDD, поэтому условимся считать сектор равным именно 512-байт. Тогда имеем..
цилиндров..(С): 14-бит = 16.383
головок. (H): 04-бит = 16
секторов. (S): 06-бит = 63
==========================================
16383*16*63 = 16.514.064 (всего секторов)
16514064*512 = 8.455.200.768 (всего байт)
Как видим, используя трёхмерную геометрию CHS, дисковый сервис биоса INT-13h способен оперировать накопителем с макс.размером ~8 Gb. Ещё в эпоху неолита таких объёмов не хватало даже для домашнего пользования. Поэтому инженеры сняли ограничения CHS=16383/16/63 и добавили в биос расширенный сервис INT-13h, более известный как "Enhanced Disk Drive" , EDD. Если традиционный сервис адресовал секторы через регистры процессора CX и DX , то в расширенном ввели линейную адресацию LBA и т.н. "адресный пакет", который находится уже в памяти ОЗУ. Теперь регистры не ограничивают макс.номер сектора, и трюк позволил растянуть адресацию до LBA-64:
Хоть биос и поддерживал теперь 64-битный дисковый сервис, на практике использовался лишь LBA-32. В результате, ёмкость диска не превышала 4.294.967.295 всего секторов (32-бит) – при размере сектора 512-байт это получалось 2 тераБайт. На этот раз засаду устроила "таблица-разделов" накопителя, что в оригинале звучит как "MBR Partition Table" . Эта таблица описывает разделы уже самого жёсткого диска, и в ней под номер сектора LBA зарезервировано именно 32-бита.
Таким образом, инженерам пришлось положить на операционный стол и MBR, чтобы перекроить его под таблицу "GUID Partition Table" , в простонародье GPT. По сути требовал этого и сам прогресс, который решил "рубануть с плеча" и полностью искоренить устаревшую систему ввода-вывода BIOS, сделав бартер на более современный EFI. Рассмотрим бегло отличительные особенности этих таблиц, после чего завернём их в ассемблерный код.
2. Формат таблицы-разделов в секторе MBR
Термин MBR берёт своё начало от "Master Boot Record", что в дословно переводится как "Основная загрузочная запись". MBR занимает самый первый сектор с координатами: головка(0), цилиндр(0), сектор(1). Здесь нужно отметить, что в геометрии CHS отсчёт цилиндров и головок начинается с нуля, а секторов с единицы. Однако если мы имеем дело с выстроенными в ряд логическими блоками LBA, то нумеровать эти блоки (аля секторы) принято уже с нуля – такая вот муть..
Посмотрим на рисунок ниже, где представлена таблица-разделов отживающего свой век диска Seagate, объёмом 80 Gb. Здесь я открыл его в HEX-редакторе HxD как физический диск, и скопировал в новое окно только интересную на данный момент таблицу-разделов MBR. Как упоминалось выше, MBR – это равно один сектор, начиная с нуля и до адреса 0200h .
Первые 446-байт до смещения 01BEh отданы в распоряжение загрузчика ОС, а следующий за ним (выделенный) 64-байтный блок и есть таблица 4-х возможных разделов диска HDD. Правда я захватил в хвосте ещё и сигнатуру 55AAh по которой BIOS делает вывод, что сектор фактически является загрузочным. Каждый раздел Partition описывает своя 16-байтная запись (одна строка в нижнем окне), итого 16*4=64 байта. Ограниченный размер данной таблицы не позволяет создавать больше 4-х основных разделов, хотя раздел может быть и расширенным, тогда в нём присутствует своя таблица, ещё для 4-х его логических томов:
В окне "Partition Table" выше я собрал в блоки одноимённые поля всех четырёх разделов. Одна строка – это запись об одном разделе. Судя по этим данным, мой диск имеет всего 2-раздела, поскольку две последние строки забиты нулями. Коричневый первый байт со-значением 80h характеризует флаг загрузочного раздела – обычно это тот, с которого загружается Win. Два серых блока хранят адрес начала и конца раздела в формате CHS. Если там лежит значение FEFFFFh , значит поле не действительно и нужно использовать геометрию LBA.
Байт с типом раздела по смещению(4) информирует о том, основной это раздел, или расширенный. Для файловой системы NTFS сейчас встречаются только три типа: 07h = основной, 0Fh = расширенный раздел, а так-же идентификатор EEh = таблица разделов GPT (о ней ниже). Со списком остальных типов можно ознакомиться
Наибольший интерес в таблицах MBR представляют последние два 32-битных значения – это номер сектора LBA с которого начинается раздел (зелёный блок), и всего секторов LBA в разделе (красный блок). Тома и разделы не могут начинаться с середины дорожки диска, а только с нового цилиндра. Поэтому в зелёном блоке первого раздела мы видим значение 0000003Fh=63 . Если вспомнить, что в одной дорожке 63-сектора, то всё совпадает. Тогда выходит, что между MBR и началом первого раздела всегда имеются бесхозные 62-сектора (~32 Кб), которые мы можем подмять под себя.
Последний dword в красном блоке хранит общее число секторов в разделе: 02711637h = 40.965.687 . Теперь можно вычислить и его размер: 40965687*512=20Gb . Аналогично и для второго партишена: 06DF8F8Ah = 115.314.570 всего секторов, а размер: 115314570*512=59Gb .
Чтобы на программном уровне было проще обращаться к MBR, имеет смысл оформить этот сектор в структуру соответствующего вида. Вот что у меня из этого вышло:
Рассмотрев анатомические особенности таблицы-разделов MBR можно сделать вывод, что она действительно отжила свой век и ей давно уже пора на покой. Во-первых, чтобы при малейшем чихе не потерять навсегда терабайты своих данных, обязательно нужно иметь резервную копию этой таблицы, с её контрольной суммой. Ведь превратить в труху диски MBR проще-простого – достаточно элементарно сбросить "Boot-Flag", сменить тип по-смещению(4) на какой-нибудь от фонаря, или поменять местами два последних поля LBA. Всё.. сушим вёсла.. После этих действий, система в лучшем случае откажется грузиться, а в худшем – вообще не распознает дисковые тома, что приведёт к полной потере информации. Все эти просчёты были учтены в более современной таблице-разделов GPT – разберём её на атомы..
3. Формат таблицы-разделов GPT
GPT это "GUID Partition Table" . Права на неё принадлежат Intel, коллектив которой разработал её в 90-х прошлого столетия. Однако распространение таблица получила только с приходом интерфейса EFI "Extensible Firmware Interface" в конце 2000-х. По задумке инженеров, весь хлам из MBR планировалось выкинуть за борт, однако пережившие себя древние устройства не разделяли такую позицию, и грязно высказываясь разработчикам пришлось тащить за собой воз обратной совместимости.
В результате, в секторе(0) GPT по прежнему лежит MBR, только теперь он ущербный без загрузчика (первые 440-байт, забиты нулями), а осталась лишь описывающая всего один раздел запись. Причём последний дворд в ней под кличкой "всего LBA" выставлен на максимум FFFFFFFFh (нет места на диске), а флаг с типом раздела по смещению(4) имеет значение EEh . Это сделано для того, чтобы не знакомый с GPT софт времён динозавров, не повредил случайно таблицу GUID.
Здесь видно, что первые 33-сектора заняты служебкой, а непосредственно под данные выделяются секторы начиная с LBA(34). Точная ксерокопия основной таблицы притаилась в хвосте и ждёт своего часа. При каждом включении машины, код системного EFI пересчитывает контрольную сумму GPT, которая хранится по смещению(10h) в её заголовке. При несовпадении CRC, основной заголовок вместе со-всеми записями восстанавливается из резервной копии, на автомате корректируя таким образом служебную инфу.
Согласно документации, вне зависимости от реального числа разделов 128 или всего пару-штук, они начинаются исключительно с сектора LBA(34). Однако на моём буке, первый раздел сдвинут ещё дальше и занимает позицию LBA(2048), хотя в заголовке указано правильно 22h=34 . Видимо в доках имелось в виду минимальный сектор(34), а дальше по настроению. Так-что их высказывания можно классифицировать по разному, а лучше не доверяя проверять всё на практике.
, а в данной статье мы рассмотрим их лишь поверхностно.
3.1. Заголовок таблицы "GPT -Header "
Программно распознать диск с разметкой GPT можно по сигнатуре "EFI PART" в начале сектора LBA(1), или-же по идентификатору типа-раздела EEh в секторе LBA(0) таблицы MBR. Ниже приводится описание полей данного заголовка, всего 1 сектор = 512-байт:
Обратите внимание, что в заголовке имеются две контрольные суммы CRC32.
Первая по смещению(10h) – сумма исключительно самого заголовка, а вторая с офсетом(58h) – всех имеющихся записей "Partition Entry". Так разрабы закрыли GPT аж на два амбарных замка, а специальная процедура дотошного EFI постоянно их проверяет. В случае малейшего несоответствия, на территории сразу включается ревун и данные тут-же восстанавливаются из резервной копии. Достопочтенный BIOS со-своим MBR о таких мелочах мог только мечтать. Остальные поля заголовка пояснений вроде не требуют.
3.2. Записи о разделах "Partition Entry "
В заключении рассмотрим формат паспорта каждого из разделов. Как упоминалось выше, таблица GPT поддерживает макс.128 разделов, и в 33-х секторах для каждого из них зарезервировано место под описатель. Лично мне не встречались диски с таким кол-вом томов, но как говорят: "лучше еврей без бороды, чем борода без еврея" – пусть лежат на чёрный день, авось когда-нибудь понадобятся.
Обратите внимание на 64-битную маску атрибутов по смещению(30h). Большая часть битов в ней отправлена в резерв, а список активных представлен ниже. Если в двух словах, то у обычных разделов маска имеет значение нуль, т.е. все биты сброшены. А если какой-то из них взведён (как-правило нулевой или под номером 63), то он означает следующее:
В записях разделов огромную роль играет GUID – "Globally Unique Identifier" , или глобально-уникальный идентификатор. В каждой записи по два таких GUID'a (см.предыдущий скрин с форматом). Первый – системный и определяет тип данного раздела, по аналогии с байтом "типа" по смещению(4) в таблице MBR. Второй GUID – это просто рандомный номер раздела, по которому виндовый диспетчер-дисков монтирует том в систему. Кстати если в ком.строке запросить утилиту mountvol.exe без параметров, то она сбросит на консоль список именно этих идентификаторов, с назначенными им буквами разделов.
Ниже перечислены некоторые GUID, предопределяющие тип раздела в операционной системе Win. Например разделы с пользовательскими данными будут иметь тип "Basic Data Partition" , а раздел типа "EFI System Partition" зарезервирован для кода загрузчика EFI. В таблице GPT любого накопителя их GUID'ы будут одинаковыми, поскольку они являются глобальными внутри системы:
4. Практика – сбор и вывод информации о разделах
В практической части напишем небольшую утилиту, которая позволит динамически определить способ разметки диска MBR или GPT. Дальше, код в цикле обойдёт все записи в "Partition Table" и отрапортует о собранной информации на консоль. Это будет просто демонстрацией того, как можно подобраться на программном уровне к данным таблицы-разделов. А что дальше делать с этими данными – это уже вопрос к нашей совести. Поскольку в атрибутах разделов GPT имеется бит(60), то можно взвести его в записи любого раздела, в результате чего раздел станет доступным только для чтения, без возможности записи на него. Или-же скрыть его к чертям, установив в единицу бит(62).
Небольшую проблему может создать вывод значений GUID на экран в приглядном виде типа: . Для этого воспользуемся функцией из библиотеки ole32.dll StringFromGUID2() . Всё-что ей нужно, это указатель на GUID для преобразования, и указатель на приёмный буфер для результирующей строки. Если на выходе получим нуль, значит приёмный буф слишком мал и в аргумент "cchMax" вернётся требуемая длинна. Вот её прототип:
Эта функция сбрасывает в буфер GUID в виде Unicode-строки, значит для вывода на консоль её нужно будет преобразовать в ASCII, просто читая по 2-байта, и сохраняя в тот-же буфер по одному (т.е. отсекать парные нули). Весь алгоритм программы можно представить так:
1. Запросить номер диска на случай, если их несколько в системе.
2. Открыть указанный диск при помощи CreateFile() обязательно с шарой Write , чтобы диск был доступен остальным для записи.
3. Функцией VirtualAlloc() выделить память под 10-секторов диска (в идеале под 33-сектора, но хватит и 10-ти).
4. Через ReadFile() считать секторы из диска в память (чтение разрешено всем, а запись только админу).
5. Проверкой байта "тип-раздела" в MBR, распознать способ разметки GPT или MBR (байт должен иметь значение EEh).
6. В зависимости от результата, спроецировать соответствующую структуру на считанные сектора, и пропарсить их.
Мы не можем заранее знать, на машину какой разрядности попадёт наш код, 32 или 64-бит. Поэтому в таких случаях лучше писать 32-битное приложение. Если оно попадёт на х64, то отработает через её WOW64 (Windows-on-Windows). Зато 64-битное приложение вообще не запуститься на х32, и мы обломаемся по полной. Так-как большинство полей в GPT 64-битные, то придётся оперировать ими через сопр FPU. Исходник этой задумки на лексиконе ассемблера FASM представлен ниже. Инклуд с описанием структур MBR/GPT я спрятал в скрепку:
Посмотрим, что в итоге получили..
Значит перед нами таблица GPT, в которой сначала идёт заголовок с информацией о самом диске, а дальше логи из записей "Partition Entry". В заголовке видно, что резервная копия заголовка лежит в конце пространства, в секторе(976773167). Сама таблица лежит в секторе(2) – это текущий сектор, ..а разделы с данными (как и утверждал производитель) начинаются с сектора(34). Чтобы вычислить ёмкость раздела в секторах, нужно от Last отнять First-LBA .
Однако в моём случае, внутри записи первого раздела видим начальный сектор(2048), а не 34. Раздел размером 500 Мб и в его атрибутах взведены биты 0 и 63, что означает "Защищённый ОЕМ-раздел, без буквы" , т.е. не отображается в проводнике Win. Это бокс для различных драйверов и прочей системной утвари, которую любезно сбросил туда производитель моего бука. Нужно сказать, что и последующие два раздела тоже из этой-же кухни с установленным битом(63), только они не принадлежат вендору ОЕМ (разработчику). В разделе EFI лежит загрузчик ОС размером 100 Мб (старушка MBR его таскала с собой), а в третьем разделе – барахло мелкомягких.
Доступ к стартовым секторам диска с правами на запись открывает большие возможности. В своё время это было излюбленное место червей и всякой нечисти, поэтому начиная с Висты, MS отобрала у нас эти права (привет Жанне Рутковской, с её "голубой пилюлей"). Читать – пожалуйста, а вот записывать в начало диска (до файловой системы), юзеру нельзя. Чтобы сидя на нарах не ждать с воли сухарей, мы всегда должны помнить об уголовной ответственности за порчу чужой информации. Поэтому всё сказанное здесь носит чисто оборонительный характер, чтобы мы были осведомлены, как при программных сбоях можно восстановить работоспособность своего накопителя. Особенно актуально это для размеченных в формате MBR дисков, где самостоятельная правка пару байт может сэкономить Вам честно заработанные шекели.
В скрепке лежит готовый исполняемый файл для тестов, исходник загрузчика из MBR, а так-же инклуд с описанием структур "Partition Table". Всем удачи, пока!
Пример 2
После того, как BIOS загрузит нашу программу из 0x7c00 , мы прочитаем и выведем завершающуюся нулем строку из смещения 2.
Программа: test2.S
Если вы скомпилируете программу и откроете исполняемый файл в эмуляторе, то в качестве вывода увидите строку “This is boot loader”.
Компиляция кода
- as test.S -o test.o
- ld -Ttext=0x0000 --oformat=binary test.o -o test.bin
- dd if=test.bin of=floppy.img
После запуска программы через эмулятор bochs nотобразится следующее:
Знакомство с сегментацией
Прежде чем переходить к рассмотрению чтения флоппи-диска, давайте освежитм в памяти тему сегментации и ее назначения.
Какие бывают типы сегментов?
В прошлой статье я уже перечислял четыре основных вида. Здесь я еще раз их напомню и приведу для каждого пояснение:
- сегмент кода;
- сегмент данных;
- сегмент стека;
- расширенный сегмент.
Один из разделов программы в памяти, содержащий исполняемые инструкции. Если вы загляните в мою предыдущую статью, то увидите метку .text , под которой мы размещаем исполняемые инструкции. При загрузке программы в память эти инструкции передаются в сегмент кода. В ЦПУ для обращения к этому сегменту мы используем регистр CS.
Сегмент данных
Раздел программы в памяти, содержащий статические и глобальные переменные. Для обращения к нему мы используем регистр DS.
Сегмент стека
Программист может использовать регистры для хранения, изменения и извлечения данных при написании программы. При этом ограниченное количество регистров зачастую приводит к усложнению логики. В итоге у разработчика постоянно возникает потребность в дополнительном пространстве, более гибком в плане хранения, обработки и извлечения данных. С целью решения подобных сложностей в ЦПУ был внедрен специальный сегмент стека. Для хранения в этом сегменте данных и их извлечения используются соответствующие инструкции push (добавление) и pop (удаление). Инструкцию push мы также используем для передачи аргументов в функции. Обращение к сегменту стека производится при помощи регистра SS. При этом важно помнить, что стек растет сверху вниз.
Расширенный сегмент
Обычно этот сегмент используется для загрузки данных, имеющих слишком большой объем для хранения в сегменте данных. Позже вы увидите, как я загружаю данные с дискеты в расширенный сегмент. Для обращения же к нему мы используем регистр ES.
Что это такое?
Основная память разделена на сегменты, индексируемые специальными сегментными регистрами CS, DS, SS и ES.
1 ответ 1
В принципе, если не хотеть странного вроде небуферизованного ввода/вывода, то там все так же, как и для файлов. Диски открываются все той же функцией CreateFile, но для dwShareMode надо обязательно указывать (FILE_SHARE_READ | FILE_SHARE_WRITE), а в dwCreationDisposition должно (естественно) стоять OPEN_EXISTING. В качестве имени должно стоять, например, для физического диска "\\.\PhysicalDrive0", а для логического - "\\.\C:". А дальше все так же, как и для файла - читаем ReadFile, пишем WriteFile, закрываем CloseHandle.
В описании CreateFile все это подробно расписано, и если вы действительно будете это делать, читать MSDN вам скорее всего все равно придется. Имейте только ввиду, что начиная с Висты доступ к диску требует поднятия привилегий (elevation). То есть программу нужно 1) запустить с учетки администратора, 2) в манифесте должно стоять требования административных привилегий, и 3) при каждом запуске юзер должен подтверждать, что да, он действительно хочет запустить эту опасную программу (которая у вас, вероятно, даже не подписана сертификатом). Без этого CreateFile будет просто возвращать ошибку ERROR_ACCESS_DENIED.
UPD: С утра набросал крохотную демонстрашку, она читает самый первый сектор диска С: (писать туда не стал, но тоже можно:). Исключение win_error только замените на какую-нибудь свою обработку.
Как уже было сказано, запускать надо командой "Run as Administrator" иначе CreateFile вернет ошибку.
В предыдущей статье я рассказал о процессе загрузки, а также продемонстрировал написание загрузочного кода на C и ассемблере, в том числе с вложением инструкций последнего в код первого. При этом мы написали несколько простых программ для проверки работоспособности внедренного в загрузочный сектор кода. В этой же статье мы рассмотрим процесс сегментации и чтения данных с дискеты в ходе загрузки, а также их вывод на экран.
Здесь я ограничусь написанием программы на ассемблере и ее копированием в загрузочный сектор образа дискеты 3.5”, после чего мы, как и в прошлой статье, протестируем записанный загрузочный код при помощи эмулятора bochs. Для реализации этих задач я задействую службы BIOS, что позволит нам лучше понять их функционирование и более уверенно работать в реальном режиме (Real Mode).
Среда программирования
- Операционная система (GNU Linux)
- Ассемблер (GNU Assembler)
- Компилятор (GNU GCC)
- Компоновщик (GNU linker ld)
- Эмулятор архитектуры x86 для тестирования (bochs).
Как вычислить размер дискеты?
-
Общий размер в байтах: кол-во сторон * кол-во дорожек * кол-во секторов в дорожке * байт в секторе.
Пример
После загрузки программы из 0x7c00 можно прочесть данные из смещения 3 и 4 и вывести их на экран.
Программа: test.S
Теперь для генерации двоичного файла и копирования кода в загрузочный сектор дискеты в командной строке введите:
- as test.S –o test.o
- ld –Ttext=0x7c00 –oformat=binary boot.o –o boot.bin
- dd if=/dev/zero of=floppy.img bs=512 count=2880
- dd if=boot.bin of=floppy.img
Здесь X и Z находятся в третьей и четвертой позиции от начала 0x7c00 .
Для проверки этого кода введите:
Чтение данных из RAM
Теперь BIOS загружает нашу программу из 0x7c00 , и та, в свою очередь, начинает поочередно выводить значения. Для обращения к данным в RAM мы устанавливаем сегмент данных со значением 0x7c00 , также указывая смещение.
Что это такое?
Это устройство, служащее для хранения и извлечения информации, которое также может использоваться в качестве средства загрузки.
Назначение сегментации
Когда мы указываем 16-битный адрес, ЦПУ автоматически вычисляет начальный адрес соответствующего сегмента. Тем не менее именно программист должен указывать начальный адрес каждого сегмента, особенно при написании такой программы, как загрузчик.
Какие виды устройств хранения бывают?
За время эволюции информационных технологий было разработано множество их видов, включая:
- Магнитные ленты;
- Флоппи-диски;
- CD и DVD-диски;
- Жесткие диски;
- USB-носители;
- …
Структура типичного флоппи-диска
Это общее схематичное строение дискеты, а вот характеристики используемой нами дискеты 3.5":
- двухсторонняя;
- стороны обозначаются согласно считывающим их магнитным головкам (head0, head1);
- каждая сторона содержит 80 дорожек (Track);
- каждая дорожка разбита на 18 секторов (Sector);
- размер каждого сектора 512 байт.
Какое прерывание мы будем использовать?
Interrupt 0x13
Service code 0x02
Как обратиться к диску с помощью прерывания 0x13?
• Команда BIOS для считывания сектора:
• Команда BIOS для считывания ‘N’-го цилиндра:
• Команда BIOS для считывания ‘N’-ой головки (стороны):
• Команда BIOS для считывания ‘N’-го сектора:
• Команда BIOS для считывания ‘N’ секторов:
Считывание данных с флоппи-диска
Давайте напишем программу для отображения меток нескольких секторов.
Программа: test.S
Что здесь происходит?
- Копирование данных в регистр общего назначения.
- Их перенос в сегментный регистр.
Далее для перемещения и достижения нужной точки сегмента данных мы используем смещение.
Что такое флоппи-диск?
Так выглядит старая-добрая дискета. Объем этого носителя невелик и составляет всего 1.4 мегабайта, чего для нашей задачи будет вполне достаточно. Дальше я кратко расскажу о способе измерения данных в вычислительных системах.
Как считывать с него данные?
Поскольку в нашем случае потребуется считывать дискету в процессе загрузки в реальном режиме, для этого нам будут доступны только службы BIOS.
Что такое мегабайт?
В компьютерах для измерения данных используются следующие величины:
- Бит: может хранить логическое значение 0 или 1.
- Полубайт: 4 бита
- Байт: 8 бит
- Килобайт (Кб): 1024 байта
- Мегабайт (Мб): 1 Кб * 1 Кб = 1,048,576 Байт = 1024 Кб = 1024 * 1024 байт
- Гигабайт (ГБ): 1,073,741,824 Байт= 2^30 Байт = 1024 Мб = 1,048,576 Кб = 1024 * 1024 * 1024 байт
- Терабайт (ТБ): 1,099,511,627,776 Байт= 2^40 Байт = 1024 ГБ = 1,048,576 Мб = 1024 * 1024 * 1024 * 1024 байт
Где на дискете находится загрузочный сектор?
Загрузочным является первый сектор диска.
Взаимодействие с флоппи-диском
План статьи
- Знакомство с сегментацией
- Среда программирования
- Чтение данных из RAM
- Знакомство с устройствами хранения
- Структура флоппи-диска
- Взаимодействие с флоппи-диском
Что делает эта программа?
В ней мы определяем макрос и функции для чтения и отображения содержимого вложенных в каждый сектор строк.
Я вкратце поясню эти макросы и функции.
Этот макрос получает в качестве аргумента строку и внутренне вызывает функцию PrintString , отвечающую за поочередное отображение символов на экране.
Эту функцию вызывает макрос mPrintString для отображения каждого байта строки, завершающейся нулем.
Этот макрос mReadSectorFromFloppy считывает сектор и помещает его в расширенный сегмент для дальнейшей обработки. Номер сектора он получает в качестве аргумента.
Эта функция отображает каждый байт данных от начала и до завершающего символа.
Это основной загрузочный код. Прежде чем начать вывод содержимого диска, мы устанавливаем в сегменте данных значения из 0x7x00 , а в расширенном сегменте из 0x9000 .
Применение сегментных регистров
Мы не можем устанавливать эти регистры напрямую и вместо этого делаем следующее:
Читайте также: