CPU

CPU内部结构解析

CPU中,负责保存指令和数据的寄存器,CPU和内存是由许多晶体管组成的电子部件,称为IC(集成电路)。

CPU内部由寄存器,控制器,运算器和时钟四个部分组成。

  • 寄存器:暂存指令,数据,20-100个寄存器
  • 控制器:负责把内存上的指令和数据读入寄存器,并且根据指令的执行结果控制整个计算机
  • 运算器:负责运算从内存读入寄存器的数据
  • 时钟:负责发出CPU开始计时的时钟信号

主存通过控制芯片等与CPU相连,主要负责存储指令和数据。主存由可写的元素构成,每个字节都带有一个地址编号,CPU可以通过该地址读取主存中的指令和数据。

程序启动后,根据时钟信号,控制器会从内存中读取指令和数据,通过对这些指令加以解释和运行,运算器就会对数据进行运算,控制器根据运算结果来控制计算机。

CPU是寄存器的集合体

程序是把寄存器作为对象来描述的

汇编语言采用助记符来编写程序,每一个原本是电气信号的机器语言指令都会有一个相应的助记符,比如mov和add

1
2
3
mov eax, dword ptr [ebp-8]
add eax, dword ptr [ebp-0Ch]
mov dword ptr [ebp-4], eax

其中eax和ebp都是寄存器,分别为累加寄存器和基址寄存器,内存的存储场所通过地址编号来区分,寄存器的种类通过名字来区分。

机器语言级别的程序是通过寄存器来处理的,寄存器中存储的内容既可以是指令也可以是数据,数据分为用于计算的数值和表示内存地址的数值,用于运算的数值放在累加寄存器中存储,表示内存地址的数值放在基址寄存器和变址寄存器中存储。

  • 累加寄存器:存储执行计算的数据和运算后的数据
  • 标志寄存器:存储运算处理后CPU的状态
  • 程序计数器:存储下一条指令所在内存的地址
  • 基址寄存器:存储数据内存的起始地址
  • 变址寄存器:存储基址寄存器的相对地址
  • 通用寄存器:存储任意数据
  • 指令寄存器:存储指令,CPU内部使用
  • 栈寄存器:存储栈区域的起始地址

程序计数器,累加寄存器,标志寄存器,指令寄存器和栈寄存器只有一个

决定程序流程的程序计数器

条件分支和循环机制

顺序执行时候,每执行一个指令程序计数器的值便自动加1,如果出现条件分支和循环,机器语言的指令就可以将程序计数器的值设定为任意地址。

条件分支和循环中使用的跳转指令,会参照当前执行的运算结果来判断是否跳转,使用了标志寄存器,无论当前累加寄存器的运算结果是正,负还是0,标志寄存器都会将其保存。运算结果的正,零,负三种状态由标志寄存器的三个位表示。

CPU执行比较的时候,会进行减法运算,结果保存在标志寄存器中(正,负或者0)

函数的调用机制

函数调用处理也是通过把程序计数器的值设定成函数的存储地址来实现的。函数原点和被调用函数之间的数据传递,通过内存或者寄存器实现。

整个调用会使用call指令和return指令,在将函数的入口地址设定到程序计数器之前,call指令会将调用函数后要执行的指令地址存储在栈的主存里。函数处理完毕后,通过函数出口来执行return命令。return命令的功能是把存在栈中的地址设定到程序计数器中。

通过地址和索引实现数组

基址寄存器和变址寄存器可以对内存上特定的内存区域进行划分,从而实现数组的操作。

对于4GB内存,可以用00000000-FFFFFFFF的地址将其划分出来,16进制,1位代表了4位二进制,总共32位,所以一个32位寄存器就可以查看全部的内存地址。

但是如果要实现数组的话,需要两个寄存器。CPU会把基址寄存器+变址寄存器的值解释为实际查看的内存地址。

机器语言指令的主要类型

  • 数据转送指令:寄存器和内存,内存和外存,寄存器和外围设备之间的数据读写操作
  • 运算指令:用累加寄存器执行算数运算,逻辑运算,比较运算和移位运算
  • 跳转指令:实现条件分支,循环,强制跳转等
  • call/return指令:函数调用/返回调用前的地址

数据是用二进制表示的

IC集成电路,微处理器,IC的所有引脚只有直流电压0V和5V两个状态,IC的一个引脚,只能表示两个状态。

唉,现在才明白很多之前微机原理学到的东西。。之前怎么就荒废过去了,啥都没学到。

IC的这个特性,决定了计算机的信息数据只能用二进制数来处理。计算机处理信息的最小单位-位bit(binary digit),就相当于二进制中的一位。

8位二进制数被称为一个字节,字节是最基本的信息计量单位,位是最小的单位,字节是基本单位。

32位微处理器,具有32个引脚用于信息输入输出,所以一次可以处理32位4字节的二进制数信息。

二进制数

位权,比如十进制数39,就是3*10^1+9*10^0,所以二进制数同理。一个是以10为基数,一个以2位基数。

移位运算和乘除运算

移位运算将二进制数值的各位数进行左右移位,左移,右移。<< >>,然后左移右移就可以相当于乘除法。二进制数左移一位,就会扩大两倍。

补码

二进制数中表示负数值时候,会把最高位作为符号位使用。符号位0是正数,1为负数。但是-1的二进制数是11111111

因为计算机做减法运算的时候其实是在做加法运算,所以表示负数的时候就需要使用二进制的补数。补数就是用正数来表示负数。

首先将二进制数的各数位的数值全部取反,然后再将结果加1。比如1:00000001 取反是 11111110,再加1,就是11111111表示的就是-1

补数求解的方法就是 取反+1

所以是用来进行负数的表示,比如3-5的结果是负数,进行的运算就是00000011+11111011的运算,结果是负数,所以也是补数11111110,根据补数和本体的运算之和为0,可以通过其补数来得到绝对值。00000010,所以11111110表示的结果就是-2

逻辑右移和算术右移

  • 逻辑右移:当二进制数的值表示图形模式而非数值的时候,移位后需要在最高位补0
  • 算术右移:当二进制数作为带符号的数值进行运算时候,移位后在最高位填充移位前符号位的值(0或者1)

负数运算要使用补数,比如-8应该是8求反+1,是11111000,所以算术右移2位,11111110,可以对这个值求补数为00000010是2,所以前者是-2,所以-8右移两位,相当于除以4,为-2.

符号扩充:将8位二进制转换为16位二进制的时候,都只需要用符号位的值填充高位即可。

逻辑运算

  • 算术运算:加减乘除
  • 逻辑运算:对二进制数各数字位0和1进行处理的运算
    • 异或XOR

异或是排斥相同数值的运算,两个位数值不同则为1,相同则为0.

小数运算

二进制数小数点后的第一位的位权是2^(-1)=0.5,所以0.1 = 1*0.5=0.5

用二进制数表示小数

1011.0011=2^3+2^1+2^0.2^-3+2^-4=11.1875

类比十进制

将11.1875转为二进制则为:

11不断的进行短除2,1875不断短乘2

计算机出错的原因

出现运算错误的原因是有一些十进制数的小数无法转换为二进制数,比如0.1就无法正常表示。所以二进制数是连续的,但是十进制数是非连贯的。实际上,十进制数0.1表示为二进制数后,会变成0.00011001100循环小数。

浮点数

1011.0011这种形式是纸面上的二进制数的形式,实际计算机内部存储不是这样。

浮点数是指用符号,尾数,基数和指数这四部分来表示的小数:0.12345*10^5,二进制数,则基数为2.所以一般只需要符号,尾数,指数三部分就可以表示浮点数。

IEEE标准,双精度浮点数和单精度浮点数在表示同一个数值时候使用的位数不同,双精度浮点数表示的数值范围大于单精度浮点数。

类别 符号部分 指数部分 尾数部分
单精度 1位 8位 23位
双精度 1位 11位 52位

数值大小通过尾数部分和指数部分来表示,尾数部分用的是将小数点前面的值固定为1的正则表达式,指数部分用的是EXCESS系统表现。

正则表达式和EXCESS系统

  • 尾数部分使用正则表达式

其实就是使用一定规则来确定表达小数的方式,在二进制中就是小数点前面的值固定为1的正则表达式,也就是将二进制表示的小数左移或者右移数次后,整数部分第一位为1,第二位之后都是0.

  • 指数部分使用EXCESS系统

主要是为了表示负数的时候不使用符号位。通过将指数部分表示范围的中间值设为0,使得负号不需要用符号来表示。比如11111111=255的1/2,即01111111=127表示的是0

比如十进制0.75用单精度浮点数表示就是0-01111110-10000000000000000000000,其中-是用来分割符号部分和指数部分的,可以看到符号位是0,为正数,指数部分是01111110,表示的是126,其中中间数是127,所以指数为126-127=-1,尾数部分其实是1.1000..,因为尾数部分使用了正则表达式。尾数部分转为十进制为:1.5,其中指数为-1,所以1.5*2^-1=0.75

避免浮点数出错

计算出错的原因就是用浮点数处理小数(也可能是位溢出导致的错误)

  • 回避,只需要近似值即可
  • 小数转换为整数,把计算结果再用小数表示出来即可。
  • BCD(Binary Coded Decimal),使用4位来表示0-9的1位数字的处理方法,所以会非常占内存

在涉及财务计算的时候,将小数转成整数或者使用BCD方法

二进制数和十六进制数

二进制4位,相当于十六进制的1位,比如二进制小数1011.011表示为16进制为:B.6

十六进制的位权是16,小数部分是16^-1

内存

高级编程语言中的数据类型表示的是占据内存区域的大小和存储在该内存区域的数据类型。物理内存是以字节为单位进行数据存储的。

内存的物理机制

内存是一种名为内存IC的电子元件,包括DRAM(动态随机存取存储器,需要经常刷新以保存数据),SRAM(静态随机存取存储器,不需要刷新电路即能保存数据),ROM等多种形式

内存IC中有电源,地址信号,数据信号,控制信号等用于输入输出的引脚,通过为其指定地址,来进行数据的读写。

比如一个IC,VCC和GND是电源,A0-A9是地址信号引脚,D0-D7是数据信号的引脚,RD和WT是控制信号的引脚。一般+5V直流电压表示1,0V表示0

D0-D7表示一次可以输入或者输出8位(1字节)数据,A0-A9表示10位,所以可以有0000000000-1111111111共1024个地址,也就是1KB,就是1千字节

现在大容量内存使用的IC就会有更多的地址信号引脚,一个IC就有更多容量,只需要几个IC,就可以达到需要的容量。

写入的时候,介入电源,使用A0-A9指定存储地址,将数据按位使用电平输入到D0-D7,即可写入数据。

内存IC内部有大量可以存储8位数据的地方,通过地址指定,可以进行数据的读写。

内存的逻辑模型 - 楼房

一层有一字节的数据!数据类型表示存储的是何种类型的数据,从内存上看,就是占用的内存大小,通过指令类型,可以实现以特定字节数为单位进行读写。

  • char: 1字节
  • short: 2字节
  • long: 4字节

如果数据低位存储在内存低位地址的话,叫做小端模式,即低字节序方式。比如short存123,123只需要8位就可以存储,那么内存地址低位存储123,高地址的那一字节存储0,这样就是小端模式。

指针

指针也是一种变量,所表示的不是数据值,而是存储着数据的内存的地址。一般计算机使用32位内存地址(4字节),所以指针变量的长度也是32位的。

1
2
3
char *d;
short *e;
long *f;

指针变量都是32位的,前面的char数据类型表示从指针存储的地址中一次能够读写的数据字节数。比如d指针中存放了32位的地址,但是地址索引到的数据需要读取1字节。

数组

数组索引和内存地址的变换工作是由编译器自动实现的。

1
2
char g[100];
short h[100];

数组的类型也表示一次能够读写的内存大小。数组和内存的物理构造是一样的,如果1字节类型的数组,就完全和内存一样了。

虽然是通过指定索引来使用数组,但是和内存的物理读写没有什么差别,所以可以有栈,队列,链表和二叉查找树等。

操作数组就类似于操作内存

栈,队列和环形缓冲区

队列一般是以环状缓冲区的方式实现的,使用数组来实现一个队列,可以从数组的起始位置开始有序的存储数据,然后再按照存储时的顺序把数据读出,在数组末尾写入数据后,后一个数据就会被写入到数组的起始位置(当然起始位置的数据需要被取出,否则环状缓冲区就满了)

链表

使用链表,可以高效的对数组数据进行追加,插入和删除

二叉查找树

使用二叉查找树,可以高效的对数组数据进行检索。考虑到数据的大小关系,可以将其分为左右两个方向的表现形式。

数组是进行这些处理的基础。

磁盘

磁盘在物理方面只能以扇区位单位进行读写,磁盘代替内存来使用的虚拟内存。

使用内存来提高磁盘访问速度的机制是磁盘缓存,在windows中程序运行时,存储着可以动态加载调用的函数和数据的文件是dll文件。在exe程序文件中,静态加载函数的方式是静态链接。函数加载方式有静态链接和动态链接。

windows中,一般磁盘的1个扇区是512字节。

读入内存

磁盘中的程序需要读入内存才可以运行,因为负责解析和运行程序内容的CPU,需要通过内部程序计数器来指定内存地址,才能读出程序。

磁盘缓存

磁盘缓存是指把从磁盘中读出的数据存储在内存空间的方式,如果需要读取同样数据,即可直接从缓存中读取。

把低速设备的数据保存在高速设备中,需要的时候从高速设备读取,缓存。

虚拟内存

是指把磁盘的一部分作为假想的内存来使用。为了实现虚拟内存,必须把实际内存的内容,和磁盘上的虚拟内存的内容进行部分置换,并同时运行程序。

虚拟内存的方法有分页式和分段式。

  • 分页:不考虑程序构造情况下,把运行的程序按照一定大小的页进行分割,以页为单位在内存和磁盘间进行置换。一般页的大小为4KB。page in or page out

磁盘上提供了虚拟内存用的文件(页文件),文件大小就是虚拟内存的大小。

节约内存

  • 通过DLL文件实现函数共有

DLL(Dynamic Link Libaray) 动态链接库,程序运行时候动态加载(函数和数据的集合)的文件。多个应用可以共有一个DLL文件,从而节约内存。

  • 通过调用_stdcall减小程序文件大小

本来在调用方执行的,放在了被调用方执行。

C语言中,调用函数后,需要执行栈清理指令。这是程序编译的时候编译器自动附加到程序中的,编译器默认把该处理附加在函数调用方。

同一个程序中,同样的函数可能会反复多次调用,

1
2
3
4
push 1C8h
push 7Bh
call @LTD+15 (MyFunc)(00401014)
add esp, 8

add esp, 8即为栈清理,esp(栈指针寄存器),使存储着栈数据的esp寄存器前进8位。栈清理处理在函数调用方通过附加一段程序进行清理,要比在被调用函数进行清理要显得臃肿和费内存,所以有_stdcall,使得栈清理变为在被调用函数一方进行。int _stdcall MyFunc(int a, int b)

磁盘的物理结构

磁盘是通过把其物理表面划分为多个空间来使用的,划分的方式有扇区方式和可变长方式。前者是将磁盘划分为固定长度的空间,后者是划分为长度可变的空间。一般都是扇区方式,将磁盘表面分成若干个同心圆的空间就是磁道,把磁道按照固定大小划分成的空间就是扇区。

扇区是对磁盘进行物理读写的最小单位,一般一个扇区为512字节。另外win中对磁盘进行读写的单位是扇区整倍簇,为了减少磁盘访问次数,加快读取速度。

数据压缩

半角英文数字是1个字节表示的,汉字等全角字符是2个字节表示的。

BMP格式没有被压缩过,因此比JPEG等压缩图像大。

文件是字节数据的集合

文件中的字节数据都是连续存储的

RLE算法

字符 * 重复次数 - RLE(Run Length Encoding) 行程长度编码

一般用于传真图像的压缩,只有两种颜色,黑白,所以有利于这种算法的压缩。

哈夫曼算法

哈夫曼算法关键在于多次出现的数据用小于8位的字节数来表示,不常用的数据则可以用超过8位的字节数来表示。

莫尔斯编码就是字符种类不同,莫尔斯电码符号长度也是不同的,出现频率高的字符用短编码表示。

为各压缩对象文件分别构造最佳的编码体系,并以该编码体系为基础进行压缩。因此,用什么样式编码对数据进行分割,需要视文件而定,用哈夫曼算法压缩过的文件中,存储着哈夫曼编码信息和压缩过的数据。

在哈夫曼算法中,通过借助哈夫曼树构造编码体系,可以构建能够明确进行区分的编码体系。

哈夫曼树是从叶生枝,再生根,先选最小频率数,层数最深编码最长。

先选两个出现频率最小的数字,然后形成树,其根节点是两个数字的和。然后循环来做同样的事情,直到最后合并为一个树,这样从根部到底部,左侧为0,右侧为1,就可以对所有字符进行编码了。

所以频率高的离根越近,这样编码短,频率小的离根远,编码长。

读取数据的时候按位来读取,然后在哈夫曼树中寻找对应字符。

可逆压缩和非可逆压缩

BMP格式是未压缩的(bitmap),JPEG是压缩过的,而且是非可逆的,因为不需要完全还原,所以可能有部分数据丢失。

能还原到压缩前状态的压缩为可逆压缩,不能还原的为非可逆压缩。

运行环境

FreeBSD是一种Unix操作系统,通过在各个环境中编译Ports中公开的代码,就可以执行由此生成的本地代码。Java虚拟机用来运行Java应用的字节代码。

运行环境=操作系统+硬件

CPU有x86,MIPS,SPARC,PowerPC等几种类型,其各自的机器语言是不同的。机器语言的程序成为本地代码,文本文件中的代码为源代码,源代码进行编译为本地代码。

在应用软件中,输入,显示,输出并不是直接向硬件发送指令,而是通过向操作系统来发送指令的,这样就不用注意内存和IO地址的不同了。

但是不同操作系统的API也是由差异的,如果CPU不同,机器语言不同,就需要能够生成该CPU支持的本地代码的编译器对源代码重新进行编译。

FreeBSD Port源代码

Port机制是指FreeBSD根据当前系统运行的硬件环境编译源代码,从而生成适合当前运行环境的机器语言代码。porting移植

虚拟机

操作系统API

提供相同运行环境的Java虚拟机

Java(作为程序运行环境)将Java代码编译,编译后形成字节代码,字节代码会通过Java虚拟机转换为本地代码运行,这样就可以形成可以移植的程序了。

可以实现同样的字节代码在不同的环境下运行,如果有适合不同环境的虚拟机,同样字节码的应用就可以在任何环境下运行了。

BIOS和引导

BIOS(Basic input and output system),存储在ROM中,是预先内置在计算机中的程序,键盘,磁盘和显卡的控制程序,引导程序。引导程序是存储在启动驱动器起始区域的小程序,一般是硬盘。功能是将硬盘等记录的OS加载到内存运行。

编译

CPU可以解析和运行的程序为机器语言代码(本地代码),将多个目标文件结合成EXE文件的工具为链接器。扩展名为.obj的目标文件内容是本地代码。

程序运行时候,用来动态申请分配的数据和对象的内存区域形式为堆。

编译器

语法解析,句法解析,语义解析。普通编译器,交叉编译器。

编译之后需要进行链接,编译之后形成的是.obj目标文件。

将多个目标文件结合,生成可执行文件的处理为链接。

库文件:把多个目标文件集成保存到一个文件,链接器指定库文件之后,就会从中把需要的目标文件抽取出来,和其他目标文件一起链接成可执行文件。

sprintf函数,不是通过源代码形式而是通过库文件形式和编译器一起提供的,这样的函数是标准函数。这样就不必提供源代码了。

DLL文件及导入库

windows中,API的目标文件,不是存储在通常的库文件中,而是在DLL文件的特殊库文件中。没有存储目标文件的实体,只是存储着dll文件的文件夹信息,这样的库文件为导入库,之后进行动态链接。相反的,存储着目标文件的实体,直接和可执行文件结合的库文件为静态链接库。

可执行文件运行

可执行文件中给变量和函数分配了虚拟的内存地址,运行时候,虚拟内存地址会转换成实际的内存地址。链接器在可执行文件开头,追加转换内存地址的必须信息,即再配置信息。这些信息就成为了变量和函数的相对地址,链接后的可执行文件里,变量和函数就会变成一个连续排列的组,各变量和函数就可以使用相对于基点的偏移量表示内存地址。

栈和堆

  • 栈是用来存储函数内部临时使用的变量,局部变量,以及函数调用时所用的参数的内存区域。参考函数调用。。
  • 堆是用来存储程序运行时的任意数据以及对象的内存区域

内存中的程序,由用于变量的内存空间,用于函数的内存空间,用于栈的内存空间,用于堆的内存空间构成的。

栈中对数据的存储和舍弃的代码,是编译器自动生成的。每当函数被调用就会申请分配,函数处理完毕后就会自动释放。

堆的空间,需要明确进行申请分配和释放。malloc()free(),如果没有及时释放,就会导致内存泄漏。

Build指的是编译和链接。

操作系统

What you see is what you get

在高级编程语言中,直接调用系统调用的语言,移植性不太好,太依赖于特定系统了。

操作系统和高级编程语言使得硬件抽象化。

汇编

汇编语言是通过助记符来记述程序的。asm是assembler汇编器的缩写,在高级编程语言的源代码中,即使指令和数据在编写时候是分散的,编译后也会在段定义中集合汇总起来。在汇编语言中,通过跳转指令,可以实现循环和条件分支。

使用助记符的编程语言-汇编语言。汇编,反汇编。编译,反编译。汇编语言中 ; 之后是注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
_TEXT segment dword public use32 'CODE'
_TEXT ends
_DATA segment dword public use32 'DATA'
_DATA ends
_BSS segment dword public use32 'BSS'
_BSS ends
DGROUP group _BSS, _DATA
_TEXT segment dword public use32 'CODE'
_AddNum proc near
push ebp ; 将ebp寄存器的值存入栈中
mov ebp, esp
mov eax, dword ptr [ebp+8]
add eax, dword ptr [ebp+12]
pop ebp
ret
_AddNum endp
_MyFunc proc near
push ebp
mov ebp, esp
push 456
push 123
call _AddNum
add esp, 8
pop ebp
ret
_MyFunc endp
_TEXT ends
end

伪指令

汇编语言的源代码,是由转换成本地代码的指令和针对汇编器的伪指令构成的。负责把程序的构造及汇编的方法指示给汇编器。

segment和ends围起来的部分,给构成程序的命令和数据的集合体加上一个名字,称为段定义。一个程序由多个段定义构成。

这里定义了三个_TEXT, _DATA, _BSS段定义。_TEXT是指令的段定义,_DATA是被初始化的数据的段定义,_BSS是尚未初始化的数据的段定义。

group伪指令,表示的是将_BSS_DATA两个段定义汇总为DGROUP的组。另外,栈和堆的内存空间会在程序运行时生成。

_AddNum_MyFunc是属于_TEXT这一段定义的,proc和endp之间的是表示函数的范围,过程的范围,相当于C语言的函数的形式称为过程,end表示源代码结束。

汇编的语法是操作码+操作数

1行表示对CPU的一个指令,操作码+操作数

操作码 操作数 功能
mov A, B 把B的值赋给A
add A, B 把A同B的值相加,并将结果赋给A
push A 把A的值存在栈中
pop A 从栈中读取值,并将其赋给A
call A 调用函数A
ret 将处理返回到函数的调用源

内存中存储着构成本地代码的指令和数据,程序运行时,CPU会从内存中把指令和数据读出,然后再将其存储在CPU内部的寄存器中进行处理。

寄存器名 名称 功能
eax 累加寄存器 运算
ebx 基址寄存器 存储内存地址
ecx 计数寄存器 计算循环次数
edx 数据计数器 存储数据
esi 源基址寄存器 存储数据发送源的内存地址
edi 目标基址寄存器 存储数据发送目标的内存地址
ebp 扩展基址指针寄存器 存储数据存储领域基点的内存地址
esp 扩展栈指针寄存器 存储栈中最高位数据的内存地址

MOV指令

1
2
mov ebp, esp
mov eax, dword ptr [ebp+8]

dword ptr(double word pointer)表示的是从指令内存地址读出4字节的数据。

对栈进行push和pop

数据在存储的时候从内存的下层(大的地址编号)逐渐往上层(小的地址编号)累积,读出时则按照从上往下顺序进行。

32位CPU中,一次push或者pop可以处理32位的数据,对栈进行读写的内存地址是由esp寄存器(栈指针)进行管理的,push和pop指令运行后,esp寄存器的值会自动进行更新,(push减4,pop加4)

函数调用机制

在汇编语言中,函数名表示的是函数所在的内存地址,并且push指令和pop指令必须以4字节为单位对数据进行入栈和出栈处理。

call指令运行后,下一行指令的内存地址会自动push入栈,这样在调用函数之后,使用ret指令pop出栈,这样程序流程就可以返回到之前的地址上。

add esp, 8 这个可以对栈中存储的两个参数进行销毁。

函数内部的处理

首先push ebp,ebp是基址寄存器,到最后pop ebp。这样是把函数中用到的ebp寄存器的内容恢复到函数调用之前的状态。所以需要暂时将其保存在栈中,然后在函数处理完毕之前出栈,返回到原来的状态。

esp栈指针寄存器,mov ebp, esp,将负责管理栈地址的esp寄存器的值赋值到了ebp寄存器,因为mov指令中方括号的参数是不允许指令esp寄存器的。所以不直接通过esp,而是ebp来读写栈内容。而ebp的内容已经暂时保存在了栈中。

mov eax, dword ptr [ebp+8],读取栈中存储的第一个参数123,将其读出到eax累加寄存器寄存器中。ebp现在是栈指针寄存器,即栈顶,而栈顶是要返回的指令地址,第二个就是参数123

add eax, dword ptr [ebp+12],读取栈中存储的第一个参数456,然后和当前eax寄存器值相加,并且存储结果在eax寄存器中。

函数的参数是通过栈来传递,返回值是通过寄存器来返回的。

ret指令运行后,栈顶的目的地内存地址就会出栈。

始终确保全局变量用的内存空间

1
2
3
int a1 = 1;
int a2 = 2;
int b1, b2;

转换为汇编代码则是:

1
2
3
4
5
6
7
8
9
10
11
12
13
_DATA segment dword publish use32 'DATA'
_a1 label dword
dd 1
_a2 label dword
dd 2
_DATA ends
_BSS segment dword public use32 'BSS'
_b1 label dword
db 4 dup(?)
_b2 label dword
db 4 dup(?)
_BSS ends

编译后的程序,会被归类到名为段定义的组,初始化的全局变量会归到_DATA中,没有初始化的全局变量,会归到_BSS的段定义中,指令会被归类到_TEXT中。

_a1 label dword定义了_a1标签,标签表示的是相对于段定义起始位置的位置,dd 1 表示的是define double word,申请了一个4字节的内存空间。

db 4 dup(?)申请分配了4字节的内存,但是值没有确定,db - define byte

汇编语言暂时学到这里,以后有时间再继续深入研究!

硬件控制方法

汇编中使用IN指令来实现IO输入,OUT指令来实现IO输出。所有连接到计算机的外围设备都会分配一个IO地址编号。IRQ指的是用来执行硬件中断请求的编号。

DMA是指不经过CPU中介处理,外围设备直接同计算机的主存进行数据传输,磁盘这种用来处理大量数据的外围设备都有DMA功能。

操作系统提供了系统调用功能来实现对硬件的控制。各API就是应用调用的函数。

IN指令和OUT指令

IN指令通过指定端口号的端口输入数据,并将其存储在CPU内部的寄存器中。OUT指令则是将CPU寄存器中存储的数据,输出到指定端口号的端口。

与外围设备的连接器,其内部有用来交换计算机主机同外围设备之间电流特性的IC,称为IO控制器。IO控制器中有用于临时保存输入输出数据的内存,即端口。一个IC可能有多个端口,并且计算机可能有多个IO控制器。端口号也称为IO地址。IN指令和OUT指令在端口号指定的端口和CPU之间进行数据的输入和输出。

蜂鸣器发声的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void man(){
int i;
_asm {
IN EAX, 61H
OR EAX, 03H
OUT 61H, EAX
}
for (i=0; i< 1000; i++);
_asm {
IN EAX, 61H
AND EAX, 0FCH
OUT 61H, EAX
}
}

外围设备的中断请求

IRQ(Interrupt Request) 中断请求,用来暂停当前正在运行的程序,并且跳转到其他程序运行的机制,称为中断处理。

实施中断请求处理的是连接外围设备的IO控制器,负责实施中断处理程序的是CPU。外围设备的中断请求会使用不同于IO端口号的其他编号,称为中断编号。

操作系统及BIOS则会提供响应中断编号的中断处理程序。

在IO控制器和CPU中间会有一个中断控制器进行缓冲,用来把从多个外围设备发出的中断有序的传递给CPU。

CPU接收到中断控制器的中断请求后,会将当前运行的程序中断,并且切换到中断处理程序。首先将CPU所有寄存器的值保存到内存的栈中,在中断处理程序完成输入输出后,再将栈中保存的数值还原到CPU寄存器中。

中断实现实时处理

不利用中断也可以从外围设备输入数据,这样的话,就需要不断检测外围设备是否有数据输入。

所以通过中断,可以进行实时处理。

DMA短时间传送大量数据

DMA(Direct Memeory Access),不通过CPU的情况,外围设备直接和主存进行数据传送,这样大量数据就可以短时间存到内存。

DMA通道,用于CPU识别哪一个外围设备使用了DMA。

IO端口号,IRQ(中断编号),DMA(DMA通道)识别外围设备的3点组合。

文字和图片的显示机制

显示器中显示的信息一直储存在某内存中,称为VRAM,往VRAM写入数据,数据就会在显示器中显示。实现该功能的程序,由操作系统或者BIOS提供,借助中断来进行处理的。

显卡一般配有与主存独立的VRAM和GPU(图形处理器),提升图形的描绘速度。

利用输入输出指令同外部设备进行输入输出处理。