0%

深入浅出理解C语言汉字编码问题

0x00 写在前面

今早看到个这样的问题

image.png
1
2
3
4
5
6
7
#include<stdio.h>
int main(int argc, char const *argv[])
{
int a[2] = {-68,-12};
printf("%c%c\n", a[0], a[1]);
return 0;
}

两个int类型的值输出出来,却是一个结果如下的汉字”剪“

image.png

0x01 负数是从哪里来的

那么,这样的负数是从哪里来的呢?

直接定义字符串数组表示汉字,逐个以int形式输出其内容结果如下:

1
2
3
4
5
6
7
#include<stdio.h>
int main(int argc, char const *argv[])
{
char b[] = "剪";
printf("%d %d\n", b[0], b[1]);
return 0;
}
image.png

可以看到,”剪“字,%d表示的是以有符号整型数输出,所以被输出成了两个负数的形式(注意这里的编码方式是GBK)

0x02 过程如何

为什么可以用两个负数来表示一个汉字呢?这就是开头提到的问题,其实,汉字不能看成是以负数来表示的,归根到底,汉字是通过编码(可以是UTF-8、GBK或者是其他编码方式)后,以二进制数据的形式保存在内存当中的,而我们看到的负数,只是内存中二进制数据的一种表示方式,即0x01代码中printf函数中%d所控制输出的值。

回到汉字的编码问题,刚刚说过,从汉字到二进制的数据,要经过编码,如果将编码方式设置为UTF-8,再去看看0x01输出的结果是什么

image.png

从上面的结果可以看到,对于相同的代码,却得到了不同的输出结果

代码中唯一的有差异的地方是在于字符串数组b的长度,由于采用了UTF-8编码1个汉字,占用4个字节(实际真正使用了三个字节,故这里输出3个char),不同于GBK的2个字节。

至此,最最开始的0x00中出现了负数的情况就可以被解释清楚了,而这样的表示方法是不规范的,通常我们是以无符号十六进制来表示内存中的数据的。

0x03 深入一点

我们通过代码来具体验证一下,回到开始使用的GBK编码”剪“这个汉字,以各种不同的形式来输出内存中表示汉字的二进制数据,再以不同的表示形式向内存中定义相同的二进制数据来表示汉字,看看结果如何

image.png

可以看到,无论是有符号整形还是无符号十六进制,又或者是,把十六进制转换回十进制来表示汉字,得到的都是同一个汉字“剪”

0x04 再深入一点

我们直接通过如下程序对应的汇编代码,具体看看内存中的二进制数据保存是否一致,再来验证一下刚刚的结论

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
int main(int argc, char const *argv[])
{
char a[] = "剪";

int b[2] = {-68,-12};
int c[2] = {0xbc, 0xf4};
int d[2] = {188, 244};

printf("%c%c\n", a[0], a[1]);
printf("%c%c\n", b[0], b[1]);
printf("%c%c\n", c[0], c[1]);
printf("%c%c\n", d[0], d[1]);

return 0;
}

对于以上的代码,输出的结果都是相同的一个汉字”剪“

编译后的exe文件导入ida,看到其对应汇编代码如下,验证了我们的结论,无论表示形式如何,内存中的二进制数据都是相同的,验证了我们的结论

image.png