1、 前语
作为一名 C/C++ 程序员,字节是咱们天天都要与之打交道的一个东西。咱们和它熟稔到简直现已忘记了它的存在。可是,它自己是不甘寂寞的,或迟或早地,总会在某些时分探出面来张望,然后给你一个腿儿绊。其实,只需你实在了解了它的内幕,你就会畅行无阻。在本文中,咱们将首要扼要了解一下字节的概念,然后侧重了解一下字节序问题和字节对齐问题。
注:笔者现已尽最大尽力确保本文信息的正确性,但的确无法供给百分之百的担保。
2、 什么是字节
咱们知道,二进制核算机(也便是咱们现在触摸到的简直一切的核算机)的最小数据单位是位( bit )。一位数据只能够表明两种意义(需求阐明,虽然咱们一般把单个位表明的两种意义挑选为彼此敌对的意义,但这并不是必定的,例如你能够以为 1 代表 5 个人, 0 代表 8 个人),关于绝大多数的核算要求,单个位明显不能满意。因而,咱们一般都会运用一连串的位,咱们能够称之为位串( bit string ,请喜好质疑的的朋友留意,此术语非我臆造)。因为种种原因,核算机系统都不会让你运用恣意长度的位串,而是运用某个特定长度的位串。一些常见的位串长度办法具有约定好的称号,如,半字节( nibble ,形似用的不多)代表四个位的组合,字节( byte ,主角进场!)代表 8 个位的组合。再多的还有,字( word )、双字( Double word ,一般简写为 Dword )、四字(Quad word ,常常简写为 Qword )、十字节( Ten byte ,也简写为 Tbyte )。
在这些里边,字( word )有时表明不同的意义。在 Intel 系统里, word 表明一个 16 位的数值,它是固定巨细的。而在别的一些场合, word 表明了 CPU 一次可处理的数据的位数,表明一个契合 CPU 字长( word-length )的数目的位串。实践上咱们触摸较多的 ARM 系统中, word 就有不同的意义,它表明一个 32 位的数据(与机器字长相同),关于 16 位巨细的数据, ARM 运用了别的的一个术语,叫作半字( half-word ),请咱们在文档阅览时加以留意。别的, Qword 也是 Intel 系统中的术语,其他的系统中或许并不运用。在本文中,咱们依照 Intel 的常规来运用字或许 word 这一术语。
一个字节中共有 8 个数据位,有时需求用图表逐位表述各个位。习气上,咱们依照下面的图来摆放各个位的次序,即,依照从右到左的次序,顺次为最低位(从第 0 位开端)到最高位(关于字节,则是第 7 位):
字节是大多数现代核算机的最小存储单元,但这并不代表它是核算机能够最高效地处理的数据单位。一般的来说,核算机能够最高效地处理的数据巨细,应该与其字长相同。在现在来讲,桌面渠道的处理器字长正处于从 32位向 64 位过渡的时期,嵌入式设备的根本稳定在 32 位,而在某些专业范畴(如高端显卡),处理器字长早现已达到了 64 位甚至更多的 128 位。
3、 字节序问题的由来
关于字、双字这些多于一个字节的数据,假如把它们放置到内存中的某个方位上,能够看出,咱们还能够将之看作是字节的序列。一个字是两个字节,双字则是四个字节。假定有以下数据: 0x12345678 、 0x9abcdef0 。在此处,我运用了咱们最习气的十六进制表明法,并给出了两个双字的值。依照常规,我把双字的左边视为高端,而把右侧视为低端。把它们次序放置在开端地址为 0 的内存中,如下图所示:
由图示可知, 0x9abcdef 的相应地址为 0x04 。现在,问题来了,假如有一个内存操作,要从地址 0x06 处读取一个字,得到的成果是多少呢?答案是:不用定。
这儿的实质问题在于,怎么把多字节的目标存储到内存中去呢?即便运用最正常的思想去考虑这个问题,你也会发现有两种办法。榜首种办法是,把最低端的字节放到指定的开端方位(即基地址处),然后依照从低到高的字节次序把其他字节顺次放入,如下图 a ;另一种办法十分相似,可是对高端字节和低端字节的处理次序正好相反,如下图 b (我坚信你还能够想出其他的办法,可是除二字节的状况外,必定会打破字节摆放次序的共同性,我视之为反常规思想的产品,此处暂不考虑)。
图 a
图 b
在好久之前,哪一种存储办法更为合理从前有过争辩。到今日,争辩的成果现已无关重要了,重要的是以下实践:这两种存储办法都被运用到了实践的核算机系统中。上图 a 中的摆放办法为 Intel 所选用并大行其道,而图 b的摆放办法则被大多数的其他渠道选用(如最近被苹果公司完全扔掉的 PowerPC ),因而上,咱们不能称之为稀有的用法。之所以形成实践上的不常常见到,其原因正如我今日正午所得到的音讯: Intel 的 CPU 占整个市场份额的 80% 以上。
这两种摆放办法一般用小端( little endian )和大端( big endian )来称谓。这两个古怪的姓名听说来源于神话《格列佛行记》,其间小人国里的公民为了鸡蛋到底是应该从小的一头翻开仍是大的一头翻开而大起争论。 Intel的办法对应于“小端”,趁便说一句,大端的办法也有一个大公司的姓名作为其代表,即最近开端衰败的 Motorola。假如有谁了解过 TIFF 图像文件格局,就会发现其文件头中用以标识文件数据字节序的标志便是“ II ”和“ MM”,别离对应于 Intel 和 Motorola 的首字母。值得提示一下,小端办法的摆放与位的摆放次序相共同,看上去好像更和谐一些。
现在咱们能够答复上面的问题了。关于小端字节序,咱们取到的字,其值为 0x9abc ,而假如是大端字节序的话,就会取到 0xdef0 。
4、 何时会出现字节序问题
字节序问题首要出现在数据在不同渠道之间进行交流时,交流的途径或许是网络传输,也或许是文件仿制。例如,假如你规划了一种或许会运用于不同渠道的文件格局,其间存储了某些数据结构,则关于巨细大于一个字节的数据就要明确地规矩其遵从的字节序,以便各渠道上的处理程序能够在运用数据时完结做必要的转化。
举一个实践的比如。 Java 是一个跨渠道的编程言语,其可履行文件(扩展名为 .class ,运用的是一种机器无关的字节码指令集)在理论上能够运转于一切的完结了 Java 运转时的渠道(包括有与特定渠道相关特性的在外)。编译后的 .class 中必定保存有比如 Integer 这样类型的数据,这就触及到了字节序的确认,不然 .class 必定不能被选用了不同字节序的渠道一起正确加载并运转。实践上, Java 言语选用的为大端字节序,这个一点都不古怪,因为最初 SUN 公司自己的 SPARC 架构便是选用的大端字节序。相同的问题和解决问题的办法,也存在于操作系统新贵 android 系统上。
网络传输则是另一个典型场景。 TCP/IP 所选用的网络传输字节序规范也是大端字节序,这个也不用古怪,因为 TCP/IP 是从 UNIX 系统发展起来的,而绝大部分的 UNIX 系统在很长的一段时间内都没有运转于 Intel 系统架构上的版别。
处理字节序问题的手法十分简略,也便是对数据进行必要的转化:将十六进制的数字从两头开端交流,直至移动到数据的中心,交流完结停止。交流的成果就好像物体与镜面之内的成像互换了方位,因而也被称为镜像交流(mirror-image swap )。请参看下图:
5、 怎么在程序中判别字节序
在实践的作业中,有时需求对字节序进行判别,然后予以不同的处理。一般的来说,编译后的程序一般只能运转在特定的渠道之上,其所选用的字节序办法在编译时即可确认,在这种状况下,程序源代码中一般是把字节序的判别作为条件编译的判别句子,而不会判别代码放在实在的可履行代码中。
在这儿,需求运用咱们的老朋友 —— 宏。以下是一个实在的跨渠道工程中代码,明晰起见,我稍做了修正:
#define SGE_LITTLE_ENDIAN 1234
#define SGE_BIG_ENDIAN 4321
#ifndef SGE_BYTEORDER
#if defined(__hppa__) || /
defined(__m68k__) || defined(mc68000) || defined(_M_M68K) || /
(defined(__MIPS__) && defined(__MISPEB__)) || /
defined(__ppc__) || defined(__POWERPC__) || defined(_M_PPC) || /
defined(__sparc__)
#define SGE_BYTEORDER SGE_BIG_ENDIAN
#else
#define SGE_BYTEORDER SGE_LITTLE_ENDIAN
#endif
#endif
以上为依据渠道的预界说宏所作的前期作业,将之存入一个头文件中,然后包括到源代码文件中运用。
在需求进行判别的时分,则像以下代码这样运用:
#if SGE_BYTEORDER == SGE_BIG_ENDIAN
#define SwapWordLe(w) SwapWord(w)
#else
#define SwapWordLe(w) (w)
#endif
因为这两个宏实践上被界说成了常量数值,因而也能够被用到可履行代码中,进行履行期的动态判别:
if(SGE_BYTEORDER == SGE_BIG_ENDIAN)
return r << 16 | g << 8 | b;
else
return r | g << 8 | b << 16;
追根寻源,上面的这种判别需求依靠编译器及其地点渠道的预界说宏。下面介绍一种履行期动态判别的办法,则不需求有宏的参加,而是奇妙有利地势用了字节序的实质。代码如下:
int IsLittleEndian()
{
const static union
{
unsigned int i;
unsigned char c[4];
} u = { 0x00000001 };
return u.c[0];
}
着手画一下内存布局即可了解其原理。还有更简练的写法,作为操练,请咱们自行去寻觅。
在完毕对字节序的评论之前,特别提示一下, ARM 系统的 CPU 在字节序上与 Intel 的系统结构是共同的。
6、 字节对齐问题的发生
冯诺依曼系统的核算机,经过地址总线来寻址内存(假定 n 为地址总线的位数,则最多能够寻址 2n 个内存方位)。依据地址总线的位数,咱们能够知道 CPU 与内存的一次交互(也即一次内存拜访)能够读写的数据的巨细。明显地,关于 8 位的 CPU ,是一个字节,关于 16 位 CPU 则是一个字, 32 位 CPU 则是一个双字,依此类推。这是 CPU 与生俱来的最实质、最方便的拜访办法。在实践的核算需求中,假如拜访的数据量超越了一次拜访的极限,则很明显需求进行屡次拜访,假如是少于的话,则需求对从内存中取回的数据进行恰当的裁剪。裁剪操作有或许是CPU 本身支撑的,也有或许是需求用软件来完结的。
有的系统是支撑寻址到单个字节地点的方位的(称为可字节寻址),而有的则不能够,只能寻址到契合某些条件的地址上。关于 Intel/ARM 系统结构的 CPU ,咱们在微观上能够以为它们都支撑字节寻址(可是 ARM 宗族的CPU 在内存拜访时有其他束缚,下文有具体叙说)。
出现这样的束缚是有原因的,终极要素就在于内存拜访的粒度与字长的相关上。用 32 位 CPU 来说,它关于地址为 4 的倍数处的内存拜访是最天然的,其他的地址就要做一些额定的作业。例如,咱们要拜访地址为 0x03 处的一个双字,关于 80×86 系统,实践上将会导致 CPU 的两次内存拜访,取回 0x00 以及 0x04 处的两个双字,别离进行恰当的截取之后再组装为一个双字回来。关于其他的系统,规划者或许以为 CPU 不应该承当数据组装的作业,因而就挑选发生一个硬件反常。
在硬件和 / 或操作系统的束缚下,进行数据拜访时对数据地点的开端方位以及数据的巨细都需求遵从必定的规矩 ,与这些规矩相关的问题,都能够称之为字节对齐问题。
举例来说。在 HP-UX (惠普公司的一个服务器产品渠道, UNIX 的一种)渠道中,系统禁止对奇地址直接进行拜访,假定你视这一准则于不管:
int i = 0; // 编译器确保 i 的开端地址不是奇地址
char c = *((char*)&i + 1); // 强制在奇地址处拜访
其履行成果便是内核转储( core dump ),为运用程序最严峻的过错。(特别注明:此处代码为回忆中的景象,现在笔者现已没有验证环境了)
在不同的硬件系统架构下,字节对齐关系到三方面的问题,一是数据拜访的可行性问题,二是数据拜访的功率问题,三是数据拜访的正确性问题。
字节对齐问题给程序员在编码时带来了额定的留意点,而且对终究程序履行的正确性也带来了必定的不确认要素。相同的代码在不同的渠道上,甚至在相同的渠道上选用不同的编译选项,都或许有不同的履行成果。
假如一切的系统都和 HP-UX 的体现相同的话,工作要简略一些,问题一般会在比较早的时间内就能够露出出来。惋惜的是,咱们现在所面临的渠道不是这样,这些渠道的规划者为最大程度地削减对开发人员的搅扰而作了辛苦的尽力,使得咱们在许多时分都感觉不到字节对齐问题的存在。但另一方面,也制作出了把问题躲藏得更深的时机。
作用最好的尽力是 Intel 的系统架构。 80×86 答应你对整个内存进行字节寻址,在不超越机器字长的状况下能够拜访恣意数目的字节(很明显,大多数状况下便是 1 字节、 2 字节、 3 字节、 4 字节这四种状况)。
ARM 系统的 CPU 好像做了必定的尽力,可是其成果和其他系统比较出现一种很古怪的状况。因为笔者没有对ARM 整个系列的 CPU 进行过完好的了解,因而此处的论说或许并不完好。 ARM CPU 答应对内存进行字节寻址,但在拜访时有额定的要求。即:假如你要拜访一个字(留意本文常规,此处的字是两字节巨细,与 ARM 渠道的规范术语不同),那么开端地址有必要在一个字的鸿沟上,假如拜访一个双字,则开端地址有必要坐落一个双字的鸿沟上(其他数据类型请参阅 ARM 的知识库文档)。这意味着,你不能在 0x03 这样的地址处拜访一个字或许一个双字。可是,令人苦楚的工作到来了,假如你非要这么拜访,大多数的 CPU 不会有显式的反常,而是回来过错的数据,其他的一些 CPU 则会形成程序溃散。