刨根究底字符编码之九——字符编码方案的演变与字节序

字符编码方案的演变与字节序

一、字符编码方案的演变

1.

前文已经提及,编号字符集CCS(简称字符集)与字符编码方式CEF(简称编码方式)这两个概念,在早期并没有必要严格区分。

在Unicode编码方案出现之前,字符集及其具体的编码方式是绑定耦合在一起的,因此,“字符集”、“编码”或“编码方式”甚至“编码方案”这几个概念经常相互指代、彼此混用。

比如,字符集里的字符编号(即码点编号)在很多文章里也称之为字符编码、字符码、码点、码位、码点值、码值等,字符编码也称之为编码实现、编码方案、编码方式、编码格式、编码形式、内码、编码值、码值(你没看错,字符编号与字符编码都有可能被简称为“码值”,头大了吧),等等,非常混乱。

2.

对于ASCII、GB2312、GBK、GB18030、Big5之类采用传统字符编码模型的历史遗留方案来说,由于基本上一个字符集只使用一种编码方式,因此这种混用问题还不大。

但在Unicode这样采用现代字符编码模型的全新方案出现之后,很多人受上述这些历史遗留方案的影响,从而导致无法正确地理解字符集和编码方式的关系,这导致了概念混乱,引起了大量误解。

然而,对于采用现代字符编码模型的Unicode标准来说,字符集和编码方式是必须明确区分的。从软件工程的角度来讲,传统字符编码模型中紧密绑定耦合在一起的字符集及编码方式这两个概念,在现代字符编码模型中被分离解耦了,而这种解耦带来了极大的灵活性。

这意味着,对于采用现代字符编码模型的同一个字符集,可以采用多个不同的编码方式对其字符编号进行编码。也因此,作为同一个Unicode字符集,目前就定义了UTF-8、UTF-16和UTF-32等(UTF,即Unicode/UCS Transformation Format)多种可选的编码方式。

3.

所以,用Unicode来称呼一个编码方式已不合适,并且容易产生误导,引发混乱和导致困惑,而应该用UTF-8、UTF-16和UTF-32来称呼编码方式,以Unicode来称呼字符集,将包括Unicode字符集及各UTF编码方式等在内的整体称之为字符编码方案或字符编码系统或字符编码标准。

另外,同一字符编码方式CEF的码元序列,在计算机实际处理、存储和传输时,还需再次编码转换为字符编码模式CES的字节序列。

字符编码方式CEF的码元序列可理解为字符编码的逻辑表示形式,相对而言,字符编码模式CES的字节序列则可理解为字符编码在计算机中的物理表示形式

而字节序列,则涉及到了不同的字节序(Byte-Order,主要分为大端序Big-Endian、小端序Little-Endian)。

二、字节序

1.

字节序,又称字节顺序,其英文为Byte-Order;另外英文中还可称为Endianness,因此也翻译为端序。

Endianness这个词,源自于1726年Jonathan Swift的名著:Gulliver's Travels(格列佛游记)。在书中有一个童话故事,大意是指Lilliput小人国的国王下了一道指令,规定其人民在剥水煮蛋时必须从little-end(小端)开始剥。这个规定惹恼了一群觉得应该要从big-end(大端)开始剥的人。事情发展到后来演变成了一场战争。后来,支持小端的人被称为little-endian,反之则被称为big-endian(在英语中后缀“-ian”表示“xx人”之意)。

1980年,Danny Cohen在他的论文“On Holy Wars and a Plea for Peace”中,第一次使用了Big-endian和Little-endian这两个术语,最终它们成为了异构计算机系统之间进行通讯、交换数据时所要考虑的极其重要的一个问题。

(注:所谓异构是指不同架构、不同结构、不同构造等,而这里的异构计算机系统,主要指的是采用不同CPU和/或不同操作系统的计算机系统。)

2.

为什么会存在字节序的问题?

当然不会是像童话故事里那样出于“无厘头”的原因,而是因为历史上设计不同计算机系统的人在当时基于各自的理由和原因(这里的理由和原因网上存在着各种不同的说法,但也或许根本就没有具体理由和原因,只是设计人员的个人偏好,甚至是随意决定的),在各自计算机系统的设计上作出了不同的选择。

字节序共分为三种:

大端序BE(Big-Endian,也称高尾端序);

小端序LE(Little-Endian,也称为低尾端序);

中间序ME(Middle-Endian,也称为混合序),不太常用,本文不作介绍。

3.

字节序,具体来说,就是多字节数据(大于一个字节的数据)在计算机中存储、读取时其各个字节的排列顺序。

字节序也被称为端序,这里的“端”,是指多字节数据中位于两端的字节,很多情况下还特指尾端字节(也称为小端字节)。

所谓尾端字节或小端字节,假设按照人对文字通常从左到右(或从上到下)的读写顺序来看的话,多字节数据位于右端(或下端)的低位字节就是尾端字节或小端字节,而将位于左端(或上端)的高位字节称为头端字节或大端字节。

当然,如果不按照通常从左到右的顺序,而是按照从右到左的顺序,那么多字节数据位于右端的高位字节就是头端字节或大端字节,而将位于左端的低位字节称为尾端字节或小端字节。

可见,不论读写顺序如何,所谓大端、头端,指的是多字节数据中,代表更大数值的那个字节所在的那一端,而相反的那一端则是小端、尾端。

4.

而要彻底讲清楚大端序(高尾端序)、小端序(低尾端序),则需要从人读写二进制数的方向和内存地址的增长方向两者相结合讲起:

人读写二进制数的方向为(这是确定不变的)左--->右,大端/头端/高位--->小端/尾端/低位;或上--->下,大端/头端/高位--->小端/尾端/低位;

内存地址的增长方向则为(这是确定不变的)左--->右,低地址--->高地址;或上--->下,低地址--->高地址。

不过,计算机在内存中存取数据的方向则不是确定不变的,而是分为两种(注意,由于人的读写方向和内存地址增长方向是确定不变的,因此这里指的是计算机在内存中书写阅读数据的方向)

1) 左--->右,大端/头端/高位--->小端/尾端/低位;或上--->下,大端/头端/高位--->小端/尾端/低位;

这种情况下,站在人的读写方向和内存地址增长方向(这两者的方向刚好一致)的角度来看,则是:大端在左(或在上),所以称之为大端序;或者说尾端在内存高地址,所以称之为高尾端序(即内存高地址存放多字节数据的尾端字节的字节顺序)。

2) 右--->左,大端/头端/高位--->小端/尾端/低位;或下--->上,大端/头端/高位--->小端/尾端/低位。

这种情况下,站在人的读写方向和内存地址增长方向(这两者的方向刚好一致)的角度来看,则是:小端在左(或在上),所以称之为小端序;或者说尾端在内存低地址,所以称之为低尾端序(即内存低地址存放多字节数据的尾端字节的字节顺序)。

【特别提示:大端序、小端序特别容易搞混,不好记忆;因此,建议使用高尾端序、低尾端序,本人是按下面这个方法来记忆的,很容易记住:存储的数据分头和尾——左头右尾,内存的地址分低和高——左低右高;因此,“高尾端”表示内存的地址存储数据的尾端,“低尾端”表示内存的地址存储数据的尾端。】

5.

注意,这里的“数据”指的是数据类型意义上的数据,因此,准确地说字节序只跟多字节的整型数据类型有关,比如intshortlong型;跟单字节的整型数据类型byte无关。

那么,为什么就只跟多字节的整型数据有关,而跟单字节的整型数据无关呢?

因为在现代计算机中,字节是计算机数据存储的基本单位,对于整体上的单一字节(a byte),涉及到的是其8个比特位的顺序(位序、比特序,由于一般直接由硬件处理,程序员大致了解即可,这里不深入探讨),显然不存在字节序问题。

然而,对于在数据类型上作为一个整体的多字节数据而言,它是由各个可被单独寻址的字节所组成的(处理器寻址的最小单位一般是1个字节),由于历史的原因,其各个字节的存储顺序在不同的系统平台(包括CPU和操作系统)上是不同的。

6.

也就是说,如果计算机处理的数据是单字节数据类型(byte),是不存在字节序问题的,因为单个字节已经是处理器寻址的最小单位以及存储和传输的最小单元了,存储时单字节数据类型直接进行,读取时也不存在根据前后2个字节才能解析出其值的情况,而构造字节流时也不会从一个单字节数据类型的值当中产生2个或以上的字节(既然是单字节数据类型,构造字节流时当然只可能产生1个字节)。

但是,如果计算机处理的数据是多字节数据类型(int、short、long等),虽然由于构成它们的2个或2个以上的字节是作为一个整体来进行处理的(比如以汇编语言中的数据类型word或dword为单位进行一次性处理,而不是以byte为单位分次处理;更深入地来讲,CPU一般是以字为一个整体来处理数据的,当单个数据不足一个字长时,则将多个数据“拼成”一个字再进行处理),但问题是字节才是CPU对内存寻址的最小单位以及存储和传输的最小单元。

因此,CPU在读取作为一个整体来进行处理的多字节数据类型的数据时,必须根据前后2个或2个以上的字节来解析出一个多字节数据类型的值;而且构造字节序列时也会从一个多字节数据类型的值当中产生2个或2个以上的字节。

7.

这样一来,多字节数据类型的数据内部各字节间的排列顺序,是会影响从字节序列恢复到数值的;反之,也会影响从数值到字节序列的构造。

所以,在存储和读取多字节数据类型的数据时,必须按照计算机系统所规定的字节序进行(这一点程序员了解即可,计算机会自动处理);而尤其是在跨字节序不同的异构计算机系统进行通讯并交换数据时,通讯的任何一方更是必须明确对方所采用的字节序,然后双方将各自接收到的数据按各自的字节序对数据进行转换(有时候需要程序员专门编写转换程序),否则通讯将会出错,甚至直接失败。

8.

实际上,int、short、long等数据类型一般是编程语言层面的概念,更进一步而言,这其实涉及到了机器硬件层面(即汇编语言)中的数据类型byte字节、word字、dword双字等在硬件中的表达与处理机制(实质上字节序跟CPU寄存器的位数、存放顺序密切相关)。具体可参看附文:《本质啊本质之一:数据类型的本质》、《寄存器与字、字节》。

【附:本质啊本质之一:数据类型的本质

CSDN博客 博主:band_of_brothers 发表于:2007-10-10 22:20

研究一个层面的问题,往往要从更深的层面找寻答案。这就如C语言与汇编、汇编与机器指令,然而终究要有个底限,这个底限以能使我们心安理得为准,就好比公理之于数学、三大定律之于宏观物理。

在这里就将机器指令作为最后的底限吧,尽管再深入下去还有微指令,但那毕竟是太机器了,可以了。以下所有从C代码编译生成汇编代码用的是命令:cl xxx..c /Fa /Ze。

类型的本质

类型这个概念,好多地方都有讲,但说实话,你真的理解吗?什么是类型?类型是一个抽象的概念还是一个真实的存在?嗯?

开始:

1)“好多相同或相似事物的综合”(辞海)。

2)X86机器的数据类型有byte、word、dword、fword、tword、qword,等等。

3)“给内存块一个明确的名字,就象邮件上的收件人一样。给其一个明确的数据类型,就好象说,邮件是一封信,还是一个包裹。”

4)类型就是一次可以操作的块的大小,就是一个单位,就像克、千克、吨一样。双字一次操作32位;字,一次操作16位;如果没有各种类型,机器只有一个类型单位——字节,那么当需要一个4字节大小的块时,就需要4次操作,而如果有双字这个类型单位,那么只需要一次操作就可以了。

5)类型,是机器层面支持的,不是软的,是硬的,有实实在在的机器码为证。

类型的反汇编

W32dasm反汇编出来的东西,可以看出不同的类型,机器码不同,说明类型是机器硬件级别支持的,不是通过软件实现的,更不是一个抽象的概念。

Opcodes上关于mov的机器码讲的更清楚:

需要说明的是,一些大的类型单位,如qword等,在mov等标准指令里是没有的,在一些特殊指令里才能用到,如浮点指令:fmul qword ptr [0067FB08] 机器码:DC0D08FB6700。】

【附:寄存器与字、字节

字节:记为byte,一个字节由8个比特(bit)组成,可以直接存在一个8位寄存器里

1 0 1 0 1 0 0 1

    一个字节

字:记为word,一个字由2个字节(共16比特)组成,可以直接存在一个16位寄存器里

 1 1 1 1 1 1 1 1  0 0 0 0 0 0 0 0

      高位字节         低位字节

一个8位寄存器用2位十六进制数表示,只能存1个字节(1个byte类型的数据)

一个16位寄存器用4位十六进制数表示,可存1个字(1个word类型的数据)或2个字节(2个byte类型的数据)

一个32位寄存器用8位十六进制数表示,可存2个字(1个dword类型的数据或2个word类型的数据)或4个字节(4个byte类型的数据)】

(笨笨阿林原创文章,转载请注明出处)

9.

下面简要介绍一下字节序的三种类型:

a) 小端序Little-Endian(低尾端序)

就是低位字节(即小端字节)存放在内存的低地址,而高位字节(即大端字节)存放在内存的高地址。

这是最符合人的直觉思维的字节序(但却不符合人的读写习惯),因为从人的第一观感来说,低位字节的值小,对应放在内存地址也小的地方,也即内存中的低位地址;反之,高位字节的值大,对应放在内存地址大的地方,也即内存中的高位地址。

b) 大端序Big-Endian(高尾端序)

就是高位字节(即大端字节)存放在内存的低地址,低位字节(即小端字节)存放在内存的高地址。

这是最符合人平时的读写习惯的字节序(但却不符合人的直觉思维),因为不用像在Little-EndIan中还需考虑字节的高位、低位与内存的高地址、低地址的对应关系,只需把数值按照人通常的书写习惯,从高位到低位的顺序直接在内存中从左到右或从上到下(下图中就是从上到下)按照由低到高的内存地址,一个字节一个字节地填充进去。

 c) 中间序Middle-Endian(混合序Mixed-Endian

混合序具有更复杂的顺序。以PDP-11为例,32位的0x0A0B0C0D被存储为:

混合序较少见,常见的多为大端序和小端序。

(注:其实还一种网络字节序(network byte order网络字节顺序、网络序),这里不做深入介绍。)

10.

Intel和AMD的X86平台,以及DEC(Digital Equipment Corporation数字设备公司,后与Compaq合并,之后Compaq又与HP合并)采用的是Little-Endian,而像IBM、Sun的SPARC采用的就是Big-Endian。有的嵌入式平台是Big-Endian的。JAVA字节序也是Big-Endian的。

当然,这不代表所有情况。有的CPU既能工作于小端序,又能工作于大端序,比如ARM、Alpha、摩托罗拉的Power PC、SPARC V9、MIPS、PA-RISC和IA64等体系结构(具体情形可参考处理器手册),其字节序是可切换的,这种可切换的特性可以提高效率或者简化网络设备和软件的逻辑。

这种可切换的字节序被称为Bi-Endian(前缀“Bi-”表示“双边的、二重的、两个的”),用于硬件上意指计算机存储时具有可以使用两种不同字节序中任意一种的能力。具体这类CPU是大端序还是小端序,和具体设置有关。如Power PC可支持Little-Endian字节序,但其默认配置为Big-Endian字节序。

11.

一般来说,大部分用户的操作系统,如Windows、FreeBsd、Linux是Little-Endian的;少部分,如Mac OS是Big-Endian的。

具体参见下表:

12.

所以说,Little Endian还是Big Endian与操作系统和CPU类型都有关系。因此在一个计算机系统中,有可能同时存在大端序和小端序两种字节序的现象。

这一现象为系统的软硬件设计带来了不小的麻烦,这要求系统设计工程师必须深入理解大端序和小端序的差别。大端序与小端序的差别体现在一个处理器的寄存器、指令集、系统总线等各个层次中。

其实,很多技术人员在实际的非跨平台桌面应用开发过程中都很少会直接和字节序打交道,但在跨平台及网络应用开发过程中因为涉及到异构计算机系统间的通讯交流,字节序是很难回避的问题。

(笨笨阿林原创文章,转载请注明出处)


上一篇:刨根究底字符编码之八——Unicode编码方案概述

下一篇:刨根究底字符编码之十——Unicode字符集的字符编码方式CEF

推荐阅读更多精彩内容