and和or是逻辑指令
and的功能是按位与(有没有想起子网掩码,就是那个!)
and al, 11111110B可以将al中的其他位不变,最低位(第0位)设为0or的功能是按位或
or al, 00000001B可以将al中的其他位不变,最低位(第0位)设为1逻辑是什么?逻辑就是0和1,就是false和true。对于二进制数来说,只有两种用途。一种是由多个二进制数组合,其映射的数据或指令。一种是由单个二进制数,其直接反应的逻辑。 补充: ASCII就是其中一种社会普遍认可的映射(编码)方案。记住这三个:48 -> 0、65 -> A、96 -> a,这三个是十进制数,对应的16进制数是30H、41H、61H,是不是更好记了呢?
屏幕上打字符a经历了什么: (假设是ASCII编码的)
敲下一个字符,被CPU按ASCII码编码为30H这个30H存储在内存中文本编辑软件从内存中取出这个30H文本编辑器把30H送到显存中显卡按照ASCII解码30H,屏幕上出现a在汇编程序中用单引号'包住的内容,为字符。编译器 ,会将它们转化成对应的ASCII码
🌰 db '0ab'相当于db 30H, 41H, 61H 🌰 mov al, 'a'相当于mov al, 41H
怎么装换大小写?
实际上就是ASCII转换的问题,先对比一下ASCII码十进制相差32,二进制相差10,0000B,十六进制相差20HA的二进制0100,0001B,A的十六进制41H a的二进制 0110,0001B,a的十六进制61H可以注意到,二进制把第五位从0变1就可以变为小写注意: 编译器只认ASCII编码
全部转换为大写: and al, 11011111B(第五位变为0)全部转换为小写: or al, 00100000B(第五位变为1)用法: 三种皆可(假设idata为200)
mov ax, [bx + 200]mov ax, 200[bx]mov ax, [bx].200汇编&C 实现一个数组:
汇编数组定位方式: 0[bx],5[bx]C数组定位方式: a[偏移地址],b[偏移地址][bx + idata] 为高级语言使用数组提供了依据。
S是source来源的缩写;d是direction目地的缩写
利用SI和DI,把源数组复制到目标数组:
注意: 一次性传一个字,顺序不会乱掉嘛?以第0个字 He 为例。
先从内存写入寄存器:ah = e,al = H再从寄存器写入内存:[0] = al,[1] = ah,最后内存里为He。顺序无误更多用法:
[SI + idata]/[DI + idata][bx + SI]/[bx + DI],也可以写成[bx][SI]/[bx][DI][bx + SI + idata]/[bx + DI + idata],也可以写成其它形式,常数在前直接写,常数在后加个点,寄存器加括号。如5[bx][SI]作用:
DS:[idata]直接定位内存单元[bx]间接定位内存单元idata[bx]在一个偏移的基础上间接定位内存单元,多用于划定一个连续的内存空间(数组)可以用来暂时存放cs的值的寄存器:dx、di、es、ss、sp、bp。那么问题来了,这些寄存器如果都被使用了怎么办?
诚然,程序简单的时候,可以使用寄存器来暂存。但是,我们应该寻求一个通用的解决办法。这个解决办法就是内存2的程序改进为: 注意看红色改动地总结:
我们用bx来索引数据行(在内存中是一条线,只是我们人为这样认为)我们用idata来索引列偏转我们用SI来灵活取内存栈常用来临时存放数据为要处理的数据设计一个清晰的数据结构,是程序设计里的一个关键问题
数据处理两大基本问题:数据地址、数据类型
描述性符号: reg寄存器,sreg段寄存器
regsregax 、bx 、cx 、dxds 、ss、 cs 、essp 、bp 、si 、di偏移地址四小龙: bx、bp、si、di
只有bx、bp、si、di可以用在[]中进行内存单元的寻址它们可以单独使用或以下四种组合:bx si 、bx di 、bp si 、bp di其中,bx默认段地址是DS数据段,bp默认段地址是SS栈段数 据 处 理 = 读 取 + 运 算 + 写 入 数据处理 = 读取 + 运算 + 写入 数据处理=读取+运算+写入
站在机器指令的这个层面,它不关心数据的值是多少(指令执行前,都还没读取呢),只关心数据的位置所要处理的数据可以在三个地方:CPU内部、内存、端口 常量(立即数)放在指令缓冲器里 1. 立即数: 在汇编指令中直接给出 2. 寄存器: 在汇编指令中给出寄存器名 3. 段地址SA + 偏移地址EA: 在汇编指令中给出[偏移地址],SA在某个段寄存器中所有寻址方式:
补充: 字符串的底层是字符数组
数据的类型: 字or字节
通过寄存器名指定是字还是字节:mov ax, [0]表示使用的是字;mov al, [0]表示使用的是字节用X ptr指明内存单元长度,X可以为word或byte。如mov word ptr DS:[0], 1;有些指令默认了数据类型:push和pop只进行字操作计算:
除数被除数8位16位 ax16位32位 dx + ax原理: 为什么被除数要比除数大呢?因为怕溢出。 计算机只会做加法。减法就是使用了补码。乘法就是进行多次加法。除法就是进行多次减法,循环的次数就是商,剩下的就是余数。
用法: div 寄存器、div 内存单元
结果:
运算商余数8位ALAH16位AXDX示例:
div byte ptr DS:[0]: (al) = (ax) / ((DS)*16 + 0)的商 (ah) = (ax) / ((DS)*16 + 0)的余数div word ptr DS:[0]: (ax) = [(dx)*10000H + (ax)] / ((DS)*16 + 0)的商 (dx) = [(dx)*10000H + (ax)] / ((DS)*16 + 0)的余数计算: 100001 / 100的值
100001的十六进制是186A1,所以要用32位,变为0001 86A1H除数为16位,用dx + ax直接在Debug程序中写程序
注意: 如果和我一样,碰到除数和被除数总要愣一下,可以直接说分子分母。先指定ax和dx,它们存的是分子;div后面跟的是分母。
dd用来定义dword(双字型)数据的。
伪指令英文dbdefine bytedwdefine worddddefine dword(double word)dup: dup是由编译器识别执行的操作符。和db、dw、dd配合使用,进行数据的重复
使用:
db 重复次数 (重复的字节型数据)dw 重复次数 (重复的字型数据)dd 重复次数 (重复的双字型数据)举例:
db 3 dup(0)相当于db 0, 0, 0db 3 dup(0, 1, 2)相当于db 0, 1, 2, 0, 1, 2, 0, 1, 2db 3 dup('abc', 'ABC')相当于db 'abcABCabcABCabcABC'此实验用到了所学的几乎一切知识,请独立完成该实验 数据如下(对着书手打的)
datasg segment ; 每行8个 ; 年份 db ‘1975’, ‘1976’, ‘1977’, ‘1978’, ‘1979’, ‘1980, ‘1981’, ‘1982’ db ‘1982’, ‘1983’, ‘1984’, ‘1985’, ‘1986’, ‘1987’, ‘1988’, ‘1989’ db ‘1990’, ‘1991’, ‘1992’, ‘1993’, ‘1994’, ‘1995’ ; 总收入 dd 16, 22, 382, 1356, 2390, 8000, 16000, 24486 dd 50065, 97479, 140417, 197514, 345980, 590827, 803530, 1183000 dd 1843000, 2759000, 3753000, 464900, 5937000 ; 雇员人数 dw 3, 7, 9, 13, 28, 38, 130, 220 dw 476, 778, 1001, 1442, 2258, 2793, 4037, 5635 dw 8226, 11542, 14430, 15257, 17800 datasg ends table segment db 21 dup('year summ ne av ') ; 定义一个表格 table ends下面我们分模块分析下: ( 初始化代码: 先把段地址设置好。观察年份,判断肯定需要内循环,所以需要一个栈来临时存放数据
先分析一波收入,最低的16(10H),和最高的5937000(5A9768H)所占内存单元情况。
567810H68H97H5AH其次,在DS段中,bx应该从 4*21 开始。 雇员和收入的写法很类似。
补充: 为什么收入16要用双字?如果不用双字就不方便索引,用了双字就浪费空间。这就是时间复杂度和空间复杂度的冲突,当两者冲突时,只要不是特别耗费空间,选择时间复杂度小的,因为现在存储都不是什么大问题。
这个时候,表格就已经差不多完成了,可以直接用表格里的数据写。
人均收入完整程序改进点: 这个程序有点复杂,复杂点在于寄存器的复用。为什么复用?因为寄存器不够!可以使用那段安全的内存空间,做缓存temp,从而不用总是复用寄存器。
8086转移指令的分类:
无条件转移指令: 如jmp条件转移指令: 类似c语言的分支语句循环指令: 如loop过程: 类似c语言的函数中断: 后来演变为通信机制,为多任务提供的依据伪指令offset: 用来取得标号的偏移地址
mov ax, offset temple取得temple标号处的偏移地址jmp为无条件转移,可以只修改IP,也可以同时修改CS/IP jmp指令要给出两种信息 (1) 转移的目的地址 (2) 转移的距离(段间转移、段内短转移、段内近转移)
注意: 我们在之前第二章使用的jmp 2000:1000,这种格式,汇编是不认识的,这是debug程序的语法
jmp short 标号
它对IP的修改范围为 -128~127负数往前跳,正数往后跳,零不跳补充: 汇编指令中的idata立即数,无论他是表示一个数据,还是表示一个内存单元的偏移地址,都直接出现在对应的机器指令中。因为CPU执行的是机器指令,它必须要处理这些数据或地址。
可以看见,划红线的地方,记录了地址,可是看看蓝线处,机器码不包含这个立即数!那么意味着,CPU在执行这句代码时,并不知道要跳转到哪
EB03后面的03指的是一个偏移地址(相对于这句代码的偏移地址)jmp short s的执行过程:
CS:IP指向jmp short s对应的机器码读取指令EB03进指令缓冲器因为IP读取了该指令了,所以IP += 2指向mov ax, 0001CPU执行指令缓冲器中的指令EB03EB03执行后,IP += 3,此时IP指向inc ax补码: 如果是负数,就用到了补码(事实上,计算机内部无论正负都以补码进行计算)。
计算: 正数原码补码反码一致。负数反码=原码位翻转;补码=反码+1。 原理: 利用的是正负数相加等于零,和溢出两个原理。 例子: 1的补码是0000 0001,-1的反码是1111 1110,补码是1111 1111。 0000 0001+1111 1111=1 0000 0000,然后高位溢出,得到0000 0000。+1+-1可不就是0嘛。 补充: 补码就是计算机拿加法代替减法的原理。上述是8位计算机的结果,如果是16位,那么-1的补码为1111 1111 1111 1111
jmp short 标号短转移 的作用是(IP) += 8位位移
8位位移的范围为-128~127,用补码表示jmp near ptr 标号近转移 的作用是(IP) += 16位位移
8位位移的范围为-32768~32767,用补码表示可以看到,其它地方都与短转移一致,就是多了一个nop占位语句。
我们在之前使用jmp ax就是段内的短转移和近转移(区别是转移的有多远),不过是通过寄存器为媒介传递IP的值
jmp far ptr 标号远转移 ,又称段间转移
前面的短转移、近转移,都是给出一个位移。这里的远转移,需要给出一个明确的段地址远转移功能:
(CS) = 标号的段地址(IP) = 标号的偏移地址可以发现,多了几个NOP占位符外,也是使用位移的,与书上不一致 。查阅书得知,原书用db 100H dup(0)来进行占位,以便与标号处于不同的段。
补充: 阅读机器码可知,EA表示远转移,EB表示近/短转移。可以发现,当隔的不够远时,默认在同一段,是EB。或者换言之,EA是段间转移,EB是段内转移。
格式1: jmp word ptr 内存单元地址
因为是字型数据(16位),所以是近转移内存单元地址,可用寻址方式的任意一种给出格式2: jmp dword ptr 内存单元地址
因为是双字型数据(32位),所以是远转移内存单元地址,可用寻址方式的任意一种给出低位是IP,高位是CSjcxj是有条件转移指令,所有的有条件跳转指令都是短转移 (之前学过的loop也是有条件转移指令)
格式: jcxz 标号用法: 如果(cx) = 0则跳转到标号处执行 如果(cx) != 0则直接向下执行类比c: jcxz 标号相当于if(cx == 0) {jmp short 标号};loop和jcxz用法一致,不同的是,(cx)=0时,是跳出循环(跳到标号处)
使用位移的意义:
方便了程序在内存中的浮动装配如果直接给定了地址,那么就对程序有了严格的限制有利于封装调用分析: 开头的三行代码,设置:DS:[0] 然后是要我们写的四行代码 接下来的jmp跳回我们写的代码,可知,我们写的代码在一个死循环内部 再下面是赋值语句,可以达成我们的要求
所以: 我们要在这个死循环内部,写一个跳出条件,当满足某些条件时,设置cx的值为0 我们需要得到数为x的,那么设(cx)=x,然后sub cx, 内存单元的值,如果(cx)=0,
答案: mov cx, 0 sub cl, [bx] jcxz ok inc bx
记录一下这个死循环格式
s: 死循环体 ; 里面要设置cx的值 jcxz ok ; 跳出循环 jmp short s ; 无条件跳转,相当于while thue ok: 后续代码相当于
while(True) { 死循环体; // 里面要设置cx的值 if(cx == 0) { break; // 跳出循环 } }注意: inc和dec操作相反,一个是自增,另一个是自减
答案: inc cx 分析: 我们注意到,当这个程序执行到loop s时,(cx)-1不是我们想要的值,它被多减了1,所以要加回来
实验8:
先看end后面的标号,是start先从start开始一步步执行,来到s倒数两步倒数两步的意思是把jmp short s1这句代码,放到s中的nop处然后执行下一条语句s0,跳转回s,然后执行s中的jmp short s1按理来说,程序不会正确返回 下面我们运行一下一次次t到第四步,查看语句 发现下一个语句是jmp 0000H,仔细分析原因,原来是位移搞得鬼。jmp语句跳转用的方法是让(IP)-8到s1,但执行这条语句的是s,于是(IP)-8 == 0了。可以发现,都是向上8个内存单元
实验9:
请独立思考哦,本人从看题到完成代码约一个半小时,还是很耗时间的,要有点耐心哦!请独立完成!
代码: 我写的代码充分使用了寄存器,所以会很简短。网上的方法大多具有复杂的循环嵌套,没有标准答案。我愿意牺牲空间内存来换取编写运行的效率。- 结果:call和ret都是转移指令,它们都修改IP,或者修改CS/IP
ret指令,用栈中的数据,修改IP的内容,实现近转移。
CPU执行ret指令时,进行下面两步操作: (从栈顶读一个字数据,这个数据的值,赋值给IP)
(IP) = ((SS)*16 + SP)(SP) += 2相当于:pop IP
retf指令,用栈中的数据,修改CS/IP的内容,实现远转移。
CPU执行retf指令时,进行下面四步操作: (从栈顶读一个字数据,这个数据的值,赋值给IP。从栈顶读一个字数据,这个数据的值,赋值给CS。也就是低位给IP,高位给CS)
(IP) = ((SS)*16 + SP)(SP) += 2(CS) = ((SS)*16 + SP)(SP) += 2相当于:pop IP、pop CS
1000H, 0H 栈是FILO,先出的是IP
call指令不能实现短转移
格式1: call 标号 作用: 将当前IP压栈后,跳到标号处执行(位移) 原理:
(SP) -= 2 ((SS)*16 +(SP)) = IP 将当前IP的值放到栈中(IP) += 16位位移注意: 16位位移 = “标号”处地址 - call指令后第一个字节的地址
相当于push IP、jmp near ptr 标号
格式2: call far ptr 标号 作用: 将当前IP压栈后,跳到标号处执行(地址) 原理:
(SP) -= 2 ((SS)*16 +(SP)) = CS 将当前CS的值放到栈中
(SP) -= 2 ((SS)*16 +(SP)) = IP 将当前IP的值放到栈中
(CS) = 标号所在段地址
(IP) = 标号所在偏移地址
相当于push CS、push IP、jmp far ptr 标号
格式3: call 16位reg 作用: 将当前IP压栈后,跳到16位reg存储的地址处执行 原理:
(SP) -= 2 ((SS)*16 +(SP)) = IP 将当前IP的值放到栈中(IP) = 16位reg相当于push IP、jmp 16位reg
格式4: call word ptr 内存单元地址 作用: 将当前IP压栈后,跳到内存存储的地址处执行 原理:
(SP) -= 2 ((SS)*16 +(SP)) = IP 将当前CS的值放到栈中(IP) = 16位reg相当于push IP、jmp word ptr 内存单元地址
格式5: call dword ptr 内存单元地址 作用: 将当前IP压栈后,跳到内存存储的地址处执行 原理:
(SP) -= 2 ((SS)*16 +(SP)) = CS 将当前CS的值放到栈中(SP) -= 2 ((SS)*16 +(SP)) = IP 将当前IP的值放到栈中(IP) = 16位reg相当于push CS、push IP、jmp dword ptr 内存单元地址
10.2: 6H 10.3: 1010H 10.4: bH。sp原本等于0000,-2后变为fffe。 10.5:
3。其实直接整体来看,答案只与mov ax, 0和三个inc ax有关,但是我们还是分析下。程序首先令((SS)*16 + SP) = ((stack)*16 + 1H),然后令(DS) = (stack),令(ax) = 0。把当前CS:IP放入栈,14~15存放CS,为stack,12~13存放IP,为当前IP。1, 0。程序首先令((SS)*16 + SP) = ((data)*16 + 1H),然后把s标号的字形数据(2B)给了SS:[0],把CS的数据给了SS:[2],也就是说,在SS中,从0开始,低地址存放s的IP,高地址存放当前CS(也是s的CS)。然后把nop处的CS/IP入栈,然后跳转到s标号处。 把s标号的IP赋值给ax,再减去nop处的IP;s标号与nop相距一个nop的长度,结果为1。把s标号的CS赋值给bx,再减去nop处的CS,结果为0。汇编语言:
call s ; 跳转到标号s处 ……其它语句 s: 函数子语句 ret ; 返回调用处的下一条语句高级语言:
s() // 调用函数 ……其它语句 s(参数) { 函数体; return 返回值;}意义:
call和ret,是程序模块化的基础。这个模块化的思想叫做解耦等等! 我们发现参数和返回值的问题没有解决
相乘的两个数,要么都是8位,要么都是16位
乘数:
8位:AL & 内存单元16位:AX & 内存字单元积:
8位:AX中16位:高16位在DX中,低16位在AX中格式:
mul 内存单元mul 寄存器会发现与div互为逆指令,我们来复习一下
假设另一个乘数在内存(字)单元中:
(AX) = (AL) * (内存单元)((DX)*16 + (AX)) = (AX) * (内存字单元)假设被除数在内存(字)单元中:
(AL) = (AX) / (内存单元)(AX) = ((DX)*16 + (AX)) / (内存字单元)高级语言中,参数和返回值是放在变量 里的,变量是一个容器 。其实,参数和返回值的问题,就是去哪里存储数据的问题。一般就放在寄存器中,就相当于一个全局变量(一般用bx)。
调用者和子程序操作相反:
调用者将参数送入参数寄存器 , 从结果寄存器 取得返回值子程序从参数寄存器 得到参数, 将返回值送入结果寄存器子程序注释模板:
; 功能:计算 N 的三次方 ; 参数:(bx) = N ; 返回值:(bx) = N ^ 3编程题:
批量数据的传递可以用内存,也可以用栈(高级语言中,形参和实参的传递,用的就是栈)
编写子程序的标准框架:
子程序开始: 子程序使用的寄存器入栈 子程序内容 子程序使用的寄存器出栈 返回(ret或retf)问题10.2:
程序执行前:程序执行后:注意: 以下所有代码由于是独立完成,并且没有看解析,与标答可能有所区别,不必太过疑惑。
1. 显示字符串
代码: 结果:2. 解决除法溢出问题
算法:
代码:
结果:
**3. ** 转变为char(三个练习的综合)
代码: 结果: