usbmon

簡介

小寫名稱“usbmon”指的是核心中的一個功能,用於收集 USB 總線上的 I/O 跟蹤資訊。此功能類似於 tcpdump(1) 或 Ethereal 等網路監控工具使用的包套接字。類似地,像 usbdump 或 USBMon(大寫字母)這樣的工具預計將用於檢查 usbmon 生成的原始跟蹤資訊。

usbmon 報告外設特定驅動程式向主機控制器驅動程式 (HCD) 發出的請求。因此,如果 HCD 有缺陷,usbmon 報告的跟蹤資訊可能與匯流排事務不完全對應。這與 tcpdump 的情況相同。

目前實現了兩種 API:“文字”和“二進位制”。二進位制 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”格式的完美超集。

如果需要在程式中區分這兩種格式,請檢視“address”欄位(見下文),其中“1u”格式添加了匯流排號。如果存在 2 個冒號,則是“1t”格式,否則是“1u”。

任何文字格式的資料都由事件流組成,例如 URB 提交、URB 回撥、提交錯誤。每個事件都是一行文字,由空格分隔的單片語成。單詞的數量或位置可能取決於事件型別,但有一組單詞是所有型別共有的。

以下是單詞列表,從左到右:

  • URB 標籤。這用於標識 URB,通常是 URB 結構在核心中的十六進位制地址,但也可以是序列號或任何其他合理的唯一字串。

  • 時間戳,單位為微秒,一個十進位制數。時間戳的解析度取決於可用時鐘,因此它可能遠低於微秒(例如,如果實現使用 jiffies)。

  • 事件型別。此型別指的是事件的格式,而不是 URB 型別。可用型別有:S - 提交 (submission),C - 回撥 (callback),E - 提交錯誤 (submission error)。

  • “地址”欄位(以前稱為“管道”)。它由四個用冒號分隔的欄位組成:URB 型別和方向、匯流排號、裝置地址、端點號。型別和方向透過兩個位元組編碼如下:

    Ci

    Co

    控制輸入和輸出

    Zi

    Zo

    同步輸入和輸出

    Ii

    Io

    中斷輸入和輸出

    Bi

    Bo

    批次輸入和輸出

    匯流排號、裝置地址和端點是十進位制數字,但為了方便人類閱讀,它們可能帶有前導零。

  • URB 狀態欄位。它要麼是一個字母,要麼是幾個用冒號分隔的數字:URB 狀態、間隔、起始幀和錯誤計數。與“地址”欄位不同,除了狀態之外的所有欄位都是可選的。間隔僅針對中斷和同步 URB 列印。起始幀僅針對同步 URB 列印。錯誤計數僅針對同步回撥事件列印。

    狀態欄位是一個十進位制數字,有時為負數,它表示 URB 的“status”欄位。此欄位對於提交沒有意義,但無論如何都存在,以幫助指令碼進行解析。當發生錯誤時,該欄位包含錯誤程式碼。

    在提交控制包的情況下,此欄位包含一個 Setup 標籤而不是一組數字。很容易判斷 Setup 標籤是否存在,因為它永遠不是一個數字。因此,如果指令碼在此欄位中找到一組數字,它們會繼續讀取資料長度(同步 URB 除外)。如果它們找到其他內容,例如字母,則在讀取資料長度或同步描述符之前讀取設定包。

  • Setup 包(如果存在)由 5 個欄位組成:bmRequestType、bRequest、wValue、wIndex、wLength 各一個,如 USB 2.0 規範所述。如果 Setup 標籤是“s”,這些欄位可以安全地解碼。否則,Setup 包存在但未被捕獲,並且欄位包含填充物。

  • 同步幀描述符的數量和描述符本身。如果一個同步傳輸事件有一組描述符,則首先列印 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

向地址為 5 的儲存裝置傳送 SCSI 命令 0x28 (READ_10) 的 31 位元組批次封裝的輸出批次傳輸

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)

此 ioctl 主要用於應用程式透過 mmap(2) 訪問緩衝區時。其引數是指向以下結構的指標

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。

儘管緩衝區是迴圈的,但返回的頭部和資料不會跨越緩衝區的末尾,因此上述虛擬碼不需要任何聚合。