上一篇文章讲解了带位域的结构体,在从大端机(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 显示文本指定行内容

    主要采用sed.head和tail命令 如果文本中使用了 \n 这类符号,cat命令会把它当成换行符,结果会出错 $ sed -n "10p" move.sh   # 显示第10行 ...

  2. 深入理解 JavaScript(二)

    立即调用的函数表达式 前言 大家学 JavaScript 的时候,经常遇到自执行匿名函数的代码,今天我们主要就来想想说一下自执行. 在详细了解这个之前,我们来谈了解一下"自执行"这 ...

  3. bzoj1012: [JSOI2008]最大数maxnumber [单调队列]

    Description 现在请求你维护一个数列,要求提供以下两种操作:1. 查询操作.语法:Q L 功能:查询当前数列中末尾L个数中的最大的数,并输出这个数的值.限制:L不超过当前数列的长度.2. 插 ...

  4. 有关java 8

    http://www.iteye.com/news/27608     Java 8 发布时间敲定,延期半年 http://www.iteye.com/news/24631/   Java 8 的重要 ...

  5. C语言指针2(空指针,野指针)

    //最近,有朋友开玩笑问 int *p  *是指针还是p是指针还是*p是指针,当然了,知道的都知道p是指针 //野指针----->>>指没有指向一个地址的指针(指针指向地址请参考上一 ...

  6. WordPress中函数钩子hook的作用及基本用法

    WordPress 的插件机制实际上只的就是这个 Hook 了,它中文被翻译成钩子,允许你参与 WordPress 核心的运行,是一个非常棒的东西,下面我们来详细了解一下它.钩子分类 钩子分为两种,一 ...

  7. 配置SSH隧道访问Ubuntu服务器上的MongoDB

    为了数据安全,在MongoDB的配置文件里,一般会把默认的27017端口port改为自定义的端口号,然后把允许访问的IP设为127.0.0.1(即主机本身).但是这样就会在开发的过程查看数据时带来麻烦 ...

  8. 斗地主[NOIP2015]

    题目描述 牛牛最近迷上了一种叫斗地主的扑克游戏.斗地主是一种使用黑桃.红心.梅花.方片的A到K加上大小王的共54张牌来进行的扑克牌游戏.在斗地主中,牌的大小关系根据牌的数码表示如下:3<4< ...

  9. ASP.NET 导出excel文件出现乱码的解决办法

    string html =TABLE ;//<table>标签,可以是多张表string modified = Regex.Replace(html, "<table &g ...

  10. java大数取余

    java大数取余: 类方法:BigInteger.divideAndRemainder() 返回一个数组,key = 0为商key = 1为余数 import java.util.*; import ...