字符编码

多年来美国人一直使用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帮助页):

简而言之,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中某些语句可以用多种方式表示。例如,有些带着重号的字符可以用单个字符(带着重号)表示,也可以用一组字符(如基本字符加上单独排版的着重号)来表示。这两种形式可能看起来完全一样。也可以在其中插入一个零宽度的空格,使看起来相似的二者有所区别。有些情况下要小心这样的隐藏文本会干扰程序。