intel-32基础语法(下)
# ASCII数值
当数字显示在屏幕上或从键盘输入时,它们是ASCII形式存储的,也就是 输入‘1’ 实际是 49
比如:
mov eax,'0'
此时,eax 真实数据并非十进制数据0,而是 0的 ASCII 值 48 或者 30 .
所以计算的时候,ASCII 十进制 的话 要先 - 48 得到真实的0
下面观察一组0-9数据表:
Bin(二进制) | Oct(八进制) | Dec(ASCII 十进制) | Hex(ASCII 十六进制) | Dec(十进制数值) | 解释 |
---|---|---|---|---|---|
0011 0000 | 060 | 48 | 0x30 | 0 | 字符0 |
0011 0001 | 061 | 49 | 0x31 | 1 | 字符1 |
0011 0010 | 062 | 50 | 0x32 | 2 | 字符2 |
0011 0011 | 063 | 51 | 0x33 | 3 | 字符3 |
0011 0100 | 064 | 52 | 0x34 | 4 | 字符4 |
0011 0101 | 065 | 53 | 0x35 | 5 | 字符5 |
0011 0110 | 066 | 54 | 0x36 | 6 | 字符6 |
0011 0111 | 067 | 55 | 0x37 | 7 | 字符7 |
0011 1000 | 070 | 56 | 0x38 | 8 | 字符8 |
0011 1001 | 071 | 57 | 0x39 | 9 | 字符9 |
# ASCII数值操作
假设输入的数值16进制结果 以ASCII 十进制 数理解如下操作:
1、最终输出eax结果:d
mov eax,'2' ;eax = 50
mov ebx,'2' ;ebx = 50
add eax, ebx ;eax = 50 + 50 = 100
;此时打印eax的值不是4,打印的是 ASCII 100 对应的值d
2、最终输出eax结果:菱形符号控制符号,不能正常输出
mov eax,2 ;eax = 2
mov ebx,2 ;ebx = 2
add eax, ebx ;eax = 2 + 2 = 4
;此时打印eax的值不是4,打印的是 ASCII 4 对应的值菱形符号控制符号
3、最终输出eax结果:4
mov eax,'2' ;eax = 50
mov ebx,2 ;ebx = 2
add eax, ebx ;eax = 50 + 2 = 52
;此时打印eax的值是 ASCII 52 对应的值4
4、最终输出eax结果:4
mov eax,'2' ;eax = 50
sub eax,'0' ;eax = 50 - 48 = 2
mov ebx,'2' ;ebx = 50
sub ebx,'0' ;ebx = 50 - 48 = 2
add eax, ebx ;eax = 2 + 2 = 4
add eax, '0' ;eax = 48 + 4 = 52
;此时打印eax的值是 ASCII 52 对应的值4
下面从网络上收集了ASCII 0 -159 对应的数据
ASCII码大致由三部分组成:
# 1、ASCII 可打印字符对照表
数字 32–126 分配给了能在键盘上找到的字符,当您查看或打印文档时就会出现。注:十进制32代表空格 ,十进制数字 127 代表 DELETE 命令。下面是ASCII码和相应数字的对照表
ASCII 码 | 字符 | ASCII 码 | 字符 | ASCII 码 | 字符 | ASCII 码 | 字符 | |||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
十进位 | 十六进位 | 十进位 | 十六进位 | 十进位 | 十六进位 | 十进位 | 十六进位 | |||||||
032 | 20 | 056 | 38 | 8 | 080 | 50 | P | 104 | 68 | h | ||||
033 | 21 | ! | 057 | 39 | 9 | 081 | 51 | Q | 105 | 69 | i | |||
034 | 22 | " | 058 | 3A | : | 082 | 52 | R | 106 | 6A | j | |||
035 | 23 | # | 059 | 3B | ; | 083 | 53 | S | 107 | 6B | k | |||
036 | 24 | $ | 060 | 3C | < | 084 | 54 | T | 108 | 6C | l | |||
037 | 25 | % | 061 | 3D | = | 085 | 55 | U | 109 | 6D | m | |||
038 | 26 | & | 062 | 3E | > | 086 | 56 | V | 110 | 6E | n | |||
039 | 27 | ' | 063 | 3F | ? | 087 | 57 | W | 111 | 6F | o | |||
040 | 28 | ( | 064 | 40 | @ | 088 | 58 | X | 112 | 70 | p | |||
041 | 29 | ) | 065 | 41 | A | 089 | 59 | Y | 113 | 71 | q | |||
042 | 2A | * | 066 | 42 | B | 090 | 5A | Z | 114 | 72 | r | |||
043 | 2B | + | 067 | 43 | C | 091 | 5B | [ | 115 | 73 | s | |||
044 | 2C | , | 068 | 44 | D | 092 | 5C | \ | 116 | 74 | t | |||
045 | 2D | - | 069 | 45 | E | 093 | 5D | ] | 117 | 75 | u | |||
046 | 2E | . | 070 | 46 | F | 094 | 5E | ^ | 118 | 76 | v | |||
047 | 2F | / | 071 | 47 | G | 095 | 5F | _ | 119 | 77 | w | |||
048 | 30 | 0 | 072 | 48 | H | 096 | 60 | ` | 120 | 78 | x | |||
049 | 31 | 1 | 073 | 49 | I | 097 | 61 | a | 121 | 79 | y | |||
050 | 32 | 2 | 074 | 4A | J | 098 | 62 | b | 122 | 7A | z | |||
051 | 33 | 3 | 075 | 4B | K | 099 | 63 | c | 123 | 7B | { | |||
052 | 34 | 4 | 076 | 4C | L | 100 | 64 | d | 124 | 7C | | | |||
053 | 35 | 5 | 077 | 4D | M | 101 | 65 | e | 125 | 7D | } | |||
054 | 36 | 6 | 078 | 4E | N | 102 | 66 | f | 126 | 7E | ~ | |||
055 | 37 | 7 | 079 | 4F | O | 103 | 67 | g | 127 | 7F | DEL |
# 2、ASCII 非打印控制字符
ASCII 表上的数字 0–31 分配给了控制字符,用于控制像打印机等一些外围设备。例如,12 代表换页/新页功能。此命令指示打印机跳到下一页的开头。
进制 | 十六进制 | 字符 | 十进制 | 十六进制 | 字符 | |
---|---|---|---|---|---|---|
0 | 00 | 空 | 16 | 10 | 数据链路转意 | |
1 | 01 | 头标开始 | 17 | 11 | 设备控制 1 | |
2 | 02 | 正文开始 | 18 | 12 | 设备控制 2 | |
3 | 03 | 正文结束 | 19 | 13 | 设备控制 3 | |
4 | 04 | 传输结束 | 20 | 14 | 设备控制 4 | |
5 | 05 | 查询 | 21 | 15 | 反确认 | |
6 | 06 | 确认 | 22 | 16 | 同步空闲 | |
7 | 07 | 震铃 | 23 | 17 | 传输块结束 | |
8 | 08 | backspace | 24 | 18 | 取消 | |
9 | 09 | 水平制表符 | 25 | 19 | 媒体结束 | |
10 | 0A | 换行/新行 | 26 | 1A | 替换 | |
11 | 0B | 竖直制表符 | 27 | 1B | 转意 | |
12 | 0C | 换页/新页 | 28 | 1C | 文件分隔符 | |
13 | 0D | 回车 | 29 | 1D | 组分隔符 | |
14 | 0E | 移出 | 30 | 1E | 记录分隔符 | |
15 | 0F | 移入 | 31 | 1F | 单元分隔符 |
# 3、扩展 ASCII 可打印字符
十进制 | 十六进制 | 字符 | 十进制 | 十六进制 | 字符 | ||
---|---|---|---|---|---|---|---|
32 | 20 | space | 80 | 50 | P | ||
33 | 21 | ! | 81 | 51 | Q | ||
34 | 22 | " | 82 | 52 | R | ||
35 | 23 | # | 83 | 53 | S | ||
36 | 24 | $ | 84 | 54 | T | ||
37 | 25 | % | 85 | 55 | U | ||
38 | 26 | & | 86 | 56 | V | ||
39 | 27 | ' | 87 | 57 | w | ||
40 | 28 | ( | 88 | 58 | X | ||
41 | 29 | ) | 89 | 59 | Y | ||
42 | 2A | * | 90 | 5A | Z | ||
43 | 2B | + | 91 | 5B | [ | ||
44 | 2C | , | 92 | 5C | / | ||
45 | 2D | - | 93 | 5D | ] | ||
46 | 2E | . | 94 | 5E | ^ | ||
47 | 2F | / | 95 | 5F | _ | ||
48 | 30 | 0 | 96 | 60 | ` | ||
49 | 31 | 1 | 97 | 61 | a | ||
50 | 32 | 2 | 98 | 62 | b | ||
51 | 33 | 3 | 99 | 63 | c | ||
52 | 34 | 4 | 100 | 64 | d | ||
53 | 35 | 5 | 101 | 65 | e | ||
54 | 36 | 6 | 102 | 66 | f | ||
55 | 37 | 7 | 103 | 67 | g | ||
56 | 38 | 8 | 104 | 68 | h | ||
57 | 39 | 9 | 105 | 69 | i | ||
58 | 3A | : | 106 | 6A | j | ||
59 | 3B | ; | 107 | 6B | k | ||
60 | 3C | < | 108 | 6C | l | ||
61 | 3D | = | 109 | 6D | m | ||
62 | 3E | > | 110 | 6E | n | ||
63 | 3F | ? | 111 | 6F | o | ||
64 | 40 | @ | 112 | 70 | p | ||
65 | 41 | A | 113 | 71 | q | ||
66 | 42 | B | 114 | 72 | r | ||
67 | 43 | C | 115 | 73 | s | ||
68 | 44 | D | 116 | 74 | t | ||
69 | 45 | E | 117 | 75 | u | ||
70 | 46 | F | 118 | 76 | v | ||
71 | 47 | G | 119 | 77 | w | ||
72 | 48 | H | 120 | 78 | x | ||
73 | 49 | I | 121 | 79 | y | ||
74 | 4A | J | 122 | 7A | z | ||
75 | 4B | K | 123 | 7B | { | ||
76 | 4C | L | 124 | 7C | |||
77 | 4D | M | 125 | 7D | } | ||
78 | 4E | N | 126 | 7E | ~ | ||
79 | 4F | O | 127 | 7F | DEL |
# 数值数据
section .text
global _start ;must be declared for using gcc
_start: ;tell linker entry point
mov eax,'5'
sub eax, '0'
mov ebx, '6'
sub ebx, '0'
add eax, ebx
add eax, '0'
mov [sum], eax
mov ecx,msg
mov edx, len
mov ebx,1 ;file descriptor (stdout)
mov eax,4 ;system call number (sys_write)
int 0x80 ;call kernel
mov ecx,sum
mov edx, 1
mov ebx,1 ;file descriptor (stdout)
mov eax,4 ;system call number (sys_write)
int 0x80 ;call kernel
mov eax,1 ;system call number (sys_exit)
int 0x80 ;call kernel
section .data
msg db "The sum is:", 0xA,0xD
len equ $ - msg
segment .bss
sum resb 1
结果:(显然结果不是我们想看到的)
The sum is:
;
TODO:
1、每次运算都要对每个寄存器处理有一定消耗
2、能计算个位,但是没法进行进位,比如不能正确表示出11
# ASCII表示
在ASCII表示中,十进制数字存储为ASCII字符字符串,运算需要使用正确的汇编指令进行处理
输入‘0’,结果其实是
30H
输入十进制值0123存储为:
30 31 32 33H
其中,30H是0的ASCII值,31H是1的ASCII值,依此类推。
所以要输出一个11需要这样:
section .text
global _start
_start:
mov ax, 3131H ;设置 ax值 11
mov [res], ax
mov edx,2 ;长度2
mov ecx,res ;赋值
mov ebx,1
mov eax,4
int 0x80
mov eax,1
int 0x80
section .bss
res resb 2
所以在本篇汇编中是不能完全直接使用 add 、sub直接加减的
1、处理ASCII运算的指令
ASCII运算的指令 (未压缩) | 解释 |
---|---|
AAA(⚠️ 都是单指令,后不接操作数) | 加法后ASCII调整 |
AAS | 减法后的ASCII调整 |
AAM | 乘法后ASCII调整 |
AAD | 除法前ASCII调整 |
2、处理对象
这些指令处理的是 ax(eax 等),不需要指定,类似 ‘隐含寻址’
3、使用方式
;加法运算
add ax, '6'
aaa
;减法运算
sub ax, '6'
aas
示例:
计算 5+6=11
section .text
global _start
_start: ;tell linker entry point
;初始化操作,清空ax中的 高地位数据
sub al, ah
sub ah, ah
;运算
mov ax, '5' ;ax = 00 35H
add ax, '6' ;ax = 00 35H + 00 36H = 00 6BH
aaa ;调整ax中值(不是其它寄存器)
;⚠️ 使用add后接着使用aaa表示格式化,将上面加法后ASCII值进行调整
;调整前 00 6BH ,处理后 ax = 01 01H,高低位各一个1表示 11的值
or al, 30h ;ax = 01 31H
or ah, 30h ;ax = 31 31H
mov [res], ax ;res = 11
mov edx,2 ;11占两个字节,如果设置1个字节的长度的话返回的是1不是11
mov ecx,res ;写入结果
mov ebx,1
mov eax,4
int 0x80
mov eax,1
int 0x80
section .bss
res resb 2
结果:
11
# 压缩
对于一个十进制数1234
1、1234 的 ASCII十六进制表示:
31 32 33 34 H
对应的未压缩十进制表示:
01 02 03 04
2、所以前面上一个示例中 aaa 的结果就是未压缩十进制表示
01 01
3、对应压缩后十进制表示:
12 34
从上可以看到,压缩后可以减少一半的存储空间。
压缩后数据运算指令:(⚠️ 不支持 )
ASCII运算的指令 (压缩) | 解释 |
---|---|
DAA | 加法后的十进制调整 |
DAS | 减后的十进制调整 |
乘法 | 不支持 |
除法 | 不支持 |
对比加法运算后 aaa(未压缩) 和 daa (压缩)结果
1、aaa
mov ax, '5' ;ax = 00 35H
add ax, '6' ;ax = 00 6BH
aaa ;ax = 01 01H
2、daa
mov ax, '5' ;ax = 00 35H
add ax, '6' ;ax = 00 6BH
daa ;ax = 00 11H
示例:
section .text
global _start ;must be declared for using gcc
_start: ;tell linker entry point
mov esi, 4 ;pointing to the rightmost digit
mov ecx, 5 ;num of digits
clc ; 清除标志位 CF的值,即CF=0
add_num:
mov al, [num1 + esi]
adc al, [num2 + esi]
aaa
pushf
or al, 30h
popf
mov [sum + esi], al
dec esi
num add_num
mov edx,len ;message length
mov ecx,msg ;message to write
mov ebx,1 ;file descriptor (stdout)
mov eax,4 ;system call number (sys_write)
int 0x80 ;call kernel
mov edx,5 ;message length
mov ecx,sum ;message to write
mov ebx,1 ;file descriptor (stdout)
mov eax,4 ;system call number (sys_write)
int 0x80 ;call kernel
mov eax,1 ;system call number (sys_exit)
int 0x80 ;call kernel
section .data
msg db 'The Sum is:',0xa
len equ $ - msg
num1 db '12345'
num2 db '23456'
sum db ' '
编译并执行上述代码后,将产生以下结果-
The Sum is:
35801
# 标志位
前面在介绍逻辑章节的时候使用到了标志位,此节详细介绍每一个标志位(intel 16、32位)。
CPU内部的寄存器中有一种特殊的寄存器,被称为标志寄存器,具有以下三种作用,:
(1)用来存储相关指令的某些执行结果;
(2)用来为CPU执行相关指令提供行为依据;
(3)用来控制CPU的相关工作方式。
# 状态字(PSW)
程序运行中一些记录信息,根据CPU不同,存储状态信息的寄存器也不同,有16位,32位,64位等等
# 标志寄存器(flag)
标志,顾名思义就是记录一些状态当前的信息,方便做判断,
flag寄存器是按位起作用的,每一位都有专门的含义,8086 CPU的flag寄存器的结构:
位(高-低) | flag(计算结果标志位) | 含义(计算后的标记) | |
---|---|---|---|
15 | |||
14 | |||
13 | |||
12 | |||
11 | OF(有符号运算是否有溢出) | if (结果溢出) { of=1} else { of=0 } | |
10 | DF(递增递减方向) | si、di递增 df=0,si、di递减df=1 | |
9 | IF(中断允许) | 允许并响应外部中断时if=1,反之屏蔽 | |
8 | TF(调试) | tf=1表示处理器每次只执行一条指令,调试模式 | |
7 | SF(非负) | if (结果负数) { sf=1} else { sf=0 } | |
6 | ZF(非0) | if (计算结果为0) { zf=1} else { zf=0 } | |
5 | |||
4 | AF(辅助进位) | af=1表示运算过程中最后四位有进位 | |
3 | |||
2 | PF(奇偶) | if (计算结果为偶数) { pf=1} else { pf=0 } | |
1 | |||
0 | CF(无符号运算是否有借位) | 结果显示不下产生了借位,cf=1 |
其中1、3、5、12、13、14、15位在8086 CPU中没有使用
# OF标志
有符号运算溢出标志位。
记录了有符号运算的结果是否发生了溢出,如果发生溢出OF=1,如果没有OF=0;
# DF标志
方向标志位。
在串处理指令中,每次操作后,如果DF=0,si、di递增,如果DF=1,si、di递减;
DF的值是由程序员进行设定的 cld命令是将DF设置为0,std命令是将DF设置为1;
# IF标志
中断允许标志位。
它用来控制是否允许接收外部中断请求。
若IF=1,能响应外部中断,反之则屏蔽外部中断;
# TF标志
调试标志位。
当TF=1时,处理器每次只执行一条指令,即单步执行;
# SF标志
flag的第7位是SF,符号标志位。
它记录相关指令执行后,其结果是否为负。如果结果为负,sf=1;如果非负,sf=0。
计算机中通常用补码来表示有符号数据(负数)。
运算后可以通过它来得知结果的正负。
mov ax,1
sub ax,2
;1-2<0,此时sf=1
# ZF标志
flag的第6位是ZF,零标志位
它记录相关指令执行后,结果为0,ZF=1(记录下是0这样的肯定信息),结果不为0,ZF=0(表示结果非0)。
mov ax,5
sub ax,5
;指令执行后,结果为0,则ZF=1
mov ax,5
and ax,0
;指令执行后,结果为0,则ZF=1
mov ax,5
sub ax,1
;指令执行后,结果为4,不为0,则ZF=0
mov ax,1
or ax,0
;指令执行后,结果为1,不为0,则ZF=0
# AF标志
辅助进位标志位。
运算过程中看最后四位,不论长度为多少。最后四位向前有进位或者借位,AF=1,否则AF=0;
# PF标志
flag的第2位是PF,奇偶标志位。
记录指令执行后结果所有的二进制位中1的个数。为偶数,PF=1;为奇数,PF=0。
mov al,1
add al,2
;执行结果为00000011B,有2个1,偶数,则PF=1
mov al,1
or al,0
;执行后结果为00000001B,有1个1,奇数,则PF=1
# CF标志
flag的第0位是CF,无符号运算进位标志位。
是无符号运算记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值。
对于位数为N的无符号数,其对应的二进制信息的最高位,即第N-1位,就是它的最高有效位,假想存在第N位是相对于最高有效位的更高位。
mov al,98h
add al,al ;98H + 98H = 01 30H; al 只有两个字节,所以执行后(al)=30H,cf=1,cf记录了从最高有效位向更高位的进位值
add al,al ;30H + 30H = 60H; 执行后(al)=60H,cf=0,cf记录了从最高有效位向更高位的进位值
sub al,al ;执行后(al)=0,cf=0,cf记录了向更高位的借位值
# 1.1 abc指令
adc是带进位加法指令,利用了CF位上记录的进位值。
格式:adc 操作对象1,操作对象2
功能:操作对象1=操作对象1+操作对象2+CF。
mov ax,2
mov bx,1
sub bx,ax
adc ax,1
执行后 (ax)=4,相当于计算(ax)+1+CF=2+1+1+4
mov ax,1
add ax,ax
adc ax,3
执行后(ax)=5,相当于执行(ax)+3+CF=2+3+0=5
mov al,98H
add al,al
adx al,3
执行后 (al)=34H,相当于执行(ax)+3+CF=30H+3+1=34H
由adc指令前面的指令,决定在执行adc指令的时候加上的CF的值的含义。也就是说关键在于所加上的CF值是被什么指令设置的。
1、进位值
如果是被add指令设置的,那么它的含义就是进位值。
加法运算分两步进行:
①低位相加
②高位相加加上低位相加产生的进位值。
CPU提供adc指令的目的,就是来进行加法的第二步运算的。adc指令和add指令相配合就可以对更大的数据进行加法运算。
2、借位值
如果CF的值是被sub指令设置的,那么它的含义就是借位值;
编程:计算1EF000H+201000H,结果存放在AX(高16位)和BX(低16位)中。
mov ax,001EH
mov bx,0F000H
add bx,1000H
adc ax,0020H
;编程:1EF0001000H+2010001EF0H,结果存放在AX(高16位)、BX(次16位)中和cx(低16位)。
计算分三步进行:
(1)先将低16位相加,完成后,CF中记录本次相加的进位值。
(2)再将次高16位和CF(来自低16位的进位值)相加,完成后,CF中记录本次相加的进位值。
(3)最后高16位和CF(来自次高16位的进位值)相加,完成后,CF中记录本次相加的进位值。
mov ax,001EH
mov bx,0F000H
mov cx,1000H
add cx,1EF0H
add bx,1000H
adc ax,0020H
编程:对两个128位数据进行相加
assume cs:code,ds:data
data segment
db 16 dup(88H)
db 16 dup(11H)
data ends
code segment
start:
mov ax,data
mov ds,ax
mov si,0
mov di,16
mov cx,8
call add128
mov ax,4C00H
int 21H
add128:
push ax
push cx
push si
push di
sub ax,ax;将CF设置为0
s:
mov ax,[si]
adc ax,[di]
mov [si],ax
inc si;不能用add si,2代替
inc si;因为会影响cf位
inc di;而loop和inc不会影响
inc di
loop s
pop di
pop si
pop cx
pop ax
ret
code ends
end start
# 1.2 sbb指令
sbb是带借位减法指令,利用了CF位上记录的借位值。
格式:sbb 操作对象1,操作对象2
功能:操作对象1=操作对象1-操作对象2-CF
利用sbb指令我们可以对任意大的数据进行减法运算。
sbb和adc是基于同样的思想设计的两条指令,在应用思路上sbb和adc类似。
编程:计算003E1000H-00202000H,结果放在ax,bx中
mov bx,1000H
mov ax,003EH
sub bx,2000H
sbb ax,0020H
# 1.3 cmp指令
cmp是比较指令,功能上相当于减法指令,只是不保存结果。
格式:cmp 操作对象1,操作对象2
功能:计算操作对象1-操作对象2但不保存结果,仅仅是根据计算结果对标志寄存器进行设置。
cmp指令运算执行后通过做减法将对标志寄存器产生影响,其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。
cmp ax,ax
;执行后结果为0,ZF=1,PF=1,SF=0,CF=0,OF=0
mov ax,8
mov bx,3
cmp ax,bx
;执行后ax、bx的值不变,ZF=0,PF=1,SF=0,CF=0,OF=0
;通过cmp指令执行后,相关标志位的值就可以看出比较的结果。
;指令"cmp ax,bx”的逻辑含义是比较ax和bx中的值
判断结果大小
指令"cmp ax,bx”的逻辑含义是比较ax和bx中的值,如果执行后:
cmp ax,bx 比较后的标志状态 | 含义 |
---|---|
ZF=1 | ax = bx |
ZF=0 | ax != bx |
CF=1 | ax < bx |
CF=0 | ax >= bx |
CF=0 & ZF=0 | ax > bx |
CF=1 & ZF=1 | ax <= bx |
判断结果正负
⚠️ sf记录的是ah中的8位二进制信息所表示的数据的正负, 而cmp并不修改寄存器的值,单纯地考察SF的值不可能知道结果的正负
OF | 0: 没有溢出,1:溢出 |
---|---|
SF | 1:表示负数,0 正数 |
OF=0 & SF=1 | (ah)<(bh) |
OF=0 & SF=0 | (ah)≥(bh) |
OF=1 & SF=1 | (ah)>(bh) |
OF=1 & SF=0 | (ah)<(bh) |
1、无溢出时通过sf标记可判断结果的正负
如果SF=1或SF=0,OF=0,逻辑上真正结果的正负=实际结果的正负。
of=0,说明没有溢出,逻辑上真正结果的正负=实际结果的正负;
若sf=1,实际结果为负,所以逻辑上真正的结果为负,所以(ah)<(bh)
若sf=0,实际结果非负,所以逻辑上真正的结果非负,所以(ah)≥(bh)
2、有溢出时通过sf标记 判断结果的**正负****
如果SF=1或SF=0,OF=1,逻辑上真正结果的负正≠实际结果的正负。
of=1,说明有溢出,逻辑上真正结果的正负≠实际结果的正负;
若sf=1,实际结果为负,而又有溢出,这说明是由于溢出导致了实际结果为负,简单分析一下,就可以看出,如果因为溢出导致了实际结果为负,那么逻辑上真正的结果必然为正,则说明(ah)>(bh)。
若sf=0,实际结果非负,而of=1说明有溢出,则结果非0,所以,实际结果为正。实际结果为正,而又有溢出,这说明是由于溢出导致了导致了实际结果非负,简单分析一下就可以看出,如果因为溢出导致了实际结果为正,那么逻辑上真正的结果必然为负,则说明(ah)<(bh)。
# 1.4 条件转移指令
条件转移指令需根据比较指令、并配合标志综合使用,
比如与cmp相配使用,根据cmp指令的比较结果(cmp指令执行后相关标志位的值)进行工作的指令。
cmp指令可以同时进行两种比较,无符号数比较和有符号数比较,所以根据cmp指令的比较结果进行转移的指令也分为两种:
1、无符号比较
根据无符号数的比较结果进行转移的条件转移指令,它们检测ZF、CF的值;
2、有符号比较
根据有符号数的比较结果进行转移的条件转移指令,它们检测SF、OF、ZF的值。
跳转指令 | 跳转条件 | 标志位 |
---|---|---|
JE | = | ZF=1 |
JNE | != | ZF=0 |
JB | < | CF=1 |
JNB | >= | CF=0 |
JA | > | CF=0 & ZF=0 |
JNA | <= | CF=1 & ZF=1 |
;编程:如果(ah)=(bh)则(ah)=(ah)+(ah),否则(ah)=(ah)+(bh)。
cmp ah,bh
je s ;ZF=1则跳转
add ah,bh
jmp short ok
s: add ah,bh
ok: ret
;je检测的是ZF的位置,不管je前面是什么指令,只要CPU执行je指令时,ZF=1那么就发生转移。
mov ax,0
mov ax,0
je s
inc ax
s:
inc ax
;执行后(ax)=1,add ax,0使得ZF=1,所以je指令将进行转移。
编程:统计data段中数值为8的字节的个数,用ax保存统计结果。
方案一
assume cs:code
data segment
db 8,11,8,1,8,5,63,38
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,0;ds:bx指向第一个字节
mov ax,0;初始化累加器
mov cx,0
s:
cmp byte ptr [bx],8;和8进行比较
jne next;如果不相等转到next,继续循环
inc ax;如果相等就计数值加1
next:
inc bx
loop s;执行后:(ax)=3
mov ax,4c00h
int 21h
code ends
end segment
方案二
assume cs:code
data segment
db 8,11,8,1,8,5,63,38
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,0;ds:bx指向第一个字节
mov ax,0;初始化累加器
mov cx,0
s:
cmp byte ptr [bx],8;和8进行比较
je ok;如果不相等转到ok,继续循环
jmp short next;如果不想等就转到next,继续循环
ok:
inc ax;如果相等就计数值加1
next:
inc bx
loop s;执行后:(ax)=3
mov ax,4c00h
int 21h
code ends
end segment
编程:统计data段中数值大于8的字节的个数,用ax保存统计结果。
assume cs:code
data segment
db 8,11,8,1,8,5,63,38
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,0;ds:bx指向第一个字节
mov ax,0;初始化累加器
mov cx,0
s:
cmp byte ptr [bx],8;和8进行比较
jna next;如果大于8转到next,继续循环
inc ax;如果大于就计数值加1
next:
inc bx
loop s;执行后:(ax)=3
mov ax,4c00h
int 21h
code ends
end segment
# 1.5 串传送指令
1、DF标志:方向标志位
在串处理指令中,控制每次操作后si(一般指向原始偏移地址)、di(一般指向目标偏移地址)的增减。
DF=0:每次操作后si、di递增;
DF=1:每次操作后so、di递减。
2、movsb串传送指令 movsb(mov string byte)串传送指令
以字节为单位传送
格式:movsb
执行movsb相当于以下操作:
(1)((es)*16+(di))=((ds)*16+(si))
(2)如果DF=0,则(si)=(si)+1,(di)=(di)+1;
如果DF=1,则(si)=(si)-1,(di)=(di)-1。
movsb功能:将ds:si指向的内存单元中的字节送入es:di中,然后根据标志寄存器DF位的值将si和di递增1或递减1。
2、movsw串传送指令
以字为单位传送。
将ds:si指向的内存单元中的字送入es:di中,然后根据标志寄存器DF位的值将si和di递增2或递减2。
movsb和movsw进行的是串传送操作中的一个步骤,一般和rep配合使用,格式:rep movsb,rep的作用是根据cx 的值,重复执行后面的串传送指令。由于每执行一次movsb指令si和di都会递增或递减指向后一个单元或前个单元,则rep movsb就可以循环实现(cx)个字符的传送。
使用串传送指令进行数据的传送,需要提供:
1、传送的原始位置;
2、传送的目的位置;
3、传送的长度;
4、传送的方向。
由于flag的DF位决定着串传送指令执行后,si和di改变的方向,8086CPU提供两条指令对DF位进行设置:
(1)cld指令:将标志寄存器的DF位设置为0;
(2)std指令:将标志寄存器的DF位设置为1。
# 1.6 pushf和popf指令
pushf的功能是将标志寄存器的值压栈,
popf是从栈中弹出数据,送入标志寄存器中。
pushf和popf为直接访问标志寄存器提供了一种方法。
下面的程序执行后ax的值是多少?
mov ax,0
push ax
popf
mov ax,0fff0h
add ax,0010h
pushf
pop ax
and al,11000101b
and ah,00001000b
# 1.7 Debug
标志寄存器在Debug中的表示
标志 | 值为1的标记 | 值为。的标记 |
---|---|---|
OF | OV | NV |
SF | NG | PL |
ZF | ZR | NZ |
PF | PE | PO |
CF | CY | NC |
DF | DN | UP |
# 字符串
可以使用 section .data 显式存储字符串,
或者 section .bss 段定义字符串变量再赋值
# 字符串
我们可以使用表示位置计数器当前值的$位置计数器符号来显式存储字符串长度
msg db 'Hello, world!',0xa ;我们常见的字符
len equ $ - msg ;长度
;$指向字符串变量msg的最后一个字符之后的字节。因此,$ - msg给出字符串的长度
# 字符串指令
对于16位地址,使用SI和DI寄存器,对于32位地址,使用ESI和EDI寄存器。
下表提供了各种版本的字符串指令和假定的操作数空间:
基本指令 | 操作的寄存器 | 字节运算 | 字运算 | 双字运算 | 说明 |
---|---|---|---|---|---|
MOVS | ES:DI,DS:SI | MOVSB | MOVSW | MOVSD | 该指令将1字节,字或双字数据从存储位置移到另一个位置 |
LODS | DS:SI | LODSB | LODSW | LODSD | 该指令从存储器加载。如果操作数是一个字节,则将其加载到AL寄存器中;如果操作数是一个字,则将其加载到AX寄存器中,并将双字加载到EAX寄存器中 |
STOS | ES:DI,AX | STOSB | STOS | STOSD | 该指令将数据从寄存器(AL,AX或EAX)存储到存储器 |
CMPS | DS:SI,ES:DI | CMPSB | CMPSW | CMPSD | 该指令比较内存中的两个数据项。数据可以是字节大小,字或双字 |
SCAS | ES:DI,AX | SCASB | SCASW | SCASD | 该指令将寄存器(AL,AX或EAX)的内容与内存中项目的内容进行比较 |
下面介绍一些会用到的指令
# 标志位重置指令
CLD | 将方向标志位DF清零 |
---|---|
STD | 将方向标志位DF置1 |
# REP重复执行指令
1、REP(Repeat)汇编指令是一种重复执行指令的控制指令,它通常和其他指令组合使用,用于在处理字符串或数组时进行重复操作。REP指令可以和多个其他指令搭配使用,如MOV、STOS等。其语法如下:
rep instruction ;比如 rep MOVSB
其中,instruction是要重复执行的指令。
2、REP指令会将ECX寄存器中的值作为计数器,循环执行instruction指定的操作。
每次循环都会将ECX减1,直到ECX的值为0为止(类似loop)
3、对于不同的操作码,REP有三种形式:
REPE、REP | Repeat while Equal | 比较结果相同时继续执行,不相同退出循环 |
---|---|---|
REPNE、REPN | Repeat while Not Equal | 比较结果不相同时继续执行,相同退出循环 |
REPZ | Repeat while Zero | 结果 =0 时继续执行,结果 !=0 退出循环 |
# MOVS指令
MOVS指令用于将数据项(字节,字或双字)从源字符串SI 到目标字符串DI :
section .text
global _start
_start:
mov ecx, len ;s1长度
mov esi, s1 ;将s1的字符串送入 si 寄存器中
mov edi, s2 ;开辟 s2空间并指定到 di 寄存器
cld ;方向标志位DF清零
rep movsb ;源字符串SI 送到目标字符串DI
mov edx,20 ;s2长度
mov ecx,s2 ;s2信息
mov ebx,1 ;stdout
mov eax,4 ;sys_write
int 0x80 ;call
mov eax,1 ;system call number (sys_exit)
int 0x80 ;call kernel
section .data
s1 db 'Hello, world!'
len equ $-s1
section .bss
s2 resb 20
结果:
Hello, world!
# LODS指令
该指令从存储器加载。如果操作数是一个字节,则将其加载到AL寄存器中;如果操作数是一个字,则将其加载到AX寄存器中,并将双字加载到EAX寄存器中
示例:
将secret加密显示,加密方式:单字节增加2位
section .text
global _start
_start:
mov ecx, 6 ;secret字符串长度,同时也是loop循环的次数
mov esi, s1 ;将s1的字符串送入 si 寄存器中
mov edi, s2 ;开辟 s2空间并指定到 di 寄存器
loop_encrypt: ;加密(循环6次)
lodsb ;将si中的'secret'加载到AL(AX或EAX)寄存器
add al, 02 ;al内容偏移2(混淆)
stosb ;该指令将数据从寄存器AL(AX或EAX)存储到存储器
loop loop_encrypt
cld ;方向标志位DF清零
rep movsb ;源字符串SI 送到目标字符串DI
mov edx,20
mov ecx,s2
mov ebx,1
mov eax,4
int 0x80
mov eax,1
int 0x80
section .data
s1 db 'secret key'
len equ $-s1
section .bss
s2 resb 10
结果:
ugetgv
同样如果想要将密文ugetgv解密,转换片段:
encrypt_code: ;解密(循环6次)
lodsb ;将si中的密文'ugetgv'加载到AL(AX或EAX)寄存器
sub al, 02 ;al内容偏移2(还原)
stosb ;该指令将数据从寄存器AL(AX或EAX)存储到存储器
loop encrypt_code
结果还原为'secret'
# STOS指令
STOS指令将数据项从AL(对于字节-STOSB),AX(对于字-STOSW)或EAX(对于双字-STOSD)到目标字符串,该目标字符串由内存中的ES:DI指向。
上面示例已经使用了stosb
将大写字符串转换为其小写值转换片段:
lower_case:
lodsb
or al, 20h
stosb
loop lower_case
cld
rep movsb
# REP、REPE 重复指令
REP前缀在字符串指令(例如-REP MOVSB)之前设置时,会根据放置在CX寄存器中的计数器使该指令重复。REP执行该指令,将CX减1,然后检查CX是否为零。重复指令处理,直到CX为零为止。
方向标志(DF)确定操作的方向。
- 使用CLD(清除方向标志,DF = 0)使操作从左到右。
- 使用STD(设置方向标志,DF = 1)使操作从右到左。
REP前缀也有以下变化:
- REP:这是无条件的重复。重复该操作,直到CX为零为止。
- REPE或REPZ:这是有条件的重复。当零标志指示等于/零时,它将重复操作。当ZF表示不等于零或CX为零时,它将停止。
- REPNE或REPNZ:这也是有条件的重复。当零标志指示不等于/零时,它将重复操作。当ZF指示等于/零或CX减为零时,它将停止。
# CMPS指令
CMPS指令:
比较两个字符串。该指令比较DS:SI和ES:DI寄存器所指向的两个数据项,即一个字节,一个字或一个双字,并相应地设置标志。
REPE:
比较结果相同时继续执行,不相同退出循环,
1、循环次数同loop使用cx寄存器值
2、不同的是,cx递减到0前满足退出循环条件的时候就结束了,不会继续循环,比loop 损耗小
JECXZ:
根据 CX、ECX 寄存器的值跳转: JCXZ(CX 为 0 则跳转)、JECXZ(ECX 为 0 则跳转);
section .text
global _start
_start:
mov esi, s1
mov edi, s2
mov ecx, lens2
cld
repe cmpsb ;重复执行cmpsb命令,直到 cx=0或者 比较结果不相同 结束
jecxz equal ;CX 为 0 则跳转
jmp nequal
nequal:
;不相同时逻辑
mov eax, 4
mov ebx, 1
mov ecx, msg_neq
mov edx, len_neq
int 80h
jmp exit
equal: ;相同时逻辑
mov eax, 4
mov ebx, 1
mov ecx, msg_eq
mov edx, len_eq
int 80h
jmp exit
exit: ;退出
mov eax, 1
mov ebx, 0
int 80h
section .data
s1 db 'Hello, world!',0 ;源字符
lens1 equ $-s1
s2 db 'Hello, world!', 0 ;比较字符
lens2 equ $-s2
msg_eq db '相同的字符串!', 0xa
len_eq equ $-msg_eq
msg_neq db '不相同的字符串!'
len_neq equ $-msg_neq
结果
相同的字符串!
# SCAS指令
SCAS指令用于搜索字符串中的特定字符或一组字符。要搜索的数据项应该在AL(对于SCASB),AX(对于SCASW)或EAX(对于SCASD)寄存器中。要搜索的字符串应在内存中,并由ES:DI(或EDI)寄存器指向。查看以下程序以了解概念-
section .text
global _start ;must be declared for using gcc
_start: ;tell linker entry point
mov ecx,len
mov edi,my_string
mov al , 'e'
cld
repne scasb
je found ; when found
; If not not then the following code
mov eax,4
mov ebx,1
mov ecx,msg_notfound
mov edx,len_notfound
int 80h
jmp exit
found:
mov eax,4
mov ebx,1
mov ecx,msg_found
mov edx,len_found
int 80h
exit:
mov eax,1
mov ebx,0
int 80h
section .data
my_string db 'hello world', 0
len equ $-my_string
msg_found db '包含该字符串!', 0xa
len_found equ $-msg_found
msg_notfound db '不包含该字符串!'
len_notfound equ $-msg_notfound
结果-:
包含该字符串!
# 数组
C++语法中数组示例:
int marks[6] = {34, 45, 56, 67, 75, 89};
# 数组
我们已经讨论过,汇编程序的数据定义指令用于为变量分配存储空间。变量也可以用一些特定的值初始化。初始化值可以以十六进制,十进制或二进制形式指定。例如,我们可以通过以下两种方式之一来定义单词变量“months”-
MONTHS DW 12 ;十进制表示12月
MONTHS DW 0CH ;十六进制表示12月
MONTHS DW 0110B ;二进制表示12月
1、连续数据定义
分配连续内存空间,初始化值0
List1 DW 0, 0 , 0 , 0 , 0 , 0
定义一维数字数组(连续的相同类型)。
List1 DW 34, 45, 56, 67, 75, 89
;分配了2x6 = 12个字节的连续存储空间
定义相同内容时可以使用 times
List1 TIMES 6 DW 0
定义6个8
List1 TIMES 6 DW 8
2、访问数据
第一个数字的符号地址为List1,第二个数字的符号地址为List1 + 2,依此类推。
比如访问 56
[List1 + 4]
访问第二个名称:
section .text
global _start ;must be declared for linker (ld)
_start:
mov edx,4 ;message length
mov ecx, names+4 ;message to write
mov ebx,1 ;file descriptor (stdout)
mov eax,4 ;system call number (sys_write)
int 0x80
mov eax,1 ;system call number (sys_exit)
int 0x80 ;call kernel
section .data
global names
names:
DW 'Lucy', 'Andy', 'Pete'
结果
Andy
# 堆栈
内存结构:
堆栈是内存中类似数组的连续内存数据结构,通常会固定一段存储空间。
可以在其中存储数据并从称为“堆栈顶部”的位置删除数据。
入栈:
需要存储的数据被“推送”到堆栈中
出栈:
要检索的数据从堆栈中“弹出”。
LIFO:
堆栈是一种LIFO(后进先出)数据结构,即首先存储的数据最后被检索。
PUSH和POP:
汇编语言为堆栈操作提供了两条指令:PUSH和POP。
语法如下:
PUSH operand
POP address/register
堆栈段中保留的内存空间用于实现堆栈。寄存器SS和ESP(或SP)用于实现堆栈。
SS栈顶指针寄存器:
指向堆栈段的开头,也就是起始地址,通常是最高位开始
SP(或ESP)偏移量:
SS起始地址上,再偏移SP的偏移量,就是栈顶的位置,也就是最后存储的一条数据位置。
比如:
从1-5将数据存储到堆栈(0x01-0x09)的存储空间上:
堆栈示意地址 | 插入的数据 | 位置 |
---|---|---|
0x09 | 1 | SS:存放栈顶的段地址(最高位为起始地址) |
0x08 | 2 | |
0x07 | 3 | |
0x06 | 4 | |
0x05 | 5 | SS: SP:sp偏移量结合的位置为栈顶(最后数据5的位置) |
0x04 | ||
0x03 | ||
0x02 | ||
0x01 |
pop的顺序依次是:5,4,3,2,1 ,和push是相反的
- 只能将字或双字保存到堆栈中,而不是字节。
- 堆栈朝反方向增长,即朝着较低的存储器地址增长
- 堆栈的顶部指向插入堆栈中的最后一个项目。它指向插入的最后一个字的低字节。
使用场景,比如:
MOV AX, 1
MOV BX, 2
; 将AX和BX寄存器内容保存在堆栈中
PUSH AX
PUSH BX
;将寄存器用着其他用途
MOV AX, 3
MOV BX, 4
...
;使用完之后恢复寄存器原始值,最后push的BX要最先pop才行。
POP BX ;BX=2
POP AX ;AX=1
以下程序显示整个ASCII字符集。主程序调用一个名为display的过程,该过程显示ASCII字符集。
section .text
global _start
_start:
;call 指令表示执行一段代码,需要ret 配合返回并继续执行后续代码
call init_count ;从'5' 开始递增打印20个ASCII字符
call next ;循环打印
call exit
init_count:
mov ecx, 20
ret
next:
push ecx
call display ;调用显示代码块
pop ecx
mov dx, [achar]
cmp byte [achar], 0dh
inc byte [achar]
loop next
ret
display:
mov eax, 4
mov ebx, 1
mov ecx, achar
mov edx, 1
int 80h
ret
exit:
mov eax,1 ;system call number (sys_exit)
int 0x80 ;call kernel
section .data
achar db '5'
结果:
56789:;<=>?@ABCDEFGH
# 递归
递归过程是一个调用自身的过程。
上一个示例中我们使用到了 call 指令; call 指令表示执行一段代码,需要ret 配合返回并继续执行后续代码
# 递归
示例:直接递归(方法内调用自己)
3阶乘c示意代码:
int a=3,b=3;
fun mul_fun {
if (b <= 1) return; // 结束
b--;
a = a*b;
mul_fun();
}
汇编示例:
section .text
global _start
_start:
mov bx, 3
mov ax, 3
call mul_fun
mul_fun: ;阶乘递归
cmp bl, 1
JLE p_total ;<= 1时结束,跳转到打印方法
dec bl
mul bl ;ax = al * bl
call mul_fun
p_total: ;打印
add ax, 30h
mov [fact], ax
mov edx,1 ;message length
mov ecx,fact ;message to write
mov ebx,1 ;file descriptor (stdout)
mov eax,4 ;system call number (sys_write)
int 0x80 ;call kernel
mov eax,1 ;system call number (sys_exit)
int 0x80 ;call kernel
section .bss
fact resb 1
结果:
6