usbmon

简介

小写的 “usbmon” 指的是内核中用于收集 USB 总线上 I/O 跟踪的工具。此功能类似于网络监控工具(如 tcpdump(1) 或 Ethereal)使用的套接字数据包。类似地,预计使用 usbdump 或 USBMon(大写字母)等工具来检查 usbmon 生成的原始跟踪。

usbmon 报告外围设备特定驱动程序向主机控制器驱动程序 (HCD) 发出的请求。因此,如果 HCD 存在错误,usbmon 报告的跟踪可能与总线事务不完全对应。这与 tcpdump 的情况相同。

目前实现了两个 API:“text” 和 “binary”。二进制 API 可通过 /dev 命名空间中的字符设备获得,并且是一个 ABI。文本 API 自 2.6.35 版本起已被弃用,但为了方便起见仍然可用。

如何使用 usbmon 收集原始文本跟踪

与数据包套接字不同,usbmon 具有以文本格式提供跟踪的接口。这用于两个目的。首先,它在更复杂的格式最终确定之前,充当工具的通用跟踪交换格式。其次,如果工具不可用,人们可以阅读它。

要收集原始文本跟踪,请执行以下步骤。

1. 准备

挂载 debugfs(必须在内核配置中启用),并加载 usbmon 模块(如果构建为模块)。如果 usbmon 构建到内核中,则跳过第二步

# mount -t debugfs none_debugs /sys/kernel/debug
# modprobe usbmon
#

验证总线套接字是否存在

# ls /sys/kernel/debug/usb/usbmon
0s  0u  1s  1t  1u  2s  2t  2u  3s  3t  3u  4s  4t  4u
#

现在,您可以选择使用套接字 ‘0u’(捕获所有总线上的数据包),并跳到步骤 3,或者通过步骤 2 找到您的设备使用的总线。这可以过滤掉不断通信的烦人设备。

2. 查找连接到所需设备的总线

运行 “cat /sys/kernel/debug/usb/devices”,并找到与设备对应的 T 行。通常,您通过查找供应商字符串来执行此操作。如果您有许多类似的设备,请拔下一个设备并比较两个 /sys/kernel/debug/usb/devices 输出。T 行将包含一个总线号。

示例

T:  Bus=03 Lev=01 Prnt=01 Port=00 Cnt=01 Dev#=  2 Spd=12  MxCh= 0
D:  Ver= 1.10 Cls=00(>ifc ) Sub=00 Prot=00 MxPS= 8 #Cfgs=  1
P:  Vendor=0557 ProdID=2004 Rev= 1.00
S:  Manufacturer=ATEN
S:  Product=UC100KM V2.00

“Bus=03” 表示它是总线 3。或者,您可以查看 “lsusb” 的输出并从相应的行获取总线号。示例

Bus 003 Device 002: ID 0557:2004 ATEN UC100KM V2.00

3. 启动 ‘cat’

# cat /sys/kernel/debug/usb/usbmon/3u > /tmp/1.mon.out

要监听单个总线,请键入,否则,要监听所有总线,请键入

# cat /sys/kernel/debug/usb/usbmon/0u > /tmp/1.mon.out

此进程将读取,直到被终止。自然地,输出可以重定向到所需的位置。这是首选方法,因为它会很长。

4. 在 USB 总线上执行所需的操作

在此处执行一些创建流量的操作:插入闪存盘、复制文件、控制网络摄像头等。

5. 终止 cat

通常使用键盘中断 (Control-C) 完成此操作。

此时,可以保存输出文件(在此示例中为 /tmp/1.mon.out),通过电子邮件发送或使用文本编辑器检查。在最后一种情况下,请确保文件大小不会对于您喜欢的编辑器而言过大。

原始文本数据格式

目前支持两种格式:原始格式,或 “1t” 格式,以及 “1u” 格式。“1t” 格式在内核 2.6.21 中已弃用。“1u” 格式添加了一些字段,例如 ISO 帧描述符、间隔等。它生成的行稍长,但除此之外,它是 “1t” 格式的完美超集。

如果希望在程序中识别一种格式与其他格式,请查看 “地址” 字(见下文),其中 ‘1u’ 格式添加了总线号。如果存在 2 个冒号,则它是 “1t” 格式,否则为 “1u” 格式。

任何文本格式数据都由事件流组成,例如 URB 提交、URB 回调、提交错误。每个事件都是一个文本行,由空格分隔的单词组成。单词的数量或位置可能取决于事件类型,但有一组单词是所有类型通用的。

以下是从左到右的单词列表

  • URB 标签。它用于标识 URB,通常是 URB 结构的十六进制内核地址,但也可以是序列号或任何其他唯一的字符串,在合理的范围内。

  • 时间戳,以微秒为单位,是一个十进制数字。时间戳的分辨率取决于可用的时钟,因此它可能比微秒差得多(例如,如果实现使用节拍数)。

  • 事件类型。此类型是指事件的格式,而不是 URB 类型。可用类型为:S - 提交、C - 回调、E - 提交错误。

  • “地址” 字(以前是 “管道”)。它由四个字段组成,用冒号分隔:URB 类型和方向、总线号、设备地址、端点号。类型和方向使用两个字节以以下方式编码

    Ci

    Co

    控制输入和输出

    Zi

    Zo

    同步输入和输出

    Ii

    Io

    中断输入和输出

    Bi

    Bo

    批量输入和输出

    总线号、设备地址和端点是十进制数字,但为了方便读者,它们可能有前导零。

  • URB 状态字。它是一个字母,或几个用冒号分隔的数字:URB 状态、间隔、起始帧和错误计数。与 “地址” 字不同,除了状态之外的所有字段都是可选的。仅对于中断和同步 URB 打印间隔。仅对于同步 URB 打印起始帧。仅对于同步回调事件打印错误计数。

    状态字段是一个十进制数字,有时为负数,表示 URB 的 “状态” 字段。此字段对于提交没有意义,但为了帮助脚本解析而存在。发生错误时,该字段包含错误代码。

    在提交控制数据包的情况下,此字段包含设置标记而不是一组数字。很容易判断设置标记是否存在,因为它永远不是数字。因此,如果脚本在此字中找到一组数字,则它们将继续读取数据长度(同步 URB 除外)。如果他们发现其他内容,例如字母,则在读取数据长度或同步描述符之前读取设置数据包。

  • 设置数据包(如果存在)由 5 个字组成:bmRequestType、bRequest、wValue、wIndex、wLength,每个字都根据 USB 规范 2.0 指定。如果设置标记为 “s”,则可以安全地解码这些字。否则,设置数据包存在,但未捕获,并且这些字段包含填充符。

  • 同步帧描述符的数量和描述符本身。如果同步传输事件有一组描述符,则首先打印 URB 中的总数,然后每个描述符一个字,最多 5 个。该字由 3 个以冒号分隔的十进制数字表示,分别表示状态、偏移量和长度。对于提交,报告初始长度。对于回调,报告实际长度。

  • 数据长度。对于提交,这是请求的长度。对于回调,这是实际长度。

  • 数据标签。即使长度为非零,usbmon 也可能不会始终捕获数据。仅当此标签为 “=” 时,才存在数据字。

  • 数据字遵循大端十六进制格式。请注意,它们不是机器字,而只是分成字的字节流,以便于阅读。因此,最后一个字可能包含一个到四个字节。收集的数据长度是有限的,可能小于数据长度字中报告的数据长度。在同步输入 (Zi) 完成的情况下,如果接收到的数据在缓冲区中稀疏,则收集的数据的长度可能大于数据长度值(因为数据长度仅计算接收到的字节,而数据字包含整个传输缓冲区)。

示例

用于获取端口状态的输入控制传输

d5ea89a0 3575914555 S Ci:1:001:0 s a3 00 0000 0003 0004 4 <
d5ea89a0 3575914560 C Ci:1:001:0 0 4 = 01050000

输出批量传输,用于将 31 字节批量包装器中的 SCSI 命令 0x28 (READ_10) 发送到地址为 5 的存储设备

dd65f0e8 4128379752 S Bo:1:005:2 -115 31 = 55534243 ad000000 00800000 80010a28 20000000 20000040 00000000 000000
dd65f0e8 4128379808 C Bo:1:005:2 0 31 >

原始二进制格式和 API

API 的总体架构与上面的架构大致相同,只是事件以二进制格式传递。每个事件都以以下结构发送(其名称是虚构的,以便我们可以引用它)

struct usbmon_packet {
      u64 id;                 /*  0: URB ID - from submission to callback */
      unsigned char type;     /*  8: Same as text; extensible. */
      unsigned char xfer_type; /*    ISO (0), Intr, Control, Bulk (3) */
      unsigned char epnum;    /*     Endpoint number and transfer direction */
      unsigned char devnum;   /*     Device address */
      u16 busnum;             /* 12: Bus number */
      char flag_setup;        /* 14: Same as text */
      char flag_data;         /* 15: Same as text; Binary zero is OK. */
      s64 ts_sec;             /* 16: gettimeofday */
      s32 ts_usec;            /* 24: gettimeofday */
      int status;             /* 28: */
      unsigned int length;    /* 32: Length of data (submitted or actual) */
      unsigned int len_cap;   /* 36: Delivered length */
      union {                 /* 40: */
              unsigned char setup[SETUP_LEN]; /* Only for Control S-type */
              struct iso_rec {                /* Only for ISO */
                      int error_count;
                      int numdesc;
              } iso;
      } s;
      int interval;           /* 48: Only for Interrupt and ISO */
      int start_frame;        /* 52: For ISO */
      unsigned int xfer_flags; /* 56: copy of URB's transfer_flags */
      unsigned int ndesc;     /* 60: Actual number of ISO descriptors */
};                            /* 64 total length */

这些事件可以通过使用 read(2) 读取字符设备,使用 ioctl(2) 或使用 mmap 访问缓冲区来接收。但是,出于兼容性原因,read(2) 仅返回前 48 个字节。

字符设备通常称为 /dev/usbmonN,其中 N 是 USB 总线号。数字零 (/dev/usbmon0) 是特殊的,表示 “所有总线”。请注意,特定的命名策略由您的 Linux 发行版设置。

如果手动创建 /dev/usbmon0,请确保其所有者为 root,并且权限模式为 0600。否则,非特权用户将能够窥探键盘流量。

以下 ioctl 调用可用,其中 MON_IOC_MAGIC 为 0x92

MON_IOCQ_URB_LEN,定义为 _IO(MON_IOC_MAGIC, 1)

此调用返回下一个事件中数据的长度。请注意,大多数事件不包含数据,因此如果此调用返回零,并不意味着没有可用的事件。

MON_IOCG_STATS,定义为 _IOR(MON_IOC_MAGIC, 3, struct mon_bin_stats)

该参数是指向以下结构的指针

struct mon_bin_stats {
      u32 queued;
      u32 dropped;
};

成员 "queued" 指的是当前在缓冲区中排队的事件数量(而不是自上次重置以来处理的事件数量)。

成员 "dropped" 是自上次调用 MON_IOCG_STATS 以来丢失的事件数量。

MON_IOCT_RING_SIZE,定义为 _IO(MON_IOC_MAGIC, 4)

此调用设置缓冲区大小。参数是大小,以字节为单位。大小可能会向下舍入到下一个块(或页面)。如果请求的大小超出此内核的 [未指定] 范围,则调用将失败并返回 -EINVAL。

MON_IOCQ_RING_SIZE,定义为 _IO(MON_IOC_MAGIC, 5)

此调用返回当前缓冲区的大小,以字节为单位。

MON_IOCX_GET,定义为 _IOW(MON_IOC_MAGIC, 6, struct mon_get_arg) MON_IOCX_GETX,定义为 _IOW(MON_IOC_MAGIC, 10, struct mon_get_arg)

如果内核缓冲区中没有事件,这些调用会等待事件到达,然后返回第一个事件。参数是指向以下结构的指针

struct mon_get_arg {
      struct usbmon_packet *hdr;
      void *data;
      size_t alloc;           /* Length of data (can be zero) */
};

在调用之前,应填充 hdr、data 和 alloc。返回时,hdr 指向的区域包含下一个事件结构,数据缓冲区包含数据(如果有)。该事件将从内核缓冲区中删除。

MON_IOCX_GET 将 48 个字节复制到 hdr 区域,MON_IOCX_GETX 复制 64 个字节。

MON_IOCX_MFETCH,定义为 _IOWR(MON_IOC_MAGIC, 7, struct mon_mfetch_arg)

当应用程序使用 mmap(2) 访问缓冲区时,主要使用此 ioctl。其参数是指向以下结构的指针

struct mon_mfetch_arg {
      uint32_t *offvec;       /* Vector of events fetched */
      uint32_t nfetch;        /* Number of events to fetch (out: fetched) */
      uint32_t nflush;        /* Number of events to flush */
};

ioctl 分 3 个阶段运行。

首先,它从内核缓冲区中删除并丢弃最多 nflush 个事件。丢弃的实际事件数量在 nflush 中返回。

其次,它等待缓冲区中出现事件,除非伪设备以 O_NONBLOCK 方式打开。

第三,它将最多 nfetch 个偏移量提取到 mmap 缓冲区中,并将它们存储到 offvec 中。实际的事件偏移量数量存储在 nfetch 中。

MON_IOCH_MFLUSH,定义为 _IO(MON_IOC_MAGIC, 8)

此调用从内核缓冲区中删除若干事件。其参数是要删除的事件数量。如果缓冲区包含的事件少于请求的事件,则会删除所有存在的事件,并且不会报告错误。即使没有可用的事件,此方法也有效。

FIONBIO

如果需要,未来可能会实现 ioctl FIONBIO。

除了 ioctl(2) 和 read(2) 之外,二进制 API 的特殊文件可以使用 select(2) 和 poll(2) 进行轮询。但是 lseek(2) 不起作用。

  • 二进制 API 的内核缓冲区内存映射访问

基本思想很简单

要准备就绪,请通过获取当前大小来映射缓冲区,然后使用 mmap(2)。然后,执行类似于下面伪代码中编写的循环

struct mon_mfetch_arg fetch;
struct usbmon_packet *hdr;
int nflush = 0;
for (;;) {
   fetch.offvec = vec; // Has N 32-bit words
   fetch.nfetch = N;   // Or less than N
   fetch.nflush = nflush;
   ioctl(fd, MON_IOCX_MFETCH, &fetch);   // Process errors, too
   nflush = fetch.nfetch;       // This many packets to flush when done
   for (i = 0; i < nflush; i++) {
      hdr = (struct ubsmon_packet *) &mmap_area[vec[i]];
      if (hdr->type == '@')     // Filler packet
         continue;
      caddr_t data = &mmap_area[vec[i]] + 64;
      process_packet(hdr, data);
   }
}

因此,主要思想是每个 N 个事件只执行一个 ioctl。

尽管缓冲区是循环的,但返回的标头和数据不会跨越缓冲区的末尾,因此上述伪代码不需要任何收集。