4、TPM固件栈与驱动:TPM TIS接口、TPM命令协议栈、Linux内核TPM驱动框架
这一章我们聊聊TPM的软件栈。说白了,就是操作系统怎么跟TPM芯片打交道。
很多人以为TPM就是个黑盒子,发个命令进去,拿个结果出来。其实没那么简单。中间隔了好几层——硬件接口、传输协议、驱动框架、上层API。每一层都有坑。
我个人习惯,先把整个栈的层次画清楚,再逐层深入。这样出了问题,你能快速定位是硬件没响应,还是驱动没加载,还是命令格式错了。
4.1 TIS接口——TPM的硬件门面
TIS,全称TPM Interface Specification。它定义了CPU怎么通过内存映射I/O去访问TPM寄存器。
说白了,TPM芯片在系统地址空间里占了一块区域。你往特定地址写数据,就是发命令;从特定地址读数据,就是收结果。
我记得第一次调TIS接口时,死活读不到状态寄存器的值。后来发现是ACPI表里没把TPM的MMIO区域正确暴露给操作系统。嗯,这种坑很常见。
TIS接口的核心寄存器
TIS规范定义了一组固定偏移的寄存器。我列几个关键的:
| 偏移地址 | 寄存器名称 | 作用 |
|---|---|---|
| 0x00 | ACCESS | 访问控制,判断TPM是否忙、是否可用 |
| 0x04 | INT_ENABLE | 中断使能,控制TPM是否触发中断 |
| 0x08 | INT_VECTOR | 中断向量号,告诉CPU中断线编号 |
| 0x0C | INT_STATUS | 中断状态,读清零,写置位 |
| 0x10 | INTF_CAPABILITY | 接口能力,支持FIFO还是CRB模式 |
| 0x18 | STS | 状态寄存器,核心!判断命令执行状态 |
| 0x24 | DATA_FIFO | 数据FIFO,读写命令和响应的地方 |
| 0x30 | INTERFACE_ID | 接口ID,标识TPM厂商和版本 |
TIS的两种传输模式
TIS支持两种模式:FIFO和CRB。FIFO是传统模式,CRB是Command Response Buffer模式,性能更好。
- FIFO模式: 数据通过DATA_FIFO寄存器逐字节或逐字传输。需要轮询STS寄存器的状态位来判断是否可写/可读。我建议新手先调通FIFO模式,逻辑简单,容易debug。
- CRB模式: 使用一块连续的缓冲区,命令和响应都放在里面。通过控制寄存器触发传输。效率高,但实现复杂一点。
你想想看,如果TPM芯片同时支持两种模式,驱动怎么选?嗯,先读INTF_CAPABILITY寄存器,看bit0和bit1。bit0置位表示支持FIFO,bit1置位表示支持CRB。优先选CRB,不行再回退到FIFO。
4.2 TPM命令协议栈——数据怎么封装
TIS接口只负责传输原始字节。这些字节怎么组织成有意义的命令,是TPM命令协议栈的事。
TPM命令的格式其实很固定。我直接给你看结构:
// TPM命令头部结构(固定长度6字节)
struct tpm_cmd_header {
__be16 tag; // 标签,标识命令类型
__be32 length; // 整个命令的长度(包括头部)
__be32 ordinal; // 命令码,比如TPM_ORD_GETRANDOM
};
// 命令码示例
#define TPM_ORD_GETRANDOM 0x14
#define TPM_ORD_EXTEND 0x14 // 注意:不同版本可能重复
#define TPM2_CC_GETRANDOM 0x17B
#define TPM2_CC_EXTEND 0x132
这里有个容易混淆的地方:TPM 1.2和TPM 2.0的命令码完全不同。TPM 1.2用16位ordinal,TPM 2.0用32位command code。我见过有人把TPM 2.0的命令码发给TPM 1.2芯片,结果返回TPM_BADTAG。
命令执行流程
一个完整的TPM命令执行,分五步:
- 准备命令缓冲区: 填充tag、length、ordinal,后面跟参数。长度要算准,多一个字节或少一个字节都会导致TPM拒绝执行。
- 获取TPM访问权: 通过ACCESS寄存器的requestUse位请求访问。如果TPM正被别的软件占用(比如BIOS),会返回忙。
- 写入命令: 把命令缓冲区写入DATA_FIFO。每次写入前检查STS的tpmGo位是否清零,确保TPM空闲。
- 通知TPM执行: 写STS寄存器的tpmGo位为1,告诉TPM“命令写完了,开始干活”。
- 等待并读取响应: 轮询STS的dataAvail位,或者等中断。数据就绪后从DATA_FIFO读响应。
4.3 Linux内核TPM驱动框架
Linux内核的TPM驱动在 drivers/char/tpm/ 目录下。这个框架设计得挺巧妙,把硬件差异和上层协议分开了。
我直接说核心结构:
驱动框架的层次
- tpm-core: 核心层,处理TPM命令的发送和接收,管理命令流水号,维护TPM状态。这部分跟具体硬件无关。
- tpm-interface: 接口层,封装了TPM 1.2和TPM 2.0的命令差异。比如tpm_get_random(),内部会判断是1.2还是2.0,然后构造不同的命令。
- tpm-dev: 字符设备层,提供/dev/tpm0这样的用户空间接口。应用程序可以通过read/write直接跟TPM交互。
- tpm_tis: TIS接口驱动,负责初始化MMIO区域、注册中断、实现FIFO/CRB传输。这是跟硬件最近的一层。
- tpm_crb: CRB模式驱动,用于支持CRB接口的TPM芯片。
我建议你看源码时,先看 tpm_tis_core.c 和 tpm_tis.c。前者是TIS核心逻辑,后者是具体平台适配。这两个文件加起来不到2000行,读起来不费劲。
关键数据结构
// TPM芯片的私有数据结构
struct tpm_chip {
struct device dev; // 设备模型
int dev_num; // 设备编号,对应/dev/tpm%d
unsigned long timeout_a; // 超时时间A
unsigned long timeout_b; // 超时时间B
unsigned long timeout_c; // 超时时间C
unsigned long timeout_d; // 超时时间D
bool timeout_adjusted; // 超时是否已调整
struct tpm_class_ops *ops; // 硬件操作函数指针
// ... 省略其他字段
};
// 硬件操作函数集
struct tpm_class_ops {
int (*send)(struct tpm_chip *chip, u8 *buf, size_t len);
int (*recv)(struct tpm_chip *chip, u8 *buf, size_t len);
void (*cancel)(struct tpm_chip *chip);
u8 (*status)(struct tpm_chip *chip);
bool (*req_complete)(struct tpm_chip *chip, u8 status);
int (*go_idle)(struct tpm_chip *chip);
int (*cmd_ready)(struct tpm_chip *chip);
};
你看这个 tpm_class_ops,其实就是硬件抽象层。不同的TPM芯片(比如Infineon SLB9670、Nuvoton NPCT75x)只要实现这几个函数,就能接入Linux TPM框架。我做过一个项目,需要把一款老旧的TPM 1.1芯片适配到Linux 5.10内核,就是靠实现这组ops搞定的。
驱动加载流程
Linux TPM驱动的加载顺序是这样的:
- ACPI表或设备树中声明TPM设备,匹配到tpm_tis或tpm_crb驱动。
- 驱动probe函数被调用,做三件事:请求MMIO内存区域、映射寄存器、注册中断。
- 读取TPM芯片的did_vid寄存器,确认厂商和型号。
- 发送TPM_ORD_GET_CAPABILITY命令,获取TPM版本(1.2还是2.0)和超时参数。
- 注册字符设备,创建/dev/tpm0节点。
- 启动内核线程,执行TPM自检(TPM_ORD_SELF_TEST)。
- "TPM: cannot read TPM PCID":MMIO映射失败,检查ACPI表
- "TPM: tpm_tis: 0x0 is busy":TPM被其他软件占用,比如BIOS的TPM驱动
- "TPM: tpm_tis: timeout":TPM没有响应,可能是硬件故障或中断没配好
用户空间交互
驱动加载成功后,用户空间可以通过 /dev/tpm0 直接发命令。但我不建议你这么做。原因很简单:TPM命令的格式太底层了,容易出错。
更好的方式是使用TSS(TPM Software Stack)。Linux上有两个主流实现:
- Trousers: 老牌TSS,支持TPM 1.2。现在基本没人用了。
- tpm2-tss: 新一代TSS,支持TPM 2.0。SAPI、ESAPI、FAPI三层API,从底层到高层都有。
我个人推荐直接用tpm2-tss。它封装了命令构造、参数编解码、会话管理等复杂逻辑。你调用tpm2_getrandom(),它内部会构造TPM2_CC_GetRandom命令,通过/dev/tpm0发送,解析响应,最后返回随机数。省心很多。