Der vorgestellte Kode kümmert sich peinlich darum, nie über das Speicherende hinauszugreifen, und das ohne aufwändige Ausnahmebehandlung.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 Bytescase 0xC2 : p+=8 ; len-=8 ; break; // FX2-Kennung, Daten beginnen nach 8 Bytesdefault: 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: EndeLen=MAKEWORD(p[ 1 ],e&0x7F); // Len und Adr...Adr=MAKEWORD(p[ 3 ],p[2 ]); //...liegen big-endian vorp+= 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; }
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!
Alles in allem der gewöhnliche Brei.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; }
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.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; }
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.
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.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 ); // NumberOfSectionsREAD(p+ 0x14 ,&w2,2 ); // SizeOfOptionalHeaderp+= 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 ); // VirtualAddressREAD(p+ 0x14 ,&p,4 ); // PointerToRawDatal-=p0=p; // Differenz (wichtig!!) READ(p+ 0x0C ,&i,4 ); // NumberOfEntriesw1=LOWORD(i); w2=HIWORD(i); p+= 0x10 +8 *w1; // NumberOfNamedEntries übergehenfor (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 dasselbew1=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) Eintragp=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 }
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.
Ich habe mich stets um präzise Weiterleitung eines NT-Fehlers gekümmert.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; }
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.
Nicht wundern, dass der SchlĂĽssel einen ganz anderen Namen hat: \REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\<Treibername>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); }
Ja, die Funktionen AllocUnicodeString und FreeUnicodeString hat Microsoft mal wieder vergessen mitzugeben. Deshalb hier ihre Implementierung:
Nun geht es ans eingemachte: das Lesen des "ImagePath"-Schlüssels. Der könnte, sieht man in die Registrierung, verschieden dekorierte Dateinamen liefern:// 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); }
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.
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.#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 == NonPagedPoolRtlInitUnicodeString(&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; }
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.