NTSTATUS DownloadIicMem(IN PDEVICE_OBJECT Dev, IN PBYTE p, LONG len) { // Firmware von IIC-Speicherabzug laden - len zur Vermeidung von UAEs // Die IIC-Datei enthält erfreulicherweise am Ende den Kode zum // Start der Firmware auf dem µC PURB U; WORD Len,Adr; BYTE e; NTSTATUS ret=STATUS_INVALID_PARAMETER; if (!p) return ret; if (len<12) return ret; switch (p[0]) { case 0xB2: p+=7; len-=7; break; // EZUSB-Kennung, Daten beginnen nach 7 Bytes case 0xC2: p+=8; len-=8; break; // FX2-Kennung, Daten beginnen nach 8 Bytes default: return ret; } #define USIZE sizeof(struct _URB_CONTROL_VENDOR_OR_CLASS_REQUEST) U=ExAllocatePoolWithTag(NonPagedPool,USIZE,'ls#h'); if (!U) return STATUS_NO_MEMORY; do{ ret=STATUS_DATA_ERROR; // hier: ungültiger Aufbau der IIC-Daten len-=4; if (len<0) break; e=p[0]; // Bit7: Ende Len=MAKEWORD(p[1],e&0x7F); // Len und Adr... Adr=MAKEWORD(p[3],p[2]); //...liegen big-endian vor p+=4; len-=Len; if (len<0) break; RtlZeroMemory(U,USIZE); // Muss immer wieder gelöscht werden! U->UrbHeader.Length =USIZE; U->UrbHeader.Function =URB_FUNCTION_VENDOR_DEVICE; U->UrbControlVendorClassRequest.Request=0xA0; U->UrbControlVendorClassRequest.TransferBufferLength=Len; U->UrbControlVendorClassRequest.TransferBuffer=p; U->UrbControlVendorClassRequest.Value=Adr; // Zieladresse im µC ret=CallUSBD(Dev,U); if (!NT_SUCCESS(ret)) break; p+=Len; }while (!(e&0x80)); // Solange bis Endekennung ExFreePool(U); #undef USIZE return ret; }Der vorgestellte Kode kümmert sich peinlich darum, nie über das Speicherende hinauszugreifen, und das ohne aufwändige Ausnahmebehandlung.
Einige Dinge wurden in den Kernel-Headern schlichtweg „vergessen“:
typedef USHORT WORD,*PWORD; typedef UCHAR BYTE,*PBYTE; #define LOWORD(x) ((WORD)(x)) #define HIWORD(x) ((WORD)((x)>>16)) #define MAKEWORD(lo,hi) ((WORD)((hi)<<8|(lo)))
Weil es im Kernel-Mode keine Funktionen zum Zugriff auf (irgend)eine Ressource gibt, muss man zu Fuß die Ressource laden. Einfach wäre das mit eingeblendeten Dateibereichen (memory mapped executable file), aber – falls es überhaupt geht – ich weiß nicht wie. Außerdem will man vielleicht auch mal eine externe Datei öffnen.
So wie hier dargestellt beschränkt sich die Funktionalität auf Windows 2000 und Windows XP!
NTSTATUS OpenFileForRead(OUT PHANDLE hFile, IN PUNICODE_STRING FName) { NTSTATUS ret=STATUS_SUCCESS; IO_STATUS_BLOCK ios; OBJECT_ATTRIBUTES oa; InitializeObjectAttributes(&oa,FName,OBJ_CASE_INSENSITIVE,NULL,NULL); ret=ZwCreateFile(hFile,GENERIC_READ,&oa,&ios,NULL,0,FILE_SHARE_READ, FILE_OPEN,FILE_SYNCHRONOUS_IO_NONALERT,NULL,0); // Zweckmäßig ist an dieser Stelle eine Fehlerausgabe, weil hier // erfahrungsgemäß öfter etwas schief geht. return ret; }Alles in allem der gewöhnliche Brei.
static NTSTATUS ReadAt(IN HANDLE hFile, ULONG pos, OUT PVOID ptr, ULONG len) { // Lesen aus einer (binären, <4GB) Datei ab einer bestimmten Position NTSTATUS ret; IO_STATUS_BLOCK ios; LARGE_INTEGER start; start.LowPart=pos; start.HighPart=0; ret=ZwReadFile(hFile,NULL,NULL,NULL,&ios,ptr,len,&start,NULL); if (!NT_SUCCESS(ret)) return ret; if (ioStatus.Information!=len) return STATUS_END_OF_FILE; return ret; }Jetzt kommt das eigentliche Bonbon: Das Aufsuchen einer RCDATA-Ressource mit einer bestimmten ID erwies sich als viel schwieriger als nur die Dutzend ExeDump-Beispiele zu kopieren! Denn diese lösen allesamt nicht die Diskrepanz zwischen Dateioffset und RVA (Relative Virtuelle [lineare] Adresse). Brauchen sie auch nicht. Ich aber – unbedingt.
Die Suche nach dem String ".rsrc" löst dieses Problem – nicht ohne Nebenwirkung: Die Echse (oder Treiber) muss mit VisualC erstellt werden! Dass der gleiche Sektions-Name in Borland-Produkten verwendet wird, wäre Zufall. Über eine DEF-Datei ließen sich diese Bezeichner auch einstellen, aber wer kennt die heute noch? (Aber: es gibt sie noch!) Linker-Optionen tun ein übriges.
NTSTATUS LoadRCData(IN HANDLE h, WORD id, OUT PBYTE* ptr, OUT PULONG len) { // Lädt RCDATA-Ressource aus Datei im Kernel-Mode - NUR numerische IDs WORD w,w1,w2; ULONG l,p,p0,i; NTSTATUS ret; #define READ(pos,p,l) {ret=ReadAt(h,pos,p,l); if (!NT_SUCCESS(ret)) return ret;} READ(0,&w,2); if (w!='ZM') return STATUS_DATA_ERROR; READ(0x3C,&p,4); READ(p,&l,4); if (l!='EP') return STATUS_DATA_ERROR; READ(p+6,&w1,2); // NumberOfSections READ(p+0x14,&w2,2); // SizeOfOptionalHeader p+=0x18+w2; for (i=w1; i; i--) { // Suche Verweis auf Ressourcen (per Name) char buf[6]; READ(p,buf,6); if (*(PULONG)buf=='rsr.' && ((PUSHORT)buf)[2]=='c') goto f0; // * s.u. p+=0x28; // sizeof(IMAGE_SECTION_HEADER) } return STATUS_RESOURCE_DATA_NOT_FOUND; f0: READ(p+0x0C,&l,4); // VirtualAddress READ(p+0x14,&p,4); // PointerToRawData l-=p0=p; // Differenz (wichtig!!) READ(p+0x0C,&i,4); // NumberOfEntries w1=LOWORD(i); w2=HIWORD(i); p+=0x10+8*w1; // NumberOfNamedEntries übergehen for (i=w2; i; i--) { // Suche RCDATA-Ressourcen READ(p,&w,2); if (w==(WORD)/*RT_RCDATA*/10) goto f1; p+=8; // sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY) } return STATUS_RESOURCE_TYPE_NOT_FOUND; f1: READ(p+4,&i,4); p=p0+(i&0x7FFFFFFF); READ(p+0x0C,&i,4); // nochmal dasselbe w1=LOWORD(i); w2=HIWORD(i); p+=0x10+8*w1; for (i=w2; i; i--) { // Suche Ressource mit passender ID READ(p,&w,2); if (w==id) goto f2; p+=8; } return STATUS_RESOURCE_NAME_NOT_FOUND; f2: READ(p+4,&i,4); p=p0+(i&0x7FFFFFFF); READ(p+0x14,&i,4); // erster (einziger) Eintrag p=p0+i; READ(p,&p0,4); p0-=l; READ(p+4,&l,4); if (ptr) { *ptr=ExAllocatePoolWithTag(PagedPool,l,'ls#h'); if (!*ptr) return STATUS_NO_MEMORY; ret=ReadAt(h,p0,*ptr,l); if (!NT_SUCCESS(ret)) ExFreePool(*ptr); } if (len) *len=l; return ret; // müsste hier STATUS_SUCCESS sein #undef READ }Zum Auffinden des Ressourcen-„Segments“ in der Datei hält der EXE-Kopf gleich zwei Strukturen bereit: Eine liefert RVAs (dabei ist die Ressource die mit dem Index 2), die andere (namensbasierte) liefert RVAs und Datei-Offsets.
Ressourcen sind in Win32 stets in drei „Verzeichnisse“ gegliedert:
Das lokale Makro READ erspart viel Tippaufwand – und erzeugt dennoch kugelsicheren Kode.
Diese recht simpel zu verwendete Routine liefert im Erfolgsfall einen Speicherzeiger und die Länge des Speicherblocks. Der Aufrufer muss den gelieferten Speicherzeiger freigeben, mit ExFreePool().
Die Beschränkung auf RCDATA und numerische IDs sollte in der Praxis unwesentlich sein. Ein Treiber wird sich wohl nie für Dialog-Ressourcen interessieren.
String-Ressourcen erscheinen wohl noch interessant, aber dafür (dagegen?) hat Microsoft schon längst MessageTables ersonnen.
NTSTATUS DownloadFileFW(IN PDEVICE_OBJECT Dev, IN PUNICODE_STRING FName, WORD id) { // Lädt Firmware aus RCDATA-Ressource einer (beliebigen) EXE/DLL/SYS-Datei NTSTATUS ret; HANDLE h; // Datei-Griff PBYTE ptr; ULONG len; ret=OpenFileForRead(&h,FName); // (Ausführbare) Datei öffnen if (NT_SUCCESS(ret)) { ret=LoadRCData(h,id,&ptr,&len); // IIC-Ressource laden if (NT_SUCCESS(ret)) { ret=DownloadIicMem(Dev,ptr,len); // Speicher-Abbild downloaden ExFreePool(ptr); } ZwClose(h); } return ret; }Ich habe mich stets um präzise Weiterleitung eines NT-Fehlers gekümmert.
Man muss ihn aus der Registry lesen. Er steht unter HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\<Treibername>:ImagePath. Den Schlüssel-Namen (bis zum Doppelpunkt) bekommt man bei DriverEntry() zugestellt; den muss man sicherstellen. Einziger Speicherort (für den Anker) ist eine globale Variable. Den String-Speicher müssen wir bei DriverUnload() wieder freigeben.
UNICODE_STRING gRegPath; NTSTATUS DriverEntry(IN PDRIVER_OBJECT drv, IN PUNICODE_STRING RegPath) { NTSTATUS ret; ret=AllocUnicodeString(&gRegPath,RegPath.Length+2); if (!NT_SUCCESS(ret)) return ret; // Fatal! RtlCopyUnicodeString(&gRegPath,RegPath); //... usw. ... } VOID DriverUnload(IN PDRIVER_OBJECT drv) { FreeUnicodeString(&gRegPath); }Nicht wundern, dass der Schlüssel einen ganz anderen Namen hat: \REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\<Treibername>
Ja, die Funktionen AllocUnicodeString und FreeUnicodeString hat Microsoft mal wieder vergessen mitzugeben. Deshalb hier ihre Implementierung:
// Vermisste RtlXxx-Routinen - buflen in BYTES, nicht in ZEICHEN (wie in Win32) NTSTATUS AllocUnicodeString(OUT PUNICODE_STRING u, ULONG buflen) { RtlInitUnicodeString(u,NULL); u->Buffer=ExAllocatePoolWithTag(NonPagedPool,buflen,'1s#h'); if (!u->Buffer) return STATUS_NO_MEMORY; u->MaximumLength=(WORD)buflen; return STATUS_SUCCESS; } // Leider weiß ich nicht, ob RtlFreeUnicodeString() das gleiche tut. VOID FreeUnicodeString(IN PUNICODE_STRING u) { if (u->Buffer) ExFreePool(u->Buffer); }Nun geht es ans eingemachte: das Lesen des "ImagePath"-Schlüssels. Der könnte, sieht man in die Registrierung, verschieden dekorierte Dateinamen liefern:
Die ersten beiden Punkte zeigen gewöhnliche Pfade. Ein Zeichen für User-Mode-Sachen (DOS und Win32, um genau zu sein.)
Aber haben Sie auf Ihrer Festplatte schon mal das Verzeichnis "\??" gefunden? Ich auch nicht. "\SystemRoot" auch nicht. Dies ist das Kernel-Mode-Verzeichnis-System! Hierin ist "\SystemRoot" sovielwie C:\WINNT (wo auch immer es auf Ihrer Festplatte liegt) und "\??\" ein Präfix, um auf DOS- und Win32-Pfade zugreifen zu können.
Wichtig zu wissen ist, dass "\SystemRoot" früh beim Bootvorgang festliegt, "\??" erst wenn das Win32-Subsystem initialisiert ist.
USB-Treiber habe ich nur in der letzten Form vorgefunden. Daraus folgt, dass USB-Treiber nur unterhalb von C:\WINNT liegen dürfen. Schließlich könnten die Treiber sehr früh geladen werden, wenn das Gerät bereits dran steckt und bspw. vor dem Einloggen notwendig sind (da denke ich an Tastaturen und Mäuse – und Fingerkuppenscanner).
Man muss zu Fuß "\SystemRoot\" davorsetzen. Die soeben implementierten Unicode-String-Funktionen vereinfachen diesen Vorgang erheblich.
#define SYSTEMROOT L"\\SystemRoot\\" NTSTATUS DownloadInternalFW(IN PDEVICE_OBJECT Dev, WORD id) { // Lädt Firmware aus RCDATA-Ressource der eigenen .SYS-Datei NTSTATUS ret; UNICODE_STRING IName,FName; RTL_QUERY_REGISTRY_TABLE Query[2]; // Stack == NonPagedPool RtlInitUnicodeString(&IName,NULL); RtlZeroMemory(Query,sizeof(Query)); Query->Flags=RTL_QUERY_REGISTRY_DIRECT; Query->Name=L"ImagePath"; Query->EntryContext=&IName; Query->DefaultType=REG_SZ; Query->DefaultData=L""; ret=RtlQueryRegistryValues(RTL_REGISTRY_ABSOLUTE,gRegPath.Buffer,Query,NULL,NULL); if (!NT_SUCCESS(ret)) return ret; ret=AllocUnicodeString(&FName,IName.Length+sizeof(SYSTEMROOT)); if (!NT_SUCCESS(ret)) return ret; RtlAppendUnicodeToString(&FName,SYSTEMROOT); RtlAppendUnicodeStringToString(&FName,&IName); FreeUnicodeString(&IName); ret=DownloadFileFW(Dev,&FName,id); FreeUnicodeString(&FName); return ret; }Nun, mit RtlQueryRegistryValues() halst man sich eine Menge Arbeit auf. ZwQueryRegistryValue() ist einfacher zu verwenden, habe ich aber im Moment nicht im Hilfe-Dschungel gefunden.
NTSTATUS StartDevice(IN PDEVICE_OBJECT Dev) { NTSTATUS ret; PURB U; USB_DEVICE_DESCRIPTOR DDesc; /* === USB-IDs beschaffen === */ #define USIZE sizeof(struct _URB_CONTROL_DESCRIPTOR_REQUEST) U=ExAllocatePoolWithTag(NonPagedPool,USIZE,'us#h'); if (U) { UsbBuildGetDescriptorRequest(U,USIZE, USB_DEVICE_DESCRIPTOR_TYPE,0,0,&DDesc,NULL,sizeof(DDesc),NULL); ntStatus=CallUSBD(Dev,U); // Geräte-Beschreiber holen ExFreePool(U); } #undef USIZE // ... usw. ... ret=DownloadInternalFW(poDevice,DDesc.bcdDevice); // ... usw. ... return ret; }
Hier ist das Demonstrationsbeispiel als User-Mode-Programm, fertig zur Einbindung und ggf. Anpassung in Ihr Treiber-Projekt.
Der Vorteil von .IIC ist, dass der Ladebereich nicht festgelegt ist und in mehrere Blöcke verteilt sein darf. Und man kann ihn so wie er ist in den EEPROM brennen. (Natürlich nur, wenn dieser groß genug ist!) Der Nachteil hingegen ist, dass der .IIC-Lader das Verhalten des eingebauten Urladers nachahmen muss, also den Header abschneiden und die Adresse/Längen-Infos interpretieren muss. Leider ist die Headergröße zwischen AN2131 und CY7C68013A um 1 Byte verschieden, man muss eine Fallunterscheidung machen.
Die Ladegeschwindigkeit von IIC-Daten ist enorm, erheblich höher als bei .HEX-Daten, weil die Blockgröße nicht auf 16 Bytes beschränkt ist, sondern auf 1023 Bytes. Grundsätzlich ist daher das IIC-Format vorzuziehen, auch wenn man's nicht in eine Ressource steckt. Alternativ sollte man beim Laden einer .HEX-Datei „zusammenhängende“ Zeilen voreinlesen und dann en bloc laden. Da die Reihenfolge der Records beliebig ist, müsste man genau genommen die gesamte .HEX-Datei einlesen und nachher die Blöcke und Lücken ausfindig machen (so im peps2win32-Projekt gemacht). Vereinfachend kann man wohl von aufeinanderfolgenden, aufsteigenden Adressen ausgehen.
Die ungeprüft-maximale Transfergröße für Endpoint-0-Daten ist 4 KByte. Bei größeren Transferlängen muss man den USBD befragen und ggf. zerhacken. Der Aufwand lohnt sich aber für Firmware nicht.