本文为看雪论坛精华文章
上周末在做plaidctf2020-emojidb的时候,发现这道题目的输入输出都是宽字节数据流涉及到字符的编码,而这方面之前一直没有研究。
借这道题的机会自己写了几个demo来对字符编码问题进行深入研究。
我们经常遇到的ASCII、unicode、Big5、GB2312、GBK等等都属于编码系统,他们都包含两方面的内容:字符集和编码方案。
字符集是编码系统收录的图形符号集合,比如ASCII码收录了现代英语字母的图形,GB2312收录了常用汉字图形。
编码方案是图形和数字之间的映射关系,使得用数字代替图形便于计算机输入输出,比如在ASCII码中,0~31及127共33个是控制字符或通信专用字符,32~126共95个是可显示字符。
Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码系统,有UCS-2和UCS-4两种编码规范,其中UCS-2编码长度为2字节,UCS-4是前者的扩展采用4字节编码,因此UCS-4字符集的容量大于UCS-2,在Linux的glibc中对unicode的处理都是UCS-4编码规范。
unicode编码系统兼容了ASCII编码系统,因此unicode的前128个编码对应了ascii码。
UTF-8、UTF-16、UTF-32是UTF编码,它的的全称是“Unicode transformation format”,也就是unicode的传输和存储格式。
unicode定义了字符和数字之间的映射关系,而UTF则定义了这个数字在内存中的存储方式,所以UTF不是编码系统,这也是UTF经常被误解的地方。
UTF-16用2字节表示一个unicode码,对应的是UCS-2编码规范,同理UTF-32对应的就是UCS-4规范。
UTF-8是unicode的变长编码方式。这么做的原因是,如果对英文字母和标点符号进行unicode编码,那么对应UCS-2和UCS-4只用到了低8位,这种方法显然会浪费一些内存。
因此UTF-8对ascii字符还是采用1个字节编码,而对其他符号则采用变长的方式进行编码和存储。
UTF-8和unicode的转换关系参考百度百科。
例如中文“今”字的几种编码关系如下:
unicode:"\xca\x4e"
UTF-16: "\xca\x4e"
UTF-32: "\x00\x00\xca\x4e"
UTF-8: "\xe4\xbb\x8a"
我们通常说的unicode其实指的就是UTF-16编码。
总结一下: unicode和ascii是两种编码系统,且ascii是unicode的子集。 UTF是unicode在内存中的存储格式,为了提高内存的使用效率提出了unicode的UTF-8编码方式。
接下来我在deepin环境中以一个可执行文件如何从源代码到最后执行并在控制台显示出字符来解释字符编码是如何被计算机处理的!
以我给出的demo1.c程序为例,它在我的vscode里面采用的是UTF-8编码:
/*
* demo1.c UTF-8
*/
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <locale.h>
char *name1 = "今天";
wchar_t *name2 = L"今天";
int main(){
printf("%s\n", name1);
printf("%s\n", name2);
return 0;
}
拖到UE里面查看,可以看到都是单字节的UTF-8编码,特别注意方框内标记的是“今”的UTF-8编码。
再将demo1.c转化为UTF-16小端编码,可以看到被扩展为2字节,文件的size也变大了!
其中开头的“\xFF\xFE”是UTF-16小端编码文件的magic,“\xCA\x4E”是汉字“今”的UTF-16编码。
可见源代码文件是采用了特定的编码系统来存储程序员写的代码,在Windows平台下的visual studio采用的是ANSI编码,Linux下的源码文件通常为UTF-8编码。
这里再次可以见到采用UTF-8对英文字符的unicode进行编码可以节约文件占用的空间!
之所以强调英文字符,是因为如果采用UTF-8对其他字符比如汉字进行编码反而不占优势。
编译器对源码文件的编码方式是有要求的,例如gcc要求源码文件必须是UTF-8编码,而Windows下的MSVC则支持多种编码方式的源文件并且需要在MSVC的编译选项中加以指定。
源码文件经过编译器编译得到可执行文件,字符在可执行文件中的表示分为2类:
将demo1.c编译之后来观察:
char类型字符的表示
char型变量在内存中占1个字节。name1是指向char类型字符串“今天”的指针,name1指向的0x400594位置的内容是“\xe4\xbb\x8a\xe5\xa4\xa9”,这是“今天”的UTF-8编码,对照前面的UE编辑器里面的内容可以看到,对于char类型的变量,gcc编译器就是把源文件中的编码直接塞到了可执行文件里。
wchar_t类型字符的表示
wchar_t是char类型的扩展,占用2字节或4字节内存,用于存储超出char范围的字符比如中文。在Linux glibc中wchar_t被定义为4字节变量,用于存储UCS-4字符。
demo1中name2为指向wchar_t类型的指针,在给wchar_t类型变量赋值时需要给字符串加上“L”前缀,表示每个字符按照UCS-4格式存储在wchar_t类型的变量中,否则中文字符还是按照UTF-8进行编码。
name2指向0x40059c的内容是“\x00\x00\xca\x4e\x00\x00\x29\x59”,这是“今天”的UTF-32编码。
所以gcc编译器在处理wchar_t类型变量的时候,将字符转化为了UTF-32编码然后塞到了可执行文件里。
这里解释了为什么文件编码格式对编译器很重要,因为编译器在进行wchar_t类型的编码转换的时候需要知道是从哪一个字符集转换到UTF-32,gcc只能编译UTF-8编码的文件,虽然提供了-finput-charset选项可以指定源文件编码类型,但归根到底也还是将源文件转化成了UTF-8文件,所以gcc知道他要将一个UTF-8编码换成UTF-32的编码,而在visual studio里面则需要在MSVC里面的编译选项中对源文件编码类型进行设置。
总结一下,对于char类型的字符,编译器直接按照源文件中的编码塞到了可执行文件中。对于加“L”前缀的wchar_t类型的字符,编译器将源文件中的编码转化为UTF-32编码塞到可执行文件中。
可执行文件被ld加载器映射到了进程地址空间,映射的过程就是复制的过程,因此可执行文件中的静态字符串被加载之后编码方式没有改变。
对于使用scanf、wscanf从外部输入的动态存储的字符串,他们在内存里面使用2种编码存储方法:1、原生编码
2、UCS-4编码
原生编码是输入函数从输入流获取的原生符号编码,他通常是操作系统设置的本地语言编码比如"zh_CN.UTF-8",终端传递给程序的字符编码就是系统的原生编码,数据类型是char。
UCS-4编码是把从输入流获取的原生符号编码转换为UCS-4编码也就是UTF-32然后存储到缓冲区,数据类型是wchar_t。
终端扮演的角色是:接收程序输出流的数据,并按照终端的本地编码格式解释输出流,将最终的解释结果显示在屏幕上。
在输入的过程中,把显示在终端上的字符按照终端的本地编码格式编码并通过输入流传递给程序。这里的输入输出流是相对于程序而言的。后面字符解释字符的动态存储和输出都是基于此模型。
对于输入函数scanf和wscanf,他们从输入流获取的原生字符编码按照何种方式存储在缓冲区,是由他们的格式化字符串决定的。
这两个函数在功能上是重叠的,格串为"%s"表示向缓冲区输出原生字符编码,格串为"%ls"表示向缓冲区输出转换后的UCS-4编码。下图展示了使用不同格串时输入函数内部发生的编码转换过程:
其中mb表示原生字符编码,wb表示UCS-4编码。原生编码转换为UCS-4是通过glibc库函数mbrtowc()完成的,UCS-4转换为原生编码通过wcrtomb()库函数完成。
发生编码转换的前提是要知道操作系统的本地语言编码是什么,这样才能建立原生编码和UCS-4编码的映射,因此在进行2、 3、 4这样的操作之前要调用setlocale在运行时告诉转换编码函数,本地编码是什么。例如:
/**demo1.c UTF-8**/
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <locale.h>
wchar_t buf[10];
int main(){
setlocale(LC_ALL, "zh_CN.UTF-8");
scanf("%ls", buf);
return 0;
}
"\xca\x4e\x00\x00\x29\x59\x00\x00"是终端输入字符"今天"的UCS-4编码。通过setlocale(LC_ALL, "zh_CN.UTF-8"),scanf就知道他要把输入流过来的UTF-8编码转换成UCS-4。
输出函数printf和wprintf,他们获得指向字符缓冲区的指针,输出字符缓冲区的内容。
printf和wprintf的功能也是重叠的,对于要输出的字符串按照什么类型存储是由格式化串解释的,"%s"表示要输出的字符串按照原生编码存储,数据类型是char,"%ls"表示要输出的字符串按照UCS-4存储,数据类型是wchar_t。
下图展示了使用不同格串时输出函数内部发生的编码转换过程:
输出函数最终的输出编码都是原生编码,因为终端只负责解析本地编码类型的编码,如果输出流里面的不是原生编码,那么终端就无法正确显示结果。
和输入函数一样,当发生编码转换的时候程序必须被运行时告知本地编码的类型,这个步骤也是由setlocale函数完成的,例如:
/** demo2.c UTF-8 **/
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <locale.h>
char *name1 = "今天";
wchar_t *name2 = L"今天";
int main(){
printf("before setlocale: %ls\n", name2);
setlocale(LC_ALL, "zh_CN.UTF-8");
printf("after setlocale: %ls\n", name2);
return 0;
}
在setlocale之前printf不能正常显示,因为此时printf将"今天"的UCS-4编码按照C语言内置的最小公共编码来转换,在没有显式地设置本地编码的时候,这个选项是默认的。第二个printf才将UCS-4正确地转化为了UTF-8。
九、两个实例
我用上面总结得到的结论来解释两个在实际编程过程中遇到的问题。
/** demo3.c UTF-8 **/
#include <wchar.h>
#include <unistd.h>
#include <stdio.h>
#include <locale.h>
char *name1 = "中文";
wchar_t *name2 = L"中文"; //UCS-4
int main(){
setlocale(LC_ALL, "zh_CN.UTF-8");
wprintf(L"%s\n", name2);
return 0;
}
程序的意图是输出UCS-4编码的"中文",可以看到已经设置了本地编码方式,但结果却是"-N"。
查阅Linux接口手册得到解释:wprintf解析格串时遇到"%s"表示要输出的字符串是一个char类型的本地编码字符串,此时会对每个本地编码字符调用mbrtowc()库函数,按照setlocale指定的本地编码方式将本地编码转化为wchar_t类型的UCS-4编码,再将UCS-4编码加入输出流,这里指定的本地编码类型是zh_CN.UTF-8编码,因此转换过程是UTF-8--->UTF-32。
在这里name2指向的地址存放着"中文"二字的wchar_t类型的UCS-4字符,也即是"\x2d\x4e\x00\x00\x87\x65\x00\x00",其中"\x2d"和"\x4e"是UTF-8格式的字符 "-"和"N",对每个本地编码字符调用mbrtowc()得到L"-"、L"N"然后加入到输出流,后面因为\x00而截断。而"-"和"N"的UTF-32和UTF-8编码相同,因此终端输出 “-N”。
/*
* demo4.c UTF-8
*/
#include <wchar.h>
#include <unistd.h>
#include <stdio.h>
#include <locale.h>
char buf[10];
int main(){
setlocale(LC_ALL, "zh_CN.UTF-8");
scanf("%s", buf);
wprintf(L"buffer: %ls\n", buf);
return 0;
}
程序的意图是通过scanf保存一个UCS-4字符串,通过wprintf打印。输入"今天"却显示了乱码。
使用gdb调试观察buf的内容为"\xe4\xbb\x8a\xe5\xa4\xa9",这是"今天"的UTF-8编码,说明scanf在"%s"的作用下存储了原生编码,并未将其转化为UCS-4。
那么接下来发生的事情就很明显了,wprintf把buf按照UCS-4来解析,试图完成UCS-4到UTF-8的转换,通过pwntools来观察程序的输出:
最终"\xe4\xbb\x8a\xe5"被解析成"\x7f",显示了问号;"\xa4\xa9\x00\x00"被解析成"\xea\xa6\xa4",显示"ꦤ"。
把scanf的格式串改正为"%ls"或者把wprintf的格串改为"%s"均能得到正确输出。
为什么会出现编码问题?是因为程序内部发生了隐式的编码转化。为什么会有隐式编码转换?
是因为glibc引入了wchar_t数据类型用来专门存放UCS-4编码,一旦用到wchar_t数据类型,那么在输入方向就需要将原生编码转换为UCS-4,在输出方向就需要UCS-4转换为原生编码。所以需要同时从输入和输出两个方向考虑才能避免出现编码问题。
*这里由看雪论坛 极目楚天舒 原创,转载请注明来自看雪社区。
本文由哈喽比特于4年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/QJKEYSUqbxAVJwGBph-zRw
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。