汇编笔记(四)长文警告

转移指令的原理

可以修改IP,或同时修改CS和IP的指令通称为转移指令。

8086CPU的转义行为有一下几类。

  • 只修改IP时,称为段内转移,比如:jmp ax
  • 同时修改CS和IP时,称为段间转移,比如:jmp 1000:0

由于转移指令对IP的修改范围不同,段内转移又分为:短转移和近转移。

  • 短转移IP的修改范围为-128~127。
  • 近转移的IP的修改范围为-32768~32767。

8086CPU的转移指令分为以下几类。

  • 无条件转移指令(如:jmp)
  • 条件转移指令
  • 循环指令(如:loop)
  • 过程
  • 中断

这些转移指令的区别在于前提条件不同,但转移的原理是相同的。

我们在这里通过深入学习无条件转移指令jmp来理解CPU执行转移指令的基本原理

操作符offset

offset的功能是取的标号的偏移地址,该指令由编译器执行。

jmp指令

jmp为无条件转移指令,可以只修改IP,也可以同时修改CS和IP。

jmp指令需要给出两种信息:

  • 转移的目的地址
  • 转移的距离(段间转移、段内段转移、段内近转移)

根据位移进行转移的jmp指令

jmp short 标号(转到标号处执行指令)

我们从一段汇编程序开始。

观察这段汇编指令对应的机器码,汇编指令中的[idata]立即数,不论是否是数据还是内存单元的偏移地址,都会在对应的机器指令中出现,CPU执行的机器指令,它必须要处理这些数据和地址。

注意jmp指令一行,机器指令中不包含转移的目的地址。

多次测试可以发现,CPU执行jmp指令的时候不需要转移的目的地址。

回忆CPU执行指令的过程。

  • 从CS:IP指向内存单元读取指令,读取的指令进入指令缓冲器;
  • (IP)=(IP)+所读取指令的长度,指向下一条指令
  • 执行指令,跳到第一步,重复这个过程

jmp指令对应的机器码EB03中的’03‘其实是转移的位移。

jmp short 标号的功能为:(IP)=(IP)+8位移。

  • 8位位移=标号处的地址-jmp指令后第一个字节的地址;
  • short 指明此处的位移长度
  • 8位移范围为-128~127,用补码表示(计算机中没有加法,正数二进制取反加一得到负数的二进制)
  • 8位位移在编译算出

类似的指令jmp near ptr 标号,实现的是段内近转移,不过它的位移范围是-32768~32767。

这里简单介绍下这个位移范围如何得到,这和补码的含义有关,正数的补码就是其本省,负数的补码是在原码的基础上,符号位(第一位)不变,其余各位取反,最后+1,8位位移的范围只能11111111~01111111(-128~127),16位位移也类似。

转移的目的地址在指令中的jmp指令

前面说的jmp far ptr 标号指令,对应的机器码为”EA 0801 6C07”,这里我特意空格分隔开,方便观察。

“EA 0801 6C07”是在指令中的内存中的排列顺序,高地址“6C07”是转移的段地址:076C,低地址的“0801”是偏移地址”0108”

转移地址在寄存器中的jmp指令

指令格式:jmp 16位 reg,功能(IP)=(16位reg),这里不在详述。

转移地址在内存中的jmp指令

  • jmp word ptr 内存单元地址(段内转移)

功能:从内存单元地址处开始存放着一个字,是转移的目的偏移地址。

内存单元地址可用寻址方式的任一格式给出。

  • jmp dword ptr 内存单元地址(段间转移)

功能:从内存中单元地址处存放着两个字,高地址处的字是转移的目的段地址,低地址处是转移的目的偏移地址。

jcxz指令

jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移,对IP的修改范围:-128~127(用补码表示)。

8位位移由编译程序在编译时算出。

功能:如果$()(CX)=0$,转移的标号处执行;如果$(CX) \neq0$,什么也不做(程序向下执行)。

jcxz= jmp CX zero

loop指令

loop指令为循环指令,所有的循环指令都是短转移。

  • 对应的机器码中只包含转移的位移
  • 8位位移有编译时算出
  • 8位位移范围为-128到127,用补码表示。

可能有读者说,这不和jcxa指令、jmp short 标号的性质差不多。是的,确实类似,所有有部分我忽略没写。

它的功能和jcxz类似又有不同:先将$(CX)=(CX)-1$,如果$(CX) \neq0$,转移到标号处执行;如果$()(CX)=0$,什么也不做(程序向下执行)

使用位移转移的意义

到现在为止,已经用过的指令有:

jmp short 标号
jmp near ptr 标号
jcxz 标号
loop 标号

它们对应的机器码中只有到目的地址的位移,这种设计方便了程序段在内存中的浮动装配,

如果在程序段写入内存地址,则对程序的执行有了前提限制。

编译器对转移位移超界的检测

根据位移进行转移的指令,它的转移范围受到转移位移的限制,如果源程序中出现了转移位移超界的问题,编译时,编译器将报错。

前面在Debug模式中使用过jmp 2000:0100的转移指令,汇编编译器并不认识。源程序中使用编译会出错。

实验八

程序如上,我们一步步来理解。

  • 程序从start开始执行,使用offset分别获取了标号“s”、”s2“处的偏移地址,并保存在DI、DI寄存器中。
  • 使用mov指令将标号”s2“处的字型数据复制的AX中,也就是指令jmp short s1
  • 标号”s2”处的指令“jmp short s1”,我们回忆下位移如何计算,前面说过

8位位移=标号出的地址-jmp指令后的第一个字节的地址,联系CPU执行指令的过程,这个位移实际上就是标号与jmp指令的长度,也就是说从nop指令到标号“s1”处的指令长度为位移,这里为10个字节。往前面移动,也就是负的,得到补码”F6“,机器码为”EBF6”。

  • 将AX中的数据复制到标号“s”处的,标号”s”的前两条nop指令被覆盖
  • 指令jmp short s,这里的位移计算不在详述,跳转到标号”s”处,
    • CS:IP指向内存单元读取指令
  • 这时候第一条指令为”EBF6“了,读取”EBF6“进入指令缓冲器;
    • (IP)=(IP)+2(所读取指令的长度)
    • 执行指令,”EBF6“的效果是IP向前位移10个字节,向前移动10个字节之后的IP所指向的指令即为mov ax,4c00h

CALLRET指令

CALLRET指令都是转移指令,它们都修改IP,或同时修改CS和IP。

这一章,我们讲解callret指令的原理

retretf

ret指令用栈中的数据,修改IP的内容,从而实现近转移。

retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移。

CPU执行ret指令时,进行了下面两步操作:

  • (IP)=((SS))*16+(SP))
  • (SP)=(SP)+2

CPU执行retf指令时,进行了下面4不操作:

  • (IP)=((SS)*16+(SP))
  • (SP)=(SP)+2
  • (CS)=((SS)*16+(SP))
  • (SP)=(SP)+2

估计你有点懵,用汇编指令来解释retretf指令,则:

CPU执行ret指令时,相当于pop IP

CPU执行retf指令时,相当于进行:

pop IP
pop CS

call指令

CPU执行call指令时,进行两步操作:

  • 将当前的IP或CS和IP压如栈中;
  • 转移

依据位移进行转移的call指令

call 标号(将当前的IP压入栈后,转到标号出执行指令)

CPU执行此种格式的call指令时,进行了如下的操作:

  • (SP)=(SP)-2 ((SS)*16)+(SP))=(IP)
  • (ip)=(IP)+16位位移

16位位移=标号出的地址-call指令后的第一个字节的地址;

16位位移范围为-32768~32767,用补码表示;

16位位移由编译程序在编译时算出

用汇编指令解释,相当于。

转移的目的地址在指令中的call指令

前面讲的call指令,对应的机器码中并没有转移的目的地址,而是位移。

call far ptr 标号实现的段间转移

CPU执行此格式的call指令是,进行了如下操作。

  • (SP)=(SP)-2 ((ss)*16+(sp))=(CS) (SP)=(SP)-2 ((SS)*16+(SP))=(IP)
  • (CS)=标号所在段的段地址 (IP)=标号所在段的偏移地址

用汇编指令解释,相当与进行。

push CS
push IP ;IP为call指令的下一条指令的IP
jmp far ptr 标号

转移地址在寄存器中的call指令

指令格式:call 16位 reg

功能:

  • (SP)=(SP)-2 ((SS)*16+(SP))=(IP) (IP)=(16位reg)

汇编指令解释,相当于进行:

push IP;同上
jmp 16位reg

注意:这里的地址应理解为偏移地址(IP寄存器中存放),请回忆jmp指令的原理。

转移地址在内存中的call指令

转移指令在内存中的call指令有两种格式。

  • call word ptr 内存单元地址

用汇编指令解释:

push IP;同上
jmp word ptr 内存单元地址
  • call dword ptr 内存单元地址

用汇编指令解释:

push CS
push IP

这里检测点第一题居然书貌似错了,看视频果然不一样。 因为call指令执行过程中对栈进行了操作,为了分析,可以手动记录栈空间的变化,这样易于分析。

callret的配合使用

先分析一段程序。

  • 前三条指令执行后,栈空间为16个字节,且用零填充
  • call指令读取后,IP指向下一条指令mov ax,4c00h,指令执行,将IP中的值入栈,IP寄存器指向标号”s”处。
  • CPU从标号“s”处开始执行,执行add ax,ax
  • ret指令执行后,相当于进行了pop IP,IP更改,IP重新指向mov ax,4c00h

回忆一下call指令和ret指令的功能。

  • call 标号指令相当于。
push IP
jmp near ptr 标号
  • ret指令相当于。
pop IP

在回顾这个程序,我们将标号“s”处的具有一定功能的程序段称为子程序,call指令在转去执行子程序之前,call指令将后面指令的地址存储到栈中,在子程序末,使用ret指令,用栈中的数据设置IP中的值,回到call指令后的代码处继续执行。

我都说到这里,有没有人有大胆的想法:-P。

标号:
	指令
	ret

我们在指令中可以使用call 标号,再通过ret返回不断的执行后续的call

这样写有什么好处?

我们说要保持源程序的阅读性,随着功能的不断增加,代码阅读性下降是必然的,而上述的这种结构,为我们提供了一种组织代码的方式,是的大量代码的情况下仍然有一定的阅读性。

mul指令

因下面会用到,这里介绍下mul指令,mul是乘法指令,使用mul做乘法时,需要主要以下几点。

  • 两个相乘的数位数需要相同,即8位和8位,16位和16位。
  • 如果是8位,一个默认在AL中,另一个在8位reg中或者内存字节单元中;如果是16位一个默认在AX中,另一个在16位reg中或者内存字单元中。
  • 结果:8位乘法结果在AX中;16位乘法,结构高位默认在AX中,低位在AX中存放

模块化程序设计

从上面我们看到,callret指令共同支持了汇编语言程序编程中的模块化设计。利用callret指令,可以用简洁的方法,实现多个相互联、功能独立的子程序来解决一个复杂的问题。

下面,我们来看一下子程序设计过程中的相关问题和解决方法。

参数和结果传递的问题

讨论参数和返回值传递的问题,实际上就是在探讨,应该如何存储子程序需要的参数和产生的返回值。

我们最先想到的是用寄存器了存储,对于存放参数的存储器和存储结果的存储器,调用者和子程序的读写恰恰相反:调用者将参数送入参数寄存器,从结果寄存器中取到返回值;子程序从参数寄存器中取到参数,将返回值送入寄存器中。

这是一段将data段第一组数据的3次方,结果保存在后面一组的dword单元中的程序,请理解此过程中的参数和结果传递。

注意,如果写好的程序你自己还要使用,或者要给别人使用,都请写好注释。

批量数据的传递

前面的程序参数和结果只有一个,可以用两个寄存器来存放,寄存器数量终究有限的,我们不可能简单第用寄存器来存放多个需要传递的数据。返回值也一样。

这里将批量数据放在内存中,所在内存空间的首地址放在寄存器中,传递给需要的子程序。

我们也可以用以前使用loop指令来实现。

call指令给我们的启发

call指令的原理不在详述,call指令告诉了我们一种组织数据和组织代码的方式

上述过程可以描述为。

  • 设置参数
  • 程序处理
  • 得到程序的返回值

从组织的方式解释。

  • 组织数据
  • 组织代码
  • 组织数据

从计算机原理层面抽象。

  • 输入
  • 程序处理
  • 输出

寄存器冲突的问题

有一个一般化的问题:子程序中使用的寄存器,很可能在主程序中也要使用,造成了寄存器使用上的冲突。

解决这个问题的简捷方法是,在子程序开始将子程序所有用到的寄存器的内存都保存起来,在子程序返回前恢复,可以用栈来保存寄存器中的内容。

实验10

实现字符串

要求使用子程序“show_str”,在指定的位置,用指定的颜色,显示一个用0结束的字符串。

提供的参数有:(DH)=行号,(DL)=列号

回忆一下实验九,这里我实现一种颜色。

根据要求不难写出如下代码(笔者其实边调试编写写了半小时)

优化一下。

该程序的内部处理和显存的结构相关,通过调用子程序,进行字符串的显示可以不必了解显存的结构,为编程提供了方便。

解决除法溢出问题

问题:div指令可以做除法。进行8为除法的时候,用AL存储接结果的商,AH存储结果的余数;进行16位除法的时候,用AX存储结果的商,DX存储结果余数。

那么,下面的程序段呢?

mov ax,1000h
mov dx,1
mov bx,1
div bx ;商应为11000H,而11000H在AX中存放不下。

当CPU执行div等除法指令的时候,如果出现这样的情况,将引发CPU的内部错误:除法溢出。

我们这里写个子程序来解决除法溢出的问题。

详细的解释过程,注释都有,这道题主要是这个公式:X/N=int(H/N)*65526+[rem(H/N)*65536+L]/N,证明过程不重要,其实和我们用的十进制除法是一样的。

数值显示

功能:将word型数据转变为十进制数的字符串,字符串以0为结尾符。

如果不看提示,这个感觉做不出来。

分析:

  1. 要得到字符串,其实是要得字符串的ASCII码,十进制数码字符对应的ASCII码=十进制数码值+30H(可观察ASCII码表得出)。
  2. 如何得到十进制数码值?利用上上面的除法不断相除,没出的余数就是每位的值。
  3. 对于2这里有一个问题,除多少次?除到商为0,用jcxz指令就可以完成。

无能嘤嘤……

这个地方卡了一周(太难了,不想做,做一半,想不出,不想。重新开始想,圆原地踏步).使用8位除法得到余数,排列重新求余,B站找了别人的代码。你们感受一下。

和题意有点区别,代码我都注释了,看不懂的看我注释。

看不清的请移步个人博客

总结

个人状态有点问题,最后一部分思考偏了,我本人很同一往一个地方深挖,后来明白是挖不完的,适度就好。想不通了不妨换个方向。

文;伍默

排版;喵喵

正文共:6167 字 21 图

原文发布于微信公众号 - 渗透云笔记(shentouyun)

原文发表时间:2019-08-21

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券