编程语言的发展
- 机器语言
由0和1组成 - 汇编语言(Assembly Language)
用符号代替了0和1,比机器语言便于阅读和记忆 - 高级语言
C\C++\Java等,更接近人类自然语言 - 操作:将寄存器BX的内容送入寄存器AX
机器指令:1000100111011000
汇编指令:mov ax, bx
编程语言的发展
汇编语言与机器语言一一对应,每一条机器指令都有与之对应的汇编指令
汇编语言可以通过编译得到机器语言,机器语言可以通过反汇编得到汇编语言
高级语言可以通过编译得到汇编语言\机器语言,但汇编语言\机器语言几乎不可能还原成高级语言
汇编语言的特点
可以直接访问、控制各种硬件设备,比如存储器、CPU等,能最大限度地发挥硬件的功能
能够不受编译器的限制,对生成的二进制代码进行完全的控制
目标代码简短,占用内存少,执行速度快
汇编指令是机器指令的助记符,同机器指令一一对应。每一种CPU都有自己的机器指令集\汇编指令集,所以汇编语言不具备可移植性
知识点过多,开发者需要对CPU等硬件结构有所了解,不易于编写、调试、维护
不区分大小写,比如mov和MOV是一样的
汇编语言 vs 高级语言
采用高级语言C++和汇编语言编写同一个功能
将a+b的结果赋值给c,然后在屏幕上打印c的结果
汇编语言的用途(为什么要学习汇编语言?)
编写驱动程序、操作系统(比如Linux内核的某些关键部分)
对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编)
软件安全
病毒分析与防治
逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客是理解整个计算机系统的最佳起点和最有效途径
为编写高效代码打下基础
弄清代码的本质
sizeof
++a + ++a + ++a
switch和if的效率究竟谁高?为什么?
汇编语言的种类
目前讨论比较多的汇编语言有
8086汇编(8086处理器是16bit的CPU)
Win32汇编
Win64汇编
ARM汇编(嵌入式、Mac、iOS)
......入门建议先从学些8086汇编开始
结构简洁、经典
参考书籍:王爽《汇编语言》
学前须知
-
要想学好汇编语言,首先要对CPU等硬件结构有一定的了解
最为关键的是需要了解CPU和内存
在学习汇编语言过程中,遇到的绝大部分指令都是跟内存、CPU有关的
总线
- 每一个CPU芯片都有许多管脚,这些管脚和总线相连,CPU通过总线跟外部器件进行交互
- 总线:一根根导线的集合
-
总线的分类
地址总线
数据总线
控制总线
-
CPU从内存的3号单元读取数据
地址总线
它的宽度决定了CPU的寻址能力
8086的地址总线宽度是20,所以寻址能力是1M( 2_20 )数据总线
它的宽度决定了CPU的单次数据传送量,也就是数据传送速度
8086的数据总线宽度是16,所以单次最大传递2个字节的数据控制总线
它的宽度决定了CPU对其他器件的控制能力、有多少根控制总线,就意味着CPU提供了对外部器件的多少种控制。
数据总线
-
8088的数据总线宽度是8,8086的数据总线宽度是16,分别向内存中写入89D8H
内存
-
各类存储器的逻辑连接情况
-
各类存储器的逻辑连接-物理地址对应图
- 各类存储器的物理地址情况
内存地址空间的大小受CPU地址总线宽度的限制。8086的地址总线宽度为20,可以定位2_20个不同的内存单元(内存地址范围0x00000~0xFFFFF),所以8086的内存空间大小为1MB
0x00000~0x9FFFF:主存储器。可读可写
0xA0000~0xBFFFF:向显存中写入数据,这些数据会被显卡输出到显示器。可读可写
0xC0000~0xFFFFF:存储各种硬件\系统信息。只读
CPU的典型构成
内部部件之间由总线相连
寄存器
- 对程序员来说,CPU中最主要部件是寄存器,可以通过改变寄存器的内容来实现对CPU的控制
- 不同的CPU,寄存器的个数、结构是不相同的(8086是16位结构的CPU)
-
8086有14个寄存器
都是16位的寄存器
可以存放2个字节
通用寄存器
AX、BX、CX、DX这4个寄存器通常用来存放一般性的数据,称为通用寄存器(有时也有特定用途)
通常,CPU会先将内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算
-
假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间
-
AX、BX、CX、DX这4个通用寄存器都是16位的,如下图所示
-
上一代8086的寄存器都是8位的,为了保证兼容, AX、BX、CX、DX都可分为2个独立的8位寄存器来使用
H代表高位寄存器
L代表低位寄存器
字节与字
在汇编的数据存储中,有2个比较常用的单位
字节:byte,1个字节由8bit组成,可以存储在8位寄存器中
字:word,1个字由2个字节组成,这2个字节分别称为字的高字节和低字节-
比如数据20000(4E20H,0100111000100000B),高字节的值是78,低字节的值是32
1个字可以存在1个16位寄存器中,这个字的高字节、低字节分别存储在这个寄存器的高8位寄存器、低8位寄存器中
8086的寻址
- CPU访问内存单元时,要给出内存单元的地址,所有的内存单元都有唯一的地址,叫做物理地址
- 8086有20位地址总线,可以传送20位的地址,1M的寻址能力
- 但它又是16位结构的CPU,它内部能够一次性处理、传输、暂时存储的地址为16位。如果将地址从内部简单地发出,那么它只能送出16位的地址,表现出来的寻址能力只有64KB
-
8086采用一种在内部用2个16位地址合成的方法来生成1个20位的物理地址
内存的分段管理
- 8086是用“基础地址(段地址×16) + 偏移地址 = 物理地址”的方式给出物理地址
- 为了开发方便,我们可以采取分段的方法来管理内存,比如:
- 地址10000H~100FFH的内存单元组成一个段,该段的起始地址(基础地址)为10000H,段地址为1000H,大小为100H
-
地址10000H-1007FH、10080H-100FFH的内存单元组成2个段,它们的起始地址(基础地址)为:10000H和10080H,段地址为1000H和1008H,大小都为80H
段寄存器
8086在访问内存时要由相关部件提供内存单元的段地址和偏移地址,送入地址加法器合成物理地址
是什么部件提供段地址?段地址在8086的段寄存器中存放
8086有4个段寄存器:CS、DS、SS、ES,当CPU需要访问内存时由这4个段寄存器提供内存单元的段地址
CS (Code Segment):代码段寄存器
DS (Data Segment):数据段寄存器
SS (Stack Segment):堆栈段寄存器
ES (Extra Segment):附加段寄存器
CS和IP
CS为代码段寄存器,IP为指令指针寄存器,它们指示了CPU当前要读取指令的地址
-
任意时刻,8086CPU都会将CS:IP指向的指令作为下一条需要取出执行的指令
指令的执行过程
指令和数据
- 在内存或者磁盘上,指令和数据没有任何区别,都是二进制信息
-
CPU在工作的时候把有的信息看做指令,有的信息看做数据,为同样的信息赋予了不同的意义
- CPU根据什么将内存中的信息看做指令?
CPU将CS:IP指向的内存单元的内容看做指令
如果内存中的某段内容曾被CPU执行过,那么它所在的内存单元必然被CS:IP指向过
jmp指令
代码段
什么是Debug
打开Debug
-
Windows键 + R,输入debug
也可以先进入cmd,再输入debug
注意:debug里面的数值默认都是采用16进制
Debug的常用功能
- “q”命令:退出debug
- “p”命令:类似于step over(“t”命令类似于step into),可用于跳过loop循环
-
“g”命令:跳过前面的代码,停留到指定的代码位置
R命令
- 输入“r”可以查看所有寄存器的值
-
输入“r 寄存器名称”可以修改寄存器的值
输入“r ax”将ax寄存器的值改为0100H
D命令
- 输入“d”可以查看内存中的内容
- 输入“d 段地址:偏移地址”查看特定位置的内存数据
- 输入“d 段地址:起始偏移地址 结尾偏移地址”查看特定位置和特定范围的内存数据
-
输入“d 偏移地址”、 “d 起始偏移地址 结尾偏移地址” ,会将DS的内容作为段地址
E指令
U命令
输入“u”、“u 段地址:偏移地址”可以将内存中的内容翻译为对应的汇编指令
- 由3部分组成
最左边一列:是指令的地址“段地址:偏移地址”
中间那一列:是指令对应的机器指令
最右边一列:是汇编指令
A命令
输入“a ”、“a 段地址:偏移地址”可以从某位置开始写入汇编指令
DS和[address]
- CPU要读写一个内存单元时,必须要先给出这个内存单元的地址,在8086中,内存地址由段地址和偏移地址组成
- 8086中有一个DS段寄存器,通常用来存放要访问数据的段地址
mov bs,1000H
mov ds,bx
mov al,[0]
上面3条指令的作用将10000H(1000:0)中的内存数据赋值到al寄存器中
mov al,[address]的意思将DS:address中的内存数据赋值到al寄存器中
由于al是8位寄存器,所以是将一个字节的数据赋值给al寄存器
- 8086不支持将数据直接送入段寄存器中,mov ds,1000H是错误的
字型数据的传递(2个字节)
大小端
mov指令
- “mov 内存单元, 内存单元”是不允许的,比如mov [0], [1]
add和sub指令
数据段
对于8086来说,在编程时,可以根据需要,将一组内存单元定义为一个段
我们可以将一组长度为N(N<=64KB)、地址连续、起始地址为16倍数的内存单元当做专门存储数据的内存空间,称为数据段。比如用123B0H-123B9H这段内存空间来存放数据,我们就可以认为123B0H-123B9H是一个数据段,它的段地址为123BH,长度为10字节
如何访问数据段中的数据?
用DS存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元
栈
- 栈:是一种具有特殊的访问方式的存储空间(后进先出, Last In Out Firt,LIFO)
- 8086会将CS作为代码段的段地址,将CS:IP指向的指令作为下一条需要取出执行的指令
- 8086会将DS作为数据段的段地址,mov ax,[address]就是取出DS:address的内存数据放到ax寄存器中
- 8086会将SS作为栈段的段地址,任意时刻,SS:SP指向栈顶元素
- 8086提供了PUSH(入栈)和POP (出栈)指令来操作栈段的数据
比如push ax是将ax的数据入栈,pop ax是将栈顶的数据送入ax
push-pop
在8086中,push、pop操作的数据都是2个字节的
栈段
对于8086来说,在编程时,可以根据需要,将一组内存单元定义为一个段
我们可以将一组长度为N(N<=64KB)、地址连续、起始地址为16倍数的内存单元,当做栈空间来使用,称为栈段。比如用10010H-1001FH这段内存空间当做栈来使用,我们就可以认为10010H-1001FH是一个栈段,它的段地址为1001H,长度为16字节
如何使用push、pop等栈操作指令访问我们定义的栈段?
用SS存放栈段的段地址,用SP存放栈顶的偏移地址
第一个完整的汇编程序
前面的实验都是利用Debug程序进行模拟测试的,并没有编写一个完整的汇编程序
使用汇编语言编写一个完整的程序,步骤大致如下
编写源代码,文件名拓展名为.asm
编译、链接(可以使用微软的MASM编译器)
调试、运行
汇编语言的组成
汇编语言由2类指令组成
汇编指令
如mov、add、sub等
有对应的机器指令,可以被编译为机器指令,最终被CPU执行伪指令
如assume、 segment、ends、end等
没有对应的机器指令,由编译器解析,最终不被CPU执行-
注释以分号开头
调试可执行程序
伪指令-segment、ends、end
- segment和ends的作用是定义一个段,segment代表一个段的开始,ends代表一个段的结束.
- 一个有意义的汇编程序中,至少要有一个段作为代码段存放代码
- assume
将用作代码段的code段和CPU中的cs寄存器关联起来 - end
编译器遇到end时,就结束对源程序的编译
退出程序
- 下面2句代码的作用是退出程序
mov ah,4ch
int 21h - 也可以写成
mov ax, 4c00h
int 21h
中断
中断
中断是由于软件的或硬件的信号,使得CPU暂停当前的任务,转而去执行另一段子程序
也就是说,在程序运行过程中,系统出现了一个必须由CPU立即处理的情况,此时,CPU暂时中止当前程序的执行转而处理这个新情况的过程就叫做中断中断的分类
硬中断(外中断),由外部设备(比如网卡、硬盘)随机引发的,比如当网卡收到数据包的时候,就会发出一个中断
软中断(内中断),由执行中断指令产生的,可以通过程序控制触发从本质上来讲,中断是一种电信号,当设备有某种事件发生时,它就会产生中断,通过总线把电信号发送给中断控制器。如果中断的线是激活的,中断控制器就把电信号发送给处理器的某个特定引脚。处理器于是立即停止自己正在做的事,跳到中断处理程序的入口点,进行中断处理
可以通过指令int n产生中断
n是中断码,内存中有一张中断向量表,用来存放中断码对应中断处理程序的入口地址
CPU在接收到中断信号后,暂停当前正在执行的程序,跳转到中断码对应的中断向量表地址处,去执行中断处理程序常见中断
int 10h用于执行BIOS中断
int 3是“断点中断”,用于调试程序
int 21h用于执行DOS系统功能调用,AH寄存器存储功能号
DOS系统功能调用
- DOS系统功能调用
由DOS提供的一组实现特殊功能的子程序供程序员在编写自己的程序时调用,以减轻编程的工作量
涉及屏幕显示、文件管理、I/O管理等等
每个子程序都有一个功能号,所有的功能调用的格式都是一致的。调用的步骤大致如下:
- 系统功能号送到寄存器AH中;
- 入口参数送到指定的寄存器中;
- 用INT 21H指令执行功能调用;
- 根据出口参数分析功能调用执行情况。
4c的意思是带返回码结束,al的值为返回码。
loop指令
- loop指令和cx配合使用,用于循环执行重复的操作,类似于高级语言中的for、while循环
- 使用格式
mov cx,循环次数
标号:
循环执行的程序段
loop 标号
- loop指令的执行流程
1、让cx的值减一,即cx = cx – 1
2、判断cx的值
如果不为零转至标号处执行程序,然后重复1
如果为零则执行loop后面的代码
段前缀
“mov ax, [bx]”中bx的值是偏移地址,段地址默认在ds中
我们也可以明确地标明段地址,比如
mov ax, ds:[bx]
mov ax, cs:[bx]
mov ax, ss:[bx]
mov ax, es:[bx]上面的“ds:”、“cs:”、“ss:”、“es:”称为段前缀
在代码段中存放数据
计算1122h、3344h、5566h的和,结果存放在ax中
dw(define word)
使用dw定义了3个字型数据,数据之间用逗号隔开
类似的还有db(define byte)、dd(define double word)start和end start是对应的,end start标记程序的执行入口
在代码段中使用栈
假设代码中有数据1122h、3344h、5566h、7788h、99aah、0aabbh,利用栈将它们逆序存放
包含多个段的程序
如果将代码、数据、栈都放到一个段里面
会显得混乱,编程时要随时注意何处是数据、何处是栈、何处是代码
一个段的大小<=64KB,这样就会让数据、代码、栈的大小受到极大的限制-
所以,一般会考虑使用多个段来存放数据、代码、栈
call和ret指令
call 标号:
将下一条指令的偏移地址入栈后
转到标号处执行指令ret:将栈顶的值出栈,赋值给ip
call和ret联合使用的作用类似于高级语言中的函数调用