WinUSB 是 Windows 自带的 USB 设备的通用驱动程序。是Windows下USB开发的一个不错的选择,免驱,开发难度低。
枚举成WINUSB设备,除了标准请求枚举过程,还需要一个特殊的字符串描述符:MS OS字符串描述符,两个厂商相关请求:一个是针对设备的扩展兼容ID OS特征描述符报告兼容ID为”WINUSB”,一个针对接口的扩展属性OS特征描述符,报告接口GUID.
参考https://learn.microsoft.com/zh-cn/windows-hardware/drivers/usbcon/automatic-installation-of-winusb#what-is-a-winusb-device
这一篇就来介绍下这三个请求的详细细节,基于此就可以快速实现一个WINUSB设备的枚举。
工欲善其事必先利其器,所以第一步先获取官方的文档。
下载Microsoft OS 1.0 Descriptors Specification
https://learn.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-os-1-0-descriptors-specification拉到最后,右键点击”I accept, download the file”,将链接另存为。
OS_Desc_Ext_Prop.zip
下载Microsoft OS 2.0 描述符规范
https://learn.microsoft.com/zh-cn/windows-hardware/drivers/usbcon/microsoft-os-2-0-descriptors-specification拉到最后,右键点击”我接受,下载文件”,将链接另存为。
MS_OS_2_0_desc.docx
该索引为0xEE的特殊的字符串描述符用于返回厂商编码,以便主机继续后续的OS相关请求。
必须返回MSFT100x才代表WINUSB才会继续请求后续OS相关请求,否则是其他厂商自定义设备,其中x是厂商编码可以变化。
请求该字符串描述的前提条件是设备描述符中的bcdUSB大于等于2000,且注册表项”计算机\HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\usbflags\vvvvpppprrrr”下osvc不存在.
如果osvc存在不管是0000还是01xx都不会再请求该字符串描述符。
主机发
80 06 EE 03 00 00 12 00
设备回
12 03 4d 00 53 00 46 00 54 00 31 00 30 00 30 00 41 00
即{0x12,0x03,'M',0,'S',0,'F',0,'T',0,'1',0,'0',0,'0',0,'A',0}
其中0x41(‘A’)即bMS_VendorCode.
请求收到正确响应后主机创建如下注册表项
“计算机\HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\usbflags\199301010100”
其中199301010100即对应3个字段vvvvpppprrrr
vvvv是16进制VID,来自设备描述符的idVendor.
pppp是16进制数PID,来自设备描述符的idProduct.
rrrr是16进制设备版本号.来自设备描述符的bcdDevice.
osvc为0000表示设备没有响应有效的索引EE的MS OS字符串描述,为01xx,这里值为01 41,表示设备正确响应了索引EE的MS OS字符串描述.其中41即来自于索引EE的MS OS字符串描述符的bMS_VendorCode.
创建osvc表项后,下次就不会再请求索引EE字符串描述符了,可以删除该注册表项,获取修改VID,PID让系统重新请求EE索引字符串描述符.
参考
https://learn.microsoft.com/en-us/windows-hardware/drivers/usbcon/usb-device-specific-registry-settings
https://learn.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-defined-usb-descriptors
以上获取到MS OS字符串描述符之后,即osvc字段为01xx之后,会继续请求本描述符。如果osvc为0000或不存在则不会请求。
参考OS_Desc_Ext_Prop.zip/OS_Desc_CompatID.doc
先获取固定16字节长度的头,然后根据返回头中的长度信息,获取所有内容。
所以先:
主机发送的请求格式如下:
bmRequestType | 0xC0 | D[7]:1 D->H D[6:5]:Vendor=2 |
bRequest | bMS_VendorCode | 前面获取的厂商编码 |
wValue | xxyy | 高字节接口号:InterfaceNumber 一般为0x00因为一个设备只能包含一个extended compat ID descriptor。 低字节:0x00 |
wIndex | 0x04 | 代表extended compat ID descriptor |
wLength | 0x10 | 本描述符头16字节长 |
设备返回内容如下: 其中Header Section正是16字节,dwLength为总长。
其中Header Section固定16字节
偏移 | 字段 | 大小 (字节) | 类型 | 描述 |
0 | dwLength | 4 | DWORD | 本描述符的总长 |
4 | bcdVersion | 2 | BCD | 描述符BCD格式的版本号 |
6 | wIndex | 2 | WORD | OS feature descriptor的索引,必须和请求发过来的值wIndex一样,上面是0x04. |
8 | bCount | 1 | BYTE | 后面function sections的个数 |
9 | RESERVED | 7 | BYTEs | 保留 |
Function Section格式如下:
偏移 | 字段 | 大小 (字节) | 类型 | 描述 |
0 | bFirstInterfaceNumber | 1 | BYTE | 本功能的第一个接口,对于IAD则是IAD下的第一个接口,其他接口连续递增. 从0开始递增,第一个Function Section的bFirstInterfaceNumber设置为0. 注意和描述符中对应。 |
1 | RESERVED | 1 | BYTEs | 保留 固定为0x01 |
2 | compatibleID | 8 | BYTEs | compatible ID未使用填充NUll. 参考OS_Desc_CompatID.doc 的Appendix 1. Compatible and Subcompatible ID Values |
10 | subCompatibleID | 8 | BYTEs | subcompatible ID未使用填充NUll. |
18 | RESERVED | 6 | BYTEs | 保留填充NUll. |
Note: 这里偏移指的从本section开始. |
再:
主机发送请求,请求全部内容.
bmRequestType | 0x00C0 | |
bRequest | bMS_VendorCode | 前面获取的厂商编码 |
wValue | xxyy | 高字节接口号:InterfaceNumber 一般为0x00因为一个设备只能包含一个extended compat ID descriptor。 低字节:0x00 |
wIndex | 0x04 | 代表extended compat ID descriptor |
wLength | dwLength | 请求完整内容 |
对于WINUSB一个描述符实例如下,即返回兼容ID为”WINUSB”
/* Extended Compat ID Descriptor Format */
static const uint8_t s_compat_id_desc[] =
{
/* Header Sector 16字节头 */
0x28,0,0,0, /* dwLength: 40 */
0,1, /* bcdVersion: 1.0 */
4,0, /* wIndex: 0x04 */
1, /* bCount: 1 */
0,0,0,0,0,0,0, /* RESERVED: 7 bytes */
/* Function Section 24bytes */
0, /* bFirstInterfaceNumber: 0 */
1, /* RESERVED: 1bytes */
(uint8_t)'W',(uint8_t)'I',(uint8_t)'N',(uint8_t)'U',(uint8_t)'S',(uint8_t)'B',0,0, /* compatibleID */
0, 0, 0, 0, 0, 0, 0, 0, /* subCompatibleID */
0, 0, 0, 0, 0, 0 /* RESERVED 6 bytes */
};
请求过程如下
此时可以看到枚举的WINUSB设备。
系统会在注册表”计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\USB\”的位置存储该设备的兼容ID.如`VID_xxxx&PID_xxxx`目录下:
以上获取到设备的Extended Compat ID OS后,可以确认为WINUSB设备,此时要继续确认其功能,前面介绍过USB功能是和接口对应的。所以会继续请求接口的GUID以确认其功能。注意该请求是接口请求,不再是设备请求。
参考OS_Desc_Ext_Prop.zip/OS_Desc_Ext_Prop/OS_Desc_Ext_Prop.doc
先获取固定10字节长度的头,然后根据返回头中的长度信息,获取所有内容。
先:请求内容如下
bmRequestType | 0xC1 | D[7]:1 D->H D[6:5]:Vendor=2 D4...0: 1 = Interface |
bRequest | bMS_VendorCode | 前面获取的厂商编码 |
wValue | xxyy | 高字节接口号:InterfaceNumber 一般为0x00因为一个设备只能包含一个extended compat ID descriptor。 低字节:0x00 |
wIndex | 0x05 | 代表extended properties OS descriptor |
wLength | 0x0A | 本描述符头10字节长 |
设备返回内容如下: 其中Header Section正是10字节,dwLength为总长。
Headet Section
偏移 | 字段 | 大小 (字节) | 类型 | 说明 |
0 | dwLength | 4 | DWORD | 本描述符长度 |
4 | bcdVersion | 2 | BCD | 描述符BCD格式的版本号 |
6 | wIndex | 2 | WORD | extended properties OS descriptors索引 和请求的wIndex一样都是0x0005 |
8 | wCount | 2 | WORD | custom property sections个数 |
Custom Property Section
偏移 | 字段 | 大小 (字节) | 类型 | 描述 |
0 | dwSize | 4 | DWORD | 本section长度 |
4 | dwPropertyDataType | 4 | DWORD | 属性数据格式 |
8 | wPropertyNameLength | 2 | DWORD | 属性名长度 |
10 | bPropertyName | PNL2 | WCHAR[ ] | 属性名 |
10 + PNL | dwPropertyDataLength | 4 | DWORD | 存储属性数据的缓存长度 |
14 + PNL | bPropertyData | PDL3 | Format-dependent | 属性数据 |
Notes: 偏移从 custom properties section开始算起. PNL:属性名长度 wPropertyNameLength. PDL:属性数据长度dwPropertyDataLength. |
其中dwPropertyDataType属性数据格式支持以下类型
Value | Description |
0 | 保留 |
1 | 以NULL结尾的Unicode 字符串 (REG_SZ) |
2 | 以NULL结尾包含环境变量的Unicode 字符串(REG_EXPAND_SZ) |
3 | Free-form 二进制 (REG_BINARY) |
4 | 小端32位整数 (REG_DWORD_LITTLE_ENDIAN) |
5 | 大端32位整数 (REG_DWORD_BIG_ENDIAN) |
6 | 以NULL结尾包含符号链接的Unicode 字符串(REG_LINK) |
7 | 多个以NULL结尾的Unicode字符串 (REG_MULTI_SZ) |
8 和以上 | 保留 |
再根据实际长度请求所有内容
bmRequestType | 0xC1 | D[7]:1 D->H D[6:5]:Vendor=2 D4...0: 1 = Interface |
bRequest | bMS_VendorCode | 前面获取的厂商编码 |
wValue | xxyy | 高字节接口号:InterfaceNumber 一般为0x00因为一个设备只能包含一个extended compat ID descriptor。 低字节:0x00 |
wIndex | 0x05 | 代表extended properties OS descriptor |
wLength | dwLength | 本描述符长度 |
一个实例如下:
static const uint8_t s_extended_properties_os_feature_desc[] =
{
/* 头 10字节 */
0x8E, 0, 0, 0, /* 长度 length 142 = 132 + 10 byte */
0x00, 0x01, /* BCD version 1.0 */
0x05, 0x00, /* Extended Property Descriptor Index(5) */
0x01, 0x00, /* number of section (1) */
/* 0x84(132) = 50(property section) + 82(property data ) */
/* property section */
0x84, 0x00, 0x00, 0x00, /* size of property section */
0x1, 0, 0, 0, /* property data type (1) (REG_SZ) 多个接口0x01写0x07 (REG_MULTI_SZ) */
0x28, 0, /* property name length (40) */
'D', 0,
'e', 0,
'v', 0,
'i', 0,
'c', 0,
'e', 0,
'I', 0,
'n', 0,
't', 0,
'e', 0,
'r', 0,
'f', 0,
'a', 0,
'c', 0,
'e', 0,
'G', 0,
'U', 0,
'I', 0,
'D', 0,
0, 0,
/* 82 = 0x4E(78) + 4 */
0x4E, 0, 0, 0, /* property data length */
'{', 0,
'1', 0,
'3', 0,
'E', 0,
'B', 0,
'3', 0,
'6', 0,
'0', 0,
'B', 0,
'-', 0,
'B', 0,
'C', 0,
'1', 0,
'E', 0,
'-', 0,
'4', 0,
'6', 0,
'C', 0,
'B', 0,
'-', 0,
'A', 0,
'C', 0,
'8', 0,
'B', 0,
'-', 0,
'E', 0,
'F', 0,
'3', 0,
'D', 0,
'A', 0,
'4', 0,
'7', 0,
'B', 0,
'4', 0,
'0', 0,
'6', 0,
'2', 0,
'}', 0,
0, 0,
};
/* 以下是多个section示例 */
{
/* 头 10字节 */
0xE0, 0, 0, 0, /* length 214+10=224(0xE0) byte */
0x00, 0x01, /* BCD version 1.0 */
0x05, 0x00, /* Extended Property Descriptor Index(5) */
0x01, 0x00, /* number of section (1) */
/* 0xD6(214) = 50+2(property section) + 162(property data ) */
/*property section */
0xD6, 0x00, 0x00, 0x00, /* 214 size of property section */
0x7, 0, 0, 0, /* property data type (1) 多个接口0x01写0x07 (REG_MULTI_SZ) */
0x2a, 0, /* property name length (42) 注意:比单个接口多2字节 's', 0, */
'D', 0,
'e', 0,
'v', 0,
'i', 0,
'c', 0,
'e', 0,
'I', 0,
'n', 0,
't', 0,
'e', 0,
'r', 0,
'f', 0,
'a', 0,
'c', 0,
'e', 0,
'G', 0,
'U', 0,
'I', 0,
'D', 0,
's', 0, /* 比单个接口这里多了个s */
0, 0,
/* 0x9E(158) = 0x4E(78)x2+2
* 162= 0x9E(158)+4
*/
0x9E, 0, 0, 0, /* property data length */
'{', 0,
'1', 0,
'3', 0,
'E', 0,
'B', 0,
'3', 0,
'6', 0,
'0', 0,
'B', 0,
'-', 0,
'B', 0,
'C', 0,
'1', 0,
'E', 0,
'-', 0,
'4', 0,
'6', 0,
'C', 0,
'B', 0,
'-', 0,
'A', 0,
'C', 0,
'8', 0,
'B', 0,
'-', 0,
'E', 0,
'F', 0,
'3', 0,
'D', 0,
'A', 0,
'4', 0,
'7', 0,
'B', 0,
'4', 0,
'0', 0,
'6', 0,
'2', 0,
'}', 0,
0, 0, /* 一个结束符间隔 */
'{', 0,
'1', 0,
'3', 0,
'E', 0,
'B', 0,
'3', 0,
'6', 0,
'0', 0,
'B', 0,
'-', 0,
'B', 0,
'C', 0,
'1', 0,
'E', 0,
'-', 0,
'4', 0,
'6', 0,
'C', 0,
'B', 0,
'-', 0,
'A', 0,
'C', 0,
'8', 0,
'B', 0,
'-', 0,
'E', 0,
'F', 0,
'3', 0,
'D', 0,
'A', 0,
'4', 0,
'7', 0,
'B', 0,
'4', 0,
'0', 0,
'6', 0,
'3', 0,
'}', 0,
0, 0,
0, 0, /* 最后以两个结束符结束 */
};
请求完后
会在注册表的”计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\USB\VID_1995&PID_0101\5&244f8fa8&0&2\Device Parameters”下新增DeviceInterfaceGUID,
请求之前
请求之后如下:
增加项目DeviceInterfaceGUIDs值为我们返回的值.
{13EB360B-BC1E-46CB-AC8B-EF3DA47B4062}
{13EB360B-BC1E-46CB-AC8B-EF3DA47B4063}
如果主机不请求该描述符可以在设备管理器中卸载设备和驱动
菜单点击查看->刷新,上述注册表会删除,下次就会重新请求。
过程如下
完成以上请求就完成了WINUSB的枚举。
我们下一节继续分享WINUSB设备枚举一个一个完整的描述符和枚举过程实例。
枚举为WINUSB设备比较简单,只需要响应3个特殊的请求即可,三个请求尤其逻辑关系,先是索引EE返回字符串高速主机继续请求后续OS相关描述符,然后请求设备相关的兼容ID “WINUSB”代表其为WINUSB设备,其实至此就知道是WINUSB设备了,继续请求接口的GUID以确认其功能。