上一篇文章讲解了带位域的结构体,在从大端机(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. 理解Linux文件系统之inode

    很少转发别人的文章,但是这篇写的太好了. 理解inode   作者: 阮一峰 inode是一个重要概念,是理解Unix/Linux文件系统和硬盘储存的基础. 我觉得,理解inode,不仅有助于提高系统 ...

  2. 基于android的语音质量评估

    最近研究如何通过android评估通话质量,希望获取的参数有:(1)接通时长 (2)掉话次数 (3)语音是否清晰,以下将给出接通时长和掉话次数的详细定义: 接通时长:通话一方开始拨号到另一方开始振铃的 ...

  3. RabbitMQ 知识总结

    RabbitMQ知识总结 AMQP协议 AMQP协议是一个提供统一消息服务的应用层标准协议,并不会受到客户端/中间件不同产品.不同开发语言等条件的影响.RabbitMQ则是基于该协议实现的. 举个例子 ...

  4. html中ul元素水平排列问题

    <!DOCTYPE html> <html> <head> <style> #pic_list { display:block; white-space ...

  5. JAVA字符串操作 (转)

    JAVA字符串操作 原帖地址:http://blog.163.com/hn_myj@126/blog/static/50555635200861133942947/ 参考:http://blog.cs ...

  6. yii2 获取从前台传过来的post数据

    第一次使用yii写接口的时候,直接用了$_POST获取post数据,发现会报400错误,根本无法获取到post数据,用$_GET却能获取get数据. 纠结了很久,然后查资料,发现原来yii中默认的开启 ...

  7. HDU - 3853

    LOOPS Time Limit: 15000/5000 MS (Java/Others)    Memory Limit: 125536/65536 K (Java/Others)Total Sub ...

  8. Linux 组配置文件(/etc/group)

    一.概述 Linux 组配置(/etc/group)文件分为4个字段,分别为: 组名.组密码.GID和组成员. 二.示例 用户apple和banana的默认组为fruit. [root@titan ~ ...

  9. [Java] Java读取Word文档

    前言 最近需要做一些NLP 方面的工作,使用的是Java,在此总结一下使用Java读取Word(.doc)格式文件的方法. Apache基金会非常厉害,开源工具包POI就可以处理微软家的文档,甚至包括 ...

  10. python学习===如何理解python中的return

    首先要了解,函数是什么?书上可能会说函数是完成功能的模块之类的话.其实说白了,函数就是个你招来的工人.你给他一些材料,告诉他怎么用这些材料拼装,然后他负责把拼装好的成品交给你.材料就是函数的参数,成品 ...