程序的结构和执行
1997字约7分钟
计算机系统结构
2021-08-02
在计算机中,信息都是二进制。这一节会尝试解答以下几个问题:
- 信息在计算机中是如何表示的?
- 计算机又是如何找到和存储这些信息的?
- 计算机对数据有哪些运算规则?
- 整数和浮点数在计算机中如何表示和运算?
信息的表示
计算机用二进制存储信息,为了兼顾人类能看懂,又方便表示,一般我们用十六进制来表达计算机中的数字。C语言用 0x
开头表示十六进制数字,如 0x8A
表示 10001010
,即 138
。
当一个数字 x 是 2 的非负整数 n 次幂时,即 x = 2^n
,我们可以很快算出这个数的十六进制。
观察下面的进制转换:
2^3
二进制为1000
2^4
二进制为1 0000
2^8
二进制为1 0000 0000
不难发现, 2的几次方转化成二进制就是1后面有多少个0 。
所以我们很快能得出 2^11
为 1000 0000 0000
, 改写成十六进制就是 0x800
。
字长
我们通常说的32位机器和64位机器,一般都是指他们的字长(word size),字长通俗地说就是一个字的长度。32位机器的字长为4个字节,64位机器则为8个字节。
字长决定虚拟地址的大小,32位字长的机器虚拟地址空间最大为 4GB ,而64位机器的则为 16EB。
当我们说32位程序还是64位程序时,说的是他们的编译方式,而不是运行的机器类型。64位的机器可以兼容32位程序。
linux> gcc -m32 hello.c
内存地址和顺序
内存地址
程序将内存视为一个非常大的字节数组,这个数组称为虚拟内存(virtual memory)
,虚拟内存中的每个字节,都可以由唯一的数字(数组下标)来标识,称为内存地址
。所有可能地址的集合,就称为虚拟地址空间
。
寻址
当我们在程序中声明一个变量(对象)时,我们会好奇这个变量在内存中的位置(即地址),如果这个变量占用多个字节,它在内存中的排列顺序又是怎么样的?在内存中寻找某个“程序对象”的地址的过程,称为寻址。
大端法和小端法
几乎所有的机器,都会把多字节对象存储在连续的地址中,例如一个4字节的int变量,地址为 0x100
,那么它四个字节分别被存储在 0x100
、0x101
、0x102
、0x103
位置。
但是有一些机器,从最高有效字节到最低有效字节的顺序排列,称为大端法(big endian)
,另一些机器又是从最低到最高,称为小端法(little endian)
。例如,一个 int 变量 0x01234567
, 用大端法表示是 01 23 45 67
, 而用小端法表示却是 67 45 23 01
。
我们常用的 x86 机器(Intel兼容机),包括 Windows、Linux 和 Mac 操作系统,都是采用了小端法。而有一些 IBM 和 Oracle 机器采用了大端法。用于智能手机的ARM芯片,本身硬件支持大端或小端,但有趣的是,搭载了 Android 或 iOS 操作系统的这些智能手机,只能运行小端模式。
大端和小端并没有谁对谁错,谁好谁坏之分,甚至对于我们开发人员来说,字节顺序对我们是隐藏的。但是了解这些知识,可以避免我们在某些时候出错,例如通过网络发送二进制数据时,大端法的机器传送到小端法的机器,如果没有特殊处理,就会导致接收到的字节顺序反了。
字符串表示
C语言中,字符串的本质是字节数组,且以null(ASCII码为00
)结尾。字符串 12345
的字节数组为 31 32 33 34 35 00
(1
的ASCII码是49
,十六进制0x31
)。
字符串的表示跟字节顺序和字大小规则无关。
运算
在C语言中,支持位运算、逻辑运算和移位运算。
布尔代数即数学中与、或、非、异或的概念。如下表:
符号 | 英文简写 | 中文含义 |
---|---|---|
& | AND | 与 |
| | OR | 或 |
~ | NOT | 非 |
^ | XOR | 异或 |
C语言可以直接用这些符号进行位运算。例如: ~0x41
(~0100 0001
) 的结果是 0xBE
(1011 1110
)
有时候我们会希望进行逻辑上的运算,比如某两个条件都成立时,或某两个条件之一成立时,这种场景则需要用逻辑运算。
符号 | 英文简写 | 中文含义 |
---|---|---|
&& | AND | 逻辑与 |
|| | OR | 逻辑或 |
! | NOT | 非 |
逻辑运算只有两种结果, TRUE
和 FALSE
, 用 1 和 0 来表示。
例如,!0x41
的结果为 0x00
,即 FALSE
。
移位运算,即把二进制数整体往左或往右移动。用 <<
和 >>
符号来表示。需要注意的是,右移分为逻辑右移和算术右移,逻辑右移无脑填充0,算术右移会根据最高位符号位,来决定是填充0还是填充1。
整数
在C语言中,有无符号数和有符号数之分。无符号数只能表示非负数,用关键字 unsigned
声明。其内部是用无符号编码的,8位的数字取值范围为 0000 0000
~ 1111 1111
。
但默认情况下,C语言的数字可以表示负数,这种称为有符号数。其内部用补码编码。二进制的第一位作为符号位,为0时表示非负数,为1时表示负数。
两个正数相加可能会得到一个负数,这是因为计算机的字长是有限的,当相加后的值超出所能表达的最大值,会出现溢出。
浮点数
在我们熟悉的十进制中,我们知道,一个数字乘以10,小数点就向右移动一位,除以10,小数点就向左移动一位。 例如 123.456
,乘10为 1234.56
,除以10为 12.3456
。
在二进制中也是类似,乘以2,向右移动一位,除以2,向左移动一位。例如 1101.1010
,乘2为 11011.010
,除以2为 110.11010
。反过来说,移多少位,就是2的多少次方。
在计算机中,处理小数有两种表示方法 —— 定点 和 浮点,定点就是小数点永远在固定的位置上,提前对齐。优点是简单,缺点是表示范围小,不能充分运用二进制的存储单元。而浮点相当于一个定点数加上一个阶码,阶码表示将这个定点数的小数点移动若干位,由于可以用阶码移动小数点,因此称为浮点数。
计算机中用三个部分组合的二进制位来表示浮点数。分别为:
- 数符:正数为0,负数为1
- 阶码:阶码的计算公式:阶数(左移多少位) + 偏移量
- 尾数:小数点后面的数
例如一个数字 178.125
,换成二进制是 10110010.001
,这个二进制数用浮点数怎么表示呢? 我们用32位的二进制数表示,第1位为符号位,第29位为阶码位,第1032为尾数。
- 首先这个数字是正数,数符肯定为0
- 之后把小数点移动到整数位只有1(
1.0110010001
),发现需要左移7位,得到阶数为7,即111
, 偏移量2^(e-1)-1 = 127(e是阶码的位数,8),即01111111
,计算阶码为111 + 01111111 = 10000110
- 尾数就是
1.0110010001
小数点后面的数0110010001
最终得到 0 10000110 0110010001 0000000000000
(32位,后面补0)