本文深入剖析 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 驱动利用了这一机制。FunctionFS + 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/(用户空间工具集)。

发表评论 取消回复