调USB设备的时候,最崩溃的莫过于代码逻辑看起来没问题,设备就是不按预期工作。前两天我处理一个USB转串口模块丢数据的问题,驱动代码翻了两遍没找到毛病,逻辑分析仪又不在手边,最后装上usbmon把总线上的URB一个个扒出来,才定位到是上层驱动批量传输缓冲区管理的问题。这种场景在嵌入式开发和驱动调试里太常见了。
Linux下做USB抓包,usbmon是公认的入门首选。它不需要任何额外的硬件,内核自带,装上就能用,能让你看到USB总线上跑过的每一个请求和对应的数据。这篇文章我会从usbmon的工作原理讲起,再带你完成一次完整的抓包操作,然后把文本模式下包数据的格式逐字段拆开讲透,最后整理几个我实际踩过的坑和排查思路。无论你是搞嵌入式、写驱动,还是单纯想弄明白USB设备到底在跟电脑传什么,这篇文章都能给你一个清晰可用的参考。
1. UsbMon到底帮你看到什么:底层机制与适用场景
1.1 USB通信的最小单位:URB
在深入usbmon之前,得先把USB通信的基本单元说清楚。USB设备之间的数据传输,在操作系统层面不是以“字节”为单位打交道的,而是以URB(USB Request Block,USB请求块)为单位。你可以把URB理解成内核里描述“一次USB传输请求”的数据结构,它包含了目标设备、端点号、传输方向、数据缓冲区、回调函数等关键信息。
驱动向USB core提交一个URB之后,USB主控制器会按照URB里的描述,把数据拆成总线上的事务、包,最终完成物理传输。所以从驱动开发者的视角看,一次USB通信的过程就是:提交URB、控制器执行、完成回调。usbmon抓的就是这个“提交”和“完成”的过程,它记录下每个URB的生命周期和携带的数据。
这里要特别区分一个概念:usbmon抓到的URB,并不等于USB总线上的物理包。一个URB在总线上可能对应多个包,尤其是控制传输,它内部还分setup、data、status三个阶段。usbmon是把这三个阶段的逻辑结果汇总成一个URB事件给你看,而不是像USB协议分析仪那样把每一个令牌包、数据包、握手包逐比特呈现。
1.2 UsbMon在内核里的工作方式
usbmon是Linux内核自带的一个模块,代码位于内核源码树的drivers/usb/mon/目录下。它采用了一种“旁路监听”的设计:USB core在URB提交(submit)和完成(complete)的关键路径上,会调用usbmon注册的钩子函数,把URB的关键信息和缓冲区数据复制一份到监听的缓冲区中。
这种设计有个重要优势:usbmon本身不参与USB数据传输,它既不会修改URB的数据内容,也不会阻塞USB的通信流程,所以正常抓包行为不会影响设备的运行。这一点和网上一些通过修改驱动或者拦截read/write的方式来抓包的做法完全不同,那些方式可能会改变时序、引入额外延迟,甚至导致驱动崩溃。usbmon在内核里跑了十几年,稳定性是通过大量生产环境验证过的。
usbmon对外暴露的接口是debugfs,默认挂载在/sys/kernel/debug/usb/usbmon/下面。每个USB总线对应一个编号文件,0代表所有总线的聚合视图,1、2、3分别代表具体某条总线。文件后缀u表示文本格式,t表示二进制格式。文本格式适合人眼读,二进制格式适合喂给Wireshark做图形化分析。
1.3 什么时候该用UsbMon,什么时候不该用
根据我这几年的使用经验,usbmon最适合的场景有三类:一是设备枚举失败,比如U盘插上没反应、设备描述符读不出来;二是数据传输内容不对,比如发出去的指令和收到的响应对不上;三是驱动程序行为异常,比如URB生命周期异常、状态码报错。
但usbmon不是万能的。它看不到物理层的信号质量,比如信号完整性、眼图、电气特性这些问题,usbmon完全无能为力。另外在USB 3.0的xHCI主控制器下,usbmon有时会漏掉一些链路层的事件,或者在某些异常恢复流程中表现不完整。如果你要调试的是USB高速信号质量问题,或者分析物理层协议流程,还是老老实实借一台USB协议分析仪更靠谱。
2. 环境准备与5分钟快速上手
2.1 内核模块加载与验证
绝大多数Linux发行版的内核都编译了usbmon模块,但出于性能考虑,默认可能不会自动加载。第一步就是把它拉起来:
modprobe usbmon执行完没有报错就说明模块加载成功了。可以用lsmod确认一下:
lsmod | grep usbmon正常会看到类似usbmon 24576 0这样的输出。如果你的系统提示modprobe找不到模块,说明当前内核没启用CONFIG_USB_MON,需要重新编译内核。内核配置路径在Device Drivers -> USB support -> USB Monitor,设为内置(y)或模块(m)都可以。这时候你需要在对应的内核源码目录下执行make menuconfig,找到这个选项打开,重新编译安装内核。
模块加载成功后,别急着抓包,先确认debugfs挂载好了。大多数发行版在开机时已经把debugfs挂载到/sys/kernel/debug,少数精简系统需要手动挂载:
mount -t debugfs none /sys/kernel/debug然后检查usbmon接口文件:
ls /sys/kernel/debug/usb/usbmon/正常情况下能看到0u、0t以及按照总线编号排列的1u、1t、2u、2t等文件。
提示:如果/sys/kernel/debug/usb/usbmon/目录不存在,先确认usbmon模块加载成功,再确认debugfs挂载成功。这两个条件缺一个都看不到接口文件。
2.2 权限配置:别卡在Permission denied
usbmon接口文件默认只允许root读取,非root用户直接cat会报Permission denied。如果你只是临时用一下,直接加sudo最省事。但如果每天都要抓包,每次sudo很麻烦,我建议做个一次性授权。
最粗暴的方式是改debugfs挂载权限:
chmod -R a+r /sys/kernel/debug/usb/usbmon/不过要提醒一句:这种改法会让所有本地用户都能读取USB总线数据,在共享的开发机上存在安全风险,毕竟USB数据里可能包含键盘输入、密钥等敏感信息。更稳妥的做法是配置udev规则,只给指定用户或指定组授权。
在/etc/udev/rules.d/下新建一个规则文件,比如99-usbmon.rules:
KERNEL=="usbmon*", MODE="0640", GROUP="usbmon"然后把需要授权的用户加入usbmon组:
groupadd usbmon usermod -aG usbmon yourname这样重新插拔或者重启后,usbmon相关节点就会以0640权限暴露给usbmon组成员。这种做法的好处是权限范围可控,不会把所有总线数据开放给所有人。
2.3 第一次抓包:从U盘枚举开始
环境就绪之后,找一个U盘插上,先跑通第一次抓包。打开终端执行:
cat /sys/kernel/debug/usb/usbmon/0u终端会进入等待状态,这时候插入U盘,屏幕上会滚动出一大堆文本行,每一行就是一个URB事件。看到这些信息就说明抓包成功了,按Ctrl+C退出。
如果你用的是0u这个文件,它会把所有USB总线上的事件都显示出来,包括鼠标键盘这些已连接设备持续产生的中断传输。第一次看会觉得很乱,这是正常的。为了观察得更清楚,建议用具体总线编号的文件,比如1u:
cat /sys/kernel/debug/usb/usbmon/1uU盘插入时,你会看到大量控制传输相关的记录,其中比较显眼的是GET_DESCRIPTOR请求和响应。这个过程就是USB设备枚举的主流程:主机请求设备描述符、分配地址、读取配置描述符、设置配置。U盘插入后能正常工作,说明这套流程顺利走完了。
注意:cat方式抓包会一直阻塞在终端里,这不是卡死,而是usbmon在等待新事件。抓到想要的内容后直接用Ctrl+C终止即可。
3. 抓包实操:文本模式与Wireshark联调
3.1 文本模式:最直接但要有筛选意识
文本模式是usbmon最简单直接的使用方式,适合在终端里快速查看。前面提到了0u代表所有总线,这个“所有”有时候是好事有时候是坏事。好处是你能总揽全局,坏处是信息量太大,尤其在系统同时跑着鼠标、键盘、摄像头的情况下,终端滚屏速度快到你根本看不清。
这时候就得靠grep做初筛。usbmon的文本行里包含总线号:设备号:端点号这样的字段,格式是busnum:devnum:epnum。假设你的U盘枚举后在总线1上的设备地址是2,对应的行里面就能找到1:002:这样的字符串。可以这样过滤:
cat /sys/kernel/debug/usb/usbmon/0u | grep '1:002'这样终端里就只剩目标设备的事件了。不过实时管道加grep有个隐患,我后面会在常见问题部分详细讲,这里先记住一个原则:如果流量很大,优先考虑先存文件再过滤。
另外文本模式有个小技巧可以显著提升阅读体验:把时间戳后面那串内容按列对齐。usbmon输出的字段本来是按固定格式排的,但终端宽度有限,数据多的时候会折行。建议把终端窗口拉宽,或者配合sed、awk做格式化输出,比如只显示某几个关键列:
cat /sys/kernel/debug/usb/usbmon/1u | awk '{print $1, $3, $4, $5, $6}'这样打印出URB ID、事件类型、地址、长度和状态,信息密度高很多,适合快速扫一眼有没有错误状态。
3.2 二进制模式与Wireshark图形化分析
文本模式看多了眼睛会花,尤其是分析复杂交互流程的时候,Wireshark的图形化界面能帮你省下大量时间。usbmon的二进制文件后缀是t,把0t文件的数据喂给Wireshark,Wireshark会自动识别usbmon格式并做协议解析。
先把二进制数据抓下来:
cat /sys/kernel/debug/usb/usbmon/0t > usb_monitor.bin抓到足够数据后Ctrl+C停止,然后用Wireshark打开这个文件:
wireshark usb_monitor.binWireshark会按照URB事件逐条展示,并且能解析出USB请求的各个字段。比如GET_DESCRIPTOR请求,Wireshark会直接把bmRequestType、bRequest、wValue、wIndex、wLength这些字段拆开来显示,不需要你手动从十六进制里数偏移量。
更进阶的用法是实时抓包实时分析。利用Wireshark的管道输入能力,可以这样做:
cat /sys/kernel/debug/usb/usbmon/0t | wireshark -k -i --k表示立即开始捕获,-i -表示从标准输入读取。这样执行后,Wireshark窗口会直接弹出并实时展示抓到的USB事件,效果比文本模式爽得多。如果你习惯用命令行,也可以把0t的数据喂给tshark做文本分析:
cat /sys/kernel/debug/usb/usbmon/0t | tshark -i - -Y usb.transfer_type == 0x02-Y是显示过滤器,usb.transfer_type字段0x02对应批量传输,这样能把批量传输单独筛出来。tshark的处理性能比grep好,而且能直接解析协议字段。
3.3 定向抓取:只关心某条总线或某个设备
很多新手会把0u当成默认抓包入口,但实际项目中我很少直接用0u,因为设备一旦多起来,数据量是成倍增长的。更合理的做法是先确定目标设备在哪条总线上,然后只监听那条总线。
用lsusb -t可以查看USB设备的总线树结构:
lsusb -t输出里能看到类似/: Bus 01.Port 1: Dev 1, Class=root_hub这样的行,这就是总线编号和设备地址的对应关系。确认目标设备在总线1上的设备地址是2之后,直接抓1u或者1t,过滤负担小很多。
如果设备地址固定不下来,比如每次插拔都会变,就需要用到usbmon的一个隐藏技巧:通过URB ID来定位设备。同一设备产生的所有URB事件,其URB ID的前缀通常是相同的(这部分空间由内核地址分配,实践中存在设备相关性)。先用0u抓一小段,找到目标设备的一行,记下URB ID的前缀,然后用正则过滤:
cat /sys/kernel/debug/usb/usbmon/0u | grep '^ffff880036dda480'这样能把同一个设备贯穿枚举、配置、传输全过程的事件都筛出来,效果相当不错。
还有一个实用建议:抓包的时候尽量把无关设备拔掉。USB是共享总线,其他设备的活动会干扰你判断。特别是调试U盘、串口这类设备时,关掉摄像头、无线网卡等频繁活动的USB外设,抓包数据会干净很多。
4. 包数据格式逐字段解析
4.1 文本行结构拆解
usbmon文本格式是理解USB通信的关键,也是本文的重点。不同内核版本在字段细节上略有差异,但整体结构是一致的。下面以一次GET_DESCRIPTOR请求为例,展示两行典型的usbmon输出:
ffff880036dda480-0001 987654.123456 S Ci:1:002:0 8 = 80 06 00 01 00 00 12 00 ffff880036dda480-0002 987654.123789 C Ci:1:002:0 0 18 > 12 01 00 02 00 00 00 40 00 00 00 00 00 00 00 00 00 00这两行看起来就是一堆十六进制和冒号,但拆开之后逻辑非常清晰。我整理了一个字段对照表:
| 字段 | 示例值 | 含义 |
|---|---|---|
| URB ID | ffff880036dda480 | 内核中URB结构体地址,用来关联同一URB的多个事件 |
| 事件序号 | -0001、-0002 | 同一URB按时间顺序递增的事件编号 |
| 时间戳 | 987654.123456 | 内核单调时钟,秒.微秒格式 |
| 事件类型 | S、C | S是submit(提交),C是complete(完成) |
| URB类型+方向 | Ci、Co、Bi、Bo、Ii、Io | 第一个字母代表传输类型,第二字母代表方向 |
| 设备地址 | 1:002:0 | 总线号:设备地址:端点号 |
| 状态 | 0 | 0表示成功,负数表示内核错误码 |
| 数据长度 | 8、18 | 本次传输的有效数据长度 |
| 数据方向标记 | =、>、< | =是提交时携带的数据,>是设备发给主机,<是主机发给设备 |
| 数据内容 | 80 06... | 十六进制表示的URB数据 |
以第一行为例:S表示这是URB提交事件,Ci是控制输入传输,1:002:0说明这是总线1上地址2的设备的端点0,8是setup阶段的8字节数据,等号后面80 06 00 01 00 00 12 00就是USB标准的设备描述符请求。为什么setup一定是8字节?USB 2.0规范里定义控制传输的setup事务固定是8字节,这8字节包含了完整的请求类型、请求码、值、索引和长度。
第二行是同一个URB的complete事件,0表示传输成功,18是设备返回的数据长度,大于号说明数据方向是设备到主机,后面的18个字节就是设备描述符的原始内容。设备描述符的第一个字节12是描述符长度,第二个字节01是描述符类型(设备描述符),这两个字节和请求里的预期完全对应。
4.2 Submit与Complete事件:一个URB的完整生命周期
为什么usbmon要为同一个URB产生两行记录?这是因为USB传输是异步的。驱动提交URB后,USB控制器需要时间完成传输,等传输完成或出错后,内核才回调驱动。usbmon在提交和完成这两个时间点分别记录状态,正好对应了URB生命周期的两个关键阶段。
S事件(submit)是URB请求被USB core接收的时刻,此时记录的内容包括请求的端点、方向、setup数据或者out方向数据。C事件(complete)是URB完成或被取消的时刻,此时记录的内容包括实际传输的字节数、传输状态码,以及in方向收到的数据。
中间如果隔了很久,说明设备响应慢,或者被总线调度延后了。如果只有S没有C,大概率是URB超时被取消,或者设备根本没有响应。这种“有去无回”的情况在故障排查中往往是最关键线索。
状态码方面,0代表成功。非零值需要结合内核错误码表来看,常见的有这么几个:
| 状态码 | 含义 | 常见场景 |
|---|---|---|
| 0 | 成功 | 正常完成 |
| -84 | EPIPE,端点STALL | 设备返回STALL握手,通常是设备不支持该请求 |
| -110 | ETIMEDOUT,超时 | 设备没有响应,URB等待超时 |
| -75 | EOVERFLOW,溢出 | 设备返回数据超过期望长度 |
| -71 | EPROTO,协议错误 | 总线协议级别出错 |
| -2 | ENOENT,URB被取消 | 驱动主动取消或设备断开 |
你在抓包时如果经常看到负状态码,基本可以断定USB通信已经出了问题,下一步就是结合具体端点定位是哪一端的责任。
4.3 控制传输的setup、data、status三阶段
控制传输是所有USB传输类型中最复杂也最常用的一种,设备枚举、配置、状态查询都靠它。usbmon日志里,一个控制传输往往对应多行记录,理解它的三阶段模型非常关键。
控制传输分为setup阶段、data阶段(可选)、status阶段。setup阶段固定8字节,内容是一个标准的USB请求(如GET_DESCRIPTOR、SET_ADDRESS、SET_CONFIGURATION)。data阶段按请求方向传输数据,比如读设备描述符时,设备会返回一串描述符字节。status阶段用来确认整个传输的完成状态。
拿4.1节的例子说,第一行的setup数据是80 06 00 01 00 00 12 00,拆解如下:
- 80:bmRequestType,最高位是1表示设备到主机方向,bit6-5是00表示标准请求,bit4-0是00000表示发给设备
- 06:bRequest,标准请求码GET_DESCRIPTOR
- 00 01:wValue,高字节01表示描述符类型为设备描述符,低字节00是描述符索引
- 00 00:wIndex,接口或端点号,对设备描述符请求来说是0
- 12 00:wLength,期望接收18字节
第二行complete事件里,设备返回了18字节,正好对应wLength。这18字节就是设备描述符的内容,包含USB版本号、设备类、端点0最大包长、厂商ID、产品ID等信息。整套流程就是“主机提问、设备回答”,USB协议里大量交互都是这样一个模式。
4.4 从一次枚举过程看完整包结构
U盘插入后,主机会顺序发出多个控制请求,usbmon会按照时间顺序记录下整个过程。下面是一个简化但真实的枚举流程示例:
S Ci:1:002:0 8 = 80 06 00 01 00 00 12 00 (读设备描述符前18字节) C Ci:1:002:0 0 18 > 12 01 00 02 00 00 00 40 ... S Co:1:002:0 8 = 00 05 05 00 00 00 00 00 (设置地址为5) C Co:1:002:0 0 0 S Ci:1:002:0 8 = 80 06 00 01 00 00 12 00 (用新地址重新读设备描述符) C Ci:1:002:0 0 18 > 12 01 00 02 00 00 00 40 ... S Ci:1:002:0 8 = 80 06 00 02 00 00 09 00 (读配置描述符) C Ci:1:002:0 0 9 > 09 02 20 00 01 01 00 80 32 S Ci:1:002:0 8 = 80 06 00 02 00 00 20 00 (读完整配置描述符) C Ci:1:002:0 0 32 > 09 02 20 00 01 01 00 80 32 ... S Co:1:002:0 8 = 00 09 01 00 00 00 00 00 (设置配置为1) C Co:1:002:0 0 0这个过程里可以看到一个细节:读设备描述符的请求发了两次。第一次是在默认地址0上做的,目的是拿到端点0的最大包长(从第一个字节描述符长度开始,设备描述符的前8字节包含了bcdUSB、设备类、最大包长等信息),主机据此调整后续传输的分包策略。设置地址完成后,主机用新地址重新读了一遍完整描述符确认。
这正是usbmon的魅力所在:把教科书里抽象的描述符格式和枚举流程,变成了你能亲眼看到总线上一来一回的通信记录。你在Wireshark里还能看到比文本更直观的字段解析,两者配合使用,对USB协议的理解会上一个台阶。
5. 常见问题与排查技巧实录
5.1 权限和模块相关的坑
我遇到过不止一次,同事按教程执行modprobe usbmon之后,lsmod能看到模块,但/sys/kernel/debug/usb/usbmon目录就是不存在。这个问题通常出在debugfs挂载上。有些系统里/sys/kernel/debug目录存在但为空,说明debugfs没挂上去。
快速排查看这里:
mount | grep debugfs如果没有任何输出,执行:
mount -t debugfs none /sys/kernel/debug挂载成功后目录就出现了。如果想开机自动挂载,可以写入/etc/fstab:
debugfs /sys/kernel/debug debugfs defaults 0 0另一个常见问题是权限。即使你用sudo执行modprobe,加sudo执行cat还是Permission denied,这时候要检查是不是SELinux或者AppArmor拦截了。在开发机上临时关闭SELinux再试一下,如果确实是SELinux的问题,建议为debugfs加上allow规则,而不是为了抓包关掉整个系统安全策略。
5.2 抓不到包的可能原因
模块加载了、权限也对了、cat命令也执行了,但插上设备后终端毫无动静。这种“静默失败”通常有以下几个原因:
第一,抓错总线了。0u是所有总线的聚合,但有些系统的usbmon在0号接口上抓不到数据,需要明确到具体总线编号。用lsusb -t查看目标设备在哪条总线上,比如在总线2上,就抓2u。
第二,设备和主控之间有别的抓包层或者虚拟化层。如果你在虚拟机里抓包,USB设备直通后的URB行为可能在宿主机上被过滤掉,导致usbmon看不到预期数据。
第三,设备是USB 3.0且使用xHCI控制器时,部分URB可能不会上报到usbmon。这种情况在高速传输数据量大的设备上更明显。解决办法是换到USB 2.0端口测试,或者暂时在BIOS里把xHCI模式切到EHCI(如果主板支持),确认是不是控制器兼容性问题。
第四,最容易被忽略的:cat命令已经在终端里跑起来了,但当前shell没有读取权限。这种请直接sudo运行cat,排除权限因素后再做其他排查。
5.3 数据量太大怎么办
用0u抓包,如果系统里鼠标键盘都接着,屏幕上每秒能滚几十行中断传输记录,几乎没法看。数据量大的时候,我的处理策略是分级处理。
第一级,从0u切到具体总线文件,比如1u。这能过滤掉其他总线上的干扰。
第二级,用grep做设备地址过滤。比如只关心1:002这个设备,grep '1:002'。这一步在实时管道里执行要注意,如果数据量特别大,grep可能来不及消费导致内核缓冲区溢出,表现出来就是丢包或者usbmon设备节点报错。
第三级,彻底一点的做法是直接用二进制文件抓下来,然后离线用Wireshark或者tshark分析。wireshark有强大的显示过滤器,比如只显示控制传输、只显示某个端点的数据、只显示错误事件,比在终端里grep高效得多。要注意的是,二进制文件会越抓越大,最好提前估计抓包时长,或者用tshark的-c参数限定抓包条数。
5.4 一个真实故障的排查思路
前面提到的USB转串口丢数据问题,我用usbmon排查的完整思路值得分享。设备是一块FT232串口芯片,应用层通过串口读取传感器数据,偶发数据错位。驱动和程序看了好几遍没找到规律,于是决定在USB层面看。
先插入设备,用lsusb -t确认设备在总线1,然后用1u抓包。因为串口数据走的是批量传输,grep 'Bo'和'Bi'把批量传输筛出来。抓了十几秒后停止,发现两个现象:
一是complete事件的状态码偶发出现-84(STALL),说明设备在某些情况下返回了STALL握手;二是正常完成的Bi事件里,实际数据长度比请求长度少了一些,但状态还是0。这两个特征结合起来,基本可以判断是设备端固件对批量传输的处理有逻辑漏洞,在特定情况下丢弃了部分数据并触发STALL。
如果用文本模式看不出来,把二进制文件用Wireshark打开,按URB ID分组看同一URB的submit和complete时间差,也能发现设备响应时间抖动剧烈。这比在应用层打日志高效太多,因为USB层已经把问题暴露得很彻底了。
提示:分析USB故障时,永远先把URB层的数据和行为理清楚,再往上层查代码。USB层的异常往往能快速区分问题是出在设备固件、驱动还是应用层,省下一大段盲目试错的时间。
结尾:一点实战体会
用usbmon抓包这几年,我最深的感觉是,很多USB问题看起来玄学,其实都是能用数据说话的。设备描述符不对、端点配置错误、批量传输数据长度异常、设备STALL、URB超时,这些特征在usbmon的日志里都有一一对应的表现。学会读URB日志,比凭感觉猜问题高效太多。
如果你刚开始接触USB调试,建议先拿一个U盘做实验,反复插拔几次,对照usbmon日志理解枚举流程,再用Wireshark看看描述符的解析结果。把基础流程吃透之后,再去碰串口、HID、摄像头这些复杂设备,你会发现所有USB设备的行为模式都是相似的。抓包工具本身很简单,但配合你对协议的理解,它能帮你省下的时间远超想象。