漫谈二进制与十六进制在计算机中的运用

七阶子
23 min, 4538 words

归类: 天马行空

标签: cs linux

漫谈二进制与十六进制在计算机中的运用

众所周知,计算机内部使用二进制,与人们日常使用的十进制大相径庭。可能有些人,包 括一些初学计算机的学生,会对此感到困惑。本文试图用浅显的语言来聊一聊这个话题。

进制的选择源于生产实践

其实,计算机是人类发明的,进制也是人类发明,所以计算机使用什么进制,只是人类的 选择。人类发明进制,主要是为了方便指导人类自身的生活及生产活动。在人类历史上, 使用最广泛的进制是十进制,大概人有十根手指,容易以此为凭计数吧。

但是,除了十进制外,人们在特定场合也经常使用其他进制。比如中国古代使用的算盘与 算筹,相当于五进制,那是在十进制为主的情况下的辅进制。现在仍然广泛使用的非十进 制是时间单位,以六十进制为主,十二进制为辅。中国古代的十天干与十二地支的循环也 是构成六十甲子循环。人类对时间的感知源于天体运动,对时间与日历的划分主要以太阳 与(或)月亮的运行规律为基础。人们不对此作十等分,而是十二等分,六十等分,是因 为发现这样更方便,更合理。譬如中国农历的二十四节气,在农业社会对农事生产的指导 是相当有意义的。所以,我们在某个领域选择某种进制,是实践导向的结果,这种进制更 适合这个领域而已。

另外,十六进也不新鲜,中国古代在称量时也用过十六进制,“半斤八两”这词就这么来的。 因为等分其实是最容易实现的,比十等分、十二等分都容易得多。不过只用一刀两半、一分 为二的粒度在很多情况下还是太粗了,所以一般要继续等分,分个三、四次,做成八等分、 十六等分,大家觉得这样的份量更方便使用,满足大部分场合的需求,就定下十六进制, 半斤八两。

计算机信息产业选择了二进制

所以,计算机使用二进制,也只是因为它更合适、更简单。首先,二进制是最小的进制, 并不存在一进制,如果一个事物只能承载一种状态,它是无法表示多种信息变化的,至少 需要两种状态,也就是二进制。其次,二进制实现也最简单。用电子元器件的高电平与低 电平就能表示 1 与 0 ,两种状态的区分与辨识是最容易的,容错也高。假设要使用所谓 的三进制,增加一种“不高不低”电平,那会使状态判断的难度剧增,完全得不偿失。这就 是大道至简,二进制足以表达任意变化,任意数据与信息,那就只用二进制就可以了。

当然,任何技术都该以人为本。在计算机之外讨论二进制的数值,经常也需要转为十进制 的“真实”数值。这种转换,只涉及一个基本的简单的数学原理。参考十进制是怎么用多项 式表示一个数的:

1985 = 1 * 1000 + 9 * 100 + 8 * 10 + 5 * 1
     = 1 * 10^3 + 9 * 10^2 + 8 * 10^1 + 5 * 10^0

然后随手写个 1101 的二进制,它就可按类似的公式求出对应的十进制表示:

1101 = 1 * 2^3 + 1 * 2^2 + 0 * 2^1 + 1 * 2^0
     = 1 * 8 + 1 * 4 + 0 * 2 + 1 * 1
     = 8 + 4 + 1  = 13

如果要反过来,将十进制的 13 转为二进制的 1101 ,那也就是个逆分解过程,不断 除 2 取余的过程,这里不再赘述。

简言之,任意进制的数值,可以用基数与进制数的指数幂多项式来表示,我们写在一起的 各位数字,在数学表达式上就是这个多项式的系数。二进制的基数只有 0 与 1 ,十进制 的基数就是常用的 0-9 这十个数,而十六进制就在 0-9 之后,借用 a-f 这六个字母分 表达到 10 至 15 的基数。

从二进制到十六进制

除二进制外,在计算机学科还常用八进制与十六进制。但后两者不算独立的进制,它们本 质上也是二进制,或者说是基于二进制的辅进制,目的是为了更方便地表达或书写二进制 数值。

就如本文开头提及的算筹(与算盘)的五进制,它也不是独立的进制,只是十进制的辅进 制。一根算筹就像长条形的筷子,竖着放一根表示 1 ,放两根表示 2 …… 放四根表示 4 ,但到了表示 5 ,它就不再是并排竖放五根了,而是横着放一根表示 5 。为啥这样规定 呢?因为一直并排着放下去,既费材料,也费空间,还对人肉识别不友好,比如并排放八 根或九根,你不能一眼很快看出具体是八根还是九根。所以就引入了“逢五转一”的辅进制, 但更根本的还是“逢十进一”的十进制。在算筹系统中,如果约定个位数用竖筹表示 1 , 横筹表示 5 ,那么在十位数就反过来,用横筹表示 1 (也就是 10),竖筹表示 5 (也 就是 50),如此轮换,进一步增加数字的识别度。当然,这些约定,就是具体的技术与 工程问题了。现在我们不需要算筹了,所以也就不需要五进制。

但二进制仍需要八进制或十六进制的辅助,也是基于类似的表达原因。如果把写在纸上 (或打印在屏幕上)的二进制数值,那长串的 1 想象为并排竖放的算筹,就能发现问题 了,它很难被人眼识别。所以为了增加识别度,我们会将它分成三个一组,或四个一组, 并且用更丰富的数字符号(而不仅有 1 与 0)来表示每个分组部分。

其实在表达常规的十进制大数时,人们也经常会采用分组的办法。比如在西方国家,习惯 于每三位一组,有时会显式用逗号(下标)或单引号(上标)分隔,依次表示 thousand (千)、million (百万)等。而在中国,更习惯按每四位分组,依次表示为万、亿等。

二进制的问题在于,它的单位太小,即使在日常十进制中并不大的数,用二进制表达也需 要很多位,太长了。所以对二进制位数分组,就显得更加迫切,如果每三位分组,就是八 进制,每四位分组,就是十六进制。理论上,也可以有按两位分组的四进制,与按五位分 组的三十二进制。但其他的分组二进制,并无实用,而八进制与十六进制,是有实用需求 才引入的,然后才逐渐流行起来,并成为事实标准。

八进制的基数有 0-7 八个符号,十六进制有基数有 0-9a-f 十六个符号(字母不分 大小写),它们与二进制 0-1 串组的对应关系如下:

基数三分组四分组十进制
000000000
100100011
201000102
301100113
410001004
510101015
611001106
711101117
810008
910019
a101010
b101111
c110012
d110113
e111014
f111115
8进制16进制

在计算机相关代码或文献中,为了将八进制或十六进制与常用十进制区分开来,在表述写 法上会加点前缀。其中八进制用 0 前缀,如 0755 表示八进制的 755 ,也就是二 进制的 111 101 101 。十六进制用 0x 前缀,或大写的 0X ,如 0xfe 表示十 六进制的 fe ,也就是二进制的 1111 1110 ,也就是十进制数值 254

由此可见,八进制、十六进制与二进制的转换是非常方便的,只要按基数表查表,分别转 换即可,基本可用人肉心算。但是二进制转十进制不是恰好倍数关系,没法分组分治,转 换起来就略麻烦些。用八进制或十六进制能大辐减少二进制数的位数长度,相比十进制的 等值数值,八进制表示略长,十六进制则短得多。

八进制的应用场景

据说,早期计算机的一个字节,不都是八位,而也有六位的情况。这可能是由于相关元器 件制造工艺与成本的原因,做六根导线比八根线会相对容易些吧。但现今这不是问题了, 一个字节八比特位已是工业与学界标准。八比特位正好分成两个四位组,也就是两个十六 进制数字,所以使用十六进制非常方便。但如果一个字节是六比特位的情况,则用八进制 更方便,这可能就是八进制出现的一个原因。

而现在,八进制与字节没关系了,所以它的应用场景远不如十六进制。现在仍然在广泛使 用的场合是 linux/unix 系统的文件权限表示位。

文件有三个重要权限,分别是读、写与执行。是否可读或写好理解,是否可执行是表示该 文件是否能像程序那样执行,否则就当作普通的数据文件。在 Linux 中,很多普通文本 文件都可能是可执行的脚本程序,所以可执行这个属性或权限很重要。这三种权限,通常 表示为 rwx ,分别只有两种状态,是否或有没有该种权限,那就可用 01 表 示,即三位二进制数,也就可用一位八进制数来表示。在 Linux 中,相对于文件的用户 又分为三种,即文件所有者(owner)、同组用户(group)与其他用户(other),每种 用户的权限用一位八进制的话,完整权限就是三位八进制数字表示了。

Linux 用 chmod 命令来修改权限,它接受八进制数值,也接收文本参数,用文本参数 可能更直观,但当熟悉八进制表示法后,用八进制更简捷与直接。假设现在有个文件 file.txt 的权限是 400 ,表示只有该文件的所有者有读权限,其他用户没有任何权 限;根据系统用户合作需要,要使其他用户也有读权限,自己及其他同组用户有写权限。 若用文本参数描叙这些权限修改动作,可能要分几条命令来执行:

chmod +r file.txt
chmod u+w file.txt
chmod g+w file.txt

最终结果的权限是 -rw-rw-r-- ,转为二进制是 110 110 100 ,转成八进制就是 664 ,所以直接按八进制数值修改权限的操作会更快:

chmod 664 file.txt

当然,另外有个常见需求,写完一个脚本后,需要给它加个可执行权限,如果想给所有用 户加个可执行权限,用文件参数 +x 更方便:

chmod +x script.sh

但如果觉得给其他用户开脚本运行权限是比较危险的事,只想给自己及或信任的同组用户 开执行权限,那就用八进制一次修改更方便了,如:

chmod 774 script.sh

除了 Linux 文件权限表示法,笔者并没有在其他方面看到八进制有良好的运用实践了。 可以想见,如果 Linux/Unix 系统完成历史使命,或者有更好的方式来表达权限特征,八 进制或许也会像五进制那样退出历史舞台。在八位字节统一标准后,只要十六进制辅助二 进制就足够了,没必要增加更多的复杂性。

十六进制为表的二进制

因此,现在在很多场合下,十六进制与二进制,几乎是同义词了。二进制为里,十六进制 为表,在内部用二进制运算,输出给人类用户看时用十六进制。

比如,很多宣称能编辑二进制的文本编辑器,它实际是展示十六进制的。Linux 下常用的 文本编辑器 vim 也有二进制编辑功能,执行 :%!xxd 就把当前文件转为二进制“打开” 了。实际上 xxd 是随 vim 安装的独立工具,其功能是将输入内容用十六进制方式打印 出来。而在 vim 中执行 :%!xxd 其实是利用了 :! 的过滤功能,调用外部 xxd 程 序将当前编辑内容转为十六进制展示,并替换当前编辑内容(注意不要用 :w 保存, 否则就将十六进制的展示方式当作实际内容写入文件了,这很可能不是想要的;看完十六 进制的展示,最好用 u 命令撤回操作,回到正常文本展示模式)。

所以我们也可直接在 shell 命令行中用 xxd 来查看文件的二进制内容,例如:

xxd ~/.bashrc
00000000: 2320 7e2f 2e62 6173 6872 633a 2065 7865  # ~/.bashrc: exe
00000010: 6375 7465 6420 6279 2062 6173 6828 3129  cuted by bash(1)
00000020: 2066 6f72 206e 6f6e 2d6c 6f67 696e 2073   for non-login s
00000030: 6865 6c6c 732e 0a23 2073 6565 202f 7573  hells..# see /us
00000040: 722f 7368 6172 652f 646f 632f 6261 7368  r/share/doc/bash
00000050: 2f65 7861 6d70 6c65 732f 7374 6172 7475  /examples/startu
00000060: 702d 6669 6c65 7320 2869 6e20 7468 6520  p-files (in the
00000070: 7061 636b 6167 6520 6261 7368 2d64 6f63  package bash-doc
00000080: 290a 2320 666f 7220 6578 616d 706c 6573  ).# for examples
00000090: 0a0a 2320 4966 206e 6f74 2072 756e 6e69  ..# If not runni
000000a0: 6e67 2069 6e74 6572 6163 7469 7665 6c79  ng interactively
000000b0: 2c20 646f 6e27 7420 646f 2061 6e79 7468  , don't do anyth
000000c0: 696e 670a 6361 7365 2024 2d20 696e 0a20  ing.case $- in.
000000d0: 2020 202a 692a 2920 3b3b 0a20 2020 2020     *i*) ;;.
......

.bashrc 其实是个普通文本文件,对应的前面几行如下:

cat ~/.bashrc
# ~/.bashrc: executed by bash(1) for non-login shells.
# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc)
# for examples

# If not running interactively, don't do anything
case $- in
    *i*) ;;
......

大家可以对照着文件的实际内存,体会一下 xxd 打印二进制文件的格式,其他二进制 编辑器也基本是类似的风格。前面的“行号”,其实是地址,第二行 10 的十六进制数值 等于十进制的 16 ,也就是每行打印 16 个字符(或字节)。中间部分的主体内容就是 每个字节的十六进制表示,每两个数值代表一字节,每两个字节间它额外加个空格也只为 整齐分隔,否则 32 个数字连在一起辨别困难。右侧部分是对应该行每个字节的文本展示, 如果是可打印字符(32-126 ascii 码),就可直接打印,其他字符统一用点占位表示不 可打印,比如该文件实际内容的第一行末尾的换行符 0a ,就大约在第 30 行中间位置。 另外注意,空格(十六进制 20)是也算可打印字符,右侧也对应一个空格。对于真正 的二进制文件,非文本文件,xxd 输出的右半侧预览基本都是一些不可识别的 . , 即使偶尔碰巧是可打印字符,也未必是原文件的本意,只是某个字节正好落在 [32, 126) 区间。

再举个常见的例子,MD5 摘要,它将任意长度的数据,通过某种算法得到 16 字节摘要。 Linux 下也有个命令 md5sum 用于求一个文件的 MD5 摘要,它打印的是 32 个数字, 用以表达内部算法求出的 16 个字节数据。如:

md5sum .bashrc
f45e5e883584d4a9f955562066cf75f3  .bashrc

二进制大数的十进制单位

如前所述,在计算机很多领域,二进制或十六进制是表示数据的,并不一定有数值意义。 在向人类传达数据信息时,用两个十六进制数字代替一个字节数据更方便。此外,将十六 进制当作数值时,一般只用在与内存、存储相关的地址或容量上。

在表示容量数值时,人们又更习惯于十进制的表达,于此又衍生出一系列容量单位,如:

  • 1K = 1024 = 2^10
  • 1M = 1024K = 2^20
  • 1G = 1024M = 2^30

这种 1024 的“进制”单位,主要是用于表达字节数量,写作 KBMB ,而不会单独 使用 K 来表示 1024,比如我们不会将某件商品卖价 1024 元写作 1K 元,不合习惯而 已。同时,这些单位是给十进制数值体系用的,比如我们会说 15G ,而不会说(十六 进制)fG

事实上,单独的 K 在十进制中也经常表示 1000 。所以一些硬盘生产商就会故意混淆 概念,标称 1GB 的容量,其实没有 1024MB ,只有 1000MB 。

结语

本文简单探讨了计算机领域使用二进制与十六进制的相关话题。这尤其说是一种技术,不 如说是一种文化习惯,毕竟进制这概念在数学原理上也不复杂。在当前信息时代,即使不 是计算机从业人员,了解基本的二进制与十六进制也是有益的,以增加对这种数值表达的 熟悉与敏感度。