PS:要转载请注明出处,本人版权所有。
PS: 这个只是基于《我自己》的理解,
如果和你的原则及想法相冲突,请谅解,勿喷。
环境说明
普通的linux 和 普通的windows。
VS2015 和 GCC 7.0
前言
曾记得,我在(https://blog.csdn.net/u011728480/article/details/100277582 《数与计算机 (编码、原码、反码、补码、移码、IEEE 754、定点数、浮点数)》)里面说过,计算机里面存储了数值和符号。数值包含定点数和浮点数。符号包含文字及其他符号。
那符号在计算机中是怎么表示的呢?这里首先就要引出一个概念叫做字符集,就是“人为”记录归纳的某种文字或者符号在这个集合中的索引(例如:Ascii 中的'a' 的索引为97)。有了这个字符集合后,我们怎么在计算机中表示这个字符集合呢?毕竟我们设计这个字符集合就是为了在计算机里面显示相关的符号的。这里我们就要引出一个字符编码的概念,就是在计算机中怎么表示相关的符号(例如:Ascii 我们发现基本的字符集只规定了128个。于是我们用一个字节来标识这个字符集中的字符即可。)。注意这里提到的Ascii即可指字符编码也可指字符集,至少我是这样理解的。
一直以来,我反正只是大概模糊的记得,ascii 代表字母及其他基本字符。 gb2312/gbk/gb18030/big5 ... 代表的就是多字节的中文及其他字符。unicode 和 utf-8 可以代表世界上大部分国家及地区的字符符号,同时unicode是两字节的,utf-8是多字节的(注意,关于unicode的说法其实有点小毛病,详见下文)。我早就想详细的了解和记录一下这些说法的含义,但是一直没有一个较好的机会。
最近做的一个项目里面,要在内存里面查找和对比中文(c++ 里面存的中文,lua要去找和对比),这NM可把我苦惨了,于是我一狠心,就要把这个问题搞明白,不求全部搞清楚,但求我能够怎么解决这个问题,以及以后遇到这些问题怎么处理。
下面是一些常见及不常见短语说明:
- UNICODE(The Unicode Consortium(统一码联盟,是大公司组合的联盟),一种字符集)
- UCS(Universal Multiple Octet Coded Character Set(通用多字符编码集,是ISO标准委员会提出的),一种字符集)
- UTF/UTF-8/UTF-16(一种基于UNICODE的字符编码格式,UTF是UCS Transfer Format的简写)
- GB2312/GBK/GB18030(全国信息技术标准化技术委员会出版的3个版本的字符集及字符编码格式)
- ASCII(American Standard Code for Information Interchange,常见的基本英文编码)
- DBCS(Double-byte character set)
- ANSI(不知道是什么的简写,但是只会出现在windows平台)
- 本地化/国际化(本地化和国际化是一个比较坑的概念,本地化可以简单理解为把文字和符号显示给对应区域的人。 国际化就是把文字和符号显示给尽量多的人。我也不知道这样翻译对不对QAQ)
常见的字符集及对应的字符编码规则说明
字符集是存放的人为定义的一个字符索引集合。 字符编码是考虑怎么把这个字符集合在计算机中表示出来。
常见中文字符集及字符编码(GB2312/GBK/GB18030)
关于GB2312/GBK/GB18030的详细说明大家可以去网上找找资料详细了解,他们有许多的历史因素在里面。我这里就只做简单的说明。
首先GB2312/GBK/GB18030是三个国标的简称。是全国信息技术标准化技术委员会参考或者说是对接ISO提出的字符集/字符编码方法,然后出版的适合中国特色的字符集/字符编码标准。注意这里的GB2312/GBK/GB18030既可以称作字符集也可以称作字符编码,我们好像常用是把这个三个当做字符编码,但是没有强调字符集是什么,所以我觉得这个是三个即是字符集又是字符编码。下面对这三个字符编码规则进行简单的说明,这些规则里面可能有些历史原因小故事在里面,感兴趣的人去网上找找看,我这里不做无用功了。
GB2312是我国第一个字符集/字符编码。其使用2个字节代表一个汉字,而且为了兼容Ascii,约定两个编码字符都必须大于0xA0(每个字节都大于127,可以区分出Ascii与GB2312)。也就是说,GB2312的编码范围为:0xA1A1~0xF7FE。而且由于标注出版的比较早,里面只包含了常见的汉字和非汉字内容。
GBK是对GB2312的扩展。同样也是使用2个字节代表一个汉字。首先GBK原封不动的继承了GB2312的编码,同时编码范围由0xA1A1~0xF7FE 扩展到0x8140~0xFEFE。多余GB2312的这些编码,又添加了一些cjk的汉字和符号,同时也提供了自定义文字区域编码的。
GB18030是2005年出的最新的中文字符集/中文字符编码。它是变长字节编码方式,和utf系列很像。下面进行简略的说明:
- 1字节,0x00~0x7F 兼容Ascii
- 2字节,0x8140~0xFEFE 兼容GBK
- 4字节,0x81308130~0xFE39FE39 存放其他文字和符号,例如我国的少数民族的文字、繁体汉字、日韩汉字等等。
这里多说一句,采用变长编码的原因是节约字符存储空间或者说是为了节约网络传输带宽。
常见的Unicode字符集与UTF系列编码
上面我们介绍了中文的字符集及字符编码,可以想象的是,非中文,非英文地区的人,也会做和我们同样的事情,他们会定义适合他们自己的字符集,并定义适合他们自己的字符编码。那就直接炸裂了,因为每个地区都有自己的字符集和字符编码,非常不适合各个地方的人们文字交流。与此同时,网络使得各个地方的人们交流更加的频繁,于是有些人就不爽了,想定义一个字符集来包含全世界的所有字符,这样人们交流的时候就不需要对字符进行专门的转码。
于是国际标准委员会和一个叫做统一码联盟的组织分别起草了一个字符集,分别是UCS 和 UNICODE。后面考虑到大家都是做的同样的事情,于是两个字符集合并了,叫做UCS/UNICODE。我们常见的是UCS-2/UNICODE。这个字符集里面包含了全世界大部分的文字和符号。其表示范围大概是0x000000 到 0x10FFFF。 UNICODE 字符索引一般表示为U+0x00AAAA
在定义UCS/UNICODE这个超大字符集后,肯定想定义一个字符编码才符合这些组织的身份。于是产生了UTF字符编码系列的格式。我们常见的就是UTF-8/UTF-16 BE/UTF-16 LE/UTF-32 BE/UTF-32 LE格式。
UTF-32简要说明:直接用4个字节表示UNICODE字符串, 例如索引U+0xABCDEFAA 就表示为0xABCDEFAA(BE 大端) 或者 0xAAEFCDAB(LE 小端)。
UTF-16简要说明(windows常用编码,与UTF-32一样有类似的字节序存在):
- U+0x0000 到 U+0xFFFF 用2个字节表示。
- U+0x1 0000 到 U+0x10 FFFF 用4个字节表示。
下面对UTF-8进行简要的说明:
- 1字节,0000/0000-0000/007F(hex), 二进制填充方式:0xxx xxxx(binary)
- 2字节,0000/0080-0000/07FF(hex), 二进制填充方式:110x xxxx/10xx xxxx(binary)
- 3字节,0000/0800-0000/7FFF(hex), 二进制填充方式:1110 xxxx/10xx xxxx/10xx xxxx(binary)
- 4字节,0001/0000-0010/FFFF(hex), 二进制填充方式:1111 0xxx/10xx xxxx/10xx xxxx/10xx xxxx(binary)
对应的编码范围是:
- 0~127
- 128~2047
- 2048~65535
- 65536以上
UTF-8的实现方式就是查出字符索引:U+0xABCD(U+43981) ,可以看到落在的编码范围是3字节范围,也就是2048~65535。于是我们看到的二进制还有16个位置,恰好,我们的编码的二进制也是16个。从左到右依次放入对应位置的x即可。0xABCD二进制为:1010 1011 1100 1101, 对应的UTF-8编码为: 1110 (1010)/10(10) (1111)/10(00) (1101)
本地化和国际化
上面我们介绍了两个系列的字符集和对应常用的字符编码。GBXXX系列是CJK区域的多字节编码,UNICDOE/UTF系列是全球大多数通用字符集及编码。那么为了我们发布的计算机文件能够在全世界方便的使用,我们有两种方案:
- 使用区域性多字节编码,例如我们发布的文件,携带多种区域性字符编码文件(GBXXX/阿拉伯的编码等等),在不同地区的电脑上,根据系统的地区(win和linux都有,很重要,设置区域),使用不同的区域字符编码文件进行显示。
- 直接使用UNICDOE/UTF系列,全球通用。
看起来,直接使用UNICDOE/UTF系列就完事儿了,花里胡哨,弄那么多。其实不然,你看了UTF-8,对我们中文区来说不公平,因为大部分都是3字节,而对于Ascii区域来说,他们的UTF-8,大部分都是1字节。这NM就坑了撒,难道我大中华的硬盘或者带宽就无限了?其次,可能有些我们可以在GB系列里面定义的偏门字符内容,可能UNICODE里面没有,对于一个足够大的市场来说,如果连他们的文字符号都表示不完,那还玩个D。于是也需要有GB系列这种区域性的来补充,换句话说,就是看实际应用。这就是软件本地化和国际化的意义,里面最要命的就是字符问题。
生活中常见的几个有趣小实验(猜到就让你嘿嘿嘿)
下面我们做一些比较有趣,而且常见的小实验。
VS的Unicode字符集 和 多字符集选项(cl.exe)
在我们编程的时候,特别是要涉及中文编程的时候,很多时候需要和这个选项打交道,也就是如图。那么这两个选项有啥区别呢?请听我慢慢道来。
这个选项的主要作用是用来帮助 cl.exe 确认启用什么样的Api,也就是我们常说的W结尾的还是A结尾的Api。下面我们用下面的小程序来实验一波。
#include <cstdio>
#include <windows.h>
int main(int argc, char * argv[]) {
const char *_str = "卧槽";
printf("_str's mem = %x %x %x %x\n", 0xFF & _str[0], 0xFF & _str[1], 0xFF & _str[2], 0xFF & _str[3]);
const wchar_t *_str_1 = L"卧槽";
printf("_str's mem = %x %x\n", 0xFFFF & _str_1[0], 0xFFFF & _str_1[1]);
//MessageBoxW(NULL, L"卧槽", L"U", MB_OK);
//MessageBoxA(NULL, "卧槽", "M", MB_OK);
system("pause");
return 0;
}
运行以上代码我们可以得到下图的内容。
然后我们根据以上的内容,通过二进制编辑器,构造了两个txt文件。然后通过vs code 不同解码下打开。才能够得到正确文字内容。
下面我们来解释Window Api中 A系列和W系列的区别。 例如在MessageBoxW(NULL, L"卧槽", L"U", MB_OK)和 MessageBoxA(NULL, "卧槽", "M", MB_OK)中,我们传入的参数一个是char *的,一个是wchar_t *,通过我们打印,可以发现对应的内存数据是完全不一样的,也就是说对应的文字编码是完全不同的。A系列对应的GB18030编码(多字节,区域编码,不同地区,可能就不是gb系列的编码了),W系列对应的UTF-16 BE编码(UNICODE)。那么,它们代表啥意思呢?
如果我们用A系列的Api,那么就是用的多字节编码,也就是对应的本地区域编码,在我们这个CJK区域,能够正常显示文字,但是如果不在我们CJK区域的话,极有可能就出现乱码。也就是说,通过A系列弄出来的程序,很有可能就只能够在我们CJK区域使用,其他区域可能需要用源代码,在其他对应区域的VS编译一下,才能够正常使用程序。
如果我们用W系列的Api,那么用的就是UTF-16 BE编码,由于UNICODE是针对全球大多数语言来做的一个字符集,那么意味着,我们这个程序只需要编译一次,把二进制分发到全世界大多数区域也能够正常使用的。
GCC的-finput-charset/-fexec-charset=gbk选项
首先GCC的默认把源文件用UTF-8解码,如果遇到不支持的字符,需要使用-finput-charset来帮助才行。然后,我们分别带和不带-fexec-charset=gbk编译如下程序,并运行。
#include <cstdio>
int main(int argc, char * argv[]) {
const char *_str = "卧槽";
printf("_str's mem = %x %x %x %x %x %x\n", 0xFF & _str[0], 0xFF & _str[1], 0xFF & _str[2], 0xFF & _str[3], 0xFF & _str[4], 0xFF & _str[5]);
const wchar_t *_str_1 = L"卧槽";
printf("_str's mem = %x %x\n", 0xFFFF & _str_1[0], 0xFFFF & _str_1[1]);
return 0;
}
得到如图的结果。
从图片结果我们可以知道,GCC对待字符串的方式和CL.exe不是很一致,但是通过传入相关参数,即可得到同样的结果。这里强调一下,-fexec-charset 参数相当于cl.exe的解码设置,类似上文vs选项,我们可以知道GCC的多字节默认编码是UTF-8。同时,GCC和CL.exe一样,对于char_t类型,都是使用的UTF-16 BE格式。
这里,我们通过如图的编码输出,手动来转换一下UTF-8和UNICODE,验证我们之前说的规则是否正确。
- “卧” 对应的是U+005367,十进制为U+21351,二进制U+0101 0011 0110 0111,根据区域值,是3字节模式,对应填入得到UTF-8二进制 1110 0101 1000 1101 1010 0111,十六进制为0xE58DA7
- “槽” 对应的是U+0069FD,十进制为U+27133,二进制U+0110 1001 1111 1101,根据区域值,是3字节模式,对应填入得到UTF-8二进制 1110 0110 1010 0111 1011 1101,十六进制为0xE6A7BD
- 参考模式:1110 xxxx/10xx xxxx/10xx xxxx(binary)
这里,我们发现,算出来的值,完全和我们的前面说的规则一样。
VS Debug模式下的“烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫” 看似搞笑行为
首先,我们在vs的debug模式下,运行如下程序。
#include <cstdio>
int main(int argc, char * argv[]) {
const char *_str = "卧槽";
printf("_str's mem = %x %x %x %x\n", 0xFF & _str[0], 0xFF & _str[1], 0xFF & _str[2], 0xFF & _str[3]);
const wchar_t *_str_1 = L"卧槽";
printf("_str's mem = %x %x\n", 0xFFFF & _str_1[0], 0xFFFF & _str_1[1]);
char _test[10];
for (int i = 0; i < 10; i++)
printf("%x ", 0xFF&_test[i]);
printf("\n%s", _test);
return 0;
}
得到如图的结果。
其实是由于vs 在debug模式下,会把我们为初始化的内存初始化为0xCC。而0xCCCC恰好是“烫”的GB18030编码,所以在我们CJK区域打印是“烫”,在其他区域可能是其他的字符。
后记
其实到了这里,我已经解决了我想要解决的问题。因为我只要知道目标程序的内存中中文的具体编码(OD或者CE等等),然后我就可以进行我想要的文字查找。
其实本文也解决了gcc生成的程序和cl.exe生成的程序字符串交换的问题。一个默认用的utf-8,一个是本地编码,对我们来说,就是GB系列。
打赏、订阅、收藏、丢香蕉、硬币,请关注公众号(攻城狮的搬砖之路)
PS: 请尊重原创,不喜勿喷。
PS: 要转载请注明出处,本人版权所有。
PS: 有问题请留言,看到后我会第一时间回复。