Firmware herunterladen (EZUSB, FX2)

Ziel

Wer sollte das lesen?

Kernel-Mode-Programmierer mit EZUSB- und USB-Erfahrung und Freunde der deutschen Sprache. Anfänger in C-Programmierung sind hier vermutlich vollkommen falsch.
Übrigens, ich verwende sehr gern gotos, wenn der Quelltext dadurch besser lesbar wird.

Derzeitiger Stand (2011)

Das Laden von Ressourcen wäre im User-Mode kein Problem. Da seit etwa 2007 ein User Mode Driver Framework seitens Microsoft exisitiert, der auch für Windows XP nachgeschoben wurde, ist es heutzutage ratsam, USB-Treiber bspw. für CY7C68013A mit nachgeschaltetem FPGA (im Bastlerbereich am ehesten Xilinx Spartan-3) im User-Mode zu entwickeln und beim Anwender XP vorauszusetzen. Mit etwas Glück läuft ein unzertifizierter 32-Bit-User-Mode-Treiber sogar auf Win64-Plattformen, und man hat jenes Laster vom Hals. (Falschaussagen bitte kommentieren, s.u.!)

Vorteile

RCDATA-Ressourcen lassen sich leichter verwalten als eine C-Datei, die ein Array aus Bytes oder Hex-Records enthält. Problematisch (will sagen, bis dato ungelöst) war der Zugriff auf eine Ressource vom Kernel-Mode heraus.

Vorgehen

Die Erklärung geht hier „von innen nach außen“. Begonnen wird mit dem Download der IIC-Datei. Vorausgesetzt wird CallUSBD() aus den Cypress-Beispielen.

Download der IIC-Datei

Die IIC-Datei liege bereits im Speicher vor. Dann sieht der Download wie folgt aus:
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)))

Öffnen einer Datei

Zum Laden von Ressourcen gibt es die Funktion KsLoadResource(). Das war mir allerdings lange Zeit nicht bekannt. Die folgenden Ausführungen gehen von der Nichtexistenz einer solchen Funktion aus.

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.

Auffinden einer bestimmten Ressource

Das Aufsuchen der Ressource erfordert eine Menge Positionier- und Lese-Operationen (seek+read). Glücklicherweise erledigt dies bereits ein Aufruf von ZwReadFile() – nur ist sein Aufruf so umständlich, da lohnt sich eine einhüllende (Wrapper-)Funktion:
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:

  1. Typ (also DIALOG, STRING, VERSIONINFO, RCDATA usw.)
  2. Nummer oder Name (mehrere Dialoge usw.)
  3. Sprache (gleicher Dialog in mehreren Sprachen möglich – man glaubt es kaum!)
RCDATA-Ressourcen nach Sprachen aufzuteilen erscheint mir etwas hochgestochen – man könnte jedoch lokalisierte Firmware einfach auswählen lassen. Aber wie kommt man im Kernel-Mode an die Sprachen-ID des Administrators?
Hier wird einfach die erste (und hoffentlich einzige) Sprach-Version herausgegriffen.

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.

Das bisherige zusammen

Nun haben wir das Rüstzeug, von einer (beliebigen) ausführbaren EXE/DLL/SYS-Datei auf eine RCDATA-Ressource zuzugreifen und zum µC herunterzuladen:
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.

Die Treiberdatei als Quelle nutzen

Jetzt fehlt bloß noch der Dateiname des Treibers. Den zu beschaffen ist gar nicht so einfach!

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>
Im Kernel-Mode ist der Aufbau der Registrierung ein anderer als in der Win32-Subsystem-Welt! (Das gleiche Phänomen wiederholt sich weiter unten beim Dateisystem.)

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: Zum Geier, warum diese Vielfalt? Das liegt daran, das „Services“ (Dienste im Microsoft-Deutsch) alles mögliche sind: User-Mode-Prozesse („Dämonen“, daemons), User-Mode-DLLs, Kernel-Mode-Prozesse (?), alte Treiber, PnP-Treiber.

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.

Geschafft – jetzt bloß noch aufrufen

Der Aufruf erfolgt in StartDevice(). Da wäre ja noch die Sache mit der ID, dem Aufrufparameter. Dieser könnte ja schlauerweise der Revisions-ID des USB-Deskriptors entsprechen. Dann kann ein Loader und eine INF-Datei (mit nur einer Match-Zeile) bis zu 65535 (BCD:9999) verschiedene Geräte bedienen, indem es so viele verschiedene Firmware-Versionen im Bauch hat.
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;
}

Schließlich…

Eine Menge Holz ist zu hacken, will man „nur“ die Firmware schön aufgeräumt in Ressourcen stecken. Aber nun steht ein Programmschnipsel bereit, welches nur wenig Platz verbraucht und dem bequemen Einbinden von Firmware steht nichts mehr im Wege.

Hier ist das Demonstrationsbeispiel als User-Mode-Programm, fertig zur Einbindung und ggf. Anpassung in Ihr Treiber-Projekt.

Hinweis für AN2131 und CY7C68013A

Ich habe mich in meiner Implementierung entschieden, .IIC-Dateien in die Resource zu binden, keinen Speicherabzug, und keine wie-auch-immer-binär-abgelegten HEX-Dateien.

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.