第一章 对程序员来说CPU是什么
-
CPU四个构成部分
image.png -
寄存器种类
image.png 程序计数器 PC
保存下一条要执行的命令的内存地址-
标志寄存器处理条件分支(if else)和循环机制(for(;;i<100))
image.png -
栈寄存器实现函数调用机制
image.png -
基址寄存器和变址寄存器实现数组功能
image.png -
机器语言指令种类
image.png
第二章 数据是用二进制表示的
-
IC的所有引脚只有直流电压0V或5V两个状态,这个特性决定了计算机只能使用二进制
image.png -
负数的二进制表示:补数 取反加一
image.png -
逻辑右移 空出来的位置补0
image.png 算数右移 空出来的位置用移位前符号位的值
逻辑左移,算数左移都是补零
-
逻辑运算
image.png
计算机进行小数运算时出错的原因
-
将0.1累加100次的结果不是10,因为十进制的0.1在二进制中是无限循环小数0.0001100110011001100(无数个1100),类似十进制的1/3=0.3333333
image.png -
什么是单精度浮点数 float
由下面三部分组成,占位32位| 符号位 | 指数位(8 bit) | 尾数位(23 bit) | | :-: | :-: | :-: |
符号位(1位)
1 : 表示负数 0 : 表示正数
指数位(8位) 使用EXCESS系统表现,值域位[-127,128]
image.png
尾数位(24位)
先把小数转成二进制小数,比如11.1875的二进制小数为1011.0011
image.png
值域表示区间为正数: [1.401298 * 10^-45, 3.402823 * 10^38] 负数: [-3.402823 * 10^38, -1.401298 * 10^-45]
-
什么是双精度浮点数 double
跟float类似,占64位| 符号位 | 指数位(11 bit) | 尾数位(52 bit) | | :-: | :-: | :-: |
值域表示区间为
正数: [4.94065645841247 * 10^-324, 1.79769313486232 * 10^308] 负数: [-1.79769313486232 * 10^308, -4.94065645841247 * 10^-324]
第四章 熟练使用有棱有角的内存
-
内存的物理机制
image.png -
数组的内存模型
image.png
第五章 内存和磁盘的亲密关系
- 虚拟内存把磁盘作为部分内存来使用
-
静态链接
从目标文件中抽对应代码出来放到应用程序的目标文件,多个应用则会有多份同样的代码,浪费内存
image.png -
动态链接 DLL
同一个DLL文件的内容运行时可以被多个应用共有,内存利用率高,修改dll文件无需重新编译应用
image.png -
磁盘物理结构
Windows采用扇区方式划分磁盘,扇区是磁盘进行物理读写的最小单位,Windows对磁盘进行逻辑读写的最小单位是簇,1簇=n*扇区,也就是一次读写多个扇区增加性能
image.png
第六章 亲自尝试压缩数据
-
RLE(Run Length Encoding 行程长度算法)
常用于压缩传真的图像,黑白的图像,不适用于重复性不高的文本AAAAAABBCDDEEEEEF 17字节 => A6B2C1D2E5F1 12字节 压缩率: 12/17=70%
-
哈夫曼编码
AAAAAABBCDDEEEEEF先按频率高到低排列的顺序整理
image.png
这个编码方案有问题,C(100) 和 BA(10 0)没有间隔符分不清楚,需要换一种编码方式
image.png
哈夫曼算法能够大幅提升压缩比率
image.png 可逆压缩和不可逆压缩
BMP格式(bitmap)是完全未压缩的,JPEG,TIFF,GIF都会用一些技法对数据进行压缩,与前面说的RLE算法,哈夫曼算法不同的是,BMP压缩成JPEG,TIFF,GIF之后没办法还原成原来同等的质量,对图片来说是可以接受的,这叫不可逆压缩压缩算法千万种,没有最好用的压缩算法,只有最合适的压缩算法
第七章 程序是在何种环境中运行的
- Intel的处理器是按照8086,80286,80386,80486,Pentium这样的顺序不断升级的,所以总称x86
-
以前的写软件要针对不同的厂商不同的机器分别写一套,Windows解决了这个问题
image.png - MS-DOC应用大多都是直接控制硬件的,而WINDOWS应用则是通过操作系统的系统调用完成对硬件的控制
- 不同的操作系统,系统调用API不同
-
JAVA的跨平台是通过先编译成字节代码(byte code),运行的时候通过不同操作系统专用的JAVA虚拟机(JVM)逐行转换成本地代码运行
image.png - BIOS(Basic Input/Output System)存储在ROM中,除了键盘,磁盘,显卡等基本控制程序外,还有启动引导程序的功能,开机后,BIOS确认硬件是否正常,然后启动引导程序把在硬盘的操作系统加载到内存中运行,此后就没BIOS什么事了
第八章 从源文件到可执行文件
-
计算机只能运行本地代码,还要是符合CPU指令集的本地代码
image.png 交叉编译器
生成和运营环境的CPU不同的CPU所使用的代码,比如在ARM的计算器生成X86架构的本地代码编译生成本地文件之后,需要进行链接(link)之后才能生成可执行文件
C语言编译生成.obj目标文件(本地代码),需要通过链接器链接生成exe文件静态链接库
当WINDOWS系统程序调用到静态链接库的代码时,比如sprintf()
,需要在cw32.lib把sprintf的代码单独抽出来,放进程序的本地代码中
Unix系统sprintf()
函数在stdio.h库里DLL (Dynamic Link Library) 文件是程序运行时动态结合的文件,这种动态文件一般都由操作系统加载在内存里面
(@todo没太搞清楚动态链接具体做了什么,后面回来补)-
链接机制
image.png 可执行文件内容分为再配置信息,变量组,函数组
当可执行文件加载到内存之后会增加堆,栈两个组
再配置信息用来设置程序起始虚拟内存地址
栈用来实现函数调用的,栈操作由编译器生成,全程无需程序员参与
堆用来存储程序运行时的任意数据及对象的内存,程序员可以编写程序申请分配或者释放堆内存
第九章 操作系统和应用的关系
操作系统发展史
没有操作系统的年代:用机器语言编写程序,使用开关输入程序
监控程序:先启动监控程序,按需求把各种程序一次性加载到内存中运行,后来键盘输入,显示器输出等共通的部分追加到监控程序中
编译器:监控程序运行的程序里面出现了编译器计算机中都安装有保存日期和时间的实用时钟(Real-time clock)
time()
和printf()
都使用了系统调用,涉及到硬件基本都涉及到系统调用-
同样的源代码在不同的操作系统编译生成本地代码使用的系统调用不一样
image.png WYSIWYG (What You See Is What You Get) 是一种显示器看到的文本和图形可以原样打印到打印机的技术
WIN32 API WIN64 API 不一样,但都是通过 DLL(动态链接库)提供的。
多任务功能是通过时钟分割技术实现的
Windows还具有以程序中的函数为单位来进行时钟分割的多线程功能
第十章 通过汇编语言了解程序的实际构成
-
汇编和反汇编
image.png 从本地代码反汇编成汇编语言很简单,从汇编语言反编译完全还原到原始的源代码不太可能,因为从源码到本地代码经历了各种优化,这种优化也没记录到本地代码中,与不可以压缩的原理差不多
-
基本的操作码
image.png -
X86系列主要的寄存器,32位CPU开头都带了一个字母e(extended),区别于16位CPU,ax,bx,cx,dx等,64位CPU开头都带了R,RAX,RBX...
image.png 简单的MOV指令
mov ebp, esp //esp寄存器的值存储到ebp寄存器
mov eax, dword ptr [ebp+8] //ebp+8的值被当成内存地址(指针)存到eax寄存器中
- 32位X86系列的CPU中,进行1次PUSH/POP即可处理32位的数据
@todo 参数是1个字节的字符 或者结构体怎么运作 - 函数调用
函数的参数是通过栈传递的(寄存器多了之后也有通过寄存器传值,性能好一点),返回值是通过寄存器来返回的,这里是eax寄存器保存AddNum函数的返回值
_MyFunc proc near
push ebp ; 保存当前函数的基址指针
mov ebp, esp ; 将当前栈指针赋值给基址指针
push 456 ; 将值456压入栈
push 123 ; 将值123压入栈
call _AddNum ; 调用_AddNum函数,跳转到下面
add esp, 8 ; 调整栈指针,释放之前压入的参数
pop ebp ; 恢复之前保存的基址指针
ret ; 返回调用方
_MyFunc endp
_AddNum proc near
push ebp ; 保存当前函数的基址指针
mov ebp, esp ; 将当前栈指针赋值给基址指针
mov eax, dword ptr [ebp+8] ; 将偏移为8的位置的值(第一个参数)赋给寄存器eax
add eax, dword ptr [ebp+12] ; 将偏移为12的位置的值(第二个参数)加到eax寄存器上
pop ebp ; 恢复之前保存的基址指针
ret ; 返回调用方
_AddNum endp
- 全局变量
已初始化的全局变量在_DATA段,未初始化的全局变量在_BSS段,虽然段不同,内存空间在程序初始都分配好的,所以其他函数随时可以引用全局变量
局部变量在寄存器空闲时存在寄存器,寄存器空间不足的话就使用栈,寄存器访问速度比内存快得多多,只要寄存器有空间,编译器就会拼命压榨它
不只是形参存在栈里,函数里面定义的局部变量也存在栈里
int a1 = 1, a2 = 2, a3 = 3, a4 = 4, a5 = 5;
int b1, b2, b3, b4, b5;
void MyFunc()
{
int c1 = 1, c2 = 2, c3 = 3, c4 = 4, c5 = 5, c6 = 6, c7 = 7, c8 = 8, c9 = 9, c10 = 10;
a1 = c1; a2 = c2; a3 = c3; a4 = c4; a5 = c5;
b1 = c6; b2 = c7; b3 = c8; b4 = c9; b5 = c10;
}
以上C语言代码的汇编代码如下
_DATA segment dword public use32 'DATA'
_a1 label dword ; 定义一个名为_a1的标签,表示一个32位整数变量
dd 1 ; 初始化_a1变量为值1
_a2 label dword ; 定义一个名为_a2的标签,表示一个32位整数变量
dd 2 ; 初始化_a2变量为值2
_a3 label dword ; 定义一个名为_a3的标签,表示一个32位整数变量
dd 3 ; 初始化_a3变量为值3
_a4 label dword ; 定义一个名为_a4的标签,表示一个32位整数变量
dd 4 ; 初始化_a4变量为值4
_a5 label dword ; 定义一个名为_a5的标签,表示一个32位整数变量
dd 5 ; 初始化_a5变量为值5
_DATA ends
_BBS segment dword public use32 'BSS'
_b1 label dword ; 定义一个名为_b1的标签,表示一个32位整数变量
db 4 dup(?) ; 分配4字节的空间,但未初始化
_b2 label dword ; 定义一个名为_b2的标签,表示一个32位整数变量
db 4 dup(?) ; 分配4字节的空间,但未初始化
_b3 label dword ; 定义一个名为_b3的标签,表示一个32位整数变量
db 4 dup(?) ; 分配4字节的空间,但未初始化
_b4 label dword ; 定义一个名为_b4的标签,表示一个32位整数变量
db 4 dup(?) ; 分配4字节的空间,但未初始化
_b5 label dword ; 定义一个名为_b5的标签,表示一个32位整数变量
db 4 dup(?) ; 分配4字节的空间,但未初始化
_BBS ends
_TEXT segment dword public use32 'CODE'
_MyFunc proc near
push ebp ; 保存当前函数的基址指针
mov ebp, esp ; 将当前栈指针赋值给基址指针
add esp, -20 ; 分配20个字节的栈空间
push ebx ; 保存ebx寄存器
push esi ; 保存esi寄存器
mov eax, 1 ; 将值1赋给eax寄存器
mov edx, 2 ; 将值2赋给edx寄存器
mov ecx, 3 ; 将值3赋给ecx寄存器
mov ebx, 4 ; 将值4赋给ebx寄存器
mov esi, 5 ; 将值5赋给esi寄存器
mov dword ptr [ebp-4], 6 ; 将值6存储到基址指针减4的位置
mov dword ptr [ebp-8], 7 ; 将值7存储到基址指针减8的位置
mov dword ptr [ebp-12], 8 ; 将值8存储到基址指针减12的位置
mov dword ptr [ebp-16], 9 ; 将值9存储到基址指针减16的位置
mov dword ptr [ebp-20], 10 ; 将值10存储到基址指针减20的位置
mov dword ptr [_a1], eax ; 将eax寄存器的值存储到_a1变量中
mov dword ptr [_a2], edx ; 将edx寄存器的值存储到_a2变量中
mov dword ptr [_a3], ecx ; 将ecx寄存器的值存储到_a3变量中
mov dword ptr [_a4], ebx ; 将ebx寄存器的值存储到_a4变量中
mov dword ptr [_a5], esi ; 将esi寄存器的值存储到_a5变量中
mov eax, dword ptr [ebp-4] ; 将基址指针减4位置的值存储到eax寄存器
mov dword ptr [_b1], eax ; 将eax寄存器的值存储到_b1变量中
mov edx, dword ptr [ebp-8] ; 将基址指针减8位置的值存储到edx寄存器
mov dword ptr [_b2], edx ; 将edx寄存器的值存储到_b2变量中
mov ecx, dword ptr [ebp-12] ; 将基址指针减12位置的值存储到ecx寄存器
mov dword ptr [_b3], ecx ; 将ecx寄存器的值存储到_b3变量中
mov eax, dword ptr [ebp-16] ; 将基址指针减16位置的值存储到eax寄存器
mov dword ptr [_b4], eax ; 将eax寄存器的值存储到_b4变量中
mov edx, dword ptr [ebp-20] ; 将基址指针减20位置的值存储到edx寄存器
mov dword ptr [_b5], edx ; 将edx寄存器的值存储到_b5变量中
pop esi ; 恢复保存的esi寄存器
pop ebx ; 恢复保存的ebx寄存器
mov esp, ebp ; 恢复之前保存的栈指针
pop ebp ; 恢复之前保存的基址指针
ret ; 返回调用方
_MyFunc endp
_TEXT ends
-
栈清理
先把esp存在ebp,函数返回之后,再把ebp的值存回esp,完成栈清理
image.png xor
比mov
处理速度更快,编译器会自动优化
xor ebx ebx ;将ebx寄存器清0,效率更高
mov ebx 0 ;将ebx寄存器清0
- 循环语句转汇编
void MySub(){}
void MyFunc(){
int i;
for (i =0; i< 10; i++) {
MySub();
}
}
xor ebx, ebx ; 将 ebx 寄存器的值设置为零(相当于 ebx = 0)
@4:
call _MySub ; 调用名为 _MySub 的子程序(函数)
inc ebx ; 将 ebx 寄存器的值加一(相当于 ebx = ebx + 1)
cmp ebx, 10 ; 比较 ebx 的值和 10
jl short @4 ; 如果 ebx 小于 10,则跳转到标签 @4 处(继续执行循环)
- 线程不安全的原因
下面一条C语言代码转换成三行汇编代码,没有原子性,线程1把counter*2的值读出来没来得及写入,线程2把counter原来的值读出来,这个时候就会出现大家都不想见到的情况
counter *= 2;
mov eax, dword ptr[_counter];
add eax,eax
mov dword ptr[_counter], eax;
硬件控制方法
现代高级编程语言很少能直接控制硬件,都是通过操作系统的系统调用控制硬件
-
主机背后附带了用来连接显示器,键盘等外围设备的连接器,连接器的内部有用来和计算机交换数据的IO控制器,IO控制器中有用于临时保存输入输出数据的内存,这个内存就叫端口(port),比如键盘输入的内容都会暂存在键盘线和主机的连接处(IO控制器)中,然后才被CPU读取,端口的地址也成为IO地址,WINDOWS的IN/OUT命令使用IO地址进行通讯
image.png 蜂鸣器启动和停止
计算机里面有个蜂鸣器,端口地址为61H(十六进制),这个地址大概存储一个字节(8位),其中低2位为1时发出蜂鸣,为0时停止蜂鸣,汇编代码如下,这段代码只能在Windows98以下的操作系统运行,之后Windows禁止了直接操作硬件的方式
; 启动
IN EAX, 61H ; 从端口61H读取数据并存储到寄存器EAX中
OR EAX, 03H ; 将寄存器EAX的值与00000011进行按位或运算,并将结果存储回EAX寄存器
OUT 61H, EAX ; 将寄存器EAX的值输出到端口61H
; 停止
IN EAX, 61H ; 从端口61H读取数据并存储到寄存器EAX中
AND EAX, 0FCH ; 将寄存器EAX的值与11111100进行按位与运算,并将结果存储回EAX寄存器
OUT 61H, EAX ; 将寄存器EAX的值输出到端口61H
-
IRQ (Interrupt Request) 中断请求
主程序运行中,CPU收到硬件的中断请求,比如键盘鼠标输入,会保存好主程序的各个寄存器,把控制权交给硬件的中断处理程序
外围设备的中断请求会使用不同于IO端口地址的中断编号
中断控制器的功能是缓冲多个外围设备同时进行中断请求,使每个中断请求有序地传递给CPU
image.png
操作系统和BIOS都有提供响应中断编号的程序 -
中断请求的顺序
image.png -
DMA (Direct Memory Access)
DMA指的是主存和外围设备不通过CPU进行直接数据传送的技术,磁盘也用到了这个机制,估计是虚拟内存这块用到
通过DMA,节省了CPU作为中间商赚差价的时间,大量数据可以短时间从外围设备的IO控制器传输到主存中
使用了DMA的外围设备资源块会显示
image.png
假如多个外围设备都设定成同样的端口号,IRQ,DMA通道,计算机就无法正常工作并出现设备冲突
的提示 -
显示器中显示的信息一直都存储在VRAM(Video RAM)内存中,需要借助中断程序往VRAM写入数据,然后在显示器显示出来
MS-DOC时代,VRAM是主存的一部分,A0000地址以后是VRAM区域,文字和图形的颜色最多只能16种,现代计算机把VRAM从主存独立出来,仍集成在主板上的叫集显
,独立出去的叫独显
image.png GPU (Graphics Processing Unit)
VRAM独立出来后,因为CPU的指令集处理图形运算效率太低,,GPU则被设计用于高并行的计算任务,特别擅长处理大规模图形数据和复杂的计算操作。
显卡是由GPU和独立出来的VRAM组成的
让计算机思考
这章就算了
附录
-
数据类型
image.png