14

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

 3 years ago
source link: http://yiiyee.cn/blog/2020/05/23/uefi%e5%bc%80%e5%8f%91%e6%8e%a2%e7%b4%a258-utf-8%e7%bc%96%e7%a0%81%e9%97%ae%e9%a2%98/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

669 total views, 2 views today


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK