目录
程序结构和执行
我们对计算机系统的探索是从学习计算机本身开始的,它由处理器和存储器子系统组成。
0x00 – 信息的表示和处理
现代计算机存储和处理的信息以二值信号表示。这些微不足道的二进制数字,或者称为位(bit),形成了数字革命的基础。
- 三种最重要的数字表示:
-
- 无符号(unsigned)编码:基于传统的二进制表示法,表示大于或者等于零的数字。
- 补码(two’s-complement)编码:表示有符号整数的最常见的方式,有符号整数就是可以为正或者为负的数字。
- 浮点数(floating-point)编码:表示实数的科学记数法以2为基数的版本。
-
计算机的表示法是用有限数量的位来对一个数字编码,当结果太大以至于不能表示时,某些运算就会出现溢出(overflow)。溢出回导致令人吃惊的结果。
整数的计算机运算可能没有产生期望的结果,但是至少它是一致的(满足人们所熟悉的整数运算的许多性质,如乘法交换律和结合律)。
浮点数运算有完全不同的数学属性。虽然溢出回产生特殊的值 +∞,但是一组正数的乘积总是正的。由于表示的精度有限,浮点运算是不可结合的。
整数和浮点数运算的数学属性不同是因为他们处理数字表示有限性的方式不同:
- 整数的表示虽然只能编码一个相对较小的数值范围,但是这种表示时精确的;
- 浮点数虽然可以变吗一个较大的数值范围,但是这种表示时近似的;
通过研究数学的实际表示,我们能了解可以表示的值的范围和不同算术运算的属性。(为了便携的程序能在全部数值范围内正确工作,切具有可跨不同机器、操作系统和编译器组合的可移植性,了解这种属性时非常重要的。)
大量计算机安全漏洞都是由于计算机算术运算的微妙细节引发的。(早起可能就是不方便,现在黑客能凭此进入他人系统。这要求程序猿有更多的责任和义务,去了解他们的程序时如何工作的,以及如何被破产生不良的行为)。
0x01 – 信息存储
大多数计算机使用8位的块,或者【字节(byte)】,最为最小的可寻址的内存单位,而不是访问内存只能够单独的位。
机器级程序将内存视为一个非常大的字节数组,称为虚拟内存(virtual memory)。
内存的每个字节都由一个唯一的数字来标识,称为它的地址(address)。
所有可能地址的集合就称为虚拟地址空间(virtual address space)。
这个虚拟地址空间只是一个展现给机器级程序的概念性映像,实际的实现时将动态随机访问存储器(DRAM)、内存、粗盘存储器、特殊硬件和操作系统软件结合起来,为程序提供一个看上去统一的自己数组。
0x01 – 1 – 十六进制表示法
以16为基数,或者叫做十六进制(hexadecimal)数,来标识位模式。十六进制(间歇位“hex”)使用数字“0”-“9”以及字符“A”-“F”来标识16个可能的值。
在 C 语言中, 以 0x 或 0X 开头的数字常量被认为是十六进制的值。
自符“A”-“F”可以是大写,也可以是小写,甚至可以是大小写混合。
0x01 – 2 – 字数据大小
每台计算机都有一个字长(word size),指明指针数据的标称大小(nominal size)。
因为虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的系统参数就是虚拟地址空间的最大大小。(一个w位的机器,虚拟地址范围位0~2w-1,程序最多访问2w个字节)
32位字长限制虚拟地址空间为4千兆字节(写作4GB),刚刚超过410^9字节;
64位字长使得虚拟地址空间位16EB,大约是 1.8410^19字节。
大多数 64 位机器可以运行 32 位机器编译的程序,这是一种向后兼容。
有些数据类型的确切数依赖于程序时如何被编译的。
- 32位和64位程序的典型值:
整数或者为有符号的:可以表示负数、零和正数;
无符号的:只能表示非负数;
为了避免由于以来“典型”大小和不同编译器设置带来的奇怪行为,ISO C99 引入了一类数据类型,其数据大小时固定的,不随编译器和机器设置而变化。其中:
int32_t:4个字节;
int64_t:8个字节;
使用确定大小的数据类型是程序猿准确控制数据表示的最佳途径。
0x01 – 3 – 寻址和字节顺序
对于跨越多字节的程序对象,我们必须建立两个规则:
- 这个对象的地址是什么?
- 在内存中如何排列这些字节?
几乎在所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中最小的地址。
排列表示一个对象的字节有两个通用的规则:
- 小端法(little endian):最低有效字节在最前面的方式;(大多数Intel兼容机都是用小端法)
- 大端法(big endian):最高有效字节在最前面的方式;(大多数IBM和Oracle的机器使用大端法;另外,IBM和Oracle制造的个人计算机使用的Intel兼容的处理器,因此使用小端法。)
许多较新的微处理器是双端法(bi-endian),可以把他们配制成大端或小端的机器运行;
实际情况:一旦选择特定操作系统,那么字节顺序也就固定下来了。
在那种字节顺序是合适的这个问题上,人们表现的非常情绪化,两个术语出自《格列佛游记》,甚至因为从鸡蛋的那一头打破鸡蛋引发战争,更多故事不赘述。
对于大多数应用程序猿来说,机器使用哪种字节顺序是完全不可见的,无论哪种机器编译的程序都会得到相同的结果。不过有时候字节顺序会成为问题:
- 如小端法机器产生的数据发送到大端法机器或者反过来时,接受程序会发现字节是反序的,为了避免这类问题,网络应用程序的代码编写必须遵守已经建立的关于字节顺序的规则。
- 当阅读表示整数数据的字节序列时字节顺序也很重要。这通常发生在检查机器级程序时,由反汇编器(disassembler)(一种确定可执行程序文件表示的指令序列的工具)生成的代码,可能是相反的顺序。
- 当编写规避正常类型系统的程序时。如:在C语言中,可以通过使用强制类型转换(cast)或联合(union)来允许以一种数据类型引用一个对象,而这种数据类型或创建这个对象时定义的数据类型不同。大多数应用编程都强烈不推荐这种编码技巧,但是他们对系统级编程来说时非常有用的,甚至是必须的。
0x01 – 4 – 表示字符串
C语言中字符串被编码为一个以null(其值为0)字符结尾的字符数组。
每个字符都由某个标准编码来表示,最常见的时 ASCII 字符码。
在使用SACII码最为字符码的任何系统上都将会得到相同的结果,与字节顺序和大小规则无关。因为,文本数据比二进制数据具有更强的平台独立性。
0x01 – 5 – 表示代码
不同的机器类型使用不同的且不兼容的指令和编码方式。
即使时完全一样的进程,运行在不同的操作系统上也会有不同的编码规则,因此二进制代码是不兼容的。
二进制代码很少能在不同机器和操作系统组合之间移植。
计算机系统的一个基本概念就是:从机器角度看程序仅仅是字节顺序。机器没有关于原始源程序的任何信息,除了可能有些用来帮助调试的辅助表意外。
0x01 – 6 – 布尔代数简介
二进制值是计算机编码、存储和操作信息的核心,所以围绕数值0和1的研究已经演化出了丰富的数学知识体系。
布尔代数(Boolean algebra):1850年起那后,乔治·布尔(George Boole)注意到:通过将逻辑值 TRUE(真)和FALSE(假)编码为二进制值1和0,能够设计出一种袋鼠,以研究逻辑推理的基本原则。
- 【布尔运算~】 ——【 逻辑运算非(取反) NOT】
- 【布尔运算&】——【逻辑运算与AND】
- 【布尔运算 |】——【逻辑运算或OR】
- 【布尔运算^】——【逻辑运算异或】
0x01 – 7 – C语言中的位级运算
C语言的一个很有用的特性就是它支持按位布尔运算。
- 【布尔运算~】 ——【 逻辑运算非(取反) NOT】
- 【布尔运算&】——【逻辑运算与AND】
- 【布尔运算 |】——【逻辑运算或OR】
- 【布尔运算^】——【逻辑运算异或】
0x01 – 8 – C语言中的逻辑运算
C语言中还提供了一组逻辑运算符 || 、&& 和 !,分别对应于命题逻辑中的 OR、AND和 NOT 运算。
逻辑运算很容易和位级运算想混淆,他们的功能是完全不同的。
- 逻辑运算认为所有非零的参数都表示TRUE,而参数0表示FALSE。他们返回1或者0,分别表示结果位TRUE或者FALSE。
表达式求值示例:
表达式 | 结果 |
---|---|
!0x41 | 0x00 |
!0x00 | 0x01 |
!!0x41 | 0x01 |
0x69&&0x55 | 0x01 |
0x69||0x55 | 0x01 |
按位运算只有在特殊情况下,也就是参数被限制为0或者1时,才和与其对应的逻辑运算有相同的行为。
- 逻辑运算&&和||与他们对应的位级运算 & 和|之间第二个重要的区别是,如果对第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值。因此表达式a&&5/a不会造成被零除,表达式p&&*p++也不回导致间接引用空指针。
0x01 – 9 – C语言中的移位运算
C语言还提供一组移位运算,向左或者向右移动位模式。
例如:
C表达式 x<<k会生成一个值(x向左移动k位,丢弃最高的k位,并在右端补齐k个0)
以为运算是从左至右可结合的,所以 x<<j<<k 等驾驭 (x<<j)<<k
有一个相应的右移运算 x>>k,但是它有点微妙。
一般而言,机器支持两种形式的右移:逻辑右移和算术右移。
- 逻辑右移:在左端补齐k个0
- 算术右移:在左端补k个最高有效值的位。这种做法看上去有点奇特,但是他对有符号整数数据的运算非常有用。
操作 | 值 |
---|---|
参数x | [01100011] [10010101] |
x<<4 | [00110000] [01010000] |
x>>4(逻辑右移) | [00000110] [00001001] |
x>>4(算术右移) | [00000110] [11111001] |
C语言标准并没有明确定义对有符号数应该使用哪种类型的右移——算术右移或者逻辑右移都可以。
这就意味着任何假设一种或者另一种右移形式的代码都可能会遇到可移植性问题。
- 实际上几乎所有的编译器/机器组合都对有符号数使用算术右移,且许多程序猿也都假设机器会使用这种右移。
- 另一方面,对于无符号数,右移必须是逻辑的。
2021年10月18日