本文深入剖析 Linux 内核 USB Gadget 子系统中的 ConfigFS 机制,完整讲解如何不写一行内核代码、仅通过文件系统操作,就能让 Linux 设备摇身一变成为主机眼中的标准 USB 设备——包括大容量存储、USB 网卡、HID 键盘鼠标、以及多复合设备组合。

一、为什么需要自定义 USB 设备?

在嵌入式开发、工业控制甚至桌面 Linux 场景中,我们经常遇到一个核心需求:让 Linux 设备对主机呈现为特定的 USB 设备类型。典型场景包括:

  • 固件升级/维护模式:开发板在烧录时暴露为 USB Mass Storage,主机直接"写盘"即烧写固件
  • 虚拟串口调试:在仅有 USB OTG 接口的设备上创建 CDC ACM 串口,免驱即可终端通信
  • PoC/攻击靶场:将树莓派伪装成键盘(BadUSB)自动注入按键序列
  • 网络共享:通过 USB CDC NDIS/RNDIS 让主机识别为网卡,实现 USB 网络共享(Android USB Tethering 即基于此)
  • 多合一工具设备:一个 USB 接口同时暴露 U 盘 + 网卡 + 串口

传统的 USB Gadget 驱动开发需要在内核态编写 usb_gadget_driver、usb_gadget_ops、usb_request 处理等一堆回调——改动任何描述符都要重新编译内核模块。直到 Linux 3.x 引入 ConfigFS,这一切才被彻底颠覆:用户空间直接操作即可定义 USB 设备。


二、USB ConfigFS 架构总览

2.1 三层抽象模型

ConfigFS 的架构层次可以从下到上分为三层:

┌─────────────────────────────────────────────────────┐
│  用户空间层 (User Space)                              │
│  mount configfs → mkdir / symlink / echo             │
├─────────────────────────────────────────────────────┤
│  ConfigFS 内核抽象层 (Kernel ConfigFS Gadget)          │
│  /sys/kernel/config/usb_gadget/                       │
│  ├── g1/                                             │
│  │   ├── idVendor / idProduct / bcdDevice            │
│  │   ├── configs/c.1/                               │
│  │   │   ├── f1 → ../functions/storage.usb0        │
│  │   │   └── f2 → ../functions/rndis.usb0          │
│  │   ├── functions/                                 │
│  │   │   ├── storage.usb0/                          │
│  │   │   └── rndis.usb0/                            │
│  │   └── UDC → "musb-hdrc"                          │
├─────────────────────────────────────────────────────┤
│  硬件 UDC 层 (USB Device Controller)                   │
│  具体 SoC 的 UDC 驱动:musb / dwc2 / dwc3 / chipidea    │
└─────────────────────────────────────────────────────┘

核心思想:将 USB 设备描述符、配置、端点、接口全部抽象为文件系统中的目录、文件和符号链接,mkdir 即创建对象、写文件即设置属性、建立链接即绑定关系。

2.2 与传统 Legacy Gadget 驱动对比

维度 Legacy(g_mass_storage 模块) ConfigFS
修改描述符 重新编译内核模块 echo 0xa4a5 > idProduct(即时生效)
组合多个设备 数种固定组合,不可编配 任意组合函数,自由链接到 Config
热插拔支持 卸载/重新加载模块 echo "" > UDC 即可瞬时断开
用户态可定制 无 完整文件系统语义权限控制
调试便利性 dmesg +内核探针 ls -la /sys/kernel/config/usb_gadget/g1/

三、从零实战:创建复合 USB 设备

以下以在一个 USB OTG 接口上创建"U盘 + HID键盘 + RNDIS网卡"三合一设备为完整示例,揭示 ConfigFS 的每一步操作。

3.1 准备工作

首先需要确保内核支持和必要的模块:

# 检查内核配置
zgrep CONFIG_USB_CONFIGFS /boot/config-$(uname -r)

# 必需的内核配置项
CONFIG_USB_LIBCOMPOSITE=m    # ConfigFS Gadget 核心
CONFIG_USB_CONFIGFS=m        # ConfigFS 接口
CONFIG_USB_CONFIGFS_RNDIS=y  # 或者 m:USB 网络(RNDIS)
CONFIG_USB_CONFIGFS_MASS_STORAGE=y  # 大容量存储
CONFIG_USB_CONFIGFS_F_HID=y        # HID 函数

# 加载通用 ConfigFS Gadget 模块
modprobe libcomposite

确认 configfs 已挂载(某些发行版在 /sys/kernel/config 上会自动挂载):

mount -t configfs none /sys/kernel/config

3.2 创建 Gadget 骨架

cd /sys/kernel/config/usb_gadget
mkdir g1
cd g1

# 设置 USB 基本描述符
echo 0x1d6b > idVendor   # Linux Foundation
echo 0x0104 > idProduct  # Multifunction Composite Gadget
echo 0x0100 > bcdDevice  # v1.0.0
echo 0x0200 > bcdUSB     # USB 2.0

# 设置设备类信息(复合设备使用 0xEF 表示 Miscellaneous)
echo 0xEF > bDeviceClass
echo 0x02 > bDeviceSubClass
echo 0x01 > bDeviceProtocol

# 设置字符串描述符
mkdir strings/0x409                          # English
echo "1234567890" > strings/0x409/serialnumber
echo "MyVendor" > strings/0x409/manufacturer
echo "MultiDevice" > strings/0x409/product

# 配置 Power 参数
echo 120 > bMaxPower                          # 120 mA

3.3 创建配置项 (Configuration)

mkdir configs/c.1
echo 120 > configs/c.1/MaxPower

# 配置字符串描述符
mkdir configs/c.1/strings/0x409
echo "Conf 1: Storage + HID + Net" > configs/c.1/strings/0x409/configuration

3.4 创建三个功能函数(Functions)

3.4.1 Mass Storage 函数

mkdir functions/mass_storage.usb0

# 设置后端(可以是磁盘镜像文件、loop 设备或真实块设备)
echo 1 > functions/mass_storage.usb0/stall                     # 支持 STALL
echo 0 > functions/mass_storage/usb0/lun.0/removable           # 不可移动介质
echo 1 > functions/mass_storage.usb0/lun.0/ro                 # 只读模式
echo "/root/firmware.img" > functions/mass_storage.usb0/lun.0/file

如果需要一个回环的 FAT32 镜像文件,可这样创建:

# 创建 64MB 空白镜像
dd if=/dev/zero of=/root/firmware.img bs=1M count=64

# 格式化为 FAT32
mkfs.vfat -F 32 -n "FIRMWARE" /root/firmware.img

# 但注意:格式化的同时 ConfigFS 会报错,请先设置好再绑定

3.4.2 HID 键盘/鼠标函数

mkdir functions/hid.usb0

# 设置 HID 报告描述符参数
echo 1 > functions/hid.usb0/subclass       # 1=Boot Interface, 0=None
echo 1 > functions/hid/usb0/protocol       # 1=Keyboard, 2=Mouse, 0=None
echo 8 > functions/hid.usb0/report_length  # 报告长度 8 字节(键盘标准)

# 写入 HID 报告描述符(一点也不能错的魔法数字)
# 通用键盘 Boot Protocol 报告描述符
echo -ne '\x05\x01\x09\x06\xa1\x01\x05\x07\x19\xe0\x29\xe7\x15\x00\x25\x01\x75\x01\x95\x08\x81\x02\x95\x01\x75\x08\x81\x03\x95\x05\x75\x01\x05\x08\x19\x01\x29\x53\x91\x02\x95\x01\x75\x03\x91\x03\x95\x06\x75\x08\x15\x00\x25\x65\x05\x07\x19\x00\x29\x65\x81\x00\xc0' | \
    xxd -r -p > functions/hid.usb0/report_desc

3.4.3 RNDIS 网卡函数

mkdir functions/rndis.usb0

# RNDIS 特定属性:设置主机 MAC 和本机 MAC
echo "00:11:22:33:44:55" > functions/rndis.usb0/host_addr
echo "00:11:22:33:44:56" > functions/rndis.usb0/self_addr

# 可选:设置 qmult 增加吞吐量
echo 5 > functions/rndis.usb0/qmult

3.5 绑定函数到配置

这一步是关键——通过符号链接将每个 Function 挂到 Configuration 的接口上:

ln -s functions/mass_storage.usb0 configs/c.1/
ln -s functions/hid.usb0        configs/c.1/
ln -s functions/rndis.usb0      configs/c.1/

此时 ConfigFS 拓扑:

g1/
├── idVendor          0x1d6b
├── idProduct         0x0104
├── strings/
├── configs/
│   └── c.1/
│       ├── MaxPower
│       ├── strings/
│       ├── mass_storage.usb0 → /sys/kernel/config/usb_gadget/g1/functions/mass_storage.usb0
│       ├── hid.usb0           → .../functions/hid.usb0
│       └── rndis.usb0         → .../functions/rndis.usb0
├── functions/
│   ├── mass_storage.usb0/
│   ├── hid.usb0/
│   └── rndis.usb0/
└── UDC              (绑定时写入)

3.6 激活 Gadget(上电)

最后一步:将 Gadget 绑定到 UDC 硬件,主机将立即识别这个新设备:

# 查看可用 UDC
ls /sys/class/udc/
# 输出示例:musb-hdrc.0.auto  (SoC 的 UDC 名称)

# 激活
echo "musb-hdrc.0.auto" > /sys/kernel/config/usb_gadget/g1/UDC

此时主机端 dmesg 将看到:

usb 1-1: new high-speed USB device number 5 using ehci-pci
usb 1-1: New USB device found, idVendor=1d6b, idProduct=0104, bcdDevice= 1.00
usb 1-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
usb 1-1: Product: MultiDevice
usb 1-1: Manufacturer: MyVendor
usb 1-1: SerialNumber: 1234567890
...
# 紧接着系统将加载 3 个驱动:usb-storage, usbhid, rndis_host

3.7 断开 Gadget

仅需 echo "" 即可瞬时从主机消失(模拟拔线):

echo "" > /sys/kernel/config/usb_gadget/g1/UDC

四、内核原理深度剖析

4.1 configfs_gadget.c 核心机制

ConfigFS 的实现位于 drivers/usb/gadget/configfs.c。其核心数据结构关系:

// ConfigFS 将每个属性映射为一个 configfs_attribute
struct usb_gadget_strings_gadget {
    struct usb_gadget_strings    strings;
    struct config_group          group;
};

// 目录的 attrs 对应可见属性文件
// mkdir 对应 config_group 的 make_item
// symlink 对应 configfs_symlink 建立 config-group 之间关系

ConfigFS 的对象生命周期被抽象为 config_group 和 config_item,Gadget 子系统中:

  • g1 目录 → config_group(对应一个 struct usb_gadget)
  • configs/c.1 → config_group(对应一个 struct usb_configuration)
  • functions/storage.usb0 → config_group(对应一个 struct usb_function_instance + struct usb_function)

4.2 绑定 UDC 时的枚举过程

echo "udc-name" > UDC 触发内核内部调用链:

configfs_store_UDC()
  → usb_gadget_probe_driver()          // 通知 UDC 驱动
    → udc_bind_to_driver()             // UDC 驱动注册
      → composite_setup()              // 开始响应描述符请求
        → usb_assign_descriptors()     // 组装来自各 Function 的描述符
          → usb_composite_dev_setup()  // 处理 SET_CONFIGURATION
            → usb_add_function()       // 逐个添加 function 实例
              → mass_storage.bind()    // 各 function 的 bind 回调
              → hid.bind()
              → rndis.bind()

关键:此时 ConfigFS 已经从文件系统结构采集到所有描述符,组装为一个完整的 USB 设备描述符集传达给 UDC 硬件。当主机执行总线复位后,这套描述符将作为响应呈现给主机。

4.3 端点自动分配机制

传统方式需要手动指定 bEndpointAddress(端点地址),ConfigFS 引入了一个更优雅的机制——自动端点分配:

ConfigFS 驱动会自动扫描每个 Function 的端点描述符(ep_* 属性),然后根据端点方向和类型自动分配 host/device 端点地址。端点方向(IN/OUT)通过 USB_DIR_IN / USB_DIR_OUT 在描述符中编码,ConfigFS 驱动以此自动映射。自动分配逻辑位于 __ffs_func_bind() 和 fsg_alloc_inst() 内部的 ep_autoconfig()。

4.4 FunctionFS (Function Filesystem)

除了 ConfigFS 这种纯文件操作模式外,对于需要自定义传输逻辑的场景,还有 FunctionFS。它允许:

• 创建 Function 时设置 no_configfs = 0(默认)

• 在用户空间 mount -t functionfs ep0 /dev/usbfunc

• 通过读写 /dev/usbfunc/ep0(控制端点)处理 USB 请求

这在实现 MTP(媒体传输协议)、ADB(Android Debug Bridge)等非标准协议时非常有用。内核源码树中 usb/ff_mtp.c 和内核 dvb-usb 驱动利用了这一机制。F​unctionFS + epoll 可实现高效异步的 USB 传输。


五、生产环境实战场景

5.1 嵌入式固件的 USB 烧录方案

某 Android OTA 开发团队的需求:在不拆机、不进入 fastboot 的情况下,通过 USB 给设备烧写 eMMC 镜像。传统方案需要进入 recovery 分区。使用 ConfigFS 可构建"USB 烧录模式":

#!/bin/bash
# recovery-usb.sh —— 一键进入烧录模式
GADGET=/sys/kernel/config/usb_gadget

# 清理旧 gadget
echo "" > $GADGET/g1/UDC 2>/dev/null

# 创建虚拟 UEM 读取块设备RAW分区
mkdir -p $GADGET/g1/functions/mass_storage.0
echo /dev/mmcblk0 > $GADGET/g1/functions/mass_storage.0/lun.0/file
echo 1 > $GADGET/g1/functions/mass_storage.0/lun.0/removable

# 绑定
echo "your-udc-name" > $GADGET/g1/UDC

echo "[OK] 设备现在浮现为主机的 /dev/sdX"
echo "[OK] 请使用 'dd if=system.img of=/dev/sdX' 烧写"

烧写完成后的安全退出:

# 先 flush 块设备缓冲
sync
echo 1 > /proc/sys/vm/drop_caches

# 安全断开 USB
echo "" > /sys/kernel/config/usb_gadget/g1/UDC

5.2 BadUSB 模拟:自动按键注入

使用 HID 函数可向主机注入标准键盘扫描码:

#!/bin/bash
# 需要 /dev/hidg0 —— hidg 是 gadget 创建的 HID 设备

HID_GADGET=/dev/hidg0

# 等待 gadget 驱动就绪
while [ ! -e $HID_GADGET ]; do sleep 0.1; done

# 写入修饰键 + 按键的报告
# 报告格式:[modifiers, reserved, keycode1, keycode2, ...]
# 修饰键: 0x02=LeftShift, 0x04=RightShift, 0x10=RightGUI(Win)
# Keycode 0x04='a', 0x0f='l', 0x15='r', 0x28=Enter, etc.

# 按 Win+R(运行)
echo -ne '\x10\x00\x15\x00\x00\x00\x00\x00' > $HID_GADGET
sleep 0.1

# 释放所有
echo -ne '\x00\x00\x00\x00\x00\x00\x00\x00' > $HID_GADGET
sleep 0.5

生产启示:此类能力攻击性也很强,建议结合 udev 规则配合安全策略——在非维护模式期间通过 sysfs 属性 udc 禁止 gadget 的加载,同时利用 IMA/EVM 度量防止 gadget 配置被篡改。

5.3 USB 串口(CDC ACM)的开发板调试接口

对于资源受限的嵌入式设备(只有 USB OTG,无独立 UART):

mkdir -p /sys/kernel/config/usb_gadget/g1/functions/acm.usb0
ln -s /sys/kernel/config/usb/gadget/g1/functions/acm.usb0 \
      /sys/kernel/config/usb/gadget/g1/configs/c.1/
echo "your-udc-name" > /sys/kernel/config/usb/gadget/g1/UDC

主机端将出现 /dev/ttyACM0,直接使用 minicom 或 screen 连接即可访问串口终端。无需任何驱动安装,这是 USB CDC ACM 类即插即用的优势。

5.4 多 Config 支持与 USB 3.0

ConfigFS 天然支持在单一 Gadget 中创建多种 Configuration:

# Config 1:仅存储("正常模式")
mkdir configs/c.1
ln -s functions/storage.usb0 configs/c.1/

# Config 2:存储 + 串口("调试模式")
mkdir configs/c.2
ln -s functions/storage.usb0 configs/c.2/
ln -s functions/acm.usb0     configs/c.2/

# Config 3:仅 RNDIS("网络模式")
mkdir configs/c.3
ln -s functions/rndis.usb0   configs/c.3/

主机可在运行时通过 SET_CONFIGURATION 切换 Config(某些桌面 Ubuntu DisplayPort/HDMI 驱动会如此请求)。

对于 USB 3.0,需要在 Gadget 中同时设置 SuperSpeed 和 HighSpeed 描述符:

# 设置 bcdUSB 为 0x0300 或同时支持
echo 0x0200 > bcdUSB  # USB 2.0
mkdir configs/c.1/
# USB 3.0 的端点伴侣描述符由内核自动处理

六、性能优化与故障排查

6.1 Gadget 配置常见问题排查表

症状 排查方法 解决方案
echo "xxx" > UDC 失败:Invalid argument `dmesg \ tail -n 20` 检查 idVendor/idProduct 非零;确认 config 中没有链接错误
主机无法识别新设备 lsusb -v 检查 bDeviceClass 设置;确认端点带宽足够
Storage 读写慢 cat /sys/kernel/debug/usb/gadget/udc/* 增加 maxburst 属性;检查 buf 大小参数
RNDIS 连接不通 主机 ip route 检查 确认 host_addr 与 self_addr 不同;检查主机是否支持 RNDIS
HID 无响应 主机端 evtest /dev/input/eventN 检查 report_desc 格式;确认报告长度正确

6.2 端点带宽计算

USB 2.0 高速 (480Mbs) 帧长度 125ms,批量传输最大包 512B,每微帧最多 3 次事务:

# 单 IN 端点理论最大吞吐量:
125us * 3 * 512B = ~187.5 KB/s per micro-frame
# 完整微帧:
8192 bit * 3 transactions per micro-frame * 8192 microframes/s ≈ 312 MB/s (理论)

# 实际受多种因素影响:
# - 帧间间隔、SOF 开销、NAK 重试
# - 实际稳定吞吐约 35-40 MB/s

大容量存储函数可通过设置 stream 属性来优化 Streaming 性能:

echo 1 > functions/mass_storage.usb0/stream_capable

6.3 DebugFS 实时调试

当 Gadget 出现问题时,debugfs 提供实时诊断窗口:

mount -t debugfs none /sys/kernel/debug

# Gadget 状态一览
cat /sys/kernel/debug/usb/gadget/g1/state 2>&1
# 输出示例:
# UDC: musb-hdrc.0.auto
# Config: c.1
# Functions: mass_storage.usb0, hid.usb0, rndis.usb0

# 端点状态
ls /sys/kernel/debug/usb/gadget/g1/functions/

# 实时查看 UDC 端点事件
cat /sys/kernel/debug/usb/udc/core/state

七、安全加固与权限管控

ConfigFS gadget 暴露给用户空间意味着安全风险。生产环境中建议:

7.1 最小权限

# 不直接暴露给普通用户,而是通过 systemd service 或 udev rules 来操作
# 创建专用 group
groupadd usb-gadget
usermod -aG usb-gadget developer

# systemd service 锁定操作权限
cat > /etc/systemd/system/[email protected] <<EOF
[Unit]
Description=USB Gadget - %i mode

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=/bin/sh -c "echo '' > /sys/kernel/config/usb_gadget/g1/UDC 2>/dev/null"
ExecStart=/usr/local/bin/configure-gadget.sh %i
ExecStop=/bin/sh -c "echo '' > /sys/kernel/config/usb/gadget/g1/UDC"
User=root
Group=usb-gadget

[Install]
WantedBy=multi-user.target
EOF

7.2 防伪造

防止恶意进程修改 Gadget 配置:

• SELinux/AppArmor 策略:限制只有 usb-gadget 域能写入 /sys/kernel/config/usb_gadget/ 下的属性文件

• 内核 lockdown:当 kernel lockdown 级别为 integrity 或 confidentiality 时,ConfigFS 不允许修改

• IMA 度量:关键 gadget 描述符文件(idVendor、functions 路径等)写入 IMA 哈希树

7.3 uudc (USB Device Controller) 与用户空间驱动

Linux 6.x 引入了 udc-core 的 uudc 框架,允许部分 UDC 管理移交用户空间。结合 FunctionFS,理论上可以实现完全用户态的 USB 设备——该方向可关注 tools/usb/usbip 的未来演进。


八、总结与展望

从工程角度看,ConfigFS Gadget 的价值不在于"可以用文件操作代替内核编程"的表面便利,而在于它揭示了操作系统基础设施的一个正确方向:

• 声明式配置优于过程式编程:通过声明对象与关系组合来定义设备,而不是逐个调用函数注册 API

• 热插拔与即时变更:无需重新加载模块,microsecond 级别的状态切换

• 安全隔离边界:用户空间直接控制设备描述符,天然适合容器化场景(将 gadget namespace 与容器绑定)

未展望:

  • USB4/Thunderbolt 复合设备:Linux 6.5+ 正在推进 f_tbm 函数类型,支持 Thunderbolt 3 隧道
  • USB4 与 CXL 融合:SDPCI 等协议可能借 ConfigFS 暴露为企业级特性
  • Gadget as KCM:将标准 Windows/Linux Gadget驱动模板化为 QEMU/libvirt 标准接口

对于任何需要 USB 功能的嵌入式开发者,ConfigFS Gadget 几乎是从原型到生产的最佳路径:编码时使用 ConfigFS 快速定型,发布时固化为 systemd 服务 + initramfs 一步到位。


延伸阅读:Documentation/usb/gadget_configfs.rst(内核源码树)、drivers/usb/gadget/function/(各函数实现)、tools/usb/(用户空间工具集)。
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部