写在前面
本人理解可能有所偏差,为了避免被误导,建议配合原书理解
话说要春节了呢~ 那么,小娜,你还要穿着那套旧棉衣吗?
好吧,可能上次答应给她装的新语言模块还没加载完全,得等上一段时间了。
不过,我们也可以看到,身为计算姬的小娜和正常的计算机有个共性,它们对于一切的数据,本质上都是以二进制看待的。毕竟从电子管开始,计算机就是由无数01逻辑运算单元构成的。至于苏维埃曾经开发过的,理论上更快的三进制计算机……另当别论。
计算机的各种运转基本都离不开二进制数,比方说高中计算机课程上学过的原码、补码、反码,以及大学C语言课提及的短长整形和无符号整数等,不同系统和编程语言还有自己对应的二进制表示方式,就不在开头细讲了。
原书中主要讨论无符号编码(unsigned)、补码(two’s-complement)和浮点数(floating-point,本质上是以2为基数的科学计数法),以及C标准和Java标准下支持的表示和运算,所以我可能会穿插一些其它的内容进行拓展。由于原书对应章节本体过长,本次笔记分上(可能有中)下篇。
所以——准备好了吗!(芭芭拉开大)
啊这……原来你的加载速度这么快的吗……
那就让我们进入正题——信息是如何存储的
众所周知,数据在计算机中的最小单位被称作位(b,bit),但是,真正可寻址的最小单位是更高一级的字节(byte),至于为什么……各位可以参考yanfeizhang和pk2017的文章,以及百度百科上的描述。
为什么一定是位
字节就是用来表示完整的字符的。在费拉不堪的早期计算机中,为了实现虚拟内存的概念,普遍采用4/6位BCD编码,但BCD面对字母和符号非常乏力,于是有了ASCII。再之后ASCII也不够用了,于是IBM设计了一套八位的EBCDIC编码,于是现行的字节大小就是8位。也就是说,从概念上看,目前依靠8位的编码可以完整地表达一个字符的信息。
从硬件上看,每一块内存都由若干芯片(chip)组成,每个芯片又分八个bank,在每个bank内部存在着一个n*8的电容矩阵,在数据意义上相当于一个每个元素存储着八个位的信息的数组,而芯片上同一位置的元素又在内存中组成连续的64bit数据,这相当于在芯片上最小的单位就是8bit的字节。
并且,使用ida的同学们都知道,程序的每条汇编指令对应的虚拟内存地址,其刚好是8位十六进制数,这刚好与64bit的大小对上了。
在硬件和软件,抽象和具象的概念互补下,8位的byte似乎成为了现行标准下最理想,最稳定的基础单位。当然,如果要像毛子一样硬薅三进制,实现新标准,那么,为之努力吧,这可是连当年的苏联都办不到的事情啊(哭
ps:我国已经在进行三进制光子计算机的尝试(喜),感兴趣的可以移步相关文章
回到正题,既然刚才提到了虚拟内存和地址等概念,那么就由此展开。我们之前提到过,在概念上,内存相当于一个庞大的线性数组,虚拟地址空间就是在此之上的一个个独立进程所占用的内存空间的抽象,其中的地址不只是抽象的数据,也是对于每一个字节的唯一映射。至于我们是如何表示字节的……
位模式和十六进制
我们引入位模式的概念,实际上,位模式就是通过二进制表示法表示一个字节的信息,数值从0到255,对应十六进制就是0到0xFF(或者FFh之类的,不同标准下有不同的表示方法)。至于为什么通常用十六进制进行位模式的表示,个人理解,十进制和二进制的转换有着天然的复杂性,而二进制本身……
非常的冗长且烦人。而十六进制作为2的幂次方,和二进制在进制转换上有着天然的适性,天然适合位运算,并且也适合表示以字节为单位的信息的值。并且,本身的数据长度也刚好能够用数字+字母的形式表示,即0 ~ 9和A ~ F,也方便编码之间的转换等操作。
但光能表示字节还不够,我们往往要表述一串字符串,或者是一串数字。我们还需要其它的概念来完善储存空间。
字长
计算机在存储、传送或操作时,作为一个单元的一组二进制码称为字,一个字中的二进制位的位数称为字长。
————百度百科
字长决定着虚拟地址空间的最大大小,而虚拟地址的范围就是2的字长次方减一。字长也与硬件条件脱不开干系,计算机中大多数寄存器的大小是一个字长。计算机处理的典型数值也可能是以字长为单位。现行常用字长即32位和64位,至于16位老古董和诸如39位和24位的奇葩另当别论。
哦对了,我好像忘了,小娜脑子里是有39位的兼容设备来着……
行了小娜,要发脾气的话待会再发,咱们先把正事讲完。
由于字长的大小也影响着编译时分配虚拟地址的过程,理论上,不同机器使用不同编译器编译程序时,不同类型数据,有无符号,是否为指针,占用的字节也不一样。详细可以参考原书的内容,本文不再赘述,但相关内容要牢记,尤其是做逆向和pwn的时候,有大用。
不过现在64位的机器一般都做了对32位的向下兼容,这使得两种字长的程序都可以在64位机器上运行。而且,ISO C99标准也特地引入了一套不随编译器和机器变化的固定数据类型,以及后来推出的一系列标准,这使得同字长的不同机器之间有一套相同的数据标准,规范化和兼容也更加容易。
寻址和字节顺序
逆向手在处理没有被ida反编译直接定义的数据的时候往往会碰到一些很麻的情况:面对不同的题目,每当你找到一个数据的地址,有时数据是按字节倒序存储的,有时则是顺序存储。
这涉及到程序中每个对象在内存中的地址和排列方式,以地址为基底,n字节长度的数据会排列在从数据地址开始直到地址+(n-1)的位置。而排列的方式,也就是字节顺序,有两个通用规则:大端法和小端法。
具体而言,我们像小娜一样用二进制看,设一个数据占了w个二进制位,每一位下标表示从w-1到0,比方说一个十六进制整数0x10face,它的第w-1位便是十六进制1的最高二进制位0。最高有效字节包含位为w-1到w-8,最低有效字节则是7到0。所谓大端和小端,便是从最高有效字节到最低有效字节,和从最低有效字节到最高有效字节的分别。
比方说,0x10face用小端序表示,按地址顺序排列就按从最低有效字节到最高有效字节,也就是“cefa10”,而大端序则是原汁原味的排列。
在不同机器中,使用的字节序也不尽相同。我们常见的大多数Intel设备使用小端序,部分新型机器使用双端序,但会根据操作系统的变化而固定某一端序,这点在书中有涉及到代码层面的详细提及,这里只做简要说明。至于IOS系统,各类衍生Linux系统的端序,各位可以自行研究。
非数字数据类型的表示
既然我们已经有一套完整的数据机制了,那么,在此之上实现各种数据的存储和处理也不是难事。
这一篇笔记并未过多提及整数相关的内容,因为太多了。
字符串
在C语言中,字符串的实现是一个以空值(null,0)为结尾的字符数组(大概也是有这层逻辑在,strlen显示的字符串长度为正常的n,而非代码层面通常的结尾下标n-1)。通常,每一个元素用某种固定编码(目前一般是ascii码)表示。任何以ASCII作为字符编码的系统上,C代码的结果都大抵一致。
在Java中,字符串的实现则更为复杂。对于已知的字符串值和对应的字符串变量,Java分别将它们置于常量池和堆中,感兴趣的可以看看一名奇怪的玩家的文章,或者自行百度,或者科学上网。(其实C也是有类似的机制的,只不过在实现方面多少跟java有点差异)
代码
前面我们也提到过,不同机器,不同系统上对代码的编译不尽相同,也正因如此,处理它们的时候往往会产生不同的机器语言代码。这里便不再赘述。
布尔代数
现如今的逻辑数学的发展,总离不开一个叫做乔治·布尔的人(laobideng)。作为二元逻辑之父,他创造了一个二进制逻辑系统,将0和1分别定义为假(false)和真(true),并定下了逻辑运算的规则。他的理论很大程度上成为了现代计算机学科的一大基础。
而布尔代数和现代电子逻辑运算纠缠不清的关系,这里就不展开了。我们需要知道的是,在计算机的逻辑运算中,也必定存在着对应的或、与、非和异或关系。
书中还提到了一个叫做叫“位向量”的概念。位向量定长,其运算可以定义为参数的每个对应元素之间的位运算。这些令人头疼的数学内容还得结合原书的拓展内容理解。
既然都扯到位运算了
那就自然要扯到我们常用的C语言位运算和逻辑运算啦~
除了最基本的,对于位运算的或(|)与(&)非( ~ )和异或( ^ ),C语言还提供了一组逻辑运算符,对应命题逻辑中的或(||)与(&&)非(!),所谓命题逻辑和位运算的差别,你可以简单地理解为,前者是针对不同表达式(可以是函数甚至程序)之间的逻辑关系,后者则是针对二进制数值之间的关系。
此外,对于位运算,C还提供了移位操作:左移(<<n)和右移(>>n),效果上对应整形十进制运算的乘2的幂次方和除以2的幂次方,实际上则分别对应着所有二进制位增n位并末尾补0,和所有二进制位降n位并取整。
与C相比,Java对于逻辑右移有着明确定义,符号“>>”代表算数右移固定位数,而“>>>”代表逻辑位移,可以结合书本以及网上相关内容理解。而python的逻辑位移则与C大致相同,可以结合钱德勒马的文章理解。
作个暂时的总结
我们明白了为什么计算机的运算会这么执着于二进制、位和字节,以及一部分数据结构是如何实现的。下一篇笔记,我们会扯一扯我们暂时还没开始细讲的整形。
那么,还是那句结束词:前面的区域,以后再来探……哎不是?!
(报纸糊脸的声音)