基于反序位域的大端协议处理方法

这篇具有很好参考价值的文章主要介绍了基于反序位域的大端协议处理方法。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

综述

本文主要描述如何在C/C++软件中以一种简便的方法处理通信协议中的大小端转换方法;由于资源限制,目前在仅在windows平台的部分编译器与cpu中进行验证。

大小端

数据在内存中存储的基础单元是字节(Byte),最小的存储单元是位(bit);在常用的ARM架构中,按照字节处理数据时,数据总是低位在后,高位在前,即位LSB(least significant bit,LSB);而单个Byte所能表达的数据量有限,C/C++中扩展了众多以多个Byte表达的基础类型,如常用的int、uint32_t等等。
CPU处理内存中存储多个Byte的方式有两种,分别是大端模式与小端模式;所谓的大端模式,就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。所谓的小端模式,就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
举个例子,比如数字 0x12 34 56 78在内存中的表示形式为:

MSB:

Byte 0 1 2 3
Data 0x12 0x34 0x56 0x78

LSB:

Byte 0 1 2 3
Data 0x78 0x56 0x34 0x12

验证大小端的方法

验证CPU的大小端的方式,可以通过指针或者联合体,判断一个32位无符号数的第1个字节的具体数值进行判断;方法如下:

#include "stdio.h"
#include "stdint.h"

union MsbVerify
{
    uint32_t m_word;
    uint8_t  m_bytes[sizeof(uint32_t)];
};

int main()
{
    union MsbVerify _verify_code;
    _verify_code.m_word = 0x12345678;

    if(_verify_code.m_bytes[0] == 0x12)
    {
        printf("MSB\n");
    }
    else if(_verify_code.m_bytes[0] == 0x78)
    {
        printf("LSB\n");
    }

    return 0;
}

大小端的优劣

以人类的视角来看,数据的大端存储上更加符合人类从左往右的阅读习惯;但从计算机的角度来说,小端在数据处理方面更具有优势。

  1. 方便拓展
    当计算机存储256以内的无符号数uint8_t时,只需要一个字节即可表示;而当超过这个范围时,小端模式下只需要在后一个地址增加一个Byte即可扩展成uint16_t。如下, 当数据从0x0034扩展成2Byte的0x1234时,小端模式只需要将地址1中的数据填充成0x12即可;

    地址 0 1 => 0 1
    数据 0x34 0x00 => 0x34 0x12

    而对于大端数据来说,则需要先将地址0中的数据搬到地址1中,再在地址0中填充数据:

    地址 0 1 => 0 1 => 0 1
    数据 0x34 0x00 => 0x00 0x34 => 0x12 0x34

    相比于小端模式,大端模式需要更多的内存操作步骤才能完成一个数据的转存,大端的操作会消耗更多的CPU指令周期,导致涉及此类操作的内存访问效率降低;

  2. 计算
    同条目1,当CPU对数据进行计算时,以同样的一个自加计算为例,小端模式只需要在0地址进行自加;而大端需要根据数据长度,移动对应Byte的地址,再进行自加;这样的寻址过程同样占用CPU指令周期,造成性能降低;

具体可以参考epcdiy大佬的视频,此处也不再继续展开。
为啥50年前的怪异设计,依然用在现代手机电脑上?

位域

位域的定义与用法

关于位域的定义以及使用方法可以参考百度百科

对齐

由于CPU总线的限制,以32位CPU为例,其内存总线是32bit,意味着CPU的一个指令周期可以读取32位的内存;对应到RAM中,如果同样以一个32bit的内存块进行访问时,效率总是最高的。
而在内存设计中,地址总是从0开始,意味着CPU每次都是从4的倍数字节访问内存;那么,当一个数据被拆分到两个32bit时,CPU就必须要访问两次内存才能获取这个完整的数据。因此,C编译器为了保障效率,通常会将结构体的被定义的变量放在CPU能一次读取内存即可访问到的位置;对应到内存地址的表现上即为4字节对齐。

参考如下结构体设计,其最终的内存排布应该是怎么样的?

struct MemAlignTest
{
    uint16_t data0;
    uint8_t  data1;
    uint16_t data2;
    uint32_t data3;
    uint8_t  array[3];
    uint16_t data4;
    uint64_t data5;
};

按照其定义中的描述,这个结构体的大小应当为22字节;在开发者的主观意图中,是希望这些数据能按照地址从小到大依次排满整个内存空间;即按照如下排布:

基地址 \偏移 0x00 0x01 0x02 0x03
0x00000000 data0_B0 data0_B1 data1 data2_B0
0x00000004 data2_B1 data3_B0 data3_B1 data3_B2
0x00000008 data3_B3 array[0] array[1] array[2]
0x0000000C data4_B0 data4_B1 data5_B0 data5_B1
0x00000010 data5_B2 data5_B3 data5_B4 data5_B5
0x0000000C data5_B6 data5_B7 reserve reserve

通过如下代码进行查看:

    struct MemAlignTest align_test ;
    memset(&align_test,0xAA,sizeof(align_test));
    align_test.data0 = 0x1234;
    align_test.data1 = 0x88;
    align_test.data2 = 0xCCCC;
    align_test.data3 = 0xCCDDEEFF;
    align_test.array[0] = 0x11;
    align_test.array[1] = 0x22;
    align_test.array[2] = 0x33;
    align_test.data4 = 0xA9A9;
    align_test.data5 = 0x1234567800ABCDEF;
    printf("MemAlignTest size :%d\n",sizeof(align_test));
    memdump(&align_test,sizeof(align_test),8,4);

其输出结果为:
基于反序位域的大端协议处理方法

可以看到实际的数据长度未32字节,与我们预想的22字节差了10个字节,这就意味着因为对齐机制导致某些字节中间被强制插入了一些用户未定义的对齐保留字节;通过预置的一些立即数进行分析,我们可以了解到,其实际内存排布如下:

基地址 \偏移 0x00 0x01 0x02 0x03
0x00000000 0x34 0x12 0x88 0xAA
0x00000004 0xCC 0xCC 0xAA 0xAA
0x00000008 0xFF 0xEE 0xDD 0xCC
0x0000000C 0x11 0x22 0x33 0xAA
0x00000010 0xA9 0xA9 0xAA 0xAA
0x00000014 0xAA 0xAA 0xAA 0xAA
0x00000018 0xEF 0xCD 0xAB 0x00
0x0000001C 0x78 0x56 0x34 0x12

在了解实际的内存排布之前,我们需要先了解对齐的一些基本规则:

  1. 编译器希望CPU希望通过一内存访问即可读取到一个变量的所有数据;
  2. 编译器处理数据对齐时,数据存储的基地址一定是数据长度的整数倍;

但是在实际上,按照开发者的预想排列,data2所存储的地方位域0x0000_0003地址与0x0000_0004地址,以32bitCPU的寻址能力,无法一次性读取到data2的完整数据,因此编译器在0x0000_0003的地址中插入了1个字节的对齐保留字节;同理,直到0x0000_0010,即data4的存储位置前都符合这一规律。
在处理data4时,编译器同样按照规则1,将数据放在0x0000_0010地址中,并且在0x0000_0012~0x0000_0013位置保留出2字节;但是由于data5是64bit数据,按照规则2,由于0x0000_0014并不是8的整数倍地址,所以编译器还要继续保留4个字节,以保证data5能填充到基地址位0x0000_0018中。
所以实际这个结构体在内存中的排布方式应该是:

基地址 \偏移 0x00 0x01 0x02 0x03
0x00000000 data0_B0 data0_B1 data1 reserve
0x00000004 data2_B0 data2_B1 reserve reserve
0x00000008 data3_B0 data3_B1 data3_B2 data3_B3
0x0000000C array[0] array[1] array[2] reserve
0x00000010 data4_B0 data4_B1 reserve reserve
0x00000014 reserve reserve reserve reserve
0x00000018 data5_B0 data5_B1 data5_B2 data5_B3
0x0000001C data5_B4 data5_B5 data5_B6 data5_B7

在编译器的合理排列下,CPU总能在一次内存访问时获取结构体中的成员数值。

紧密排列的方法

如果不希望编译器对数据进行如此编排,可以通过

#pragma pack(1)
/*结构体定义*/
struct _sname
{
    /*……*/
};
#pragma pack()

对结构体进行定义;在两个pack的声明间隔之间,编译器会按照1字节对齐的方式编排结构体。

基于反序位域的大端协议传输方法

写在开头的一个防杠声明:
注意:以下规则仅在arm-none-eabi-gcc v5.3的ARM平台以及CygWin中进行实验,其他类似场景需要根据其适应性进行评估,并非绝对地可移植;可以理解为一个仅作为特例特用使用

使用场景说明

以下声明的方法主要依靠位域实现,而位域这一功能仅部分编程语言具备该功能,并且在不同平台、不同CPU、不同编译器中都有用差异,因此在实际使用该内容进行设计时,需要先验证平台的可用性,包括且不限于:

  1. 位域的对齐方法,是4Byte、8Byte对齐等;
  2. 编译器是否支持位域跨字节;
  3. CPU支持的大小端编码方法;
  4. 协议按照多少字节对齐进行设计;

例如,在MSVC下,由于位域的限制;跨越基础类型的位域定义还是会发生对齐现象,导致位域结构与设计结构并不相同,而使得以下转换方法受到限制。

原理

由于一些历史原因与主观原因,在嵌入式设备中,多数自定义协议以大端进行传输;大端的传输方式无疑在程序员对传输数据抓包解析时具备无可替代的优势;但是在以ARM为主流的小端CPU的协议程序设计中,将内存中以小端存储的数据转换为协议中的大端数据是极为不便的,多数情况需要开发者针对协议进行逐个bit移位以及模掩处理。

一般的数据传输总线设计中,比如UART、CAN、SPI以及I2C等;多数以MSB的比特顺序在物理层进行传输;即在这些类似的总线中,类似于0x1A传输的顺序是0b00011010b;我们以物理层为MSB传输、内存存储为小端的多数ARM处理器的情况进行讨论。

传输方向如下所示,以0x1A为例:

传输方向 ======>
数据 0 0 0 1 1 0 1 0
  • 32Bit数据的传输方法

在小端内存中,一个32bit的数据0x12345678,其实际的存储的形式为:

字节 Byte0 Byte1 Byte2 Byte3
HEX 0x78 0x56 0x34 0x12
Bin 0b01111000 0b01010110 0b00110100 0b00010010

如果协议中要求以大端传输,在MSB传输中则需要将Byte0与Byte3调转,Byte2与Byte1进行调转传输。所以在传输时,如果以大端传输的情况下,需要先传Byte3的MSB,那么,我们可以通过指针或者联合体,将32bit数据转换成8bit数组,并且从最后一个字节进行传输,此时我们观察Bit传输方向与数据传输方向如下:

Bit序 => => => =>
小端 0x78 0x56 0x34 0x12
字节序 <= <= <= <=

转换成通用大端的情况可以发现,传输路径如下:

Bit序 => => => =>
小端 0x12 0x34 0x56 0x78
字节序 => => => =>
  • 扩展位域

从前述的位域描述中可知,数据总是按照低字节向高字节堆叠,也就是数据同样可以通过反序进行排列,并从高Byte进行发送;但是问题在于,这个排列是否有限制?

以下的代码均在cygwin下进行运行
让我们针对一个Byte对齐的32bit数据结构展示,其结构如下:

typedef struct
{
    uint32_t datafield0 : 4;
    uint32_t datafield1 : 2;
    uint32_t datafield2 : 2;
    uint32_t datafield3 : 8;
    uint32_t datafield4 : 16;
}TestStruct;

让我们给它指定一些初始值,并且以Byte视角来看看它在内存中的样子:

int i = 0;
TestStruct temp;
uint8_t* ptr = (uint8_t*)&temp;
temp.datafield0 = 0xC;
temp.datafield1 = 0x1;
temp.datafield2 = 0x2;
temp.datafield3 = 0xA5;
temp.datafield4 = 0x1234;
for(i = 0; i < sizeof(temp); i ++)
{
    printf("0x%.2X ",ptr[i]);
}
printf("\n");

输出结果如下:

基于反序位域的大端协议处理方法

可以发现,datafield0被放在了Byte0的低4bit,而datafield1与datafield2被合并成一个0x9(0b1001)放在了Byte0的高4bit,当我们以字节高地址先发数据时,我们可以发现,所得的顺序刚好是整个结构体进行数据反转的大端格式,即是按大端传输的格式如下:

typedef struct
{
    uint32_t datafield4 : 16;
    uint32_t datafield3 : 8;
    uint32_t datafield2 : 2;
    uint32_t datafield1 : 2;
}TestStruct;

依此类推,可以发现在具备实体定义的位域结构中,这个规则是通用的。即:在MSB物理传输线中,从位域结构的高地址开始依次传输数据时,数据在物理总线的传输格式即是位域各个字段反转序列后的大端格式。

局限性

根据上述描述,在C/C++使用位域进行协议处理时,按照协议翻转顺序的方式进行定义,并以从高字节地址向低字节地址逐Byte进行数据传输时,可以借由位域与物理总线的特性将数据转换成大端,而不需要在代码中编写许多可读性极差并且难以维护的移位操作。
但是这种方式仍然存在许多局限性,比如:

  • 结构中的数组不适合该特性
    当结构中出现数组时,如果只是简单进行反序传输处理,在数组区域时会发现数组内的数据虽然是正常的大端格式,但是整个数组的排列也同样变成了从大索引到小索引的变化。
    因此,当传输的结构中出现数组时,需要在传输之前先将数组的元素排序预先进行反转。

  • 变长数据不适合该特性
    变长的数据通常以数组或者指针的形式存储在其他位置,而非存储在结构中;因此,针对变长的数据内容,需要进行分段处理,并在每一段中分别适合该特征。

  • 未对齐位置发生了反转
    当数据结构没有按照字节对齐时,正序进行设计的协议默认保留位置在协议末尾;此时如果以 #pragma pack(1) 进行1字节对齐的声明后,原本按照顺序定义的协议将在结构末尾设置未对齐的保留字段;但是在此设计中,需要将这样的未对齐字段在反序的结构头中进行显示声明,否则数据传输时,最先传输的便是未被设计到协议内的一些保留字段。

结语

本博文仅对在嵌入式领域使用的二进制协议大小端转换提出一种简单有效的处理方法,但该处理方法具备非常强的局限性;包括对编译器、CPU、开发平台以及编程语言具备非常强的依赖性;在移植过程中需要嵌入式开发者对自身的使用场景进行充分评估后再进行设计;该博文提出的方案也更偏向于 “特例特用” ;移植与使用过程均需要开发者谨慎处理,特别是针对编译器特性以及开发平台特性等内容,对该方法影响极大,具体如何使用需要进行评估后再决定。文章来源地址https://www.toymoban.com/news/detail-825227.html

到了这里,关于基于反序位域的大端协议处理方法的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • SpringBoot解决跨域的方法详细教程

    Spring Boot提供了多种解决跨域问题的方法,以下是其中几种常用的方法: 使用@CrossOrigin注解是一种简单且快速的解决跨域问题的方法。在Spring Boot的Controller类或方法上添加@CrossOrigin注解,可以指定允许跨域访问的源、方法、头部等信息。 1. 在Controller类或方法上添加@CrossOrig

    2024年02月13日
    浏览(41)
  • uniapp小程序位置信息配置

    uniapp 小程序获取当前位置信息报错 报错信息: getLocation:fail the api need to be declared in the requiredPrivateInfos field in app.json/ext.json 需要在manifest.json配置文件中进行配置:

    2024年02月11日
    浏览(37)
  • MODBUS TCP转CCLINK IE协议网关cclink通讯异常的处理方法

    你是否曾经遇到过需要将不同的设备连接到一个统一的网络中?或者你是否曾经遇到过设备之间的通讯协议不兼容的问题?捷米的JM-CCLKIE-TCP通讯网关就是为解决这些问题而设计的。   JM-CCLKIE-TCP通讯网关是一款自主研发的CCLINK IE FIELD BASIC从站功能的通讯网关,它的主要功能是

    2024年02月12日
    浏览(44)
  • 智能文档处理技术综述

    智能文档处理(Intelligent Document Processing, IDP)是利用人工智能(AI)、机器学习(ML)、计算机视觉(CV)、自然语言处理(NLP)等技术自动化地捕获、理解、处理和分析文档内容的过程。不同于传统的文档管理系统,IDP能够处理结构化、半结构化和非结构化的文档,从而提

    2024年03月27日
    浏览(65)
  • uni-app 应对微信小程序最新隐私协议接口要求的处理方法

    https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncementannounce_id=11691660367cfUvXversion=lang=zh_CNtoken= vue2版本 vue3版本 vue2版本 vue3版本(建议点击事件触发)  

    2024年02月07日
    浏览(67)
  • 自然语言处理基本任务综述

    ​ 在自然语言处理中,分词(Tokenization)是指将自然语言文本中的连续字符序列划分为一个个有意义的符号(token)的过程。分词是文本预处理中的一项基础任务,它是自然语言处理中的一环,而且是非常重要的一环。 ​ 在英文中,单词之间通常由空格隔开,因此英文单词

    2024年02月02日
    浏览(38)
  • 含源码|基于MATLAB的去雾系统(5种去雾算法+1种本文的改进算法)

    去雾系统V2包括作者新加入的 多尺度Retinex去雾算法以及改进去雾算法 ,以及 4种 评价去雾效果的 客观指标 。 引言 去雾系统新增功能 结果分析 源码获取 展望 参考文献 在作者前面写过的文章中,已经介绍过图像去雾算法的应用价值及研究现状,并且也介绍了4种去雾算法的

    2024年01月23日
    浏览(79)
  • 基于STOMP协议的WebSocket消息代理和相关的安全握手处理器以及消息拦截器

    这段代码是一个Spring配置类 WsChatConfig ,基于STOMP协议的WebSocket消息代理和相关的安全握手处理器以及消息拦截器。这个类通过实现 WebSocketMessageBrokerConfigurer 接口来定义WebSocket通信的路由、安全握手以及消息拦截的逻辑。 核心方法详解 configureMessageBroker(MessageBrokerRegistry confi

    2024年04月26日
    浏览(54)
  • 【论文解读】用于代码处理的语言模型综述

    目录 1.简要介绍 2.代码处理的语言模型的评估 3.通用语言模型 4.用于代码处理的特定语言模型 5.语言模型的代码特性 6.软件开发中的LLM 7.结论与挑战 ​​​​​​​ 1.简要介绍 在这项工作中,论文系统地回顾了在代码处理方面的最新进展,包括50个+模型,30个+评估任务和5

    2024年01月18日
    浏览(47)
  • H5中使用微信小程序位置API

    (1)vue导入 (2)链式导入 html的静态页面在前端通过ajax将url传到后台签名,前端需要用js获取当前页面除去’#‘hash部分的链接(可用location.href.split(’#\\\')[0]获取,而且需要encodeURIComponent),因为页面一旦分享,微信客户端会在你的链接末尾加入其它参数,如果不是动态获取当

    2024年02月15日
    浏览(32)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包