Chapter V: 程序设计
躺雷记录:
关于汇编中大小写的问题:
- 各条指令不区分大小写
- 但是用户定义的各个变量, 标号等, 将区分大小写
关于汇编中的中括号:
对于MOV:
num dword 2
mov eax,2
mov ebx,num
mov ecx,[num]
;执行完ebx==ecx==2
;证明了对于变量, 加不加[]得到的都是符号地址中储存的值
;想要获得符号地址的地址, 需要用LEA, 或者是offset前缀
mov ebx,eax
;ebx==2
;将eax中的值传递给ebx中
mov ecx,[eax]
;报错,因为这里翻译成汇编是mov ecx,DS:[eax]
;证明了对于寄存器, 不加[]取的是寄存器中的储存的值
;而加[]是将其中的值解释为地址
;(BX)=1F23H
;(1F23H)=3
;(BL)=23
MOV [BX], BL
;执行后(1F23H)=BL=23
;即将BX中的值解释为地址 并将BL的值传递给该地址
对于LEA:
mov eax,2
lea ebx,[eax]
;执行后ebx=2
mov ebx,eax
;等同于上句
lea ebx,eax
;编译器报错: error A2070: invalid instruction operands
;证明了lea指令对于寄存器, 加[]取得是寄存器中的值
num dword 2
lea ebx,num
lea eax,[num]
; eax为num的地址,而ebx==eax
;证明了lea指令对于符号地址, 加不加[]效果相同
关于汇编中的立即数:
- 汇编中默认的立即数为10进制, 使用16进制需要特别声明
- 而针对16进制, 当首位为字母时, 需要在前头补0
如:
MOV AL, 0ABCDH ;必须加0, 否则报错
关于串处理指令:
由于串处理指令中使用的默认寄存器是:
SRC string: DS:SI
DST string: ES:DI
注意此处的DST使用的是ES前缀, 所以必须要设置ES
即将DST定义的segment name存到ES中
DATA SEGMENT
S1 DB 'personal computer'
S2 DB 'personal computer'
MESS1 DB 'match.$'
MESS2 DB 'no match.$'
DATA ENDS
...
MOV AX, DATA
MOV DS, AX
MOV ES, AX ;需要单独操作
关于Debug的应用:
Debug 是实模式(i7 8086K 8086模式)程序的调试工具。
使用它,可以查看 CPU 各种寄存器中的内容、内存的情况和在机器码级跟踪程序的运行。
Debug 的常用功能:
R 命令:查看、改变 CPU 寄存器的内容;
D 命令:显示寄存器中的内容;
E 命令:改写内存中的内容;
U 命令:将内存中的机器指令翻译成汇编指令;
T 命令:执行一条机器指令;
(只会用这个)
A 命令:以汇编指令的格式在内存中写入一条机器指令
汇编程序基础模板:
;******************
DATAS SEGMENT
;数据段, 用于变量的定义
DATAS ENDS
;******************
STACKS SEGMENT
;堆栈段, 当前程序暂时没有用到
STACKS ENDS
;******************
CODES SEGMENT
ASSUME CS:CODES,DS:DATAS,SS:STACKS ;assume只起声明的作用, 并不真正操作
START:
MOV AX,DATAS ;将DATAS送ds寄存器, 才有用
MOV DS,AX
;******************
;主程序编写部分
;******************
MOV AX, 4C00H ;相当于main函数最后的return
INT 21H ;只有在程序需要结束时才使用, 平时使用int 21时并不需要
CODES ENDS
END START ;标准格式
循环结构:
循环程序由三部分组成:
- 设置循环的初始条件
- 循环体
- 循环控制部分:
计数控制、特征值控制、地址边界控制
单循环:
循环输出BX内储存的值:
MOV CH, 4 ;循环控制
rotate:
MOV CL, 4 ;每次移位次数为4
ROL BX, CL ;移位指令
MOV AL, BL ;将移位后BX的第8位赋给AL
AND AL, 0FH ;将AH置零, 由于计算机最低只能进行字节操作, 这里需要4bit操作, 所以这么整
ADD AL, 30H ;转换为ASCII码
CMP AL, 3AH ;将小写a~f转化为大写A~F
JL printit ;跳转指令
ADD AL, 7 ;如果为a~f, 则转化为大写
printit:
MOV DL, AL ;显示输出的入口参数
MOV AH, 02H ;显示输出的功能号
INT 21H ;CPU中断
DEC CH ;循环控制
JNZ rotate ;跳回程序, 完成循环
把Y中1的个数存入COUNT单元中
;***************
DATAS SEGMENT
number dw 78a4h ;这玩意就是Y
address dw number
count dw ?
DATAS ENDS
;***************
CODES SEGMENT
ASSUME CS:CODES, DS:DATAS
START:
MOV AX, DATAS
MOV DS, AX
;***************
MOV CX, 0 ;重置CX,用于后头的计数
MOV BX, address ;将address中的值传给BX
MOV BX, [BX] ;将BX中的值指向的内存空间中的值传递给BX
;执行后(BX)=78a4H
REPEAT1:
TEST BX, 0FFFFH ;AND一下, 主要是刷新FLAGS, 用于后头的判定
JZ EXIT1 ;检测BX中的1是否全都移出了, 满足则代表循环结束
JNS SHIFT ;通过符号位判定最高位是否为1
INC CX ;如果为1, 则CX++
SHIFT:
SHL BX, 1 ;逻辑左移1位
JMP REPEAT1 ;建立循环
EXIT1:
MOV CX, count ;最后将CX中的结果装入count中
;***************
MOV AX, 4C00H
INT 21H
CODES ENDS
END START
插入排序:
;***************
DATAS SEGMENT
X DW ? ;-1
ARRAY_HEAD DW 3,5,15,23,37,49,52,65,78,99
ARRAY_END DW 105
N DW 32
DATAS ENDS
;***************
CODES SEGMENT
ASSUME CS:CODES, DS:DATAS
START:
MOV AX, DATAS
MOV DS, AX ;基本操作
;***************
MOV AX,N
MOV X,-1 ;执行后(X)=0FFFFH
MOV SI,0 ;初始化下标
COMPARE:
CMP ARRAY_END[SI],AX ;将array中的数从后往前与N比较
JLE INSERT ;如果array[si]<N, 则插入N
MOV BX,ARRAY_END[SI] ;否则将array[si]后移动一位
MOV ARRAY_END[SI+2],BX
SUB SI,2 ;改变下标, array是DW,所以SI-=2
JMP COMPARE ;跳转达成循环
INSERT:
MOV ARRAY_END[SI+2],AX ;在已经空出的位置插入N
;****************
MOV AX, 4C00H
INT 21H
CODES ENDS
END START
多重循环:
冒泡排序:
//CPP实现
for (i=0; i<n-1; ++i) //比较n-1轮
{
for (j=0; j<n-1-i; ++j) //每轮比较n-1-i次,
{
if (a[j] < a[j+1])
{
buf = a[j];
a[j] = a[j+1];
a[j+1] = buf;
}
}
}
;***************
DATAS SEGMENT
A DW 5, 8, 16, 32, 84
N DW 5
DATAS ENDS
;***************
CODES SEGMENT
ASSUME CS:CODES, DS:DATAS
;***************
START:
MOV AX, DATAS
MOV DS, AX ;基本操作
;***************
MOV CX,N ;将数组元素个数存放到CX
DEC CX ;CX--
LOOPOUT: ;外层循环
MOV DI,CX ;在DI中暂存外层循环数
MOV BX,0 ;BX作为数组下标, 初始为0
LOOPIN: ;内层循环
MOV AX,A[BX] ;将数组的A[I]装入AX
CMP AX,A[BX+2] ;A[I]与A[I+1]比较
JGE CONTINUE ;如果A[I]>=A[I+1]则跳过交换的步骤
XCHG AX,A[BX+2] ;交换数组两个元素的值
MOV A[BX],AX ;交换数组两个元素的值
CONTINUE:
ADD BX,2 ;循环控制: 下标++
LOOP LOOPIN ;每次LOOP都使CX--
MOV CX,DI ;重置CX, 即外层循环数
LOOP LOOPOUT ;每次LOOP都使CX--
;***************
MOV AX, 4C00H
INT 21H
CODES ENDS
END START
汇编附加段:
附加段用于数据的定义
但是实际上在8086中, 数据定义在附加段和数据段的作用是相同的, 只需要在使用时整对段地址就好, 即
;将对应的段地址存到段地址寄存器
MOV AX, DATA
MOV DS, AX
MOV AX, EXTRA
MOV ES, AX
但通常在附加段中保存大数据, 如串和数组
汇编的标号&标识符:
-
标号:
为指令的符号地址
长度限制31个字符 -
标识符:
为变量, 常量等的符号地址
长度通常也是31个字符
后头将Ch.6
Chapter VI: 子程序
躺雷记录:
过程定义伪操作PROC:
用于定义子程序(就是函数)
标准格式:
label PROC [attributes] [USES reglist], parameter_list
...
label ENDP
- label: 作为符号地址保存子程序的段地址
与其他标识符的定义有相同的要求 - PROC: 伪操作标志
- ENDP伪操作结束标志
- attributes: 子程序属性, 具体可以是以下任意内容
[distance] [langtype] [visibility] [prologuearg]
但是通常使用的只有distance:
- NEAR:
提供段内调用
当子程序与调用点定义在同一个段中时使用 - FAR:
提供段间调用
当子程序与调用点定义在不同的段中时使用
但是当定义在同一个段中时也可以使用
所以这是一个通用的选择
注意这两个控制的都是RET指令的返回方式
关于汇编中的start:
start为程序的入口, 程序加载到内存之后CS:IP会指向这个标号, 从START指向的指令开始运行
同时, start只是一个标号, 并没有强制的语法特定, 使用其他也可以…
如改成FUCK
也是可以运行的…
子程序的定义位置:
由于上头的start程序入口的问题, 所以子程序不能定义在start & END start中
可以定义的位置有两个:
- 与start & END start定义在同一代码段中, 但是需要定义在其之前或其之后
(类似于main与其他函数的关系) - 可以定义在其他代码段中
子程序的传参&返回值:
-
寄存器传参:
类似于形参传递.寄存器法寄存器法就是将入口参数和出口参数存放在约定的寄存器中。
-
优点:数据传递书读快、编程较方便、节省内存单元。
-
缺点:当传递参数过多时候,由于寄存器个数有限,及寄存器的频繁使用,将导致寄存器不足。
-
适用:参数较少的子程序
-
-
储存单元传参:
类似于函数使用全局变量把入口参数和出口参数都放在既定的储存单元中
-
优点:不占用寄存器、参数个数任意、每个子程序要处理的数据和送出的结构都有独立的存储单元
-
缺点:但用一定数量的存储单元,增加编程中对变量定义的难度
-
-
堆栈传参:
类似于传递变量指针堆栈法是利用堆栈来传递参数
通常将变量的地址保存到堆栈中-
优点:参数不占用寄存器,和存储单元。参数存放在公共堆栈区,处理完后客恢复。参数个数一般不限
-
缺点:由于参数和子程序混杂在一起,存取参数时候必须小心计算它在堆栈中的位置。要注意断点的保存和恢复
-
-
地址表传参:
类似于传递变量指针这种方法是把参数组成的一张参数表放在某个存储区中,然后只要主程序和子程序约定好这个存储区的首地址和存放的内容,在主程序中将参数传递给地址表,在子程序中根据地址表给定的参数就可以完成操作。
伪操作equ:
EQU 伪指令把一个符号名称与一个整数表达式或一个任意文本连接起来
可以理解为define宏定义, 功能与其相似
格式:
name EQU expression
name EQU symbol
name EQU <text>
- 第一种, expression 必须是一个有效整数表达式
- 第二种, symbol 是一个已存在的符号名称,已经用 = 或 EQU 定义过了
- 第三种, 任何文本都可以岀现在<…>内。当汇编器在程序后面遇到 name 时,它就用整数值或文本来代替符号。
例如:
//将几个DOS标志号和助记符相绑定
display equ 2h
key_in equ 1h
doscall equ 21h
10进制到16进制的转换程序:
DECIHEX SEGMENT
ASSUME CS: DECIHEX
;******************
DECIBIN PROC NEAR ;负责从键盘读取10进制数值
MOV BX, 00H
NEWCHAR:
MOV AH, 01H ;DOS调用,键盘输入值
INT 21H
SUB AL, 30H ;将读取的ASCII转化为数值
JL EXIT ;如果小于30H,则代表不是0~9,直接退出
CMP AL, 09H ;与09H比较
JG EXIT ;如果大于09H,则代表输入的不是0~9,直接退出
CBW ;将AL拓展到AX
XCHG AX, BX ;将AX中新输入的数与BX中原有的结果交换
MOV CX, 10 ;将乘数存入CX预备
MUL CX ;将AX中原有的数*10
XCHG AX, BX ;将*10后的结果与BX交换,
ADD BX, AX ;将新输入的值加到原有的值上
JMP NEWCHAR ;输入新字符, 达成循环
EXIT:
RET
DECIBIN ENDP
;******************
BINIHEX PROC NEAR ;负责将10进制转化为16进制并输出
MOV CH, 4
ROTATE:
MOV CL, 4 ;设定ROL的移位次数为4
ROL BX, CL ;将BX中的数循环左移4位, MSB的4位出现在LSB的4位中
MOV AL, BL ;将BL转移到AL中用于后头计算
AND AL, 0FH ;掩码覆盖MSB4位
ADD AL, 30H ;将数值转化为相应的ASCII码
CMP AL, 3AH ;检测是否大于9
JL PRINTIT ;如果大于9,则为A~F
ADD AL, 7 ;转化为A~F
PRINTIT:
MOV DL, AL ;将AL输出
MOV AH, 02H
INT 21H
DEC CH ;循环控制
JNZ ROTATE ;跳回达成循环, 如果CH==0,则退出循环
RET
BINIHEX ENDP
;******************
CRLF PROC NEAR ;格式控制,输出回车与换行</br>
MOV DL, 0DH
MOV AH, 02H
INT 21H
MOV DL, 0AH
MOV AH, 02H
INT 21H
RET
CRLF ENDP
;******************
MAIN PROC FAR ;MAIN主程序
PUSH DS ;由于是DOS调用Main,所以需要保存寄存器
SUB AX, AX
PUSH AX
CALL DECIBIN ;执行几个子程序
CALL CRLF
CALL BINIHEX
CALL CRLF
RET
MAIN ENDP
;******************
DECIHEX ENDS
END MAIN ;程序结尾必有的东西
输入16进制输出10进制:
程序框架:
本程序在main中调用各个子程序, 而main与几个子程序定义在同一个代码段中, 所以其他子程序为near
DISPLAY EQU 2H
KEY_IN EQU 1H
DOSCALL EQU 21H
;******************
HEXIDEC SEGMENT
;******************
MAIN PROC FAR ;MAIN主程序
ASSUME CS: HEXIDEC
START:
PUSH DS ;由于是DOS调用Main,所以需要保存寄存器
SUB AX, AX
PUSH AX
CALL HEXIBIN ;执行几个子程序
CALL CRLF
CALL BINIDEC
CALL CRLF
JMP MAIN ;跳回MAIN达成死循环
RET
MAIN ENDP
;*******************
HEXIBIN PROC NEAR;输入16进制ASCII字符并转化为16进制数值储存
MOV BX, 0 ;初始化BX
NEWCHAR:
MOV AH, KEY_IN ;键盘输入
INT DOSCALL
;0~9 ?
SUB AL, 30H ;判定输入的值是否在数字之前
JL EXIT ;ASCII在数字之前, 直接结束循环
CMP AL, 10D ;判定输入的值是否在数字之后
JL ADD_TO ;如果小于10,则是0~9
;a~f ?
SUB AL, 27H ;如果输入大于9,则跳转到字符a开始
CMP AL, 0AH ;边界值a
JL EXIT ;ASCII在a~f之前, 直接结束循环
CMP AL, 10H ;边界值f
JGE EXIT ;ASCII在a~f之后, 直接结束循环
;(BX)<-(BX)*16+(AX)
ADD_TO:
MOV CL, 4 ;设置SHL移位次数
SHL BX, CL ;逻辑左移
MOV AH, 0 ;AX高位置零
ADD BX, AX ;将BX左移后空出的4位填入新输入的数值
;完成循环
JMP NEWCHAR ;输入新字符,直到最后一个不满足条件才跳出
EXIT:
RET
HEXIBIN ENDP
;*********************
;将BX中的数转换为10进制并输出
;按位取并转化为ACSII输出
BINIDEC PROC NEAR
MOV CX, 10000D
CALL DEC_DIV
MOV CX, 1000D
CALL DEC_DIV
MOV CX, 100D
CALL DEC_DIV
MOV CX, 10D
CALL DEC_DIV
MOV CX, 1D
CALL DEC_DIV
RET
BINIDEC ENDP
;****************
;****************
;被除数 BX
;除数 CX
;商 AX
;余数 DX
DEC_DIV PROC NEAR
MOV AX, BX ;将被除数拷贝到AX中执行运算
MOV DX, 0 ;DX初始化
DIV CX ;除以CX,按位取出BX中的10进制数
MOV BX, DX ;将余数丢到BX中重新作为除数
MOV DL, AL ;将商丢到DL中
ADD DL, 30H ;转化为ASCII数字
MOV AH, DISPLAY ;召唤DOS显示
INT DOSCALL
RET
DEC_DIV ENDP
;*****************
CRLF PROC NEAR ;格式控制,输出回车与换行</br>
MOV DL, 0DH
MOV AH, 02H
INT 21H
MOV DL, 0AH
MOV AH, 02H
INT 21H
RET
CRLF ENDP
;******************
HEXIDEC ENDS
;******************
END START
ASCII码表:
上机程序分析:
都是之前的程序拼起来的
实验4:
DATA SEGMENT
CRLF_STR DB 13,10,'$'
X DW -1
ARRAY_HEAD DW 5H,10H,18H,22H,2AH,3CH,45H,58H
ARRAY_END DW 6FH
N DW ?
COUNT DW 10
DATA ENDS
;*******************
CODE SEGMENT
;*******************
MAIN PROC FAR
ASSUME CS:CODE,DS:DATA
PUSH DS
SUB AX, AX
PUSH AX
MOV AX,DATA
MOV DS,AX
;*******************
CALL HEXIBIN
CALL CRLF
CALL INSERT_ARR
CALL PRINT_ARR
MOV AX,4C00H
INT 21H
MAIN ENDP
;*******************
HEXIBIN PROC NEAR ;输入16进制数值并转化为ASCII码
MOV BX,0
NEWCHAR:
MOV AH,1H
INT 21H
SUB AL,30H
JL EXIT
CMP AL,10D
JL ADD_TO
SUB AL,27H
CMP AL,0AH
JL EXIT
CMP AL,10H
JGE EXIT
ADD_TO:
MOV CL,4
SHL BX,CL
MOV AH,0
ADD BX,AX
JMP NEWCHAR
EXIT:
RET
HEXIBIN ENDP
;*******************
INSERT_ARR PROC NEAR
MOV AX,BX ;
MOV X,-1 ;执行后(X)=0FFFFH
MOV SI,0 ;初始化下标
COMPARE:
CMP ARRAY_END[SI],AX ;将array中的数从后往前与N比较
JLE INSERT ;如果array[si]<N, 则插入N
MOV BX,ARRAY_END[SI] ;否则将array[si]后移动一位
MOV ARRAY_END[SI+2],BX
SUB SI,2 ;改变下标, array是DW,所以SI-=2
JMP COMPARE ;跳转达成循环
INSERT:
MOV ARRAY_END[SI+2],AX ;在已经空出的位置插入N
RET
INSERT_ARR ENDP
;*******************
PRINT_ARR PROC NEAR
MOV CX,COUNT
MOV SI,0
OUTPUT:
MOV BX,ARRAY_HEAD[SI]
ADD SI,2
PUSH CX
CALL BINIHEX
MOV DL,32
MOV AH,2
INT 21H
POP CX
DEC CX
JNZ OUTPUT
RET
PRINT_ARR ENDP
;*******************
BINIHEX PROC NEAR
MOV CH,4
ROTATE:
MOV CL,4
ROL BX,CL
MOV AL,BL
AND AL,0FH
ADD AL,30H
CMP AL,3AH
JL PRINTIT
ADD AL,7
PRINTIT:
MOV DL,AL
MOV AH,2
INT 21H
DEC CH
JNZ ROTATE
RET
BINIHEX ENDP
;*******************
CRLF PROC NEAR
MOV DL, 0DH
MOV AH, 02H
INT 21H
MOV DL, 0AH
MOV AH, 02H
INT 21H
RET
RET
CRLF ENDP
;*******************
CODE ENDS
;*******************
END MAIN
试卷组成:
填空 & 选择
每个2分, 总共50
程序填空
15空, 30分
编程题
2题10分
都是上课讲过的程序
刷题坑点整合:
80X86的数据储存方式:
80X86的CPU绝大多数是用小端模式进行存储
而ARM绝大多数都是大端存储
小端模式:
将数据的高位放在低字节, 低位放在高字节
取数据时, 向低字节方向进行解析
大端模式:
和小端模式相反
将数据的高位放在高字节, 低位放在低字节
去数据时, 向高字节方向进行解析
所以有如下代码:|
DATA SEGMENT
TABLE DW 10H,20H,30H,40H,50H,60H,70H,80H
ENTRY DW 5
DATA ENDS
;*******************
CODE SEGMENT
ASSUME CS: CODE, DS: DATA
START:
MOV AX, DATA
MOV DS, AX
;*******************
MOV BX, OFFSET TABLE
MOV CX, 10
RE:
MOV AX, [BX]
ADD BX, 1
LOOP RE
;*******************
MOV AX, 4C00H
INT 21H
CODE ENDS
;*******************
END START
每次MOV AX, [BX]后, AX的值为:
0010, 2000, 0020, 3000, 0030, 4000 …
MOV补充:
MOV [BX],10H ;MOV实际上不支持这种操作, 但是在编译器中是可以通过的
;必须在前头说明地址的数据类型:
MOV WORD PTR[BX], 1001H ;正确
;因为立即数无类型, 所以需要显式说明
实际上相当于DST为储存器寻址方式, SRC为立即数寻址方式
关于算数指令:
不可以使用立即数的指令:
- 除法: DIV & IDIV
- 乘法: MUL & IMUL
而加法和减法都可以使用立即数
关于数值回送操作符:
总之, 滚回去再看, 以为不考, 都忘了
SEG补充:
MOV BX , SEG oper1
其oper1只能是variable或label, 不能是其他的任何东西, 即使使用的是variable或label的地址也不行
如:
MOV AX, OFFSET ARR
MOV BX, SEG [AX] ;还是报错
关于寻址方式:
你以为不会考吗?
滚回去看!
关于双精度数的比较:
首先, 汇编中所有的数都默认当做是有符号数, 即使有无符号完全依靠程序员的解释
所以, 对于有符号双精度数的比较
- 高位使用有符号的比较法
LESS & GREATER 系列JMP - 低位使用无符号的比较法
BELOW & ABOVE 系列的JMP
如:
CMP DX, BX
JL LABLE1 ;符号位在高字节
JG LABLE2
CMP AX, CX
JB LABLE1 ;低字节相当于无符号位
JAE LABLE2
关于8086寻址:
注意, 之前给出的EA公式不是白给的!
EA(effective address) = 基址 + (变址 * 比例因子) + 位移量
8086几个地址要点:
-
最后得到的EA一定要是TMD20位, 否则就TM各种报错!
-
段地址一定要使用段寄存器段寄存器, 否则报错
(包含隐藏的默认使用段寄存器的情况)
如:MOV 1234H:2H, 2 ;报错 ;而这种方法就OK: MOV AX, 1234H MOV DS, AX MOV DS:2H, 2
-
除了段寄存器中的段地址, 后头所有的都TM属于偏移地址
而偏移地址都TM需要一个中括号[]而能够出现在中括号之中的, 只有那4个寄存器, 和立即数
BX, BP, SI, DI, 立即数其他任何东西要是出现, 狗腿给你打断
-
各种隐含段寄存器的情况:
主要用作计算物理地址的题-
访问指令时:
默认的段寄存器为CS -
访问堆栈时:
默认的段寄存器为SS注意, 当偏移地址中有BP时, 也视作访问堆栈, 其默认寄存器也是SS
-
访问数据时:
默认的段寄存器为DS -
访问目的串时:
默认的段寄存器为ES
-