上一篇文章讲解了带位域的结构体,在从大端机(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. redhat设置开机自动连接网络

    一.设置开机自动连接网络1.用root账号登录2.打开etcsysconfignetwork-scrpts目录3.vi ifcfg-eth04.将ONBOOT改为yes 二.没有图形界面如何连接网络1 ...

  2. docker~使用阿里加速器安centos

    回到目录 上一篇说了hub.docker.com里拉个镜像太,而阿里云为我们做了不少本国镜像,这样下载的速度就很惊人了,下面看一下在centos7下配置阿里云加速器的方法 打开服务配置文件 vi /e ...

  3. SpringMVC原理及非注解配置详解

    1. Spring介绍 Spring MVC是Spring提供的一个强大而灵活的web框架.借助于注解,Spring MVC提供了几乎是POJO的开发模式,使得控制器的开发和测试更加简单. 这些控制器 ...

  4. NOIP模拟:能源(二分答案)

    题目描述 小美为了拯救世界能源危机,她准备了 n 台蓄电池.一开始每台蓄电池有 ai 个单位的能量. 现在她想把 n 台蓄电池调整到能量相同.对于每台蓄电池可以给另一台蓄电池传递能量.但是会有能量损耗 ...

  5. [js] webgl 初探 - 绘制三角形

    摘要: 1. webgl 概念挺多的, 顶点着色器.片段着色器, 坐标 2. 绘制前期准备工作好多 目前看的比较好的教材: https://developer.mozilla.org/zh-CN/do ...

  6. 【ECHART】实例

    1. 带timeline   datazoom的例子 <!doctype html> <html> <head> <meta http-equiv=" ...

  7. Java操作Excel和Word

    这是一个URL它提供了Java项目所推荐的处理此项目所用的类库 http://www.oschina.net/project/tag/258/excel-tools?company=0&sor ...

  8. Android学习笔记-TextView(文本框)(一)

    1.基础属性详解: id:为TextView设置一个组件id,根据id,我们可以在Java代码中通过findViewById()的方法获取到该对象,然后进行相关属性的设置,又或者使用RelativeL ...

  9. Linux修改网卡名称、主机名

    Linux修改网卡名称.主机名 环境:VirtualBox 5.0.14 + RHEL 6.5 需求:个人实验搭建一套Standby RAC时,为了节约时间,直接复制之前安装RAC的主机模板. 但复制 ...

  10. 消息队列NetMQ 原理分析5-StreamEngine、Encord和Decord

    消息队列NetMQ 原理分析5-StreamEngine,Encord和Decord 前言 介绍 目的 StreamEngine 发送数据 接收数据 流程分析 Encoder V2Encoder V1 ...