Вот интересный вопрос — как сделать так, чтобы два винчестера не видели друг друга? Так что-бы вирус, попавший в одну систему, никоим образом не мог заразить другую. В ответах предлагались довольно стандартные методы — использовать полное шифрование диска, отключить диск в диспетчере устройств и даже включить выключатель питания. Но что, если взглянуть на задачу с совершенно другой точки зрения и все делать с помощью самого диска? Да, да, сегодня мы снова окунаемся в модификацию программного обеспечения и обратное проектирование! Модифицируем ноутбук для двух HDD.
За поломки при попытке повторить описанные в статье действия, авторы ответственности не несут!
Основная идея очень проста: взять жесткий диск, скажем, на 320 гигабайт, преобразовать его в 160, а затем непосредственно перед запуском команды «переключить» половинки.
Схема проекта “двойное дно”. Всё средствами самого диска!
Да, звучит просто, но мы попытаемся осуществить это на деле!
Уполовинивание диска
Уменьшим диск до минимального размера. Чтобы это осуществить воспользуемся утилитой для Western Digital — WDMarvel.
В ней как раз есть удобный пункт редактирования паспорта
С помощью паспорта получаем данные о том, что нам нужно – имя модели и размер диска в LBA.
Не верится, что это может быть так просто и скучно?
Но есть проблема – демо-версия утилиты позволяет сохранить измененный паспорт в файл, а для записи его обратно на диск, вам потребуется полная версия программы. Конечно же можно использовать китайский WD-R 6.0 , но это не так уж и интересно.
В WD Marvel есть интересный раздел «команды», где они дают нам советы в виде открытого текста в виде некоторых запросов службы ATA:
При помощи этого окошка в теории можно посылать на диск любые команды.
Из описания окна узнаем, что есть команда «Super ON», а также запросы на загрузку команд и передачу данных. Подключим наш накопитель через адаптер USB-SATA и попробуем найти эти команды с помощью Wireshark:
45 0b 00 44 57 …схоже как «Super On», не так ли? Все это передается в «SCSI Command: 0xa1», это команда SCSI ATA Pass Through, предназначенная для выполнения команд ATA с использованием протокола SCSI. Из анализа трафика мы также видим, что после каждой команды ATA считываются регистры для определения результата запроса:
Запрос ATA регистров и ответ на него
Это согласуется с документацией:
Интересный факт, что современные ATA драйвера также поддерживают эти команды.
В логе ATA «запрос на загрузку команды» также присутствует. Кроме записи регистров, происходит ещё и трансляция блока данных с самой командой:
Вместе с “запросом на передачу информации”, диск нам возвращает служебный модуль.
модуль с паспортом
Поработав с 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"\xa1\x08\x0e" + cmd + b"\x00\x00" #PIO_IN + DIR_IN
reply = self.ScsiIn(scsicmd, size, timeout)
self.UpdateRegs()
return reply
def AtaOut(self, cmd, data, timeout=5):
scsicmd = b"\xa1\x0a\x06" + cmd + b"\x00\x00" #PIO_OUT + DIR_OUT
result = self.ScsiOut(scsicmd, data, timeout)
self.UpdateRegs()
return result
def UpdateRegs(self):
scsicmd = b"\xa1\x1f\x0d" + b"\x00" * 9
self.regs.parse(self.ScsiIn(scsicmd, 0x20)[3:14])
И наконец, поверх всего этого — сервисные команды Western Digital:
def WdSu(self):
self.AtaIn(b"\x45\x0b\x00\x44\x57\xa0\x80", 0)
def WdSendCmd(self, cmd):
self.WdSu()
self.AtaOut(b"\xd6\x01\xbe\x4f\xc2\xa0\xb0", cmd)
def WdReadData(self):
sectors = self.regs.lbas[1] + (self.regs.lbas[2] << 8)
return self.AtaIn(b"\xd5" + bytes([sectors]) + b"\xbf\x4f\xc2\xa0\xb0")
def WdWriteData(self, data):
sectors = self.regs.lbas[1] + (self.regs.lbas[2] << 8)
self.AtaOut(b"\xd5" + bytes([sectors]) + b"\xbf\x4f\xc2\xa0\xb0", 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)
Настало время переписать паспорт накопителя!
А теперь меняем размер в соответствии с выдаваемым компьютеру и имя диска.
Остальные значения оставил прежними
Вносим новый паспорт в файл и записываем три строчки:
disk = WdDev("\\\\.\\PhysicalDrive1")
data = open("C:\\WDMarv_demo\\Default\\Modified\\02.mod", "rb").read()
disk.WdWriteModule(2, data)
Как оказалось, после этого нужно было произвести форматирование диска, иначе система видела его как 320 GB. В конечном счете всё прошло хорошо.
Увеличить размер таким же способом не получится
В результате у нас есть жесткий диск, фактический размер которого составляет половину заявленного, а также утилита, позволяющая отправлять на жесткий диск любую ATA-команду — как обычную, так и дежурную.
Патчим прошивку HDD
Есть RAM для чтения / записи, умеем читать / писать модули, есть посты других людей с реверсом этих же дисков, проблем быть не должно, правда? Это был не тот случай ! Прошивка огромная, и сразу не разобрался. Поэтому включаем аппаратную отладку. FT232H с Аliexpress, распиновка JTAG из интернета, разводка ашановского кабеля SCART.
Адаптер
Пробный запуск 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
И:
Третье ядро нас не интересует, поэтому только два таргета
Отладка прошла успешно! Теперь мы используем магию breakpoints и watchpoints, пытаясь отследить путь запроса ATA. Самое важное, что нужно найти, — это то, где обрабатываются LBA (номера секторов) команд чтения / записи, это то место, где должен быть помещен патч для переключения половин диска.
Очень похоже на Super On
Ставим вreakpoints в те места кода, которые (как нам кажется) должны участвовать в обработке запроса, watchpoints — в ячейки памяти, куда кто-то должен поставить (или прочитать) значения из запроса, чтобы этого самого кого-то отыскать. И так по цепочке и так далее. Дело немного усложняется тем, что есть два ядра и только один аппаратный модуль breakpoint/watchpoint (на каждое ядро), но в целом жить можно.
Так и происходит вся отладка
Наконец, находим, что в этой функции из АTA регистров читают параметры команды:
И вот самое интересное — в LBA добавляется 0x300000 из запроса. Учитывая, что все запросы на чтение / запись проходят через эту функцию, имеет смысл разместить здесь наш собственный код:
Последний кусок головоломки, здесь мы проверяем, не превышен ли максимальный предел LBA, эту проверку придется отключить, чтобы перейти на верхнюю половину диска:
Располагается сразу после функции прибавления LBA
Если кратко обобщить, ATA команда в прошивке проходит следующие этапы:
- Чтение команды из аппаратных регистров ⇐ здесь начинали отладку
- Преобразование ATA кода команды во внутренний код операции ⇐ не так интересно
- Преобразование LBA из ATA команды во внутренний LBA ⇐ это патчим
- Проверка границ LBA ⇐ это отключаем
- Исполнение команды (кеширование, чтение…) ⇐ это не трогаем
Необходимые процедуры находятся в диапазоне 0x0000-0x10000 RAM. Как показала отладка, этот код загружается из SPI ROM и исправление далеко не удобно. Но все же все вызовы трансляции LBA для «обычных» операций чтения / записи происходят в функции 0x1459C, код которой я нашел в модуле 13 (Cache Overlay):
Этот модуль грузится в ОЗУ по адресу 0x10840
Значит внедряемся туда! Более того, в конце модуля есть свободное место, как раз хватит для патча:
«Добивали” до кратности размеру сектора
Итак, после пары минут размышлений получилось вот это:
#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"\x03\xF0\x5F\xFC") # прыжок на код
И пробуем переключать режимы (да, для этого в ATA код пришлось добавить поддержку 48-битных команд):
disk.AtaIn(b"\x00\x00\x00\x01\xff\xf0\x00\xff\x00\xff\xe0\x24", 0x200) # режим "0"
data = disk.AtaIn(b"\x00\x01\x00\x00\x00\xe0\x20", 0x200) # читаем 0 сектор
hexdump(data)
disk.AtaIn(b"\x00\x00\x00\x01\xff\xf1\x00\xff\x00\xff\xe0\x24", 0x200) # режим "1"
data = disk.AtaIn(b"\x00\x01\x00\x00\x00\xe0\x20", 0x200) # снова читаем 0 сектор
hexdump(data)
И всё работает, читаются разные сектора!
Переключатель готов
Теперь осталось пропатчить 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))
На выходе имеем полный пропатченный и полностью готовый к работе диск с возможностью переключаться между половинками обычными командами чтения.
Но как подавать эти команды при запуске ноутбука?
UEFI и его отладка
Теперь перейдем к делу. Наш главный подопытный Lenovo 310-15 IKB:
Lenovo 310-15 IKB:
О переключении дисков позаботится UEFI (самописный драйвер). Нам нужно разработать и впихнуть код UEFI, который, нажав комбинацию кнопок, отправит на диск ту же команду ATA.
Но сначала давайте посмотрим на отладку. Можно ли каждый раз перепрошивать биос для проверки драйвера ?! Как я могу отлаживать? Ведь полноценные отладчики похожи на чугунный мост … Оказывается, есть гаджет, решающий сразу обе проблемы — встречайте эмулятор SPI DediProg EM100 Pro:
Эмулятор DediProg EM100 Pro:
Устройство подключается вместо микросхемы BIOS, имеет возможность перезагружать весь образ за секунды и в конечном счете может распечатывать отладочные сообщения! Припаиваем к ноутбуку обычным радужным кабелем:
Желательно закрепить шлейф, чтобы случайно не вырвать контакты
Ноутбук начинает работать с виртуальной флешки, и отлично себя чувствует:
Автономно эмулятор не работает, нужен ещё один ноутбук для загрузки данных
В качестве основы для драйвера возьмем проект VisualUefi, выкинем из кода всё, кроме основной процедуры:
EFI_STATUS UefiMain (EFI_HANDLE Handle, EFI_SYSTEM_TABLE *SystemTable) {
return EFI_SUCCESS;
}
И автоматизируем процедуру тестирования по-полной!
Сначала через UEFITool в образе UEFI от нашего ноута скопипастим какой-нибудь драйвер под другим идентификатором, это будет болванка для нашего драйвера:
Извлечь любой DXE без зависимостей, подправить, вставить в начало..
Потом сделаем батник, что будет внедрять только что собранный драйвер в образ UEFI и сразу перезаливать его в эмулятор:
Потом сделаем батник, что будет внедрять только что собранный драйвер в образ 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)\DediProg\EM100\smucmd.exe" --stop
"C:\Program Files (x86)\DediProg\EM100\smucmd.exe" --set W25Q64FV -d C:\Work\lenmod_upd.bin
"C:\Program Files (x86)\DediProg\EM100\smucmd.exe" --start
И наконец пропишем его в Post-Build Events:
И тут же, после установки драйвера, он автоматически обновится в ноутбуке. Осталось передернуть блок питания и включить. Чтобы убедиться, что все работает, сделаем самое глупое — добавим в код while (1) и попробуем его запустить. Если ноут завис на этой линии, но система загружается без нее — все готово, можно экспериментировать!
А теперь к самой отладке. Функция вывода отладочных сообщений почему-то описана в официальном руководстве DediProg EM100 следующим образом:
Пишите на почту, вышлем доки
Все хорошо! Написал на почту, получил документы. Доками рекомендовано три способа доставки дебага:
- Специальная SPI команда 0x11, данные передаются в DATA самого запроса
- Последовательность команд SPI Read (0x03 / 0x0B), данные передаются побайтно, изменением адреса чтения
- Последовательность команд SPI Read (0x03 / 0x0B) и SPI Write (0x02), данные передаются в DATA команды записи
От первого варианта пришлось отказаться . И даже не потому, что в даташите на мой набор микросхем (Intel Skylake / Kaby Lake) нет описания того, как послать произвольную команду SPI через шину, я смог найти это в даташите на китайском языке. Чипсет грубо игнорирует мои попытки отправить запрос, как будто эта функция (SPI Software Sequencing) отключена, короче говоря, я так и не нашел, где ее включить.
Второй вариант тоже отпадает. Здесь причина еще банальнее. По идее, так должен осуществляться вход и выход в режим передачи отладочной информации:
А так выглядит сама запись (Write uFIFO) — посылаем команду чтения (03h) и три байта за ней. Последний байт (Byte 4 ) — байт данных, который и пойдёт на комп:
Учитывая, что эти три байта за командой — сам адрес чтения, я написал такой код:
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:
К счастью, поддержку 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 и обнуляем:
Убеждаемся, что теперь всё работает как нужно, и наконец!!! делаем свой 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;
}
После нескольких неудачных попыток, я решил все же попробовать написать драйвер без отладки. Но с отладочной печатью в разы удобнее:
Можно приступать к работе
Драйвер
Из того, что я читал в документации в последнее время, я обнаружил, что 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, в которых реализованы драйверы для многих аппаратных платформ. Тысячи разных примеров на все случаи жизни.
В сдобренном отладочными логами виде это выглядит как-то так:
С отладкой разрабатывается гораздо легче
По JTAG видим, что флаг внутри диска поменялся, а значит команда сработала.
Переключились на вторую половинку
Взгляд на рабочий стол в этот момент со стороны
Можно загружать BIOS в обычную флешку, все отпаивать и собирать
Жесткие диски — это часть программирования, окутанная тьмой и тайной. Отсутствие информации по этому поводу накладывает свой отпечаток, и даже простое копирование больших объемов данных может стать нетривиальной задачей. Но если копнуть глубже, вы обнаружите, что все не так уж и плохо, и большинство базовых операций на низком уровне доступны не только администратору, но и простому смертному пользователю. На практике мифы о высоком либидо и буйном настроении дисков несколько преувеличены, и я надеюсь, что эта статья помогла нам это понять.