|
|
|||||||||||||||||||||||||||||||||||||||||
|
Работа с жестким диском на современных SATA котроллерах© Олег Васильев. BVG Group 15.09.2012 При выполнении задач по ремонту жестких дисков или восстановлению данных, часто необходимо написать свою простенькую программку, которая решала конкретные задачи, а также не содержала ошибок, присущих покупному или бесплатно распространяемому программному обеспечению. Во времена IDE контроллеров, это делалось просто. Каждый программист знал, что Secondary накопитель располагается на портах со 170 по 177, а также на порту 376. Способов "достучаться" до этих портов была масса, чаще всего народ пользовался драйвером GiveIO или его аналогом. Но шло время, всё менялось. Сначала изменились накопители. Они пересели с интерфейса IDE на SATA. Конечно, некоторое время производители контроллеров держали совместимость с IDE, но постепенно она уходит. Переходники IDE-SATA уже не спасают, так как с материнских плат уходят порты IDE. И самый страшный удар нанесла фирма Microsoft, убрав из 64 битной версии Windows возможность установки драйвера GiveIO. Так что же, теперь придётся сидеть на старом железе или бросать саму мысль о разработке своих программ по непосредственной работе с жестким диском? Нет, нет, и ещё раз нет! В данной статье будет показан способ доступа к накопителю, доступный любому желающему, так как все необходимые материалы находятся в свободном или условно свободном (30 дней оценочного периода) доступе. Во-первых, давайте осмотримся в Интернете. Если внимательно поглядеть, то спецификация AHCI распространяется свободно фирмой Intel (а большинство контроллеров соответствуют ей). Чуть в сторонке стоят контроллеры фирмы Silicon Image. Но на их контроллер SIL3132 документация тоже вполне доступна. Что же мы там видим? А видим мы то, что просто чуточку изменился метод доступа к накопителю. Раньше он обеспечивался через так называемый таскфайл. Исходно таскфайл состоял из значений, которые следует положить в порты 171-177. Затем, с появлением трансляции LBA48, таскфайл стал состоять из двух наборов. Первый набор клался в порты 171-176, а затем - второй набор в 171-177. Сразу после записи в порт 177, начиналось исполнение команды накопителем. Так вот, сейчас таскфайл чуточку изменился и поменял имя. Теперь он гордо именуется FIS27, где FIS расшифровывается, как Frame Information Structure, а 27 - это первый байт, который в данную структуру кладётся (и является идентификатором его типа). Наша задача - сформировать этот блок, а затем сказать контроллеру, чтобы он передал его по последовательной шине SATA в накопитель. Кроме того, мы должны сообщить контроллеру, хотим ли мы послать вслед за этим блоком данные, или наоборот, хотим принять их. Ну, или ничего не делать, это тоже возможно. Как это мы сообщаем контроллеру - зависит от спецификации (AHCI, SIL3132 или ещё какая), а вот сам формат определяется стандартом SATA, так что давайте рассмотрим его поподробнее. Ничего не напоминает? За исключением нескольких полей, это всё тот же таскфайл, но разложенный не по двум наборам портов, а по структуре в памяти (пять DWORDов, один из которых зарезервирован на будущее). Так что не так страшен чёрт, как его малюют. Въедливый читатель, пробежав глазами по FIS27, затем - внимательно рассмотрев его, а уже потом - перепроверив себя, воскликнет: "Постойте! А где порт 170? Как я буду пересылать данные?". Действительно, на шине IDE было два метода передачи данных - PIO (через порт данных, имевший адрес 0x170 для Secondary IDE) и UDMA (настолько страшный, что не все его использовали). И там для PIO команд всё было просто - подали команду, дождались DRQ, начали передавать или принимать данные. Как же быть с шиной SATA? Здесь всё интереснее. Дело в том, что команды у SATA накопителей как делились на PIO и DMA, так и делятся, но делается это скорее для совместимости с IDE предками, так как данные и для тех и для других команд передаются через FIS46. Единственное отличие - для PIO команд максимальная длина FIS46 составляет 512 байт, а для DMA - 2 килобайта. Если данные не влезают - идёт несколько FIS46 подряд. В начале и конце передачи каждого FIS46 некоторое время тратится на "рукопожатие", поэтому в PIO, пакетов для той же длины получается больше, накладные расходы оказываются выше, а скорость передачи - ниже. В остальном - НИКАКИХ ОТЛИЧИЙ! Одни и те же FIS46 одного и того же формата!!! Если с точки зрения накопителя, ещё есть какие-то там PIO и DMA, то с точки зрения шины PCI Express, данные всегда передаются в режиме DMA. Итак. Из сказанного выше, становится ясно, что для работы с SATA накопителем, мы должны где-то сформировать FIS27, а затем - отправить его в накопитель, после чего либо послать вдогонку FIS46 с данными, либо принять из накопителя аналогичный FIS46. И под завес, накопитель отдаст нам FIS с образами портов, который в точности соответствует структуре FIS27, только отдаётся от накопителя к нам. Раньше мы могли в любое время считать любой порт, теперь же нам выдаётся блок один раз после команды. А уже потом мы рассматриваем его хоть до посинения. Его и только его. Но, собственно, при реальном программировании этого хватает. Как же всё это сделать? Ну, во-первых, надо выяснить, через какой контроллер мы работаем, и посмотреть документацию на него. Автор настоятельно рекомендует начинать с чипов Silicon Image, так как там всё проще,- на одну косвенность меньше. Но каков бы ни был чип, до него надо достучаться. И тут хочешь-не хочешь, а нужен драйвер. В принципе, достаточно удобен и доступен Jungo WinDriver, Первые 30 дней Вы можете пользоваться им в ознакомительных целях бесплатно. Дальше - зависит от обстоятельств, можно найти ключеделку, коих на Google - море. Если Вы - частник, может быть, даже обойдётся. Если же Вы работаете в организации, то в один прекрасный день туда может наведаться дядечка (или тётечка), показать книжечку и начать дело о пиратстве. Поэтому лучше купить лицензию. Но она стоит просто невероятных денег. Кажется, 7000 долларов США за каждого разработчика. В общем, вариантов "что делать потом" - масса, но входить во вкус лучше именно с использованием Jungo WinDriver. Он самодостаточен для того, чтобы достучаться до SATA накопителя. Давайте рассмотрим, как же им пользоваться. Хотелось бы, конечно, дать готовый класс, но мы через этот драйвер работали более года назад. С тех пор наше понимание контроллера SIL3132 изменилось. Если тот класс прекрасно работал с исправными накопителями, то при первом же BADе, мог произойти неисправимый сбой. А ведь ремонтникам и восстановителям данных попадаются именно такие накопители. Разумеется, у нас есть класс, который учитывает все эти проблемы, но вот беда - мы, как законопослушные граждане, пользовались тем драйвером не больше месяца, затем - написали свой. Поэтому давайте рассмотрим только опорные точки, а дальше - глядя в них и в документацию на Jungo WinDriver и на SIL3132, Вы сможете дописать свою программу до полноценно работающей. Итак. Создаём при помощи мастера проект, "натравив" его на нашу PCI Express карту. Попутно нам создадут INF файл, при помощи которого адаптер будет устанавливаться в операционной системе. Дальнейшая работа будет вестись с созданным шаблоном. Сначала нам надо найти карты. Заведём список найденных карт следующим образом:
#include "WDrep.h" #include "wdc_defs.h" #include "windrvr.h" ... Объявление переменных-членов: WD_PCI_SCAN_CARDS m_scanResult; DWORD m_vendor; DWORD m_device; ... Код заполнения списка m_list.SetExtendedStyle (LVS_EX_FULLROWSELECT|LVS_EX_GRIDLINES); m_list.InsertColumn (0,"Адаптер",LVCFMT_LEFT,400); memset (&m_scanResult,0,sizeof(m_scanResult)); m_scanResult.searchId.dwVendorId = m_vendor; m_scanResult.searchId.dwDeviceId = m_device; DWORD dwStatus = WD_PciScanCards(m_hDev, &m_scanResult); if (WD_STATUS_SUCCESS != dwStatus) { return FALSE; } for (DWORD i=0;i<m_scanResult.dwCards;i++) { char txt [100]; wsprintf (txt,"Bus %d Slot %d",m_scanResult.cardSlot[i].dwBus,m_scanResult.cardSlot[i].dwSlot); m_list.InsertItem (i,txt); } Вызываем это дело так: CJungoPortDlg dlg (m_cardDescriptor); strcpy (m_licenseString,"СЮДА ВПИСЫВАЕМ ЛИЦЕНЗИЮ, КОТОРУЮ НАМ ПРОДАЛИ"); m_hDrive = WD_Open(); if (m_hDrive == INVALID_HANDLE_VALUE) { MessageBox (GetActiveWindow(),"Не найден Jungo Driver","",MB_ICONSTOP); return CHDDTransport::hddErrCanNotOpenDrive; } WD_LICENSE lic; memset (&lic,0,sizeof(lic)); strcpy (lic.cLicense,m_licenseString); WD_License (m_hDrive,&lic); WD_VERSION ver; memset (&ver,0,sizeof(ver)); WD_Version(m_hDrive,&ver); dlg.m_vendor = 0x1095; dlg.m_device = 0x3132; dlg.m_hDev = m_hDrive; // Первичная подготовка адаптера будет произведена здесь if (dlg.DoModal() != IDOK) { return CHDDTransport::hddErrCanceledByUser; } Заведём переменную-член класса #include "windrvr.h" ... struct cardDescriptor { WD_PCI_SLOT pciSlot; WD_CARD_REGISTER cardReg; WD_INTERRUPT Intrp; DWORD* m_memPtrs[16]; }; cardDescriptor m_cardDescriptor; WD_DMA m_dmaMem1; BYTE m_lastSlot0 [0x80]; И для выбранной карты заполним её так: int pos = m_list.GetSelectionMark(); if (pos < 0) { MessageBox ("Не выбран адаптер","",MB_ICONSTOP); } // Получаем информацию об адаптере WD_PCI_CARD_INFO pciCardInfo; memset (&pciCardInfo,0,sizeof(pciCardInfo)); // Получаем расширенную информацию об адаптере pciCardInfo.pciSlot = m_scanResult.cardSlot[pos]; WD_PciGetCardInfo(m_hDev, &pciCardInfo); // На основании полученных данных, заполняем необходимые поля m_cardDescriptor.pciSlot = pciCardInfo.pciSlot; m_cardDescriptor.cardReg.Card = pciCardInfo.Card; m_cardDescriptor.cardReg.fCheckLockOnly = FALSE; // Проецируем ресурсы адаптера на системную память DWORD dwStatus; dwStatus = WD_CardRegister(m_hDev, &m_cardDescriptor.cardReg); Итак. Сначала это выглядит, как шаманский ритуал, но после внимательного ознакомления, начинаешь замечать что-то логичное. В частности, именно здесь происходит проецирование блоков "памяти" контроллера на адресное пространство нашего EXEшника, после чего мы можем достучаться до платы, уже минуя драйвер. Дело в том, что современные контроллеры SATA позволяют нам программировать вообще без обращения к портам. У SIL3132 портов попросту нет. У AHCI они есть, но их функции дублируются и памятью. В менеджере устройств это выглядит примерно так: То есть, теперь все функции управления контроллером переехали из портов в "память" (размещённую в самом контроллере и доступную через шину PCIe). А Jungo WinDriver как раз позволяет спроецировать память контроллера на адресное пространство нашего EXE файла. Что это значит? А то, что драйвер нам нужен исключительно на этапе инициализации. Мы с его помощью проецируем память на себя, а дальше - просто обращаемся к ней, как к обычным C++ным переменным. Теперь нам надо выделить буфер для DMA транзакций. У этого буфера будет смешная особенность - у него два адреса. Один адрес - логический, через него к нему будет обращаться наш EXEшник. Второй адрес - шинный. Его мы будем передавать в контроллер. Есть ещё третий адрес, "физический", им драйвер пользуется для собственных нужд, на него не обращаем внимания. Нас интересуют именно логический и шинный адреса. // Теперь выполним специфичные для SIL3132 действия // Выделим буфер под DMA memset (&m_dmaMem1,0,sizeof (m_dmaMem1)); // 256К m_dmaMem1.dwBytes = 0x40000; m_dmaMem1.dwOptions = DMA_ALLOW_64BIT_ADDRESS | DMA_KERNEL_BUFFER_ALLOC | DMA_TO_FROM_DEVICE; /* Initialization of dma.hCard, value obtained from WD_CardRegister call: */ m_dmaMem1.hCard = m_cardDescriptor.cardReg.hCard; DWORD dwStatus = WD_DMALock(m_hDrive, &m_dmaMem1); Ну, и выясним логические адреса тех блоков памяти контроллера, которые спроецировались на наше адресное пространство: // Берём порт Global Comtrol DWORD* bar0 = (DWORD*)m_cardDescriptor.cardReg.Card.Item[1].I.Mem.dwUserDirectAddr; // Накопитель 1 Control Set // Port Reset DWORD* bar1 = (DWORD*) m_cardDescriptor.cardReg.Card.Item[2].I.Mem.dwUserDirectAddr; ВСЁЁЁЁЁ! Теперь контроллер полностью в нашей власти. Хотим мы сбросить его? Да пожалуйста! DWORD dwTemp = bar0[0x40/4]; // Global Reset dwTemp |= 0x80000000; dwTemp &= 0x7ffffffc; bar0[0x40/4] = dwTemp; Здесь и далее деление на 4 - потому что указатель у нас DWORD*, поэтому гранулярность равна четырём байтам. А хочется-то вбивать адреса, как в документации. Поэтому делим их на 4. Это чисто особенность языка С++. Хотим сбросить один конкретный порт? Нет ничего проще, только не забывайте сопоставлять наши переменные и адреса памяти из документации на SIL3132 // Сбросили порт bar1[(m_portOffset+0)/4] = 4; DWORD dwTimeOut = GetTickCount()+1000; while ((bar1[(m_portOffset)/4] &0x80000000)==0) { if (GetTickCount()>dwTimeOut) { break; } Sleep (10); } здесь и далее m_portOffset - это смещение блока управления накопителем. Из документации на SIL видно, что порт 0 имеет смещение 0x1000, порт 1 - 0x3000 и так далее. Опишем FIS27 (с дополнениями от SILа) в виде Сишной структуры #pragma pack (push,1) struct portRequestStruct { DWORD protocolAndOverride; DWORD receivedTransferCount; BYTE fisType; BYTE flags; BYTE port2_177; BYTE port2_171; BYTE port2_173; // BYTE port2_174; BYTE port2_175; BYTE port2_176; BYTE port1_173; // BYTE port1_174; BYTE port1_175; BYTE port1_176; BYTE port2_172; BYTE port1_172; BYTE reserved1; BYTE devControl; DWORD reserved2; DWORD mustBeZero; UINT64 sgeAddress1; DWORD count1; DWORD flags1; UINT64 sgeAddress2; DWORD count2; DWORD flags2; BYTE pad [0x40]; }; #pragma pack (pop) Ещё, как любитель красивого кода, я сделал несколько дополнительных объявлений на основании документации на SIL3132 #define SIL3132_PRB_CONTROL_PROTOCOL_OVERRIDE 0x01 #define SIL3132_PRB_CONTROL_RETRANSMIT 0x02 #define SIL3132_PRB_CONTROL_EXTERNAL_COMMAND 0x04 #define SIL3132_PRB_CONTROL_RECEIVE 0x08 #define SIL3132_PRB_CONTROL_PACKET_READ 0x10 #define SIL3132_PRB_CONTROL_PACKET_WRITE 0x20 #define SIL3132_PRB_CONTROL_INTERRUPT_MASK 0x40 #define SIL3132_PRB_CONTROL_CONTROL_SOFT_RESET 0x80 #define SIL3131_PROTOCOL_OVERRIDE_PACKET 0x10000 #define SIL3131_PROTOCOL_OVERRIDE_LEGACY_QUEUE 0x20000 #define SIL3131_PROTOCOL_OVERRIDE_NATIVE_QUEUE 0x40000 #define SIL3131_PROTOCOL_OVERRIDE_READ 0x80000 #define SIL3131_PROTOCOL_OVERRIDE_WRITE 0x100000 #define SIL3131_PROTOCOL_OVERRIDE_TRANSPARENT 0x200000 И тогда функция для связи с накопителем (как я видел её год назад) будет выглядеть примерно так: Сначала мы формируем FIS27 в памяти компьютера (она быстрая, PCIe медленней, так что формируем не в чипе, а в обычной памяти) portRequestStruct req; memset (&req,0,sizeof(req)); req.protocolAndOverride = SIL3132_PRB_CONTROL_PROTOCOL_OVERRIDE; if (dataTransfer.GetSize() != 0) { // Если надо передавать данные - скопировали их в буфер DMA if (dataTransfer.bSendToDrive) { memcpy (m_dmaMem1.pUserAddr,dataTransfer.buf,dataTransfer.GetSize()); req.protocolAndOverride |= SIL3131_PROTOCOL_OVERRIDE_WRITE; } else { req.protocolAndOverride |= SIL3131_PROTOCOL_OVERRIDE_READ; } } req.fisType = 0x27; req.flags = 0x80; req.port1_172 = taskFile.ports1[2]; req.port1_173 = taskFile.ports1[3]; req.port1_174 = taskFile.ports1[4]; req.port1_175 = taskFile.ports1[5]; req.port2_171 = taskFile.ports2[1]; req.port2_172 = taskFile.ports2[2]; req.port2_173 = taskFile.ports2[3]; req.port2_174 = taskFile.ports2[4]; req.port2_175 = taskFile.ports2[5]; req.port2_176 = taskFile.ports2[6]; req.port2_177 = taskFile.ports2[7]; // Это мы кладём физический адрес того же буфера DMA!!! req.sgeAddress1 = m_dmaMem1.Page[0].pPhysicalAddr; if ((DWORD)dataTransfer.GetSize()<=m_dmaMem1.Page[0].dwBytes) { req.count1 = (DWORD) dataTransfer.GetSize(); if (dataTransfer.actualSize != 0) { req.count1 = dataTransfer.actualSize; } req.flags1 = 0x80000000; } // TODO - добавить else на случай двухстраничных запросов!!! Когда FIS27 и служебные поля, добавленные к нему разработчиками SILа (сведения о буфере и о направлении данных) сформированы, скопируем их в "слот 0" контроллера (что это такое - смотрите в документации на SIL3132). Копирование пройдёт относительно быстро, так как функция memcpy копирует 64 битными полями DWORD* bar1 = (DWORD*) m_cardDescriptor.cardReg.Card.Item[2].I.Mem.dwUserDirectAddr; memcpy (bar1,&req,sizeof(req)); Ну, и наконец, сообщим контроллеру, чтобы он начал исполнять задание, помещённое в слот 0 bar1[(m_portOffset+0x20)/4] = 0; А дальше будем ждать завершения транзакции DWORD dwTimeOut = GetTickCount () + dataTransfer.dwTimeout; while (bar1[(m_portOffset+0x800)/4] & 0x00000001) { // Sleep (0); if (GetTickCount()>dwTimeOut) { // Имитация залипшего BSY ((portRequestStruct*) m_lastSlot0)->port2_177 = 0x80; return CHDDTransport::hddErrTimeout; } // Возникла аппаратная ошибка контроллера if (bar1[(m_portOffset+0x24)/4] != 0) { break; } } DWORD dwTemp = bar1[(m_portOffset+0x24)/4]; if (dwTemp >1) { // Имитация залипшего BSY ((portRequestStruct*) m_lastSlot0)->port2_177 = 0x80; return CHDDTransport::hddErrChannelError; } // Это мы скопировали FIS с ответными портами memcpy (m_lastSlot0,bar1,sizeof(m_lastSlot0)); if (dataTransfer.GetSize()!=0) { if (!dataTransfer.bSendToDrive) { memcpy (dataTransfer.buf,m_dmaMem1.pUserAddr,dataTransfer.GetSize()); } } Ну, и дальше - старый вариант разруливания ошибок, он содержит несколько недочётов if (dwTemp != 0) { // Сбросили порт bar1[(m_portOffset+0)/4] = 4; DWORD dwTimeOut = GetTickCount()+1000; while ((bar1[(m_portOffset)/4] &0x80000000)==0) { if (GetTickCount()>dwTimeOut) { break; } // Sleep (0); } return CHDDTransport::hddErrDriveError; } return CHDDTransport::hddErrNoError; Вот и всё. Вот так легко и непринуждённо мы научились достукиваться до SATA накопителя через контроллер SIL3132. Аналогично можно достучаться и через AHCI-совместимый контроллер, благо транспорт всё тот же. Проецируем память платы на своё адресное пространство - вот мы уже и получили доступ к любой ячейке. Сделали DMA буфер - у него есть адрес для нас (логический) и для аппаратуры (шинный). А больше ничего и не надо. И нет никакой UDMA-фобии. Потому что на самом деле, все команды работают одинаково. Если вы производите выезд к Заказчику, то наверняка вам будет небезынтересно узнать, что одни и те же микросхемы ставятся и в PCIe платы для настольных систем, и в адаптеры CardBus. Чаще всего там используются, конечно, микросхемы JMicron, совместимые с AHCI. Но кто ищет, тот всегда найдёт, поэтому нам удалось приобрести и адаптеры для ноутбуков, на базе микросхем Silicon Image. Поэтому при выезде на заказ, всегда можно взять ноутбук и произвести первичную диагностику, а если повезёт, и полное восстановление информации. |
|||||||||||||||||||||||||||||||||||||||||
|