上一篇文章讲解了带位域的结构体,在从大端机(Big Endian)传输到小端机(Little Endian)后如何解析位域值。下面继续深入详解字节序,以及位域存储的方式。

(1) 我们知道,存储数字时,对小端机而言,数字的低位,存在低地址,高位存在高地址。大端机正相反。

(2) 读取的方式,也是一样的。对于小端机,读出的低地址位作为数字的低位。

(3) 此外Big-Endian/Little-Endian存储顺序,不仅仅针对字节,还针对字节内的比特位。对于小端机而言,字节内的8个比特,低地址端比特位,对应二进制数字的低位。

(4) 对于结构体的多个位域,和普通成员一样,编译器同样按照地址由低到高顺序存储,无论是大端机还是小端机。只是位域内的比特顺序有区别罢了。

(5) 表述一个数值,可以使用两种视图 :

第一个是“逻辑视图”,通俗的表述方式,也就是我们平时在书本上看到的,手写数字时的方式。左边为高位,右边为低位。例如 375,-4.1,036,0xAF4D215B

另一个是“内存视图”,即数字在内存中的存储方式,是我们程序员专有的一种表述方式。左边为低地址字节,右边为高地址字节。字节内左边为低地址比特位,右边为高地址比特位。很明显,同一个unsigned int值,在大端机、小端机上,分别有两种不同的“内存视图”。

例如,uint16 0x2A1F,二进制比特位为0010 1010 0001 1111 (显然这一行使用的就是“逻辑视图”)

在小端机上的“内存视图”为:1111 1000 0101 0100 (低地址 -> 高地址)

在大端机上的“内存视图”为:0010 1010 0001 1111 (低地址 -> 高地址)

另外可以看到,大端机的"内存视图"和"逻辑视图"是相同的。在很多相关的文章里,并没有去区分数字的两种表述方式,导致了很多混淆。其次,很多例子使用16进制,只能用于表达字节序,无法精确表达内部的比特顺序。

再举一个上一节使用过的例子:

typedef struct _exam_
{
  unsigned int tag : 6;
  unsigned int field1 : 3;
  unsigned int field2 : 7;
  unsigned int field3 : 11;
  unsigned int pad : 5;
}Exam; Exam ex;
ex.tag = 4;
ex.field1 = 2;
ex.field2 = 0x3a;
ex.field3 = 0x4C1;
ex.pad = 0;

  

变量ex的6个位域的"内存视图",在大端机是000100 010 0111010 10010110001 00000(低地址->高地址),在小端机是001000 010 0101110 10001101001 00000(低地址->高地址)。可见位域顺序是一样的,但是位域内比特位顺序不同。

若按照4位一组,大端机"内存视图"为 0001 0001 0011 1010 1001 0110 0010 0000,如果按照unsigned int的方式读取这块内存,结果是0x113A9620,四个比特位对应一个16进制数,和"逻辑视图"完全一样

在小端机上4位一组排列,"内存视图"为 0010 0001 0010 1110 1000 1101 0010 0000,如果按照unsigned int的方式读取这块内存,就会按照小端机的方式来解析内存。可以先把二进制翻译为"逻辑视图" - 把整个"内存视图"32位颠倒顺序,结果是0x04B17484,注意不是0x212E8D20.

那么这些规则,对位域值的读取有什么影响呢?

字节流在网络上传输是按照网络字节序传输的,也就是大端序。网卡不知道数据的含义(到底是int还是double,还是什么image),只能看到一个个字节,因此它做的就是把每个字节的8个比特位转换为本机的位序。而具体的内容,则由我们的程序处理。比如对于整形等,调用socket接口的ntohl(),htonl()...等函数转换字节序。顺便提一句,对于float/double类型,可以直接memcpy到一个整形里面,之后按照整形正常的处理流程,到了目标机后,再memcpy到一个float/double里。

char,short,int,long等2次幂大小的整形,作为一个单独的整体,经过整个流程梳理是没有任何问题的。但无法保证结构体内的多个位域,按照定义的先后顺序,从低地址到高地址排列。这意味着,无论如何,直接在代码中使用ex.tag的方式,是读不出tag位域的数据的。

细分有如下几种情况:
(1) 主机内部传输无任何影响,毕竟是一样的CPU架构。

(2) 相同字节序的主机间传输,同样没有影响。因为经过二次socket+网卡转换后,码流是相同的。读者可自行验证。

(3) 大端机传输到小端机(上一节所描述的)。下列二进制值如没有特殊说明,都是"内存视图"。

还以上面的位域为例,在大端机的为 (低地址->高地址),按照四比特一组为: 0001 0001  0011 1010  1001 0110  0010 0000

传输到网络中,由于大端序和网络序相同,所以网卡不做转换,字节流按照先后,依然是 0001 0001  0011 1010  1001 0110  0010 0000

传输到小端机,网卡自动转换每个字节的比特序,但字节顺序维持原状, 00 1000 0101 110 0110 1001 0000 0,可见原先跨字节相连的位域被"打散了"。字节内的位域,虽然比特顺序对了,但是从低比特位挪到了高比特位,位置错了。

调用ntohl,比特序不变,转换字节序,0000 0100 0110 1001 0101 1100 1000 1000,效果是跨字节位域再次连通了。位域内存地址顺序,正好和原先相反。如果把大端机的内存视图画到一张纸上,相当于翻到纸的背面。

此时,将这4个字节码流当作unsigned int,得到一个"无符号整形",其"逻辑视图"等于大端机上的“内存视图”。左边恰好是结构体最开始的位域:0001 0001  0011 1010  1001 0110  0010 0000。因此我们将错就错,直接使用位操作符来左移相应的位数(需要计算后边所有位域的总比特数),即可得到对应的位域值。位移操作符等,都是对"逻辑视图"操作的。

(4) 小端机传到大端机。网卡转换+ntohl转换后,依然在内存中得到一个位域顺序和正常顺序相反的"无符号整形"。只是这次使用位运算符要注意,第一个位域在"逻辑视图"的最右边,依次向左类推,和(3)的情形是相反的。

字节序转换与结构体位域(bit field)值的读取 Part 2 - 深入理解字节序和结构体位域存储方式的更多相关文章

  1. 字节序转换与结构体位域(bit field)值的读取

    最近又遇到了几年前遇到的问题,标记一下. 对于跨字节位域(bit field)而言,如果数据传输前后环境的字节序不同(LE->BE,BE->LE),简单地调用(ntohs/ntohl/ht ...

  2. socket编程相关的结构体和字节序转换、IP、PORT转换函数

    注意:结构体之间不能直接进行强制转换, 必须先转换成指针类型才可以进行结构体间的类型转换, 这里需要明确的定义就是什么才叫强制转换. 强制转换是将内存中一段代码以另一种不同类型的方式进行解读, 因此转 ...

  3. 套接字编程相关函数(1:套接字地址结构、字节序转换、IP地址转换)

    1. 套接字地址结构 1.1 IPv4套接字地址结构 IPv4套接字地址结构通常也称为“网际套接字地址结构”,它以sockaddr_in命名,定义在<netinet/in.h>头文件中.下 ...

  4. c/c++字节序转换(转)

    字节序(byte order)关系到多字节整数(short/int16.int/int32,int64)和浮点数的各字节在内存中的存放顺序.字节序分为两种:小端字节序(little endian)和大 ...

  5. 顶级c程序员之路 选学篇-1 深入理解字节,字节序与字节对齐

     深入理解字节,字节序与字节对齐 一 总述 作为一个职业的coder玩家,首先应该对计算机的字节有所了解. 我们经常谈到的2进制流,字节(字符)流,数据类型流(针对编程),结构流等说法,2进制流,0和 ...

  6. C#字节数组转换成字符串

    C#字节数组转换成字符串 如果还想从 System.String 类中找到方法进行字符串和字节数组之间的转换,恐怕你会失望了.为了进行这样的转换,我们不得不借助另一个类:System.Text.Enc ...

  7. 三联运算&&字节码转换

    三联运算 if 1 == 1: name = 'alex'else: name = 'sb' name = 'alex' if 1 == 1 else 'sb lambda f2 = lambda a ...

  8. Swift超详细的基础语法-结构体,结构体构造器,定义成员方法, 值类型, 扩充函数

    知识点 基本概念 结构体的基本使用 结构体构造器(构造函数/构造方法) 结构体扩充函数(方法), 又称成员方法 结构体是值类型 1. 基本概念 1.1 概念介绍 结构体(struct)是由一系列具有相 ...

  9. JAVA IO分析一:File类、字节流、字符流、字节字符转换流

    因为工作事宜,又有一段时间没有写博客了,趁着今天不是很忙开始IO之路:IO往往是我们忽略但是却又非常重要的部分,在这个讲究人机交互体验的年代,IO问题渐渐成了核心问题. 一.File类 在讲解File ...

随机推荐

  1. vue 实现 tomato timer(蕃茄钟)

    近期在学习[时间管理]方面的课程,其中有一期讲了蕃茄工作法,发现是个好多东西.蕃茄工作法核心思想就是:工作25分钟,休息5分钟.如果您好了解更多可以自行度娘. 在加上本人是一个程序猿,就想用程序的方式 ...

  2. [bzoj 2243]: [SDOI2011]染色 [树链剖分][线段树]

    Description 给定一棵有n个节点的无根树和m个操作,操作有2类: 1.将节点a到节点b路径上所有点都染成颜色c: 2.询问节点a到节点b路径上的颜色段数量(连续相同颜色被认为是同一段),如“ ...

  3. 机器学习 —— 基础整理(四)特征提取之线性方法:主成分分析PCA、独立成分分析ICA、线性判别分析LDA

    本文简单整理了以下内容: (一)维数灾难 (二)特征提取--线性方法 1. 主成分分析PCA 2. 独立成分分析ICA 3. 线性判别分析LDA (一)维数灾难(Curse of dimensiona ...

  4. HTML5 开发APP( 环境配置)

    上一篇我写了关于新建项目,这一篇说一下配置环境我们新建一个移动app后,会发现一个mainifest.json文件,开发app所要配置的环境就在这个文件里 点击打开文件后会有配置界面,在配置界面的下方 ...

  5. 基于.NET CORE微服务框架 -谈谈Cache中间件和缓存降级

    1.前言 surging受到不少.net同学的青睐,也提了不少问题,提的最多的是什么时候集成API 网关,在这里回答大家最近已经开始着手研发,应该在1,2个月内会有个初版API网关,其它像Token身 ...

  6. tomcat的常用配置方法

    1.tomcat配置某个站点的欢迎页面的方法 2.tomcat配置虚拟目录的方法 3.tomcat配置虚拟主机的方法

  7. spring-boot整合mybatis(1)

    sprig-boot是一个微服务架构,加快了spring工程快速开发,以及简便了配置.接下来开始spring-boot与mybatis的整合. 1.创建一个maven工程命名为spring-boot- ...

  8. Redis-aof持久化

    什么是redis的aof? aof 是 appendonly file 的缩写, 是redis系统提供的一种记录redis操作的持久化方案, 在aof生成的文件中, 将记录发生在redis的操作, 从 ...

  9. 菜鸟帮你跳过openstack配置过程中的坑

    一:前言 对于一个以前做java全栈工程师而言,而且没学过Linux,很少用虚拟机(还是在大学的时候简单的用过),去配置openstack我想我入的坑肯定比有基础的一定要多,躺在每个坑中徘徊思索的时间 ...

  10. C# 开机自启动和最小化托盘显示

    C# 开机自启动和最小化托盘显示 一.      C# 开机自启动 C# 开机自启动,这个功能是大多数服务型软件很常用一个功能,但是这个功能确是不 太好做,花了两天想对策.不过最终呢,结果还是很满意的 ...