起因

        今天,笔者写了一个关于stm32串口通信的程序。程序大概的流程是上位机发送数据,stm32接收数据后进入中断,并在中断中将相关的数据接收完成标志位置位(暂且称为flag)。在main函数中不断扫描该标志位,若标志位置位则进入相应的函数进行业务处理,处理完毕后将该标志位复位等待下次处理。

        在接收到上位机发送来的数据后,在串口中断里会启动定时器进行计时,若计时器到达预设值则进入定时器中断,表示上位机数据发送结束。在定时器中断里对接收到的数据进行解码,将不不同的信息保存在一个名为 Modbus_FrameTypeDef 的结构体中。

调试

        其中一个业务处理函数如下:

int ModbusSlave_HandleFC03(const Modbus_FrameTypeDef* frame,ModbusSlave* slave)
{
	uint8_t resp_buf[5];
	uint8_t is_valid;
	Modbus_ExceptCodeTypeDef except_code;
    uint16_t start_addr,reg_count;
	uint16_t crc,data_len,i,reg_val;
	uint16_t resp_len;
	
	// Modbus RTU功能码03的请求帧长度固定为8字节(从站地址1+功能码1+数据域4+CRC2)
	if ((frame->data_len + 4) != 8)
	{
		slave->err_count++;
        slave->last_exception = MODBUS_EXCEPT_ILLEGAL_DATA;
        goto EXCEPT_RESP;  // 帧长度非法,返回异常响应
    }
	
	// 解析请求帧数据域(起始地址+寄存器数量)
    start_addr = (frame->data[0] << 8) | frame->data[1];  // 起始地址(偏移量,对应40001=0x0000)
    reg_count = (frame->data[2] << 8) | frame->data[3];  // 请求读取的寄存器数量
	
	 // 合法性校验(核心步骤)
    is_valid = !0x00;
    except_code = MODBUS_EXCEPT_NONE;
	
    // 校验1:寄存器数量是否在Modbus协议允许范围(1~125个,因响应帧数据域最大250字节=125×2)
    if (reg_count < 1 || reg_count > 125) 
	{
        is_valid = 0x00;
        except_code = MODBUS_EXCEPT_ILLEGAL_ADDR;
    }
	
    // 校验2:起始地址+数量是否超出从站支持的最大保持寄存器地址
    else if ((start_addr + reg_count) > MODBUS_HOLD_REGS_LEN)
	{
        is_valid = 0x00;
        except_code = MODBUS_EXCEPT_ILLEGAL_ADDR;
    }

    //非法请求:生成异常响应帧
    if (!is_valid) 
	{
        slave->err_count++;
        slave->last_exception = except_code;
EXCEPT_RESP:
        // 异常响应帧格式:从站地址1 + 功能码(最高位置1)1 + 异常码1 + CRC2
        resp_buf[0] = slave->slave_addr;
        resp_buf[1] = MODBUS_FC_READ_HOLD_REG | 0x80;  // 功能码最高位置1(03→83)
        resp_buf[2] = slave->last_exception;
        // 计算CRC校验码
        crc = Modbus_CalculateCRC(resp_buf, 3);
        resp_buf[3] = crc & 0xFF;        // CRC低位
        resp_buf[4] = (crc >> 8) & 0xFF; // CRC高位
		resp_len = 5;  // 异常响应帧固定长度5字节
		
		Modbus_TRANSMISSION_FRAME(resp_buf,resp_len);
		
        return -2;
    }
	
	 //合法请求:生成正常响应帧
    // 响应帧格式:从站地址1 + 功能码1 + 数据字节数1 + 寄存器数据N×2 + CRC2
	data_len = reg_count * 2;  // 每个寄存器2字节,总数据字节数
    resp_buf[0] = slave->slave_addr;    // 从站地址(与请求一致)
    resp_buf[1] = MODBUS_FC_READ_HOLD_REG;  // 功能码(与请求一致)
    resp_buf[2] = data_len;             // 后续数据域的总字节数

    // 复制保持寄存器数据到响应帧(高字节在前,低字节在后)
    for (i = 0; i < reg_count; i++) 
	{
        reg_val = slave->hold_regs[start_addr + i];  // 读取对应寄存器值
        resp_buf[3 + i*2] = (reg_val >> 8) & 0xFF;  // 高字节
        resp_buf[4 + i*2] = reg_val & 0xFF;         // 低字节
    }

    // 计算CRC校验码(覆盖从站地址→数据域的所有字节)
    crc = Modbus_CalculateCRC(resp_buf, 3 + data_len);
    resp_buf[3 + data_len] = crc & 0xFF;        // CRC低位
    resp_buf[4 + data_len] = (crc >> 8) & 0xFF; // CRC高位

    // 6. 更新从站状态统计
    slave->req_count++;
    slave->resp_count++;
    slave->run_state = SLAVE_STATE_IDLE;

    // 7. 设置响应帧长度(地址1 + 功能码1 + 字节数1 + 数据N×2 + CRC2)
    resp_len = 3 + data_len + 2;
	
	Modbus_TRANSMISSION_FRAME(resp_buf,resp_len);
	
    return 0;	
}

        这个函数大致的流程就是根据上位机发送的数据进行响应,当接收到上位机发来的数据后会将数据解析保存在frame结构体变量中(frame中有一个标志位变量,即前文提到的标志位flag),然后在这个函数根据frame结构体中相关信息,将slave中的信息打包,通过串口发送给上位机。

        当一切完成,进行调试的时候,发生了一件非常奇怪的事情。flag标志位一直处于置位的状态,笔者检查了所有出现了标志位flag的代码块,均未找到问题所在,并且有时候还会出现串口中断只会在复位后第一次接收数据时触发的情况。

        一开始,笔者不断检查串口配置、定时器配置、还有所有可能修改该标志位的地方,均为找到问题原因,一度怀疑是不是硬件出问题了。但是没写该函数前一切外设都能够正常运行啊,所以肯定是软件出问题了。

        经过不断的注释代码、调试,大概耗费了两个小时后,最终将问题出现的原因锁定在了下面两行代码上面。

int ModbusSlave_HandleFC03(const Modbus_FrameTypeDef* frame,ModbusSlave* slave)
{
	uint8_t resp_buf[5];
	uint8_t is_valid;
	Modbus_ExceptCodeTypeDef except_code;
    uint16_t start_addr,reg_count;
	uint16_t crc,data_len,i,reg_val;
	uint16_t resp_len;
	
    ……

    // 复制保持寄存器数据到响应帧(高字节在前,低字节在后)
    for (i = 0; i < reg_count; i++) 
	{
        reg_val = slave->hold_regs[start_addr + i]; 
        resp_buf[3 + i*2] = (reg_val >> 8) & 0xFF;  //**第一行**
        resp_buf[4 + i*2] = reg_val & 0xFF;         //**第二行**    
    }

    ……
	
    return 0;	
}

        当我对这两行代码不进行任何操作时,上位机发送数据,标志位flag一直处于置位状态,上位机不断接收单片机发送的数据。

        当我把第一行注释掉后,运行调试。上位机第一次发送数据,标志位flag被正常复位,系统正常运行。但同时出现了另一个bug——串口接收中失效,上位机第一次发送数据,执行一次该函数后串口中断就失效了,无法被触发。

        当我把第二行注释掉后,运行调试。上位机第一次发送数据,标志位flag一直被置位,上位机不断接收单片机发送的数据。但是串口中断却始终有效,能够被触发。

        当我把第一行、第二行全部注释掉后,系统正常运行。上位机发送一次数据,单片机响应一次并且串口中断始终有效。

分析

        resp_buf数据是在该函数中定义的临时变量,函数调用完就销毁,与标志位所在的结构体对象frame没有任何关系,为什么这两行代码会将标志位flag置位?

        笔者曾经在一本书上看到过一种木马程序,由于C语言不会主动检查数组下标越界,所以黑客利用C语言的数组下标越界将函数的返回地址修改成木马程序的入口地址,从而夺取整个应用程序的控制权。

        比如说定义了一个数组int a[3],按理说数组的最后一个元素下标应该为2,即a[2];但是如果你访问数组a中下标为4的元素(即a[4]),编译器不会报错,因为C语言不会主动检查数组下标越界。但是&a[4]元素指向的地址不属于数组a,该地址可能已经分配给别的变量或者函数(假设该变量为b)使用了,这时候如果你给a[4]赋值(比如a[4] = 12),这个新赋的值就会覆盖掉该地址里存储的原数据的值。当程序中其他地方使用b时,此时b中的数据已经被a[4]覆盖,造成bug。

        回到木马程序上,如果&a[4]的元素指向的地址原本是用来存储正在运行的函数调用结束后返回被调用前的下一条语句的返回地址(比如说从函数a中调用了函数b,函数b运行完后根据返回地址回到函数a),那么就可以将a[4]存储的返回地址修改为木马程序的入口地址,当函数b运行完后就会进入木马程序,而不是回到函数a,从而夺去了整个程序的控制权。

原因

int ModbusSlave_HandleFC03(const Modbus_FrameTypeDef* frame,ModbusSlave* slave)
{
	uint8_t resp_buf[5];
	uint8_t is_valid;
	Modbus_ExceptCodeTypeDef except_code;
    uint16_t start_addr,reg_count;
	uint16_t crc,data_len,i,reg_val;
	uint16_t resp_len;
	
    ……

    // 复制保持寄存器数据到响应帧(高字节在前,低字节在后)
    for (i = 0; i < reg_count; i++) 
	{
        reg_val = slave->hold_regs[start_addr + i]; 
        resp_buf[3 + i*2] = (reg_val >> 8) & 0xFF;  //**第一行**
        resp_buf[4 + i*2] = reg_val & 0xFF;         //**第二行**    
    }

    ……
	
    return 0;	
}

        回到笔者遇到的这个bug,同样也是由于数组下标越界,而越界后的地址正好有用来存储flag变量数据的地址,甚至可能还有串口外设配置寄存器的地址。所以flag标志位中的数据和串口外设寄存器中存储的配置数据被需要发送给上位机的数据给覆盖了,从而导致了前面遇到的BUG。

        当我把数组resp_buf的大小改为100后,系统正常运行。

int ModbusSlave_HandleFC03(const Modbus_FrameTypeDef* frame,ModbusSlave* slave)
{
	uint8_t resp_buf[100];    //当我把这个数组的大小改为100后,程序正常运行
	……
	
    ……

    // 复制保持寄存器数据到响应帧(高字节在前,低字节在后)
    for (i = 0; i < reg_count; i++) 
	{
        reg_val = slave->hold_regs[start_addr + i]; 
        resp_buf[3 + i*2] = (reg_val >> 8) & 0xFF;  //**第一行**
        resp_buf[4 + i*2] = reg_val & 0xFF;         //**第二行**    
    }

    ……
	
    return 0;	
}

        不得不说这个BUG确实极其隐秘,很难发现,曾一度让笔者怀疑人生。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐