Science for all





字符编码

字符编码

多年来美国人一直使用ASCII对字符编码,可以方便地交换英语文本。但不幸的是,ASCII完全不足以处理大多数其它语言的字符集。多年来不同的国家采用不同的技术交换不同语言的文本。最近,ISO推出了ISO 10646,用于表示世界上所有字符的单一31比特编码方案,被称为通用字符集(UCS)。可用16比特(UCS的前65536个字符)表示的字符被称为“基本多语言平台”(BMP),而且BMP意图覆盖所有口头语言。Unicode论坛推出了Unicode标准,注重于16比特字符集,并为了有助于互操作性增加了一些附加的约定。

尽管如此,大多数软件不是为处理16比特或32比特字符设计的,所以开发出一种叫做“UTF-8”的特殊格式来编码这些潜在的国际字符,现有的程序和库更容易处理这种格式。在IETF RFC 2279和其它一些地方有UTF-8的定义,所以它是一种可以自由阅读和使用的定义完好的标准。UTF-8是可变长度编码;从0到0x7f (127)的字符作为单个字节被编码为自身,更大值的字符则被编码为2到6个字节信息(依赖于具体值)。编码被特别设计以具有以下良好特性(取自RFC和Linux中utf-8的man帮助页):

  • 标准的US ASCII字符(0--0x7f)编码为自身,这样只包含7比特ASCII字符的文件和字符串在ASCII和UTF-8编码下是相同的。这对于许多已有美国程序和数据文件的反向兼容性而言是太好了。

  • 所有大于0x7f的UCS字符被编码为仅由0x80到0xfd范围内的字节组成的多字节序列。这就意味着ASCII字节不会变成另一个字符的一部分。许多其它的编码方法允许嵌入NIL的字符,会导致程序失效。

  • 在UTF-8和2字节或4字节固定长度的字符表示(分别被称为UCS-2和UCS-4)之间进行转换很容易。

  • UCS-4字符串保持了按字典分类的顺序,可以直接对UTF-8数据应用Boyer-Moore快速查找算法。

  • 所有可能的2^31个UCS代码都可以用UTF-8进行编码。

  • 表示单个非ASCII的UCS字符的多字节序列的第一个字节总是在0xc0到0xfd范围内,表示该多字节序列有多长。多字节序列的所有其它字节都在0x80到0xbf范围内。这就可以很容易地实行重新同步;如果丢失了某个字节,可以很容易地跳到“下一个”字符,而且总是可以很容易地跳到“前一个”或“后一个”字符。

简而言之,UTF-8转换格式正在成为交换国际文本信息的主要方法,因为它可以支持世界上的所有语言,而且还与美国的ASCII文件反向兼容,并具有其它一些良好特性。出于诸多理由,我推荐使用这种编码,特别是在把数据保存到“文本”文件时。

提及UTF-8的原因在于某些字节序列不是合法的UTF-8,而且可能成为可利用的安全漏洞。RFC提到了以下内容:

UTF-8的实现者需要考虑如何处理非法的UTF-8序列的安全方面的问题。可以想象,在某些情况下黑客可以通过发送一个UTF-8语法不允许的八进制序列,来利用一个不谨慎的UTF-8解释器。

对于执行对UTF-8编码形式的输入进行关乎安全的合法性检查的解释器,可以采用一个方式特别微妙的攻击,使得特定的非法八进制序列被解释为字符。例如,某个解释器可能禁止编码为单个八进制序列00的NUL字符,但允许非法的两个八进制序列C0 80,把它解释为一个NUL字符。另一个例子就是可能会有个解释器禁止八进制序列2F 2E 2E 2F(“/../”),但允许非法的八进制序列2F C0 AE 2E 2F。

有关此问题的较为充分的讨论可以在 http://www.cl.cam.ac.uk/~mgk25/unicode.html 上的Markus Kuhn的 UTF-8 and Unicode FAQ for Unix/Linux 里看到。

UTF-8字符集是可以列出所有非法值(并证明已经全部列出)的一种情况。如果要确定是否得到一个合法的UTF-8序列,需要检查两件事:(1)初始序列是否合法,(2)如果合法,是否第一个字节后面跟随了所要求的合法后续字符数目?进行第一项检查很简单,可以证明下面是所有非法UTF-8初始序列的完全列表:

Table 4-1. 非法UTF-8初始序列

UTF-8序列非法的原因
10xxxxxx作为字符的起始字节非法(80..BF)
1100000x 非法,过长(C0 80..BF)
11100000 100xxxxx 非法,过长(E0 80..9F)
11110000 1000xxxx 非法,过长(F0 80..8F)
11111000 10000xxx 非法,过长(F8 80..87)
11111100 100000xx 非法,过长(FC 80..83)
1111111x 非法;为规格说明所禁止

需要说明的是在某些情况下,可能会要松散地打断(或内部使用)十六进制序列C0 80。这是个可以代表ASCII NUL(NIL)的过长序列。由于C/C++在把NIL字符包含在普通字符串里会出问题,有些人在想要把NIL作为数据流的一部分时就使用该序列;Java甚至把这种方法奉为经典。在处理数据时可以自由地在内部使用C0 80,但从技术角度来说,在保存数据时应该把它转换回00。根据实际需要,可以自行决定是否“马虎”一下,把C0 80作为UTF-8数据流的输入。

第二个步骤是检查正确的后续字符数目是否包含在字符串中。如果第一个字节的头两个比特被置位了,那么数一下头一个比特位后被置位比特的个数,然后再检查是否有那么多以比特“10”开头的后续字节。因此,二进制数11100001就要求两个以上的后续字节。

与此有关的一个问题是在ISO 10646/Unicode中某些语句可以用多种方式表示。例如,有些带着重号的字符可以用单个字符(带着重号)表示,也可以用一组字符(如基本字符加上单独排版的着重号)来表示。这两种形式可能看起来完全一样。也可以在其中插入一个零宽度的空格,使看起来相似的二者有所区别。有些情况下要小心这样的隐藏文本会干扰程序。