
调USB设备的时候最崩溃的莫过于代码逻辑看起来没问题设备就是不按预期工作。前两天我处理一个USB转串口模块丢数据的问题驱动代码翻了两遍没找到毛病逻辑分析仪又不在手边最后装上usbmon把总线上的URB一个个扒出来才定位到是上层驱动批量传输缓冲区管理的问题。这种场景在嵌入式开发和驱动调试里太常见了。Linux下做USB抓包usbmon是公认的入门首选。它不需要任何额外的硬件内核自带装上就能用能让你看到USB总线上跑过的每一个请求和对应的数据。这篇文章我会从usbmon的工作原理讲起再带你完成一次完整的抓包操作然后把文本模式下包数据的格式逐字段拆开讲透最后整理几个我实际踩过的坑和排查思路。无论你是搞嵌入式、写驱动还是单纯想弄明白USB设备到底在跟电脑传什么这篇文章都能给你一个清晰可用的参考。1. UsbMon到底帮你看到什么底层机制与适用场景1.1 USB通信的最小单位URB在深入usbmon之前得先把USB通信的基本单元说清楚。USB设备之间的数据传输在操作系统层面不是以“字节”为单位打交道的而是以URBUSB Request BlockUSB请求块为单位。你可以把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 deniedusbmon接口文件默认只允许root读取非root用户直接cat会报Permission denied。如果你只是临时用一下直接加sudo最省事。但如果每天都要抓包每次sudo很麻烦我建议做个一次性授权。最粗暴的方式是改debugfs挂载权限chmod -R ar /sys/kernel/debug/usb/usbmon/不过要提醒一句这种改法会让所有本地用户都能读取USB总线数据在共享的开发机上存在安全风险毕竟USB数据里可能包含键盘输入、密钥等敏感信息。更稳妥的做法是配置udev规则只给指定用户或指定组授权。在/etc/udev/rules.d/下新建一个规则文件比如99-usbmon.rulesKERNELusbmon*, MODE0640, GROUPusbmon然后把需要授权的用户加入usbmon组groupadd usbmon usermod -aG usbmon yourname这样重新插拔或者重启后usbmon相关节点就会以0640权限暴露给usbmon组成员。这种做法的好处是权限范围可控不会把所有总线数据开放给所有人。2.3 第一次抓包从U盘枚举开始环境就绪之后找一个U盘插上先跑通第一次抓包。打开终端执行cat /sys/kernel/debug/usb/usbmon/0u终端会进入等待状态这时候插入U盘屏幕上会滚动出一大堆文本行每一行就是一个URB事件。看到这些信息就说明抓包成功了按CtrlC退出。如果你用的是0u这个文件它会把所有USB总线上的事件都显示出来包括鼠标键盘这些已连接设备持续产生的中断传输。第一次看会觉得很乱这是正常的。为了观察得更清楚建议用具体总线编号的文件比如1ucat /sys/kernel/debug/usb/usbmon/1uU盘插入时你会看到大量控制传输相关的记录其中比较显眼的是GET_DESCRIPTOR请求和响应。这个过程就是USB设备枚举的主流程主机请求设备描述符、分配地址、读取配置描述符、设置配置。U盘插入后能正常工作说明这套流程顺利走完了。注意cat方式抓包会一直阻塞在终端里这不是卡死而是usbmon在等待新事件。抓到想要的内容后直接用CtrlC终止即可。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文件的数据喂给WiresharkWireshark会自动识别usbmon格式并做协议解析。先把二进制数据抓下来cat /sys/kernel/debug/usb/usbmon/0t usb_monitor.bin抓到足够数据后CtrlC停止然后用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, Classroot_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 IDffff880036dda480内核中URB结构体地址用来关联同一URB的多个事件事件序号-0001、-0002同一URB按时间顺序递增的事件编号时间戳987654.123456内核单调时钟秒.微秒格式事件类型S、CS是submit提交C是complete完成URB类型方向Ci、Co、Bi、Bo、Ii、Io第一个字母代表传输类型第二字母代表方向设备地址1:002:0总线号:设备地址:端点号状态00表示成功负数表示内核错误码数据长度8、18本次传输的有效数据长度数据方向标记、、是提交时携带的数据是设备发给主机是主机发给设备数据内容80 06...十六进制表示的URB数据以第一行为例S表示这是URB提交事件Ci是控制输入传输1:002:0说明这是总线1上地址2的设备的端点08是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成功正常完成-84EPIPE端点STALL设备返回STALL握手通常是设备不支持该请求-110ETIMEDOUT超时设备没有响应URB等待超时-75EOVERFLOW溢出设备返回数据超过期望长度-71EPROTO协议错误总线协议级别出错-2ENOENTURB被取消驱动主动取消或设备断开你在抓包时如果经常看到负状态码基本可以断定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拆解如下80bmRequestType最高位是1表示设备到主机方向bit6-5是00表示标准请求bit4-0是00000表示发给设备06bRequest标准请求码GET_DESCRIPTOR00 01wValue高字节01表示描述符类型为设备描述符低字节00是描述符索引00 00wIndex接口或端点号对设备描述符请求来说是012 00wLength期望接收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/fstabdebugfs /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事件的状态码偶发出现-84STALL说明设备在某些情况下返回了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设备的行为模式都是相似的。抓包工具本身很简单但配合你对协议的理解它能帮你省下的时间远超想象。