汇编语言学习笔记(四)

0x00 转移指令

可以修改IP,或同时修改CS和IP的指令统称为转移指令。概括地讲,转移指令就是可以控制CPU执行内存中某处代码的指令。

8086CPU转移行为包括以下几类:

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

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

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

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

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

操作符offset在汇编语言中是由编译器处理的符号,它的功能是取得标号的偏移地址

0x01 jmp指令

jmp为无条件转移指令,可以只修改IP,也可以同时修改CS和IP。jmp指令给出转移目的地址转移的距离(段内转移、段内短转移、段内近转移)。不同的给出目的地址的方法和不同的转移位置,对应不同格式的jmp指令。

依据位移转移

CPU在执行jmp指令的时候并不需要转移的目的地址,而是向CPU传递要转移的位移,这种设计方便程序段在内存中的浮动装配。根据位移进行转移的指令受到转移位移的限制。如果在源程序中出现转移范围越界的问题,则编译器报错。

1

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

    该指令中的short符号说明其实现段内短转移,它对IP的修改范围是-128~127。jmp指令中的标号是代码段中的标号,指明了指令要转移的目的地,转移指令结束后,CS:IP应指向标号处的指令。实际上该指令的功能为(IP)=(IP)+8位位移

    • 8位位移=标号处地址-jmp指令后的第一个字节的地址;
    • short指明此处的位移为8位位移;
    • 8位位移的范围是-128~127,使用补码表示;
    • 8位位移由编译程序在编译时算出。
  • jmp near ptr 标号

    该指令实现段内近转移,功能为(IP)=(IP)+16位位移

    • 16位位移=标号处的地址-jmp指令后的第一个字节的地址;
    • near ptr指明此处的位移为16位位移,进行的是段内近转移;
    • 16位位移的范围是-32768~32767,使用补码表示;
    • 16位位移由编译程序在编译时算出。

转移地址在指令中

jmp far ptr 标号

该指令实现段间转移,又称为远转移。功能如下:(CS)=标号所在段的地址,(IP)=标号在段中的偏移地址。far ptr指明了指令用标号的段地址和偏移地址修改CS和IP。

转移地址在寄存器中

jmp 16位reg

该指令功能为:(IP)=(16位reg)

转移地址在内存中

转移地址在内存中的jmp指令有两种格式:

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

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

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

    功能:从内存单元地址处开始存放的两个字,高地址处的字是转移的目的段地址低地址处是转移的目的偏移地址。即(CS)=(内存单元地址+2),(IP)=(内存单元地址)。

0x02 jcxz&loop&ret

jcxz指令

jcxz指令为有条件短转移指令,在对应的机器码中包含转移的位移,而不是目的地址,对IP的修改范围均为-128~127。

  • 指令格式

    jcxz 标号(若(cx)=0,则转移到标号处执行)

  • 操作

    • 当(cx)=0时,(IP)=(IP)+8位位移;

      8位位移=标号处地址-jcxz指令后的第一个字节的地址,使用补码表示,由编译程序在编译时加以计算。

    • 当$(cx) /neq 0$时,程序向下执行。

综上,jcxz的功能相当于if((cx)==0) jmp short 标号

loop指令

loop指令为循环短转移指令,在对应的机器码中包含转移的位移而非目的地址,对IP的修改范围为-128~127。

  • 指令格式

    loop 标号((cx)=(cx)-1,若$(cx) \neq 0$,转移到标号处执行)

  • 操作

    • (cx)=(cx)-1;
    • 若$(cx) \neq 0$,(IP)=(IP)+8位位移
    • 若(cx)=0,则程序向下执行。

综上,loop的功能相当于cx-- if((cx)≠0 jmp short 标号)

ret&retf

ret指令修改IP,retf指令修改CS和IP。

  • ret指令用栈中的数据修改IP的内容,从而实现近转移。汇编语法表示为pop IP
  • retf指令用栈中的数据修改CS和IP的内容,从而实现远转移。汇编语法表示为pop IP pop CS

0x03 CALL

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

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

call指令不能实现短转移,除此之外其实现转移的方法和jmp指令的原理相同。

依据位移转移

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

该指令操作如下:

  • (sp)=(sp)-2

    ((ss)*16+(sp))=(IP)

  • (IP)=(IP)+16位位移

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

1
2
push IP
jmp near ptr 标号

转移地址在指令中

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

该指令操作如下:

  • (sp)=(sp)-2

    ((ss)*16+(sp))=(CS)

    (sp)=(sp)-2

    ((ss)*16+(sp))=(IP)

  • (CS)=标号所在段的段地址

    (IP)=标号在段中的偏移地址

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

1
2
3
push CS
push IP
jmp far ptr 标号

转移地址在寄存器中

call 16位reg

该指令操作如下:

  • (sp)=(sp)-2

  • ((ss)*16+(sp))=(IP)

    (IP)=(16位reg)

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

1
2
push IP
jmp 16位reg

转移地址在内存中

转移地址在内存中的call指令同样存在两种格式。

  • call word ptr 内存单元地址

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

    1
    2
    push IP
    jmp word ptr 内存单元地址
  • call dword ptr 内存单元地址

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

    1
    2
    3
    push CS
    push IP
    jmp dword ptr 内存单元地址

0x04 模块化程序设计

call&ret

call指令可以与ret指令组合使用以实现子程序机制。call指令跳转之前,其后面的指令的地址将存储在栈中,所以可在子程序的尾部使用ret指令,用栈中的数据设置IP的值,从而转到call指令后面的代码处继续执行。

具有子程序的源程序框架如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
assume cs:code
code segment
main: :
:
call sub1 ;调用子程序sub1
:
:
mov ax,4c00h
int 21h

sub1: : ;子程序sub1开始
:
call sub2 ;调用子程序sub2
:
:
ret ;子程序sub1返回
sub2: : ;子程序sub2开始
:
ret ;子程序sub2返回
code ends
end main

一些问题

  • 参数&结果传递问题

    调用者将参数送入参数寄存器,从结果寄存器中取到返回值;子程序从参数寄存器中取到参数,将返回值送入结果寄存器。

  • 批量数据传递

    可以将批量数据放到内存中,然后将它们所在内存空间的首地址放在寄存器中,传递给需要的子程序。对于具有批量返回结果的程序也可以使用以上方法。当然也可以通过数据栈的方法批量传递数据。

  • 寄存器冲突问题

    在子程序的开始将子程序中所有用到的寄存器中的内容都保存至栈中,当子程序返回前再予以恢复,需要注意寄存器入栈和出栈的顺序

    从而编写子程序的标准框架如下:

    1
    2
    3
    4
    子程序开始:	子程序中使用的寄存器入栈
    子程序内容
    子程序使用的寄存器出栈
    返回(ret,retf)

0x05 标志寄存器

8086CPU中,标志寄存器具有以下三种作用:存储相关指令的某些执行结果、为CPU执行相关指令提供行为依据、控制CPU的相关工作方式。8086CPU的标志寄存器有16位,简称为flag,其中存储的信息通常被称为程序状态字(PSW)。

flag寄存器按位起作用,即每一位都记录特定的信息,结构如图:

2

如图所示,flag的第0、2、4、6、7、8、9、10、11位都具有特殊的含义。

重要标志位

ZF标志

flag的第6位是ZF即零标志位。若相关指令执行后,结果为0,则zf=1;若结果不为0,则zf=0。

注意,在8086CPU指令集中,进行逻辑运算或算术运算的运算指令的执行影响标志寄存器,而传送指令的执行对标志寄存器没有影响。

PF标志

flag的第2位是PF即奇偶标志位。若相关指令执行后,其结果的所有bit位中1的个数为偶数,则pf=1;若1的个数为奇数,则pf=0。

SF标志

flag的第7位是SF即符号标志位。若相关指令执行后,其结果为负数,则sf=1;若非负,则sf=0。

显然,SF标志就是CPU对有符号数运算结果的一种记录,即它可以记录数据的正负,对于无符号数运算则没有任何意义。

CF标志

flag的第0位是CF即进位标志位。一般情况下,在进行无符号数运算时,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的错位值。

OF标志

进行有符号数运算的时候,若结果超过了机器所能表示的范围称为溢出。

flag的第11位是OF即溢出标志位。一般情况下,若有符号数运算的结果发生溢出,则OF=1;若没有发生溢出,则OF=0。

TF标志

flag的第8位是TF,该标志位与单步中断有关。CPU在执行完成一条指令之后,若检测到TF=1,则产生单步中断,引发中断过程;若检测到TF=0,则继续执行,不产生任何中断。

IF标志

flag的第9位是IF,该标志位决定CPU是否响应可屏蔽中断。若CPU检测到IF=1,则其在执行完成当前指令后响应中断,引发中断过程;若IF=0,则不响应可屏蔽中断。中断过程中常将IF置为0,是为了在进入中断处理程序后,禁止其他的可屏蔽中断。当然,如果在中断处理程序中需要处理可屏蔽中断,可利用指令将IF置1。8086CPU提供设置IF的指令如下:

  • sti,设置IF=1;
  • cli,设置IF=0。

DF标志

flag的第10位是DF即方向标志位。在串处理指令中,控制每次操作后si、di的增减。

  • df=0,每次操作后si、di递增
  • df=1,每次操作后si、di递减

8086CPU提供下面两条指令对df位进行设置:

  • cld指令:将标志寄存器的df位置0
  • std指令:将标志寄存器的df位置1

重要指令

adc指令

adc是带进位加法指令,它利用了CF位上记录的进位值。

  • 指令格式

    1
    adc 操作对象1, 操作对象2
  • 功能

    操作对象1=操作对象1+操作对象2+CF

  • 由于adc指令执行后,也可能产生进位值,所以也会对CF位进行设置。因此该指令可对任意大的数据进行加法运算。

sbb指令

sbb是带错位减法指令,它利用了CF位上记录的错位值。

  • 指令格式

    1
    sbb 操作对象1, 操作对象2
  • 功能

    操作对象1=操作对象1-操作对象2-CF

  • sbb指令执行后,将对CF进行设置,故利用sbb指令可以对任意大的数据进行减法运算。

cmp指令

cmp是比较指令,其执行后将对标志寄存器产生影响,但不保存结果。其他相关指令通过识别这些被影响的标志寄存器来得知比较结果。该指令通过做减法运算,影响标志寄存器,标志寄存器的相关位记录了比较结果。CPU在执行cmp指令的时候,也包含无符号数比较有符号数比较两种情况。

  • 指令格式

    1
    cmp 操作对象1, 操作对象2
  • 功能

    计算操作对象1-操作对象2但并不保存结果,仅仅根据计算结果对标志寄存器进行设置。

  • 溢出

    由于有符号数在进行减法操作时会产生溢出现象,而sf标志位只记录实际结果的正负,从而cmp指令的比较结果与sf标志位的结果并不完全等同,需要结合of标志位的情况进行判断。

    • of=0

      of=0说明没有溢出,逻辑上真正结果的正负=实际结果的正负。

    • of=1

      of=1说明存在溢出,逻辑上真正结果的正负$\neq$实际结果的正负。同时,若因溢出导致实际结果为负,则逻辑上结果必然为正;若因溢出导致实际结果为正,则逻辑上结果必然为负。

检测比较结果的条件转移指令

根据cmp指令的比较结果进行转移的指令也分为两种,即根据无符号数的比较结果进行转移的条件转移指令(检测zf和cf的值)和根据有符号数的比较结果进行转移的条件转移指令(检测sf、of和zf的值)。

常用根据无符号数的比较结果进行转移的条件转移指令如下:

3

串传送指令

  • 格式

    movsb&movsw

  • 功能

    该指令的功能是将ds:si指向的内存单元中的字节(字)送入es:di中,然后根据标志寄存器df位的值,将si和di递增(2)或递减(2)。采用汇编语法描述如下:

    1
    2
    3
    4
    5
    6
    7
    mov es:[di],byte/word ptr ds:[si]
    if df=0:
    inc si/add si,2
    inc di/add di,2
    if df=1:
    dec si/sub si,2
    dec di/sub di,2
  • movsb和movsw都可以和rep配合使用,从而循环实现(cx)个字节(字)的传送。

pushf&popf

pushf的功能是将标志寄存器的值压栈,而popf是从栈中弹出数据,送入标志寄存器中。pushf和popf为直接访问标志寄存器提供了一种方法。

Debug中的标志寄存器

4

在Debug中,标志寄存器按照有意义的各个标志位单独表示。如上图所示,其中NV、UP、PL、NZ、PO和NC分别对应OF、DF、SF、ZF、PF和CF标志位。下面列出Debug对已知标志位的表示。

5

请作者吃个小鱼饼干吧