汇编三 - Debug、EMU8086、指令、伪指令、中断、loop指令
Debug
- Debug是DOS/Windows都提供的实模式(8086方式)程序的调试工具。使用它,可以查看CPU各种寄存器中的内容、内内存的情况和在机器码级跟踪程序程序的运行。
- win32系统中直接有这个工具,位置如下图。
- 打开方法
- Windows键 + R,输入debug
- 也可以先进入cmd,再输入debug
- 注意:debug里面的数值默认都是采用16进制
- win64需要自己安装
- 先安装DOSBOX(搞一个假的32位环境)
- 将debug文件放入到C盘或者D盘的根目录下(这里放到了C盘下了)
- 打开DOSBox
-
输入:
mount C C:/ //将DOSBox这个假的32位系统的C盘映射到当前系统的C盘 C: //进入C盘 debug //启动debug 此时一闪一闪的光标,就说明已经打开debug了 输入r就可以看看到CPU所有寄存器的值了
- Debug的常用功能
R
命令: 查看、改变CPU寄存器的内容- 输入
r
可以查看所有寄存器的值 - 输入
r 寄存器名称
可以修改寄存器的值 -
输入
r ax
将ax寄存器的值改为0100Hr ax 1111 r //可以查看AX的值变化
- 输入
D
命令:查看内存中的内容- 输入
d
可以查看内存中的内容 - 输入
d 段地址:偏移地址
查看特定位置的内存数据 - 输入
d 段地址:起始偏移地址 结尾偏移地址
查看特定位置和特定范围的内存数据 - 输入
d 偏移地址
、d 起始偏移地址 结尾偏移地址
,会将DS的内容作为段地址
- 输入
E
命令:改写内存中的内容- 输入
e 段地址:偏移地址 数据串
修改特定位置的内存数据 - 输入
e 段地址:偏移地址
后按Enter也可以修改特定位置的内存数据,数据之间用空格隔开
- 输入
U
命令:将内存中的机器指令翻译成汇编指令- 输入
u 段地址:偏移地址
可以将内存中的内容翻译为对应的汇编指令 - 由3部分组成
- 最左边一列:是指令的地址
段地址:偏移地址
- 中间那一列:是指令对应的机器指令
- 最右边一列:是汇编指令
- 最左边一列:是指令的地址
- 输入
T
命令:执行一条机器指令A
命令:以汇编指令的格式在内存中写入一条机器指令。- 输入
a
、a 段地址:偏移地址
可以从某位置开始写入汇编指令
- 输入
q
命令:退出debugp
命令:类似于step over(“t”命令类似于step into),可用于跳过loop循环g
命令:跳过前面的代码,停留到指定的代码位置
emu8086
- EMU8086是学习汇编必不可少的工具,它结合了一个先进的原始编辑器、组译器、反组译器、具除错功能的软件模拟工具(虚拟PC),还有一个循序渐进的指导工具。该软件包含了学习汇编语言的全部内容。Emu8086集源代码编辑器,汇编/反汇编工具以及可以运行debug的模拟器(虚拟机器)于一身,此外,还有循序渐进的教程。
- 安装8086CPU模拟器
emu8086
双击安装,然后输入注册码即可
- emu8086常用快捷键
- F5:调试运行
- F4:重新加载
- F8:下一步(单步执行)
- F9:直接一步到位运行整个程序
- Ctrl + F8:跳过前面代码,断点到单击选中的代码那行
- 注意:emu8086的语法检查并非100%严格(比较严谨的还是使用MASM进行编译)
编写完整的汇编程序(指令、伪指令)
- 前面的实验都是利用Debug程序进行模拟测试的,并没有编写一个完整的汇编程序
- 使用汇编语言编写一个完整的程序,步骤大致如下
- 编写源代码,文件名拓展名为.asm
- 编译、链接(可以使用微软的MASM编译器)
- 调试、运行
- 汇编语言的组成
- 汇编语言由2类指令组成
- 汇编指令
如:mov、add、sub等
- 有对应的机器指令,可以被编译为机器指令,最终被CPU执行
- 伪指令
如assume、 segment、ends、end等
- 没有对应的机器指令,由编译器解析,最终不被CPU执行
- 就是告诉编译器编译代码的时候,怎么处理.
- 比如:某段是代码段,那么编译器编译的时候,就把该段分配为代码段的虚拟内存地址。
- 汇编指令
- 注释以分号开头
- 汇编语言由2类指令组成
- 伪指令 – segment、ends、end
-
segment和ends的作用是定义一个段,segment代表一个段的开始,ends代表一个段的结束,使用格式为
段名 segment ... 段名 ends
- 一个有意义的汇编程序中,至少要有一个段作为代码段存放代码
- assume
- 声明一下code段是cs段、代码段
assume 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系统有很多功能子程序,每个子程序用功能号表示,那么,怎么知道调用哪一个呢?
-
AH寄存器就是制定调用DOS系统的哪一个功能子程序。
; 调用DOS系统的功能号为9h的子程序-屏幕显示字符串 mov ah,9h ;相当于函数的参数 int 21h ;相当于调用函数
- 中断可以简单的理解为操作系统自带的一些编写好的程序,这些程序可以实现一些功能,可以通过硬件设备、代码直接调用
- DOS系统功能调用
- 由DOS提供的一组实现特殊功能的子程序供程序员在编写自己的程序时调用,以减轻编程的工作量
- 涉及屏幕显示、文件管理、I/O管理等等
- 每个子程序都有一个功能号,所有的功能调用的格式都是一致的。调用的步骤大致如下:
- 系统功能号送到寄存器AH中;
- 入口参数送到指定的寄存器中;
- 用INT 21H指令执行功能调用;
- 根据出口参数分析功能调用执行情况
-
代码举例:打印字符串
assume cs:code, ds:data ; 数据段 data segment db 40, 41, 42, 64, 'Hello World!$' data ends ; 代码段 code segment ;start: 程序标号,告诉系统程序从这里开始 start: ; 设置数据段寄存器ds的值 mov ax, data mov ds, ax ;DOS系统打印字符串的要求:所打印的字符串必须由DS:DX给出 mov dx, 4h ; DOS系统打印ds:dx所指的字符串,而且从该地址开始一直往后打印,把该地址下的所有数据看做是字符打印,比如上面的40看做是“(”字符对应的ASCII,直到遇见$为止。 mov ah, 9h int 21h ; 退出程序 mov ax, 4c00h int 21h code ends ; 程序入口在start end start
包含多个段的程序
- 包含多个段的程序
- 如果将代码、数据、栈都放到一个段里面
- 会显得混乱,编程时要随时注意何处是数据、何处是栈、何处是代码
- 一个段的大小<=64KB,这样就会让数据、代码、栈的大小受到极大的限制
- 所以,一般会考虑使用多个段来存放数据、代码、栈
- 如果将代码、数据、栈都放到一个段里面
-
数据段+代码段:
; 仅仅用于提醒开发者每个段的含义 assume cs:code, ds:data ; ----- 数据段 begin ----- data segment age db 20h ;定义变量 no dw 30h db 10 dup(6) ; 生成10个连续的6 string db 'Hello World!$' data ends ; ----- 数据段 end ----- ; ----- 代码段 begin ----- code segment ;start和end start是对应的,end start标记程序的执行入口 start: ; 手动设置ds的值 mov ax, data mov ds, ax mov ax, no mov bl, age ;由于前面定义的no为单字节数据,所以这里必须要用单字节寄存器 ; 打印 mov dx, offset string ; offset string代表string的偏移地址 ;为啥不直接:mov dx,string ? 因为string是实际的值"hello..”,但是dx需要的是string的偏移地址 mov ah, 9h int 21h ; 退出 mov ax, 4c00h int 21h code ends ; ----- 代码段 end ----- ; 编译结束,start是程序入口 ; start所在的段就是代码段 ; 所以cs的值就是code段的段地址 ; 相当于cs的值已经自动设置完毕 end start
-
数据+代码+堆栈
; 仅仅用于提醒开发者每个段的含义 ; CS/DS/SS中的值并没有被相应的设置!!! ; CS要通过标出start自动设定 , DS/SS需要手动自己设置 assume cs:code, ds:data, ss:stack ; 栈段 stack segment db 10 dup(8) stack ends ; 数据段 data segment db 20 dup(9) data ends ; 代码段 code segment start: ; 手动设置ss和ds mov ax, stack mov ss, ax mov ax, data mov ds, ax mov ax, 1122h mov bx, 3344h mov [0], ax ; 使用栈 ; 功能:交换AX与bx的值 ; SP可以不用手动初始化赋值,只要定义完栈,SP就会被初始化 ; mov sp, 10 push ax push bx pop ax pop bx ; 退出 mov ax, 4c00h int 21h code ends end start
- start 和 end start
- start是程序入口,start所在的段就是代码段
- 注意:这个start必须写,这就代表程序的入口地址,否则编译器会把程序的第一行就当成代码编译
- 可以看出:DS/SS/ES都需要程序员用代码初始化,然而CS不需要,那是因为只要你写上START:就告诉编译器哪里是代码段的开始
- 那么编译器就会自动将代码段的开始(start)地址设置给CS,因此,不需要程序员手动设置了
- start 和 end start
loop指令
- loop指令和cx配合使用,用于循环执行重复的操作,类似于高级语言中的for、while循环
-
使用格式
mov cx, 循环次数 标号: 循环执行的程序段 loop 标号
- loop指令的执行流程
- 让cx的值减1,即cx = cx – 1
- 判断cx的值
- 如果不为零转至标号处执行程序,然后重复1
- 如果为零则执行loop后面的代码
- 常见做法
- loop循环中经常使用bx来存储变化的偏移地址,用法如
[bx]
- loop循环中经常使用bx来存储变化的偏移地址,用法如
- 段前缀
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:”称为段前缀