动态链接
程序的链接,是把对应的不同文件内的代码段,合并到一起,成为最后的可执行文件。
- 在动态链接的过程中,我们想要“链接”的,不是存储在硬盘上的目标文件代码,而是加载到内存中的共享库(Shared Libraries)。
- 动态代码库内部的变量和函数调用都很容易解决,我们只需要使用相对地址(Relative Address)就好了。各种指令中使用到的内存地址,给出的不是一个绝对的地址空间,而是一个相对于当前指令偏移量的内存地址。因为整个共享库是放在一段连续的虚拟内存地址中的,无论装载到哪一段地址,不同指令之间的相对地址都是不变的。
在 Windows 下,这些共享库文件就是.dll 文件,也就是 Dynamic-Link Libary(DLL,动态链接库)。在 Linux 下,这些共享库文件就是.so 文件,也就是 Shared Object(一般我们也称之为动态链接库)。这个加载到内存中的共享库会被很多个程序的指令调用到。
- 在动态链接对应的共享库,我们在共享库的 data section 里面,保存了一张全局偏移表(GOT,Global Offset Table)。虽然共享库的代码部分的物理内存是共享的,但是数据部分是各个动态链接它的应用程序里面各加载一份的。所有需要引用当前共享库外部的地址的指令,都会查询 GOT,来找到当前运行程序的虚拟内存里的对应位置。而 GOT 表里的数据,则是在我们加载一个个共享库的时候写进去的。
- 不同的进程,调用同样的 lib.so,各自 GOT 里面指向最终加载的动态链接库里面的虚拟内存地址是不同的。这样,虽然不同的程序调用的同样的动态库,各自的内存地址是独立的,调用的又都是同一个动态库,但是不需要去修改动态库里面的代码所使用的地址,而是各个程序各自维护好自己的 GOT,能够找到对应的动态库就好了。
二进制编码
不管是整数也好,浮点数也好,采用二进制序列化会比存储文本省下不少空间。
- 为了使信号可以一段段传输下去,而不会因为距离太长,导致电阻太大,没有办法成功传输信号。为了能够实现这样接力传输信号,在电路里面,工程师们造了一个叫作继电器(Relay)的设备。
中继,其实就是不断地通过新的电源重新放大已经开始衰减的原有信号.
常见门电路的标识
- 今天包含十亿级别晶体管的现代 CPU,都是由这样一个一个的门电路组合而成的。
- 作为一个基本电路。其实,异或门就是一个最简单的整数加法,所需要使用的基本门电路。
两个门电路打包,给它取一个名字,就叫作半加器
两个半加器和一个或门,就能组合成一个全加器
乘法器
仅仅需要简单的加法器、一个可以左移一位的电路和一个右移一位的电路,就能完成整个乘法。
通过精巧地设计电路,用较少的门电路和寄存器,就能够计算弯沉过程发这样相对复杂的运算,是用更少更简单的电路,但是需要更长的门延迟和时钟周期;还是用更复杂的电路,但是更短的门延迟和时钟周期来计算一个复杂的指令,这之间的权衡,其实就是计算机体系结构中的RISC和CISC的经典历史线路之争
浮点数的不精确性
- Chrome 浏览器里面通过开发者工具,打开浏览器里的 Console,在里面输入“0.3 + 0.6”,然后看看你会得到一个什么样的结果。
这是为什么呢?
- 在回答为什么之前,我们先来想一个更抽象的问题。现在用的计算机通常用16/32 个比特(bit)来表示一个数。用 32 个比特,能够表示所有实数吗?
- 答案很显然是不能。32 个比特,只能表示 2 的 32 次方个不同的数,差不多是 40 亿个。如果表示的数要超过这个数,就会有两个不同的数的二进制表示是一样的。那计算机可就会一筹莫展,不知道这个数到底是多少。
- 40 亿个数看似已经很多了,但是比起无限多的实数集合却只是沧海一粟。所以,这个时候,计算机的设计者们,就要面临一个问题了:我到底应该让这 40 亿个数映射到实数集合上的哪些数,在实际应用中才能最划得来呢?
定点数的表示
- 用 4 个比特来表示 0~9 的整数,那么 32 个比特就可以表示 8 个这样的整数。然后我们把最右边的 2 个 0~9 的整数,当成小数部分;把左边 6 个 0~9 的整数,当成整数部分。这样,我们就可以用 32 个比特,来表示从 0 到 999999.99 这样 1 亿个实数了。这种用二进制来表示十进制的编码方式,叫作BCD 编码(Binary-Coded Decimal)。
浮点数的表示
- 在计算机里,我们也可以用一样的办法,用科学计数法来表示实数。浮点数的科学计数法的表示,有一个IEEE的标准,它定义了两个基本的格式。一个是用32 比特表示单精度的浮点数,也就是我们常常说的 float 或者 float32 类型。另外一个是用64 比特表示双精度的浮点数,也就是我们平时说的 double 或者 float64 类型。
来看单精度类型,双精度你自然也就明白了
单精度的 32 个比特可以分成三部分
- 第一部分是一个符号位,用来表示是正数还是负数。我们一般用s来表示。在浮点数里,我们不像正数分符号数还是无符号数,所有的浮点数都是有符号的。
- 第二部分是一个 8 个比特组成的指数位。我们一般用e来表示。8 个比特能够表示的整数空间,就是 0~255。我们在这里用 1~254 映射到 -126~127 这 254 个有正有负的数上。因为我们的浮点数,不仅仅想要表示很大的数,还希望能够表示很小的数,所以指数位也会有负数。你发现没,我们没有用到 0 和 255。没错,这里的 0(也就是 8 个比特全部为 0) 和 255 (也就是 8 个比特全部为 1)另有它用。
- 第三部分是一个 23 个比特组成的有效数位。我们用f来表示。综合科学计数法,我们的浮点数就可以表示成下面这样:
浮点数,没有办法表示 0。的确,要表示 0 和一些特殊的数,我们就要用上在 e 里面留下的 0 和 255 这两个表示,这两个表示其实是两个标记位。在 e 为 0 且 f 为 0 的时候,我们就把这个浮点数认为是 0。至于其它的 e 是 0 或者 255 的特殊情况,你可以看下面这个表格,分别可以表示出无穷大、无穷小、NAN 以及一个特殊的不规范数。
为什么我们用 0.3 + 0.6 不能得到 0.9 呢?这是因为,浮点数没有办法精确表示 0.3、0.6 和 0.9。事实上,我们拿出 0.1~0.9 这 9 个数,其中只有 0.5 能够被精确地表示成二进制的浮点数,也就是 s = 0、e = -1、f = 0 这样的情况。
浮点数的二进制转化
- 十进制浮点数 9.1。那么按照之前的讲解,在二进制里面,我们应该把它变成一个“符号位 s+ 指数位 e+ 有效位数 f”的组合。
那么二进制 0.1001,转化成十进制就是:
小数部分转换成二进制是用一个相似的反方向操作,就是乘以 2,然后看看是否超过 1。如果超过 1,我们就记下 1,并把结果减去 1,进一步循环操作。
十进制 0.1 其实变成了一个无限循环的二进制小数,0.000110011。这里的“0011”会无限循环下去。
- 9.1 这个十进制数就变成了 1001.000110011…这样一个二进制表示。
- 浮点数其实是用二进制的科学计数法来表示的,所以我们可以把小数点左移三位,这个数就变成了:
重要的一部分
- 这个网址(https://www.h-schmidt.net/FloatConverter/IEEE754.html)提供了直接交互式地设置符号位、指数位和有效位数的操作。你可以直观地看到,32 位浮点数每一个 bit 的变化,对应的有效位数、指数会变成什么样子以及最后的十进制的计算结果是怎样的。