Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

6.4 踩坑与修复:真实调试经验

一个场景:代码编译通过了,你满怀信心地运行测试程序。突然,程序崩溃了——段错误。你打开日志,发现错误发生在某个SDK内部函数。供应商提供的代码也会有bug?

一个问题浮出水面:如何定位和修复SDK中的bug?哪些是常见的错误模式?

一个隐喻:这像装修新房——即使开发商交付了“精装修”,你也会发现柜门螺丝松了、插座位置不对、水管有轻微漏水。找到问题、分析原因、动手修复,这是工程师的基本功。

Bug一:数组下标无符号数下溢导致段错误

问题现象

程序运行时崩溃,错误日志显示:

Segmentation fault (core dumped)

问题定位

通过调试器追踪,崩溃发生在tpdu.c文件的tpdu_execute函数:

// 原始代码(有bug)
se_error_t tpdu_execute(...) {
    uint32_t off = 0;
    
    ret = tpdu_send(...);
    if (ret != HSM_OK) {
        // 异常处理分支
        q_buf[off-2] = SW1;   // ← off=0时,off-2在uint32_t下绕回到UINT32_MAX-1,内存访问出错
        q_buf[off-1] = SW2;   // ← off=0时,off-1=-1,下溢出,内存访问出错
        return ret;
    }
    
    off = queue_out->rear_node;  // ← 这行在正常分支才会执行
    // ...
}

问题分析

变量off定义时初始化为0。当tpdu_send返回错误时,代码直接进入异常处理分支,此时off仍然是0。

异常处理分支中使用了q_buf[off-2]q_buf[off-1],由于off是无符号数,所以产生下溢出,变为很大的数(UINT32_MAX-1和UINT32_MAX),导致内存访问异常。

修复方案

在使用off之前,先给它赋正确的值:

// 修复后的代码
se_error_t tpdu_execute(...) {
    uint32_t off;
    
    ret = tpdu_send(...);
    if (ret != HSM_OK) {
        off = queue_out->rear_node;  // ← 先赋值,再使用
        q_buf[off-2] = SW1;
        q_buf[off-1] = SW2;
        return ret;
    }
    
    off = queue_out->rear_node;
    // ...
}

教训总结

错误模式:变量初始化值在错误分支中被错误使用。

预防措施

  1. 初始化变量时,考虑它在所有分支中的使用情况
  2. 异常处理分支中使用的变量,确保在该分支开始时就已赋值
  3. 使用静态分析工具检测潜在的数组越界问题

Bug二:指针类型转换导致数据错误

问题现象

调用hsm_get_key_info获取密钥信息时,HSM返回CRC错误:

Error: CRC check failed

问题定位

通过逻辑分析仪观察I2C波形,发现主设备发送的命令数据不正确。追踪代码,问题出在driver_i2c.c文件:

// 原始代码(有bug)
uint16_t sRspLen = 3;  // ← 定义为2字节类型

ret = driver_i2c_receive_frame(fd, rx_buf, (uint32_t *)&sRspLen);
// ← 强制转换为4字节指针类型

问题分析

sRspLen定义为uint16_t类型(2字节),但传入driver_i2c_receive_frame函数时,被强制转换为uint32_t*类型(指向4字节数据的指针)。

driver_i2c_receive_frame函数内部,接收数据后会写入这个指针指向的内存:

se_error_t driver_i2c_receive_frame(int fd, uint8_t *buf, uint32_t *len) {
    // ...
    *len = received_length;  // ← 写入4字节!
    // 但sRspLen只有2字节空间
}

当函数写入4字节到只有2字节空间的变量时,会覆盖相邻的内存,导致其他数据被破坏。同时,读取这个值时也会取错(只取2字节,丢失了高2字节)。

修复方案

将变量类型改为与指针类型一致:

// 修复后的代码
uint32_t sRspLen = 3;  // ← 改为4字节类型

ret = driver_i2c_receive_frame(fd, rx_buf, &sRspLen);
// ← 无需强制转换,类型匹配

同样的bug还出现在其他函数:

  • driver_i2c_reset_request
  • driver_i2c_atr_request

都需要统一修复。

教训总结

错误模式:指针类型与实际数据类型不匹配。

预防措施

  1. 避免强制类型转换指针,特别是跨字节宽度转换
  2. 函数参数类型应与实际变量类型一致
  3. 使用sizeof检查变量大小与预期是否一致
  4. 代码审查时重点关注指针类型转换

Bug三:双端队列实现缺陷

问题现象

多次调用API后,程序出现数据错乱或崩溃。追踪发现,队列管理相关函数有多个问题。

问题定位

util.c文件中的双端队列实现存在多处bug:

问题1:队列大小计算错误

// 原始代码(有bug)
uint32_t util_queue_size(queue_t *q) {
    return q->rear - q->front;  // ← 环形缓冲区不能简单相减
}

对于环形缓冲区,rear可能小于front,简单相减会得到负数或错误的大小。

问题2:内存越界风险

// 原始代码(有bug)
void util_queue_front_pop(queue_t *q, uint8_t *data, uint32_t len) {
    memcpy(data, &q->buffer[q->front], len);  // ← 没有检查len是否超出队列大小
    q->front += len;                           // ← 没有处理环形边界
}

没有边界检查,len大于队列数据量时会发生越界读取。

问题3:逻辑错误

// 原始代码(有bug)
void util_queue_rear_pop(queue_t *q, uint8_t *data, uint32_t len) {
    q->capacity -= len;  // ← 错误!容量不应该变化
    // ...
}

capacity是队列的容量,不应该随数据弹出而变化。

修复方案

修复队列大小计算

// 修复后的代码
uint32_t util_queue_size(queue_t *q) {
    if (q->rear >= q->front) {
        return q->rear - q->front;
    } else {
        return q->capacity - q->front + q->rear;  // ← 处理环形情况
    }
}

修复内存越界风险

// 修复后的代码
se_error_t util_queue_front_pop(queue_t *q, uint8_t *data, uint32_t len) {
    if (util_queue_size(q) < len) {
        return HSM_ERR_INVALID_PARAM;  // ← 添加边界检查
    }
    
    uint32_t first_part = q->capacity - q->front;
    if (first_part >= len) {
        memcpy(data, &q->buffer[q->front], len);
    } else {
        // ← 处理环形边界:数据跨越尾部和头部
        memcpy(data, &q->buffer[q->front], first_part);
        memcpy(data + first_part, q->buffer, len - first_part);
    }
    
    q->front = (q->front + len) % q->capacity;  // ← 环形索引更新
    return HSM_OK;
}

修复逻辑错误

// 修复后的代码
void util_queue_rear_pop(queue_t *q, uint8_t *data, uint32_t len) {
    // ← 移除错误的 capacity -= len
    // ...
}

教训总结

错误模式:环形缓冲区实现未考虑边界情况。

预防措施

  1. 环形缓冲区的索引更新必须使用模运算
  2. 所有数据访问前必须检查边界
  3. 区分“容量”(capacity)和“大小”(size)
  4. 编写专门的测试用例覆盖边界情况

Bug四:I2C硬件限制导致长帧接收失败

问题现象

调用hsm_get_key_info时,返回CRC错误。通过逻辑分析仪观察,发现:

期望接收:519字节完整帧
实际接收:重复的数据片段

原始帧:20 00 FF 01 ...(数据)... CRC
接收数据:20 00 XX 01 20 00 XX 01 20 00 XX 01 ...
          ↑ 数据重复,HSM从头重新发送

问题分析

这是硬件层面的限制,而非软件bug。

某些I2C控制器在处理长数据读取时,会自动将一次事务拆分成多个小事务:

I2C主设备行为

START → 地址 → 读取250字节 → STOP
START → 地址 → 继续读取 → STOP
START → 地址 → 继续读取 → STOP
...

每次STOP后,I2C总线被释放。主设备再次START时,从设备(HSM)收到新的START信号,会从头开始发送数据。

HSM从设备行为

收到START → 从头发送帧
收到START → 从头发送帧
收到START → 从头发送帧
...

主设备收到的数据是多个"帧头部片段"拼接而成,内容完全错误。

尝试的解决方案

方案一:链式传输

查询HSM是否支持链式传输(允许多次读取完成一帧)。供应商反馈:当前固件不支持。

方案二:调整I2C驱动参数

尝试调整I2C控制器驱动的块大小限制。但某些硬件控制器无法通过软件修改这一行为。

方案三:使用I2C_RDWR ioctl

尝试使用Linux的I2C_RDWR接口实现连续读取:

struct i2c_msg msgs[2] = {
    { .addr = slave_addr, .flags = 0, .len = 1, .buf = &cmd },
    { .addr = slave_addr, .flags = I2C_M_RD, .len = data_len, .buf = data }
};
struct i2c_rdwr_ioctl_data ioctl_data = {
    .msgs = msgs,
    .nmsgs = 2
};
ioctl(fd, I2C_RDWR, &ioctl_data);

但某些I2C控制器仍然会拆分长读取。

最终解决方案

切换到SPI协议

SPI协议没有这种硬件限制:

SPI通信特点

Host发送片选信号(CS拉低)
    ↓
全双工同步传输,一次完成所有数据
    ↓
Host拉高CS,结束传输
    ↓
无STOP-START拆分问题

修改步骤:

  1. 在系统启动配置中开启SPI控制器
  2. 在硬件设计中连接SPI线路(SCK、MOSI、MISO、CS)
  3. 修改SDK通信层代码,使用SPI传输
// SPI配置
#define SPI_DEVICE "/dev/spidev1.1"
#define SPI_MODE   1    // CPOL=0, CPHA=1
#define SPI_SPEED  1000000  // 1MHz
#define SPI_BITS   8

切换后,长帧接收正常,问题解决。

教训总结

问题根源:硬件控制器的设计选择,而非软件错误。

关键认知

  1. 不同平台的I2C控制器行为可能不同
  2. 某些硬件限制无法通过软件绕过
  3. SPI比I2C更适合大数据量传输
  4. 选择通信协议时要考虑数据长度需求

调试技巧

  1. 逻辑分析仪是定位通信问题的关键工具
  2. 对比不同平台的波形差异,发现硬件行为区别
  3. 咨询硬件供应商了解固件能力边界

Bug修复总结表

Bug类型问题现象根本原因修复方案
数组越界段错误变量初始化值在错误分支被错误使用使用前先赋正确值
类型转换CRC错误指针类型与实际数据类型不匹配统一变量与指针类型
队列实现数据错乱环形缓冲区边界处理缺失正确处理环形索引
I2C硬件限制长帧接收失败主设备自动拆分事务切换到SPI协议

调试经验总结

一个隐喻:调试像侦探破案——从现象入手,收集证据(日志、波形),分析动机(代码逻辑),找到嫌疑人(bug位置),最后修复。

调试工具箱

  1. GDB调试器:追踪崩溃位置,检查变量值
  2. 逻辑分析仪:观察实际通信波形,对比预期数据
  3. 静态分析工具:检测潜在的数组越界、类型不匹配
  4. 日志系统:记录关键函数的输入输出

调试流程

发现问题
    ↓
定位位置(日志/调试器)
    ↓
分析原因(代码审查/波形对比)
    ↓
制定修复方案
    ↓
验证修复效果
    ↓
总结教训,预防同类问题

下一节,我们将深入安全机制——看PIN验证和密钥管理。

【下集预告】

PIN验证怎么实现?传输密钥是什么?

密钥怎么生成、导入、导出、删除?

安全属性有哪些?怎么组合?

下一节,安全机制实现。