Необычный дуалбут: ноутбук с «двойным дном»

в 13:03, , рубрики: Без рубрики

Необычный дуалбут: ноутбук с «двойным дном» - 1


Не так давно на Habr Q&A я наткнулся на интересный вопрос — как сделать, чтобы два жестких диска не видели друг друга? Чтобы вирус, попав на одну систему, никоим образом не мог заразить другую. В ответах предлагали достаточно стандартные способы — использовать полнодисковое шифрование, отключить диск в диспетчере устройств и даже поставить переключатель на питание. Но что если взглянуть на задачу совершенно с другого угла и сделать всё средствами самого HDD? Да-да, сегодня мы снова погружаемся в пучины модификации прошивок и реверс-инжиниринга!

Дисклеймер: За поломки при попытках повторить описанные в статье действия, автор и компания RUVDS ответственности не несут!

Основная идея довольно простая — взять диск, скажем, на 320 гигов, переделать его в 160, а затем перед самым стартом некоторой командой “переключать” половинки. Тогда никто, в том числе и сам ПК, не будет подозревать, что на этом диске есть другая операционная система!

Необычный дуалбут: ноутбук с «двойным дном» - 2
Схема проекта “двойное дно”. Всё средствами самого диска!

Звучит легко, но попробуем на деле!
(кто устанет читать — в конце есть краткое видео с описанием идеи и результатом тык)

Разминка — “уполовиниваем” диск

Начнем с самого простого, уменьшим размер диска. Для этого возьмем известную утилиту для Western Digital — WD Marvel:

Необычный дуалбут: ноутбук с «двойным дном» - 3
В ней как раз есть удобный пункт редактирования паспорта

В паспорте находим то, что нужно — имя модели и размер диска в LBA:

Необычный дуалбут: ноутбук с «двойным дном» - 4
Неужели всё будет так просто и скучно?

Но есть загвоздка — демо версия утилиты дает только сохранить отредактированный паспорт в файл, а вот чтобы записать его обратно в диск, нужна полная версия программы. Конечно, можно воспользоваться китайской WD-R 6.0, но это же не так интересно :) Попробуем сделать всё сами!
В WD Marvel есть интересный раздел “команды”, где прямым текстом нам дают подсказки в виде некоторых служебных ATA запросов:

Необычный дуалбут: ноутбук с «двойным дном» - 5
С помощью вот этого окошка в теории можно посылать любые команды диску

Из описания окошка, узнаём, что есть команда “Super ON”, а также запросы на загрузку команды и передачу данных. Подключим наш диск через USB-SATA адаптер и попробуем найти эти команды через Wireshark:

Необычный дуалбут: ноутбук с «двойным дном» - 6
И правда, есть что-то похожее!

45 0b 00 44 57… похоже на “Super On”, не так ли? Всё это передаётся в “SCSI Command: 0xa1 ”, это команда SCSI ATA Pass Through, предназначенная для выполнения ATA команд по протоколу SCSI. Из анализа трафика также видим, что после каждой ATA команды происходит чтение регистров, чтобы определить результат запроса:

Необычный дуалбут: ноутбук с «двойным дном» - 7
Запрос ATA регистров и ответ на него

Что полностью согласуется с документацией:

Необычный дуалбут: ноутбук с «двойным дном» - 8
Интересно, что современные ATA драйвера тоже поддерживают эти команды

Точно так же видим в логе и ATA “запрос на загрузку команды”. Только помимо записи регистров, происходит ещё и передача блока данных с самой командой:

Необычный дуалбут: ноутбук с «двойным дном» - 9
Так много пустого места… А всё потому что передача должна быть кратна размеру сектора

И наконец, вместе с “запросом на передачу данных”, диск нам возвращает служебный модуль:

Необычный дуалбут: ноутбук с «двойным дном» - 10
А вот и наш модуль с паспортом

Немного поигравшись с WD Marvel, делаем следующие выводы:

  • все сервис-команды исполняются схожим образом:
    Super ON ⇒ передача команды ⇒ передача данных
  • команда чтения модулей — 08 / 01
  • команда чтения RAM — 13 / 01
  • команда записи RAM — 13 / 02

Логично предположить, что команда записи модулей будет 08 / 02! Конечно, можно её послать через тот же интерфейс “Команды” в WD Marvel, но мы же не ищем легких путей, верно?
В любом случае утилита для взаимодействия с SATA пригодится в дальнейшей разработке, поэтому… делаем отправку USB-ATA команд на Python (взяв за основу вот этот пример)!
Сначала приём / отправку SCSI команд:

def GenSpdt(DataIn, Timeout, Cmd, Size):
    scsi = SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER()
    scsi.sptd.Length = ctypes.sizeof(scsi.sptd)
    scsi.sptd.TimeOutValue = Timeout
    scsi.sptd.SenseInfoOffset = SENSE_OFFSET
    scsi.sptd.SenseInfoLength = ctypes.sizeof(scsi.sense)
    scsi.sptd.CdbLength = len(Cmd)
    scsi.sptd.Cdb = (ctypes.c_byte * 16)(*Cmd)
    scsi.sptd.DataTransferLength = Size
    scsi.sptd.DataIn = DataIn
    return scsi

def ScsiIn(self, cmd, size, timeout=5):
    scsi = GenSpdt(1, timeout, cmd, size)
    buffer = ctypes.create_string_buffer(size)
    scsi.sptd.DataBuffer = ctypes.cast(buffer, ctypes.POINTER(ctypes.c_char))
    request = bytearray(scsi)
    win32file.DeviceIoControl(self.handle, IOCTL_SCSI_PASS_THROUGH_DIRECT, request, len(request), None)
    return bytearray(buffer)

def ScsiOut(self, cmd, data, timeout=5):
    scsi = GenSpdt(0, timeout, cmd, len(data))
    scsi.sptd.DataBuffer = ctypes.cast(data, ctypes.POINTER(ctypes.c_char))
    request = bytearray(scsi)
    win32file.DeviceIoControl(self.handle, IOCTL_SCSI_PASS_THROUGH_DIRECT, request, len(request), None)

Затем ATA команды и ATA регистры:

def AtaIn(self, cmd, size, timeout=5):
    scsicmd = b"xa1x08x0e" + cmd + b"x00x00" #PIO_IN + DIR_IN
    reply = self.ScsiIn(scsicmd, size, timeout)
    self.UpdateRegs()
    return reply

def AtaOut(self, cmd, data, timeout=5):
    scsicmd = b"xa1x0ax06" + cmd + b"x00x00" #PIO_OUT + DIR_OUT
    result = self.ScsiOut(scsicmd, data, timeout)
    self.UpdateRegs()
    return result
def UpdateRegs(self):
    scsicmd = b"xa1x1fx0d" + b"x00" * 9
    self.regs.parse(self.ScsiIn(scsicmd, 0x20)[3:14])

И наконец, поверх всего этого — сервисные команды Western Digital:

def WdSu(self):
    self.AtaIn(b"x45x0bx00x44x57xa0x80", 0)

def WdSendCmd(self, cmd):
    self.WdSu()
    self.AtaOut(b"xd6x01xbex4fxc2xa0xb0", cmd)

def WdReadData(self):
    sectors = self.regs.lbas[1] + (self.regs.lbas[2] << 8)
    return self.AtaIn(b"xd5" + bytes([sectors]) + b"xbfx4fxc2xa0xb0")

def WdWriteData(self, data):
    sectors = self.regs.lbas[1] + (self.regs.lbas[2] << 8)
    self.AtaOut(b"xd5" + bytes([sectors]) + b"xbfx4fxc2xa0xb0", data)

def WdReadModule(self, idx):
    self.WdSendCmd(struct.pack("<HHH", 8, 1, idx))
    return self.WdReadData()

def WdWriteModule(self, idx, data):
    self.WdSendCmd(struct.pack("<HHH", 8, 2, idx))
    return self.WdWriteData(data)

def WdReadRam(self, offset, length):
    self.WdSendCmd(struct.pack("<HHII", 0x13, 1, offset, length))
    return self.WdReadData()

def WdWriteRam(self, offset, data):
    self.WdSendCmd(struct.pack("<HHII", 0x13, 2, offset, len(data)))
    return self.WdWriteData(data)

Вот теперь можно и переписать паспорт накопителя!
Меняем размер, выдаваемый компьютеру, ну и имя диска на всякий случай:

Необычный дуалбут: ноутбук с «двойным дном» - 11
Остальные значения оставил прежними, мало ли

Сохраняем новый паспорт в файл и записываем его тремя строчками:

disk = WdDev("\\.\PhysicalDrive1")
data = open("C:\WDMarv_demo\Default\Modified\02.mod", "rb").read()
disk.WdWriteModule(2, data)

После этого почему-то потребовалось переформатировать диск, иначе система по-прежнему видела его как 320 GB. Но в итоге всё получилось:

Необычный дуалбут: ноутбук с «двойным дном» - 12
Увы, увеличить размер таким же способом не получится

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

Тренировка — патчим прошивку HDD

Чтение/запись RAM есть, читать/писать модули умеем, есть пара чужих статей с реверсом этих же дисков, проблем быть не должно, верно? Не тут-то было! Прошивка гигантская, и разобраться в ней сходу у меня не вышло. Поэтому (что ж поделать) подключаем аппаратную отладку. FT232H с алиэкспресс, распиновка JTAG из интернета, проводки из ашановского SCART кабеля…

Необычный дуалбут: ноутбук с «двойным дном» - 13
Адаптер очень крут за свою цену. Рекомендую

Пробный запуск OpenOCD показал, что в диске три ARM ядра, так и укажем в конфиге:

interface ftdi
ftdi_vid_pid 0x0403 0x6014
ftdi_layout_init 0x0008 0x000b
ftdi_layout_signal nTRST -data 0x0010 -oe 0x0010
ftdi_layout_signal nSRST -data 0x0020 -oe 0x0020
reset_config trst_and_srst
adapter_khz 500
telnet_port 4444
gdb_port 3333
jtag newtap mv c -irlen 4 -ircapture 0x1 -irmask 0xf -expected-id 0x4ba00477
jtag newtap mv s -irlen 4 -ircapture 0x1 -irmask 0xf -expected-id 0x140003d3
jtag newtap mv m -irlen 4 -ircapture 0x1 -irmask 0xf -expected-id 0x140003d3
target create s feroceon -chain-position mv.s
target create m feroceon -chain-position mv.m

Иии:

Необычный дуалбут: ноутбук с «двойным дном» - 14
Третье ядро нам не особо интересно, поэтому только два таргета

Есть отладка! Теперь применяем магию breakpoints и watchpoints, пытаясь проследить путь ATA запроса. Самое главное, что нужно найти — место обработки LBA (номера сектора) команд чтения/записи, именно там удобно разместить патч для переключения половинок диска.

Необычный дуалбут: ноутбук с «двойным дном» - 15
Очень похоже на Super On, почему бы сюда не поставить бряк?

Breakpoints ставим в места кода, которые (как нам кажется) должны участвовать в обработке запроса, watchpoints — на ячейки памяти, в которые некто должен положить (или прочитать) значения из запроса, чтобы этого самого некто отыскать. И так по цепочке и идём. Дело немного осложняется тем, что здесь два ядра и всего один аппаратный модуль breakpoint/watchpoint (у каждого ядра), но в целом жить можно.

Необычный дуалбут: ноутбук с «двойным дном» - 16
Вот как-то так и происходит вся отладка

В конце концов находим, что в этой функции из ATA регистров читают параметры команды:

Необычный дуалбут: ноутбук с «двойным дном» - 17
Зачем-то разные регистры для 24- и 48-битных команд

А вот здесь самое интересное — к LBA из запроса прибавляется 0x300000. Учитывая, что все запросы чтения/записи проходят через эту функцию, именно здесь будет разумно разместить наш кастомный код:

Необычный дуалбут: ноутбук с «двойным дном» - 18

И наконец последний кусочек мозаики, тут проверяется выход за пределы максимального LBA, эту проверку придётся отключить, чтобы после переключения на верхнюю половинку диск не послал нас куда подальше:

Необычный дуалбут: ноутбук с «двойным дном» - 19
Кстати, располагается сразу после функции прибавления LBA

Если кратко обобщить, ATA команда в прошивке проходит следующие этапы:

  • Чтение команды из аппаратных регистров ⇐ здесь начинали отладку
  • Преобразование ATA кода команды во внутренний код операции ⇐ не так интересно
  • Преобразование LBA из ATA команды во внутренний LBA ⇐ это патчим
  • Проверка границ LBA ⇐ это отключаем
  • Исполнение команды (кеширование, чтение…) ⇐ это не трогаем

Нужные процедуры находятся в диапазоне ОЗУ 0x0000-0x10000. Как показала отладка, этот код грузится из SPI ROM, и патчить его далеко не так удобно. Но! Все вызовы трансляции LBA при “обычных” чтении/записи происходят в функции 0x1459C, чей код я обнаружил в модуле №13 (Cache Overlay):

Необычный дуалбут: ноутбук с «двойным дном» - 20
Этот модуль, кстати, грузится в ОЗУ по адресу 0x10840

Значит внедряемся туда! Более того, в конце модуля есть свободное место, как раз хватит для патча:

Необычный дуалбут: ноутбук с «двойным дном» - 21
Очевидно, снова “добивали” до кратности размеру сектора

Итак, после пары итераций мозгомыслия получилось вот это:

#define PART_SIZE 0x12A17558
typedef uint32 (*convert_lba_func)(uint32 * descriptor);
uint32 my_convert_lba (uint32 * descriptor)
{
	// здесь будем запоминать текущий "режим"
	uint32 * translate_flag = (uint32*)0x17FFC;
	convert_lba_func orig_convert_lba = (convert_lba_func)0x21F7;
	uint32 input_lba = descriptor[2];
	// подача вот таких LBA будет переключать режимы диска
	if (input_lba == 0xFFFFFFF1) {
		*translate_flag = 1;
		*(unsigned short *)0x5642 = 0x46C0; // патчим проверку LBA
	} else if (input_lba == 0xFFFFFFF0) {
		*translate_flag = 0;
	} else if (input_lba < PART_SIZE) {
		if (*translate_flag == 1) // читаем из верхней половины
			descriptor[2] = input_lba + PART_SIZE;
	} else { // сами не даём читать вне разрешенного диапазона
		descriptor[2] = 0xFFFFFFFF;
	} // и уходим в дефолтную трансляцию
	return orig_convert_lba(descriptor);
}

Компилируем в режиме Thumb для экономии места. Чтобы быстро и безопасно проверить — заливаем патч в оперативку:

data = open("C:\Work\wddpatch\patch.bin", "rb").read()
disk.WdWriteRam(0x17F20, data) # сам код
disk.WdWriteRam(0x1465E, b"x03xF0x5FxFC") # прыжок на код

И пробуем переключать режимы (да, для этого в ATA код пришлось добавить поддержку 48-битных команд):

disk.AtaIn(b"x00x00x00x01xffxf0x00xffx00xffxe0x24", 0x200) # режим "0"
data = disk.AtaIn(b"x00x01x00x00x00xe0x20", 0x200) # читаем 0 сектор
hexdump(data)
disk.AtaIn(b"x00x00x00x01xffxf1x00xffx00xffxe0x24", 0x200) # режим "1"
data = disk.AtaIn(b"x00x01x00x00x00xe0x20", 0x200) # снова читаем 0 сектор
hexdump(data)

И всё работает, читаются разные сектора!

Необычный дуалбут: ноутбук с «двойным дном» - 22
Переключалка готова!

Осталось пропатчить 13 модуль и залить его в диск. Чтобы залить модуль в диск, у него должна быть верная контрольная сумма. Если для модуля 02 за нас её пересчитала программа, то сейчас придется считать самостоятельно. К счастью, в WD Marvel есть кнопка пересчета суммы, немного потыкав в которую, узнаем, что это обычное дополнение до нуля суммы всех 32-битных слов модуля:

def ModuleCsum(data):
    csum = 0
    for i in range(0, len(data), 4):
        csum += struct.unpack("<I", data[i:i+4])[0] if i != 0xC else 0
    csum = 0x100000000 - (csum & 0xFFFFFFFF)
    print(hex(csum))
    return data[0:0xC] + struct.pack("<I", csum) + data[0x10:]

Вот теперь, кажется, всё:

data = open("C:\WDMarv_demo\Default\Modified\13.mod", "rb").read()
disk.WdWriteModule(0x13, ModuleCsum(data))

После этого получаем полностью готовый пропатченный диск с возможностью переключения между половинками обычными командами чтения, йееей!

Но как подавать эти команды при запуске ноутбука?

Необычный дуалбут: ноутбук с «двойным дном» - 23
Это далеко не конец, это только...

Начало — UEFI и его отладка

Наконец-таки переходим к делу. А вот и наш главный подопытный — не очень молодой Lenovo 310-15IKB:

Необычный дуалбут: ноутбук с «двойным дном» - 24
Потрепан жизнью, но для опытов сгодится

Как вы уже поняли, переключением диска будет заниматься UEFI, а точнее небольшой самописный драйвер. Нам предстоит разработать и запихнуть в UEFI код, что по нажатию комбинации кнопок, отправит ту самую ATA команду диску!
Но для начала разберемся с отладкой. Неужели каждый раз чтобы проверить драйвер, BIOS перепрошивать?! Как вообще его отлаживать? Ведь полноценные отладчики стоят как чугунный мост… Оказывается, есть прибамбас, решающий сразу обе проблемы — встречайте, SPI эмулятор DediProg EM100 Pro:

Необычный дуалбут: ноутбук с «двойным дном» - 25
На оф сайте стоит $750, но я взял на ebay за $80

Этот приборчик подключается вместо микросхемы BIOS, позволяет перезалить весь образ за секунды, а ещё, как выяснилось, умеет печатать отладочные сообщения! Паяем его к ноуту обычным радужным шлейфом:

Необычный дуалбут: ноутбук с «двойным дном» - 26
Очень желательно закрепить шлейф, чтобы случайно не вырвать контакты

И вот уже ноутбук запускается в матрице с виртуальной флешки, и неплохо себя чувствует:

Необычный дуалбут: ноутбук с «двойным дном» - 27
Увы, автономно эмулятор не работает, нужен ещё один ноутбук для загрузки данных

В качестве основы для драйвера возьмем проект VisualUefi, выкинем из кода всё, кроме основной процедуры:

EFI_STATUS UefiMain (EFI_HANDLE Handle, EFI_SYSTEM_TABLE *SystemTable) {
    return EFI_SUCCESS;
}

И автоматизируем процедуру тестирования по-полной!
Сначала через UEFITool в образе UEFI от нашего ноута скопипастим какой-нибудь драйвер под другим идентификатором, это будет болванка для нашего драйвера:

Необычный дуалбут: ноутбук с «двойным дном» - 28
Всё просто — извлечь любой DXE без зависимостей, подправить, вставить в начало..

Потом сделаем батник, что будет внедрять только что собранный драйвер в образ UEFI и сразу перезаливать его в эмулятор:

UEFIReplace.exe ../lenbios_mod.bin 77777777-7777-7777-7777-777777777777 10 ../vs/samples/x64/Release/UefiDriver.efi -o ../lenmod_upd.bin

"C:Program Files (x86)DediProgEM100smucmd.exe" --stop
"C:Program Files (x86)DediProgEM100smucmd.exe" --set W25Q64FV -d C:Worklenmod_upd.bin
"C:Program Files (x86)DediProgEM100smucmd.exe" --start

Сначала прокололся, написал без --set, и возмущался, что ничего не меняется

И наконец пропишем его в Post-Build Events:

Необычный дуалбут: ноутбук с «двойным дном» - 29
Да, установил русскую VS, не подумав, переставлять язык лень

Красота — собираем драйвер, и он сразу же обновляется в ноутбуке! Остаётся питание передернуть и включить. Для проверки, что всё работает, делаем самую тупую вещь — добавляем в код while(1) и пробуем запустить. Если с этой строчкой ноут виснет, а без него загружает систему — всё готово, можно экспериментировать!
А теперь по, собственно, самой отладке. Почему-то функционал печати дебажных сообщений в официальном мануале DediProg EM100 описан вот так:

Необычный дуалбут: ноутбук с «двойным дном» - 30
Пишите на почту, вышлем доки? Почему бы сразу не разместить на сайте??

Ну да ладно, написал на почту, доки действительно прислали. Согласно докам, для отправки дебага предлагается три метода:

  • Специальная SPI команда 0x11, данные передаются в DATA самого запроса
  • Последовательность команд SPI Read (0x03 / 0x0B), данные передаются побайтно, изменением адреса чтения
  • Последовательность команд SPI Read (0x03 / 0x0B) и SPI Write (0x02), данные передаются в DATA команды записи

После многочасовых мучений выяснилось, что первый вариант отпадает. И даже не потому что в даташите на мой чипсет (Intel Skylake / Kaby Lake) нет описания как послать произвольную SPI команду по шине, это я смог найти в даташите у китайцев. Чипсет нагло игнорирует мои попытки послать запрос, будто эта фича (SPI Software Sequencing) отключена, короче я так и не нашёл, где её включить.
Второй вариант тоже отпадает. Здесь причина ещё банальнее. По идее, вот так должен производиться вход и выход в режим передачи отладочной информации:

Необычный дуалбут: ноутбук с «двойным дном» - 31

А так выглядит сама запись (Write uFIFO) — посылаем команду чтения (03h) и три байта за ней. Последний байт (Byte 4 ) — байт данных, который и пойдёт на комп:

Необычный дуалбут: ноутбук с «двойным дном» - 32

Учитывая, что эти три байта за командой — сам адрес чтения, я написал такой код:

void TestSend(uchar * buf, int data_size) {
    SpiRead(0xAAAA, 1);                  // входим в "HyperTerminal mode"
    SpiRead(0x5555, 1);
    SpiRead(0xAAAA, 1);
    for (i = 0; i < data_size + 6; i++)
        SpiRead(0xC000 + buf[i], 1);     // посылаем пакет в uFIFO побайтно
    SpiRead(0xE000, 1);                  // выходим из "HT mode"
}

Вроде бы всё отлично, да? Фиг! В таблице не зря Byte 5 помечен как None, эмулятор сохраняет любые данные на шине SPI, в том числе и те, что прочитались в ответ на команду чтения (а мы как раз читаем 1 байт). В итоге вместо 1 байта на каждом цикле передается два, формат нарушается, отладка не работает. Я бы с радостью вызвал чтение 0 байт, но чипсет банально такое не умеет…
Нормально завёлся только третий вариант, с передачей отладки через SpiWrite(). Но и тут не обошлось без нюансов. Если заглянуть в лог эмулятора, можно увидеть, что чипсет для чтения использует 0x3B (Dual Read) и 0x6B (Quad Read), а такое эмулятор использовать для отладки отказывается, только 0x03 / 0x0B распознаёт:

Необычный дуалбут: ноутбук с «двойным дном» - 33

К счастью, поддержку Fast Read можно отключить в дескрипторе BIOS, тем самым вынудив чипсет использовать только команды 0x03. Структура дескриптора есть в исходниках UEFITool:

typedef struct _FLASH_PARAMETERS {
    UINT8 FirstChipDensity : 4;
    UINT8 SecondChipDensity : 4;
    UINT8 : 8;
    UINT8 : 1;
    UINT8 ReadClockFrequency : 3;
    UINT8 FastReadEnabled : 1;  // <======== вот этот битик
    UINT8 FastReadFrequency : 3;
    UINT8 FlashWriteFrequency : 3;
    UINT8 FlashReadStatusFrequency : 3;
    UINT8 DualOutputFastReadSupported : 1;
    UINT8 : 1;
} FLASH_PARAMETERS;

Недолго копаемся в структурах дескриптора, в итоге находим этот бит в нашем образе в байте по смещению 0x32 и обнуляем:

Необычный дуалбут: ноутбук с «двойным дном» - 34

Убеждаемся, что теперь всё работает как нужно, и наконец!!! делаем свой printf:

void HabraPrint(CONST CHAR8* FormatString, ...) {
    UINT8 buf[0x110];
    VA_LIST Marker;
    VA_START(Marker, FormatString);
    UINTN data_size = AsciiVSPrint(buf + 6, 248, FormatString, Marker) + 1;
    VA_END(Marker);
    *(UINT32*)buf = 0x47364440;         // Сигнатура протокола
    buf[4] = 0x05;                      // тип = ASCII текст
    buf[5] = data_size;                 // длина текста (с терминирующим нулем)
    SpiRead(0xAAAA, 1);                 // входим в "HyperTerminal mode"
    SpiRead(0x5555, 1);
    SpiRead(0xAAAA, 1);
    SpiWrite(0xC000, buf, data_size + 6); // посылаем текст + заголовок в uFIFO
    SpiRead(0xE000, 1);                 // выходим из "HT mode"
}

EFI_STATUS UefiMain (EFI_HANDLE Handle, EFI_SYSTEM_TABLE *SystemTable) {
    HabraPrint("Hello, habr!");
    return EFI_SUCCESS;
}

Неоднократно возникало желание всё бросить и попробовать написать драйвер вообще без отладки. Тем не менее, с отладочной печатью в разы комфортнее:

Необычный дуалбут: ноутбук с «двойным дном» - 35
Ну наконец-то можно нормально дебажить!

Финишная прямая. Собственно, драйвер

Что такое EFI? — Это море! Море протоколов под спидами...

Из того, что я вычитал в документации за эти дни, я понял — основу UEFI составляют протоколы. Почти каждый объект в этой системе имеет некоторый их набор. Тот же объект SATA диска обладает протоколами DevicePath, DiskInfo, BlockIo, AtaPassThru (и др.). При этом в UEFI есть возможность найти все объекты с заданным протоколом, получить объект по экземпляру протокола и наоборот, экземпляр протокола из объекта и многие другие вещи.
Например, в драйвере нам нужно отреагировать на нажатие комбинации кнопок. Но клавиатур может быть несколько, да и наш драйвер запускается одним из первых, когда ни одна клавиатура ещё не подключена. Как быть? Всё просто — мы берём и просим UEFI уведомлять нас о появлении всех новых клавиатур в системе:

void RegisterKbdProtoHandler() {
    EFI_EVENT TextInExInstallEvent;
    // это событие будет вызывать наш callback
    gBS->CreateEvent(EVT_NOTIFY_SIGNAL, TPL_CALLBACK, OnTextInExInstall, NULL, &TextInExInstallEvent);
    // а здесь мы просим дергать событие именно на
    // новые протоколы SimpleTextInputEx (ввод символов)
    gBS->RegisterProtocolNotify(&gEfiSimpleTextInputExProtocolGuid, TextInExInstallEvent, &TextInExInstallRegistration);
}
// сам callback
VOID EFIAPI OnTextInExInstall(EFI_EVENT Event, VOID* Context) {
    EFI_HANDLE HandleBuffer;
    UINTN BufferSize = sizeof(EFI_HANDLE);
    // здесь мы получаем из события хендл на сам объект (клаву)
    Status = gBS->LocateHandle(ByRegisterNotify, NULL, TextInExInstallRegistration, &BufferSize, &HandleBuffer);
    if (!EFI_ERROR(Status)) // и вызываем обработчик
        SetupHotkeyOnHandle(HandleBuffer);
}

В обработчике каждую клавиатуру просим сообщать о нажатии комбинации Ctrl + “C”:

void SetupHotkeyOnHandle(EFI_HANDLE Handle) {
    EFI_KEY_DATA MyKey;
    EFI_HANDLE NotifyHandle;
    // получаем экземпляр протокола по хендлу объекта
    EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL* SimpleTextInEx;
    gBS->HandleProtocol(Handle, gEfiSimpleTextInputExProtocolGuid, 
                        (VOID**)&SimpleTextInEx);
    // заполняем, какую комбинацию мы хотим отследить (Ctrl + 'c')
    MyKey.Key.ScanCode = 0;
    MyKey.Key.UnicodeChar = L'c';
    MyKey.KeyState.KeyShiftState = EFI_SHIFT_STATE_VALID 
                                 | EFI_LEFT_CONTROL_PRESSED;
    MyKey.KeyState.KeyToggleState = 0;
    // и вешаем callback!
    SimpleTextInEx->RegisterKeyNotify(SimpleTextInEx, &MyKey,
                                      HotkeyHandler, &NotifyHandle);
}

Остальная логика очень простая. Если комбинация была нажата — выставляем флажок. И при подключении каждого нового диска, если флаг был выставлен, отправляем команду чтения LBA 0xFFFFFFF1, чтобы переключиться на скрытую половинку. Помимо этого, на случай, если на момент нажатия некоторые диски уже были в системе, отправляем команду ещё и им:

EFI_STATUS HotkeyHandler(IN EFI_KEY_DATA* KeyData)
{
    // флажок уже был выставлен ранее, ничего не делаем
    if (alt_hdd)
        return EFI_SUCCESS;
    // выставляем флаг
    alt_hdd = TRUE;
    // шлём команду на все текущие диски
    ProcessExistingHdds();
    return EFI_SUCCESS;
}
void ProcessExistingHdds() {
    UINTN Index, HandleCount = 0;
    EFI_HANDLE* HandleBuffer;
    // ищем все объекты (диски, флешки) с протоколом BlockIo
    gBS->LocateHandleBuffer(ByProtocol, &gEfiBlockIoProtocolGuid, NULL,
                            &HandleCount, &HandleBuffer);
    for (Index = 0; Index < HandleCount; Index++) {
        // посылаем нашу команду чтения
        SendHddCommand(HandleBuffer[Index]);
    }
    FreePool(HandleBuffer);
}

И, в заключении, самое главное — отправка команды на диск

// абсолютно аналогично OnTextInExInstall, реагирует на новые диски
VOID EFIAPI OnHddInstall(EFI_EVENT Event, VOID* Context) {
    EFI_HANDLE HandleBuffer;
    UINTN BufferSize = sizeof(EFI_HANDLE);
    // получаем хендл диска и вызываем отправку команды
    Status = gBS->LocateHandle(ByRegisterNotify, NULL, HddInstallRegistration,
                     &BufferSize, &HandleBuffer);
    if (!EFI_ERROR(Status))
        SendHddCommand(HandleBuffer);
}

void SendHddCommand(EFI_HANDLE Handle) {
    EFI_BLOCK_IO_PROTOCOL* BlockIoProto;
    EFI_LBA OrigLba;
    UINT8 Buffer[0x200];
    if (!alt_hdd) {
        // комбинации Ctrl+C не было, ничего не шлём
        return;
    }
    // достаем BlockIo протокол по хендлу
    gBS->HandleProtocol(Handle, &gEfiBlockIoProtocolGuid,
                        (VOID**)&BlockIoProto);
    // а вот тут нехитрый трюк, заставляем UEFI послать некорректный LBA
    OrigLba = BlockIoProto->Media->LastBlock;
    BlockIoProto->Media->LastBlock = 0xFFFFFFF8;
    // собственно, само чтение LBA
    BlockIoProto->ReadBlocks(BlockIoProto, BlockIoProto->Media->MediaId, 0xFFFFFFF1, 0x200, Buffer);
    // и возвращаем исходное состояние информации о диске
    BlockIoProto->Media->LastBlock = OrigLba;
}

Вообще, помимо документации очень полезными оказались репозитории как самого EDK II, так и lampone-edk2, в котором реализованы драйвера под многие аппаратные платформы. Тысячи различных примеров на все случаи жизни.

В сдобренном отладочными логами виде это выглядит как-то так:

Необычный дуалбут: ноутбук с «двойным дном» - 36
С отладкой и правда гораздо легче разрабатывается

По JTAG видим, что флаг внутри диска поменялся, а значит команда сработала! Успех!

Необычный дуалбут: ноутбук с «двойным дном» - 37
Оно фурычит! Переключились на вторую половинку!

Ну и взгляд на рабочий стол в этот момент со стороны, для истории:

Необычный дуалбут: ноутбук с «двойным дном» - 38
Можно заливать BIOS в нормальную флешку, всё отпаивать и собирать!

Готово, показываем!

После сборки, наведения марафета и установки на диск ОСей, получаем вот такой итог:

Наконец можно передохнуть, удивиться, что затея удалась, и вставить последнюю картинку:

Необычный дуалбут: ноутбук с «двойным дном» - 39

Автор: Алексей Шалпегин

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js