UEFI开发探索58-UTF-8编码问题

请保留-> 【原文:  https://blog.csdn.net/luobing4365 和 http://yiiyee.cn/blog/author/luobing/】

这篇要记录的知识,实际上不限于UEFI编程,在其他编程上也一样会遇到。只是因为最近这段时间,沉迷于国产机器开发的项目,频繁地调试UEFI代码,遇到了这些问题,我觉得应该有普遍性,姑且记录下来。

在日常的编程中,特别是处理字符串以及显示汉字的时候,会被各种编码搞得糊里糊涂。毕竟编码是给计算机程序看的,记忆起来还是比较费事,即使当时搞清楚了,过一段时间不用,好像又会混淆。

1 历史

计算机从美国发展起来,最早是使用1个字节来表示各种字符和控制符。0x020以下的用来做控制,称为为“控制码”;0x20以上直至0x7F,用来表示各种英文字母、标点和各种符号的编码。

这种编码方式,就是我们现在熟知的ANSI的ASCII编码。其后计算机广泛发展,曾经也使用过0x7F~0xFF用来表示新的符号和字模,比如交叉等形状,这些字符集被称为“扩展字符集”。

在中国,我们有上万汉字,几个个常用字,使用1个字节明显无法表示完。我们制定了一个汉字的解决方案:小于0x7F的字符的意义与原来相同,但两个大于0x7F的字符连在一起时,就表示一个汉字,前面的一个字节(高字节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE,这样就可以组合出大约7000多个简体汉字了。

在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的“全角”字符,而原来在0x7F号以下的那些就叫“半角”字符了。

这种方案称为GB2312,是对ASCII的扩展。后来发现还是不够用,于是干脆取消了低字节必须大于0x7F的规定了,只要高字节大于0x7F,就是汉字了。这个方案就是GBK,它比GB2312多了2万多个新汉字和符号。再后来增加几千少数民族的字,GBK扩展成了GB18030。

好了,我们汉字这么搞,其他国家也制定自己的文字编码方案。互相之间谁都不懂,也不支持。于是ISO(国际标准化组织)出来搞定这件事,他们准备废了所有地区性编码方案,建立一套新的编码方案,包括地球上所有文化、所有字母和符号的编码,取名为“Universal Multiple-Octet Coded Character Set”,简称 UCS, 就是我们常说的 UNICODE方案。

图1 我的Unicode启蒙书

这让我记起了大学时候看的《Windows程序设计》,如图1,就是从这本书中,我才了解到怎么在Windows程序中使用Unicode。

至此,我们了解了ASCII、GB2312和Unicode的来源和它们想要解决的问题了。

不过,事情还没有结束。

Unicode规定了如何编码,并没有规定如何传输、保存这个编码。在发展过程中,出现了UTF-8、UTF-16等应用比较广泛的标准,UTF是“UCS Transformation Format”的缩写。

Unicode 可以使用的三种编码,分别是:

(1) UFT-8:一种变长的编码方案,使用 1~6 个字节来存储;
(2) UFT-32:一种固定长度的编码方案,不管字符编号大小,始终使用 4 个字节来存储;
(3) UTF-16:介于 UTF-8 和 UTF-32 之间,使用 2 个或者 4 个字节来存储,长度既固定又可变。

其中,只有 UTF-8 兼容 ASCII,UTF-32 和 UTF-16 都不兼容 ASCII,因为它们没有单字节编码。

因此,我们主要关注UTF-8。UTF-8就是以8位为单元对Unicode进行编码,从Unicode到UTF-8的编码方式如下:

Unicode(UCS-2)(十六进制) UTF-8(二进制)
0000 – 007F 0xxxxxxx
0080 – 07FF 110xxxxx 10xxxxxx
0800 – FFFF 1110xxxx 10xxxxxx 10xxxxxx

比如“罗”的Unicode编码为0x7F57(https://www.qqxiuzi.cn/bianma/zifuji.phphttp://www.mytju.com/classcode/tools/encode_utf8.asp上可查),它大于0x800,必须采用3个字节来编码UTF-8了。0x7F57转换为二进制是0111 111101 010111,替代表格中的x部分,则UTF-8编码为11100111 10111101 10010111,也就是0xE7BD97。可以使用UltraEdit等工具来验证下(存为UTF-8文档,使用二进制查看)。

UTF-8以字节为编码单元,没有字节序的问题。UTF-16以两个字节为编码单元,在解释一个UTF-16文本前,首先要弄清楚每个编码单元的字节序。例如“奎”的Unicode编码是594E,“乙”的Unicode编码是4E59。如果我们收到UTF-16字节流“594E”,那么这是“奎”还是“乙”?

Unicode规范中推荐的标记字节顺序的方法是BOM,BOM是Byte order Mark的简称,简介如下:

在UCS编码中有一个叫做”ZERO WIDTH NO-BREAKSPACE”的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议我们在传输字节流前,先传输字符”ZERO WIDTH NO-BREAK SPACE”。

这样如果接收者收到FEFF,就表明这个字节流是Big-Endian的;如果收到FFFE,就表明这个字节流是Little-Endian的。因此字符”ZERO WIDTH NO-BREAK SPACE”又被称作BOM。

UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符”ZERO WIDTH NO-BREAKSPACE”的UTF-8编码是EF BB BF。所以如果接收者收到以EF BBBF开头的字节流,就知道这是UTF-8编码了。

2 遇到的问题

我一直使用VS cdoe编写代码,如图2,它支持多种编码方式存储源代码。

图2 VS code编码选择

在使用Vs code编写UEFI代码时,我常遇到两种问题:

一是汉字的注释。在图2中也能看到,有些注释会直接用汉字来写。在编写UEFI代码之前,我常用的编辑器是UltraEdit和Vim。涉及到嵌入式下的汉字,会习惯性的用GB2312编码来保存源文件。而VS code在Linux下缺省是用UTF-8的,打开这些源文件,注释就变成乱码了。

解决方法也很简单,VS code右下角有“Select Encoding”的蹿下,重新用UTF-8编码保存下源码就行了。

二是汉字字符串的问题。这个问题比较复杂,涉及到源代码的编码存储、编译器对字符串的识别。到程序运行的时候,会出现各种意外的情况。

具体介绍见下一小节。

3 UEFI下汉字字符串

这实际上不光是UEFI代码的问题,实际上与编译器有很大关系。以下的实验,都是在Windows10上,使用VS2015编译UEFI代码。

先上一个例子。随便找一个UEFI的代码,在主程序中加入这一行:

UINT8 *s_text = “朝辞白帝彩云间”;

编译之后,提示这样的错误:

c:\myworkspace\RobinPkg\Applications\Luo2\Luo2.c(119): error C2001: 常量中有换行符

我的源文件是用UTF-8来存储的,注释的汉字也显示正常。这个错误是怎么产生的呢?

从源头上来说,这是Visual Studio应该背的锅。在VS中,缺省是以Unicode(准确地说,是UCS-2)存储的,对于汉字,在UTF-8下,一般是3个字节存储的。也就是说,s_text是奇数个字节,VS认为这是不可原谅的,就报了这个错误。

可以试着把s_text变为偶数个汉字,或者加一个英文字符(数字、英文的符号都行),让字符串变量变为偶数个字节,这个错误就不会报了。

这个类似的问题曾经有人报给过微软,是关于UTF-8 ROM的,不过网页已经找不到了。微软的员工回应说,设计就是这样的,你丫忍着吧!

上面只是开个玩笑,微软员工的回应其实从技术角度看是很有道理的,而且也有解决办法。再做一个实验,在主程序中添加如下代码,把汉字编码打印出来:

UINT8 *str=”严格”;
    UINT8 *ptr;
    ptr=str;
    Print(L”str:”);
    while(*ptr!=’\0′)   {
      Print(L” %02x”,*ptr);
      ++ptr;
    }
Print(L”\n”);

以UTF-8编码存储源代码,编译运行,结果为:

str: E4 B8 A5 E6 A0 BC

以GB2312编码存储源码,编译运行,结果为:

str: D1 CF B8 F1

也就是说,在源码以不同编码存储的时候,字符串中的值已经以相应的编码改变了(废话,这是当然的)。

例子中,我特意以偶数个汉字来做实验的,所以没有提示错误。那么,如果我就是想以UTF-8来保存源码,该怎么办呢?

可以在编译的时候,在cl的参数中,添加/utf-8(VS中文版默认以GB2312存储源码的,代码页为936,默认行尾为CRLF)。或者,用硬编码的方式表示汉字,比如这样的:

char *str=”\xe6\x9c\x9d\xe8\xbe\x9e”;

真是很不友好的代码,估计维护人员看了后会想杀人。上面这串字符串,实际就是“朝辞”的汉字UTF-8硬编码,我是非常不喜欢这种方式。

当然,如果修改Conf\tools_def.txt中的命令行参数,会影响其他程序的编译。可以在模块的inf文件中,最后的[BuildOptions]部分,添加这么一行:

MSFT:*_*_*_CC_FLAGS = /utf-8

后面所有的源文件都用UTF-8编码存储,就可以了。可以在编译命令行下,查看与此相关的参数说明(cl -?),节选如下:

/source-charset:<iana-name>|.nnnn set source character set (源码使用此编码集)
/execution-charset:<iana-name>|.nnnn set execution character set  (执行使用此编码集)
/utf-8 set source and execution character set to UTF-8 (源码和执行都用UTF-8)

注意,上面的讨论都是在Windows平台上的,Linux(gcc)下没这个问题。

(查询Unicode码的网站:https://unicode-table.com/cn

115 total views, 18 views today

发表评论

电子邮件地址不会被公开。