蜀道之难,难于上青天

0%

UTF-8 字符编码

问题

11100110 10110001 10011111表示什么?

ASCII

American Standard Code for Information Interchange,美国信息交换标准代码

ASCII最初由美国提出,解决了英语世界在计算机里的编码问题。
ASCII00开始,一直到7f,包含了128个字符,其中33个控制字符,95个可见字符。

  1. 控制字符比较常见的如换行键、空字符、退出键等。
  2. 可见字符如空格、数字、大小写字母、符号等。

可以通过echo配合xxd命令很简单的获取字符的ASCII值,如下我们就拿到了aASCII值为0x61

1
2
3
4
$ echo a | xxd
00000000: 610a a.
$ echo a | xxd -b
00000000: 01100001 00001010 a.

随着互联网在全世界的普及,ASCII的局限性慢慢凸显,其无法表示其他语种如汉语、日语的字符。于是发展出了Unicode

Unicode

Unicode,又称为万国码、国际码、统一码、单一码

Unicode是一项业界标准,为每一个字符都制定了唯一的编码,统一了各语种的编码。
比如U+0061表示aU+660e代表汉字的字,U+1f600表示露齿而笑的脸😀,U+1F1F3表示中国国旗🇨🇳

Unicode里一个字型可以代表多种字符编码,甚至是某些编码的组合。
é既可以用U+00e9表示,也可以用U+0065U+0301的组合表示。所以我们使用的时候,还是得根据实际使用的意义来输入,否则在计算长度以及字符串比较等方面就会不一致,造成不必要的麻烦,而且这些问题排查起来也很困难。

UTF-8

8-bit Unicode Transformation Format

UTF-8是一种针对Unicode的可变长度字符编码,是一种前缀码。

  1. 128US-ASCII字符只需一个字节编码(Unicode范围由U+0000U+007F
  2. 带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要两个字节编码(Unicode范围由U+0080U+07FF
  3. 其他基本多文种平面(BMP)中的字符(这包含了大部分常用字,如大部分的汉字)使用三个字节编码(Unicode范围由U+0800U+FFFF
  4. 其他极少使用的Unicode辅助平面的字符使用四至六字节编码(Unicode范围由U+10000U+1FFFFF使用四字节,Unicode范围由U+200000U+3FFFFFF使用五字节,Unicode范围由U+4000000U+7FFFFFFF使用六字节)。
代码范围 标量值 UTF-8 注释 个数
000000 - 00007f 00000000 00000000 0zzzzzzz 00-f7 ASCII,字节由0开始 128
000080 - 0007ff 00000000 00000yyy yyzzzzzz 110yyyyy 10zzzzzz 第一个字节由110开始,接着的字节由10开始 1920
000800 - 00d7ff , 00e000 - 00ffff 00000000 xxxxyyyy yyzzzzzz 1110xxxx 10yyyyyy 10zzzzzz 第一个字节由1110开始,接着的字节由10开始 61440
010000 - 10ffff 000wwwxx xxxxyyyy yyzzzzzz 11110www 10xxxxxx 10yyyyyy 10zzzzzz 第一个字节由11110开始,接着的字节由10开始 1048576

Unicode在范围D800-DFFF中不存在任何字符,基本多文种平面中约定了这个范围用于UTF-16扩展标识辅助平面(两个UTF-16表示一个辅助平面字符)。当然,任何编码都是可以被转换到这个范围,但在Unicode中他们并不代表任何合法的值。

根据上面的表可见,UTF-8完美兼容ASCII编码,Unicode范围很大,而且是可以继续扩展的
同时我们可以根据Unicode值推导出UTF-8的编码

1
2
3
4
graph TD
A[Unicode:黄,U+9ec4] -->|二进制| B(1001 1110 1100 0100)
B --> |UTF-8| C(11101001 10111011 10000100)
C --> |十六进制| D(UTF-8:e9bb84)

现在我们回到文章开头的问题

11100110 10110001 10011111表示什么?

可以看出这段二进制编码实际上是UTF-8编码,按照上表的规则,可以推导出其Unicode字符

1
2
3
graph TD 
B(11100110 10110001 10011111) --> |Unicode| C(0110 1100 0101 1111)
C --> |十六进制| D(Unicode:江,U+6c5f)

应用:修改机器码字符段

学以致用,这样效果才会好
这是一段简单的输出字符串的程序

1
2
3
4
5
6
7
#include "stdio.h"

int main(int argc, char const *argv[])
{
printf("hello,world!黄");
return 0;
}

编译
$ gcc main.c | ./a.out
输出:hello,world!黄

查看目标文件的段信息
$ objdump -s a.out
输出里的字符段如下:

1
2
Contents of section __cstring:
100000fa2 68656c6c 6f2c776f 726c6521 e9bb8400 hello,world!....

我们可以知道,这一串十六进制编码代表了UTF-8编码的hello,world!黄。可以尝试对其进行修改
vim -b a.out
打开十六进制模式
%!xxd
查找到hello部分进行修改,将 e9bb84(黄) 改成 e6b19f(江)。
/hello
恢复成二进制模式
%!xxd -r
保存退出
wq!
查看字符段
$ objdump -s a.out
输出:
1
2
Contents of section __cstring:
100000fa2 68656c6c 6f2c776f 726c6521 e6b19f00 hello,world!....

$ ./a.out
输出:hello,world!江
这样,我们就修改了机器码。

到这里,笔者也想通了一些问题,比如hardCode进代码里的密钥肯定是不安全的,存在被查看以及被篡改的风险;二进制文件也是不安全的,所以Apple使用了代码签名的机制,来保证设备上运行的代码是未被篡改过的。

参考链接: