汇编语言学习笔记-ch1-4
Table of Contents
汇编语言的构成 #
- 汇编指令:机器指令的别名。因此一条汇编指令就一一对应了一条机器指令。
- 汇编指令是汇编语言的主要组成部分。
- 伪指令:没有对应的机器指令。其只是被编译器执行的指令,而CPU看不懂。
- 其他符号:例如+、-、*、/。编译器识别并根据此进行一些编译。
接口卡 #
- 接口卡是真正直接控制外设的部件。接口卡插在扩展插槽上,通过总线链接CPU。因此CPU是通过给接口卡发送命令从而间接控制了外部设备。
BIOS #
- 只要是独立的部件就可以有BIOS(Basic InputOutput System)。
- BIOS由厂商提供,实现此设备的最基本的输入输出的控制。例如显卡有显卡BIOS,网卡有网卡BIOS。
- BIOS一般存在ROM中。例如主板上有一个ROM专门存放主板BIOS。
- 无论是哪个设备的BIOS,由于这些设备都通过总线链接CPU,所以CPU的地址空间中都有存放这些BIOS的ROM的一份。因此可见CPU的地址空间并不完全由内存构成,此外还由若干外设构成!!虽然都是外设,但是CPU可以直接把他们都当作内存来看待。
显卡 #
- 显卡随时把显存中的数据往屏幕上送。
- 把东西放到显存中,就能实时显示在屏幕上。
8086的字节和字 #
- 为了对上一代CPU有更好的兼容性,8086把上一代CPU能处理的最大位数(8bits)称为一个字节,把自己能一次性处理的最大位数(16bits)称为一个字。
- 因此,最初对于字和字长的定义就是为了8086CPU可以兼容为上一代CPU编写的程序。
8086的通用寄存器 #
- 通用寄存器就是存放==一般数据==的寄存器。以X结尾。在8086中记作AX,BX,CX,DX。
- 8086是16位的。为了兼容为上一代CPU编写的程序,8086把自己的寄存器划分为可以支持两个8位存储的“虚拟的小寄存器”,以AX为例,记作AL、AH。
- 注意,如果在汇编指令中使用AL、AH,而不是AX,例如
add AL,9999H
,则结果只是会在AL中溢出,即最高位丢失,而不会把进位保存在AH中。因为在这种情况下,AH和AL看作两个相互独立,没有任何联系的寄存器。
- 注意,如果在汇编指令中使用AL、AH,而不是AX,例如
- ==不允许==在AX和BL等类似于这样的位数不同的寄存器之间执行
MOV
、ADD
等操作。
16位机是什么意思? #
- 寄存器都是16位的
- 运算器一次可以处理16位的数据
- 寄存器和运算器之间的通路是16位的
即:Reg和ALU是16位的且他们之间用16位连接。
为什么要段地址左移4位+偏移地址来形成物理地址? #
- 8086是16位机,所以CPU只能处理16位的数据。但是8086的IO有20根数据线,希望能在16位机的条件下,达到20位地址的寻址能力。
怎么形成20位地址,20位地址怎么给了内存? #
- CPU提供16位段地址和16位偏移地址,都送入地址加法器。
- 注意:因为16位的限制,所以一个段的大小==最大只能是64KB==。
- 地址加法器将段地址左移四位,加偏移地址,获得物理地址。(==即由段地址和偏移地址获取了物理地址==)
- 物理地址从地址加法器送到IO控制电路,传递给内存。
段起始地址(基础地址)、段地址的区别 #
- 段起始地址是一个物理地址。
- 段地址是逻辑地址。
- 它们的关系是:后者左移4位得到前者。
- 因此段起始地址必然是16的倍数。
8086的段寄存器 #
- 存放段地址的寄存器称为段寄存器,无论这个段是什么段。
- 8086有4个段寄存器,都以S结尾,分别是CS代码段寄存器、ES附加段寄存器(可以看作是通用段寄存器,如果实在没有段寄存器可用了就用它)、DS数据段寄存器、SS栈段寄存器。
CS:IP和指令执行的关系 #
IP(Instruction Pointer)指令指针寄存器。
- CS:IP指向的是CPU要执行的指令。
- 因此,可以通过是否被CS:IP指向判断这里存的是指令还是数据。
- 如果一条指令被执行了,那么它一定被CS:IP指向过。
- 8086在加电或重置后,CS:IP被设置为FFFFH:0000H。这是8086CPU在开始运行时执行的第一条指令。
- IP指向下一指令的过程
- CS:IP的指令取出后,放入IR,分析此指令的长度
- 根据上述指令的长度,==让IP+这个长度==
- 重复上述步骤
如何手动修改CS:IP的值? #
- 8086CPU不提供使用
MOV
指令直接给CS或IP寄存器赋值的方式.例如MOV CS,3
是不合法的. - JMP指令有两种用法:
- JMP数字,可以同时给CS:IP赋值。例如
JMP 32:14
相当于MOV CS,32 MOV IP,14
的功能。(只是用来解释。实际上后者不合法。) - JMP寄存器,可以只给IP赋值。例如
JMP AX
,就相当于MOV IP,CS
. - 因此,==JMP指令可以说是以特定的方式代替了MOV给CS或者IP赋值的操作==。
- JMP数字,可以同时给CS:IP赋值。例如
字单元和内存单元 #
- ==8086的内存是以字节编址的==,所以一个内存单元就是一个字节的大小,8bits。
- 8086的字长是16bits,所以一个字单元应包含两个内存单元。
- 如果要存储一个字,那么字的高8位存在较高的那8位地址中(即起始地址较高的内存单元中),字的低8位类似。
- 字单元按其起始内存单元的编号来编址,即其低8位所在内存单元的地址就是整个字单元的地址。例如我们说地址为2的字单元,就是指由地址2、地址3构成的字单元。
- 因此,任意两个连续的内存单元,可以看成是两个独立的内存单元,也可以看成是以较低的那个内存单元为地址的字单元。
如何利用DS以正确读取数据? #
-
==8086的数据读取只能以偏移地址的方式读取。==因为8086会默认地从DS中取段地址并且和偏移地址合成物理地址。
- 因此,需要正确设置DS寄存器的值才能正确读取数据。
-
如何设置DS寄存器的值?
-
由于DS寄存器属于段寄存器,而8086的硬件设计不允许直接对段寄存器进行MOV操作,所以需要使用中转寄存器(任意一个通用寄存器即可)。例如:
-
mov bx,1000H mov ds,bx mov al,[1] //成功读取到了10001H的内存单元的数据,送入al寄存器。 //其中,中括号表示内存单元,其中的1表示偏移地址。
-
-
对字节的操作和对字的操作(8086作为16位机,支持一次性读取一个字的操作):如果从内存单元
mov
到一个8位寄存器,那么读取这个地址的内存单元;如果从内存单元mov
到一个16位的寄存器,那么读取==这个地址的字单元==(例如到AX,则AH和这个字单元的高8位对应,AL和这个字单元的低8位对应)。
8086的栈 #
- 8086和很多CPU一样,都提供==以栈的方式来==访问内存空间的指令。
- 因此,所谓栈,实际上是访问内存空间的一种方式,即以LIFO的方式对内存进行访问。这段内存表现在8086中,就是一个“segment”。
- 注意,8086中,栈的压入和弹出是==以字为单位==进行的。因此,对于一段栈内存空间,其中的存储都是==字单元==。
- 8086无法对栈进行越界的检查。因此如果出现了栈溢出,可能会损害整个计算机系统。(我们既然把一段内存安排为栈段,就说明我们肯定不会用到其他空间,因此其他空间可能跑着其他程序的重要数据,甚至是OS。)
- 8086只知道栈顶在哪里,却不知道栈的大小是多少。
- 同样的原理,8086只知道要执行的指令在哪里,但是不知道一共有多少指令。
- 在利用栈编写汇编程序时,要注意栈有将数据==反向排列==的功能。因此入栈和出栈的顺序是相反的。
- 像定义代码段那样,我们可以把起始物理地址为==16的倍数==,且==连续的==不超过==64KB==的一段内存空间定义为栈段。
- 如何让我们的程序访问我们的栈段?需要我们自己设定SS、SP寄存器的值。因为8086 CPU==不会==因为我们安排了一段内存空间为栈段,就自动寻找到它。
SS和SP寄存器 #
-
SS:SP指向栈顶元素。SS是段地址寄存器,SP是栈顶指针寄存器。
-
在8086中,栈底是较大的地址号,栈顶是较小的地址号。因此如果把内存空间按地址编号从小到大向下排列的方式,则栈顶才会看起来是内容越多,其越往上走;随着内容的逐渐弹出,它才会下降。但是实际上从编号上来看,栈顶上升实际上是内存编号减小。
- 又因为8086中的栈是以字为单位进行操作,所以入栈操作时==SP-2==.
-
当栈为空(或者说是刚刚定义),SS指向栈的==最顶端==,然后再右移四位(如果栈超过了这里那就是爆栈了,也就是SP为负了),SP指向栈的==最底栈空间的再下一个==字单元(如果该单元不存在,也就是如果特殊情况,该栈空间到最底下就到了整块内存的最底处了,那么也得这么做。例如CPU的最大地址空间到了FFFF1H,那么如果栈空间最底部的元素的地址就是FFFF1H,根据规则,栈空时,SP应该指向此地址再+2,即FFFF3H,显然该地址在物理上不可能存在,但是写在这里是没有任何问题的。)(+2是因为我们采用字单元的地址,但是8086采用按字节编址。)
-
由上述空栈时SP和SS的取值关系,可以看出==SP的初始值直接就定义了整个栈的大小。==例如一个16B的栈空间,那么不管SS是什么,其SP一定是000EH(即000FH-1H,因为我们采用字单元地址。)
-
出栈操作并不会抹除原有的数据,而只是移动一下SP的值就完成了整个出栈操作。因为下次有值入栈的时候,可以直接覆写对应的单元。因此可以看出抹除数据是没有任何意义的。
- 这就类比C++ iterator,当把vector的最大下标元素删除后,使用下标仍然能访问到,且其值并没有任何变化。因为也是采用了类似原理。
“盲人大厨”CPU #
- 我们可以自行安排段,安排我们的数据段、代码段、栈段。但是这和CPU没有关系。你不能用自然语言告诉CPU你要把这些段安排在哪里。你告诉它的方式是:
- 我们设置CS、IP的值,告诉CPU代码段在这里,你可以把它们当作代码执行了;
- 我们设置SS、SP的值,告诉CPU栈段在这里,你可以按照栈的方式访问这段内存了;
- 我们设置DS的值,告诉CPU代码段在这里,你可以正常访问属于你的内存了。
- CPU就像一个盲人厨师,什么都懂,就是看不见调料、锅、菜在哪里。你要拿着他的手,让他知道这些东西在哪里,他就记住了。然后他就能非常自如地炒出一盘好菜。但是如若你没有准确地告诉他正确的位置,那他可能把勺子炒了。(CPU自己不知道栈段在哪里,但是他知道应该SS和SP指向的就应该按照栈的方式访问;CPU自己不知道代码段在哪里,但是他知道CS、IP指向的就是要执行的东西,…,重要的是你要告诉CPU这些东西。)
- 因此,就算你把代码段、数据段、栈段都放在同一段内存里,CPU自己也不知道。==因此你可以这样来调戏CPU(bushi)==
程序如何从源文本文件变成可执行程序? #
- 编译(汇编):生成目标文件
.o
- 连接:==生成可执行文件==。
- 如果源程序由很多
.obj
文件构成,则需要把他们连接成单个可执行文件; - 如果源程序调用了很多外部库,则需要把他们整合进最终的可执行文件;
.obj
文件中虽然存放了机器码,但是其中的一些内容还不能直接用来运行。需要经过连接程序处理后,生成最终可供直接运行的文件。(因此,即便没有调用外部库,即便没有采用多个源文件,也需要进行连接。)- 连接器将
.o
中定义的各个section组织到内存中的segment中去。其中section就是例如data section、ubuntu info section,是一些小片段,只是.o
文件的单位。而segment是之前所讲的代码段、数据段等,是内存中的单位了。
- 连接器将
- 如果源程序由很多
可执行文件的构成 #
- 可执行文件在总体上由头、代码、数据构成。
- 头中存在了关于程序的描述,例如占用大小、二进制代码的排列等
- 程序就是机器码,是经过汇编得到的。
- 数据是源程序中预先定义的数据。
- OS把CS:IP指向程序开始,并且把数据、程序按其段装入内存。CPU直接取指令执行即可。
编写一个汇编程序 #
-
伪指令:不存在对应的机器码,只是供编译器进行识别,指导编译器进行编译操作。
-
XXX segment
和XXX ends
声明了一个段。它们配对构成,在它们之间书写段的内容。(注意它们两个都是后缀。即先写段名称再写关键字)-
一个程序由若干个段构成,每个段有其自己的作用。一个程序至少要由一个代码段构成才叫程序。不存在无段的程序。
-
例如:
-
codesg segment //codesg段内容 codesg ends
-
-
-
end
用来标志整个程序的结束。编译器读取到end
才知道可以结束工作了,否则编译器无法退出。 -
assume Reg:XXX
的意思是假设,用于建议编译器把某个段XXX
和物理的段寄存器Reg
相关联。在需要的情况下,编译器会采纳这一建议,并真的把它们关联起来。例如:assume cs:codesg
就是把codesg
这一代码段和cs
程序段寄存器关联起来了。-
可以在声明一个段之前使用assume指令去关联这个段使之==有效化==。例如:
-
assume cs:codesg //有效化这个段 codesg segment //codesg段内容 codesg ends
-
-
-
-
标号
- 标号代表了一个地址。例如上面所说的代码段名称
codesg
就是一个标号。 - 虽然在源代码中体现不出是短地址,但是编译器、连接器经过处理,可以把它处理为一个地址,也就是代码段的段地址。
- 标号代表了一个地址。例如上面所说的代码段名称
-
程序的结构
- ==程序就是一些段的集合。== 至少要包含一个段,那就是代码段。
-
编写程序的流程
-
首先定义一个段。因为程序的基本单位是==段==。
-
abc segment abc ends
-
-
然后,==补充==这个段的内容。这个段是代码段,所以写入程序。
-
abc segment add ax,bx mov ax,2 abc ends
-
-
然后,==有效化==这个段。
-
assume cs:abc abc segment add ax,bx mov ax,2 abc ends
-
-
最后,指出程序在何处==结束==。
-
assume cs:abc abc segment add ax,bx mov ax,2 abc ends end
-
-
-
程序执行
- 在DOS系统中,我们运行一个可执行文件,则CPU控制权由DOS系统交给了此程序(因为DOS是单任务操作系统)。
- 具体地说,DOS启动后,完成各项准备工作,就开始运行
command.com
程序。这就是DOS 的shell。 - 我们键入可执行文件的名称,
command
程序就把可执行文件的程序装入内存,设置CS:IP
,然后CPU执行它,同时command
停止执行,装入的程序开始执行。直到装入的程序执行完毕,command
才开始继续执行。 debug
和command
的区别是:debug
并不放弃对CPU的控制权。因此当其装入的程序开始执行时,debug
仍然保持对CPU的控制。这就允许我们能够单步调试目标程序了。- 我们输入
debug a.exe
,实际上是command
装入了debug
,debug
又装入了1.exe
。因此1.exe
执行完毕后,先返回到debug
,再返回到command
。如果我们在1.exe
执行完毕后不给debug
退出的命令,你会发现你仍然一直处于debug
的命令提示符里面。 - 程序装入后,
CX
寄存器中的值代表了程序的大小(即==长度==)。 - 程序装入后,
DS
寄存器中的值是sa
,也即是程序被分配的内存区的所在段地址(因此,程序被分配的内存的起始物理地址就是DS:0
)。- 在sa:0到sa:00FFH这段256个B(即从0000H到00FFH,十进制0到255)的空间中,存放PSP,也就是DOS系统中的程序段前缀。
- 因此,程序段地址实际上是sa+10H(因为10H左移4位是100H也就是256),程序从sa+10H:0开始。
- 确定后,将sa存入
DS
寄存器,然后设置CS:IP到sa+10H==即可完成程序的装入,完全可以直接开始执行了==。
- 具体地说,DOS启动后,完成各项准备工作,就开始运行
- 在DOS系统中,我们运行一个可执行文件,则CPU控制权由DOS系统交给了此程序(因为DOS是单任务操作系统)。
-
程序返回
-
当此程序运行完毕,应该再交还此CPU控制权。这个交还的过程就是==返回==。
-
注意在
debug
时,单步执行其他指令用t
,但是执行int 21H
时必须用p
。 -
mov ax,4c00H //中断参数 int 21H //21H中断:返回DOS系统
-