【汇编语言学习笔记·二】内存处理技巧和流程控制

loop 指令

可用于实现循环。

  • 格式:loop 标号

执行loop指令时,要进行两步操作:

  • (cx) -= 1
  • 判断 cx 的值,如果不为0,就转移至标号处执行程序,如果为0就向下执行。

使用 loop 指令可以实现循环功能,cx中存放循环的次数

以下是一个计算 2^12 的程序

assume cs:code
code segment
     mov ax, 2
     
     mov cx, 11
s:   add ax, ax
     loop s
     
     mov ax, 4c00h
     int 21h
code ends
end
  • 标号s:在汇编语言中,标号标识了一个地址,而这个地址处有一条指令add ax, ax

与内存的交互

描述性符号(),用于表示一个寄存器或者内存单元中的内容。

[bx]

[bx]表示一个内存单元的偏移地址,而偏移地址的值就是寄存器BX中的数据。

例如:
mov ax, [bx]就是把DS:BX指向的长度为2的内存单元的内容存入ax
mov al, [bx]就是把DS:BX指向的长度为1的内存单元的内容存入ax

约定符号idata表示常量:如:mov ax, [idata]就表示mov ax, [1]mov ax, [2]

masm 会将 mov ax, [idata] 解释为 mov ax, idata
但是 mov ax, [bx] 仍是 mov ax, [bx]
此外,mov ax, ds:[idata] 可以用来表示把 ds:[idata] 处的数据存入 ax。
也可以使用 mov bx, idata; mov ax, [bx]

类C语言的表示法

可以用诸如这样的指令:

mov ax, [bx + 200]

来访问内存空间。
其数学化的描述为:(ax) = [(ds) * 16 + (bx) + 200]

此外,类似的有四种不同写法,以下四条指令都是等价的:

mov ax, [bx + 200]
mov ax, [200 + bx]
mov ax, 200[bx]
mov ax, [bx].200

其中后两种形式分别类似于数组和结构体。

可以看到,寄存器 bx 一般都会用在这样的情形下,被置于中括号中用于索引数据。此外,si 和 di 寄存器也可以当作 bx 使用。

以下指令等价:

mov ax, [bx + 200 + si]
mov ax, [200 + bx + si]
mov ax, 200[bx][si]
mov ax, [bx].200[si]
mov ax, [bx][si].200

segment

程序需要一段内存空间来存储数据,且需要保证这段空间是安全的,不会影响其他程序正在使用的内存。0:200 ~ 0:300这段空间就是相对安全的空间。
在操作系统的环境中,程序可以从操作系统申请一段合法空间来使用。
要在一个程序被加载的时候取得所需空间,就必须在源程序中做出说明。

示例代码:

assume cs:codesg, ds:datasg, ss:stacksg
datasg segment
     db 'ibm             '
     db 'dec             '
     db 'dos             '
     db 'vax             '
datasg ends

stacksg segment
     dw 0, 0, 0, 0, 0, 0, 0, 0
stacksg ends

codesg segment
start:
     mov ax, datasg
     mov ds, ax
     mov ax, stacksg
     mov ss, ax
     mov sp, 16
     mov bx, 0
     
     mov cx, 4
s0:
     push cx
     mov si, 0
     mov cx, 3
s1:
     mov al, [bx][si]
     and al, 11011111B
     mov [bx][si], al
     inc si
     loop s1
     
     add bx, 16
     pop cx
     loop s0
     
     mov ax, 4c00H
     int 21H
codesg ends
end start

其中,伪指令 start 标志了程序入口的位置,注意 end 后面也有 start。如果不这样写,程序会在写入了 'BaSiC' 的那段内存开始执行代码,由于其未必是合法的机器码,可能会造成异常。

伪指令 assume 并不会在程序运行时将 ds, ss 寄存器的值设定为段 datasg, stacksg 的地址,因此要在程序开始时把对应的段地址传入对应的段寄存器。

伪指令 db, dw 表示在对应内存位置申请内存,并填入预设的数据。

这个程序的作用是把在 datasg 内存储的字符串从小写转为大写,可以看到这里处理转换大小写的技巧。效果如图所示:
1

在这里插入图片描述

CPU的栈机制

8086提供了相关指令,可以用栈的方式来访问内存空间。
两个最基础的指令:

  • push: push ax 表示将 ax 中的数据入栈
  • pop: pop ax 表示把栈中的数据弹入 ax 中

8086CPU的入栈和出栈操作都是以为单位进行的。

8086CPU中的两个寄存器 SS 和 SP 用来记录栈顶地址。栈顶的段地址存放在SS中,偏移地址存放在SP中,任意时刻,SS:SP指向栈顶元素

在现代CPU中,往往使用ESP或者RSP(32位和64位),其主要作用是维护函数调用栈,当然,函数调用栈的概念主要是由C语言编译器所维护的。由于这些CPU没有使用段寄存器的必要,所以没有相应的SS。

push 和 pop

push ax的执行过程:

  • SP -= 2,也就是令SS : SP指向当前栈顶前面的一个字单元。
  • 将ax的内容送入SS : SP指向的内存单元处,此时SS : SP就是新的栈顶。

pop ax的执行过程:

  • SS : SP指向的内容送入ax
  • SP += 2,指向当前栈顶下面的单元,作为新的栈顶。

8086不会因为栈内存已满就拒绝push指令的执行,程序员需要自己担心栈溢出的问题。

栈段

我们可以在编程的时候做这样的安排,也就是把一段内存当作栈段

注意:假如我们用X ~ Y作为一个栈段(X < Y)

则栈为空时
S P = ( Y − S S × 16 ) + 1 \mathrm{SP} = (Y - \mathrm{SS} \times 16)+1 SP=(YSS×16)+1
对于,8086CPU,栈段最大为64KB

数据处理

要考虑两个基本问题:

  • 处理的数据在什么地方?
  • 要处理的数据有多长?

寻址方法

下面,SA 表示段地址,EA 表示偏移地址

指令要处理的数据在内存中,在汇编指令中可以用 [x] 的格式给出EA,而SA存在于某个段寄存器中。

寻址方式含义名称常用格式举例
[idata]EA = idata; SA = (ds)直接寻址[idata]
[bx]
[si]
[di]
[bp]
EA = (bx); SA = (ds)
EA = (si); SA = (ds)
EA = (di); SA = (ds)
EA = (bp); SA = (ss)
寄存器间接寻址[bx]
[bx + idata]
[si + idata]
[di + idata]
[bp + idata]
EA = (bx) + idata; SA = (ds)
EA = (si) + idata; SA = (ds)
EA = (di) + idata; SA = (ds)
EA = (bp) + idata; SA = (ss)
寄存器相对寻址用于结构体: [bx].idata
用于数组: idata[si]
用于二维数组: [bx][idata]
[bx + si]
[bx + di]
[bp + si]
[bp + di]
EA = (bx) + (si); SA = (ds)
EA = (bx) + (di); SA = (ds)
EA = (bp) + (si); SA = (ss)
EA = (bp) + (di); SA = (ss)
基址变址寻址用于二维数组: [bx][si]
[bx + si + idata]
[bx + di + idata]
[bp + si + idata]
[bp + di + idata]
EA = (bx) + (si) + idata; SA = (ds)
EA = (bx) + (di) + idata; SA = (ds)
EA = (bp) + (si) + idata; SA = (ss)
EA = (bp) + (di) + idata; SA = (ss)
相对基址变址寻址用于结构体中的数组项: [bx].idata[si]
用于二维数组: idata[bx][si]

数据长度控制

8086CPU的指令可以处理两种尺寸的数据:byteword,因此在机器指令中要指明指令要进行字操作还是字节操作。

汇编语言有以下处理方法:

通过寄存器名指明要处理的数据的尺寸

例如以下指令,就指明了要进行字操作:

mov ax, 1  
mov bx, ds:[0]  
mov ds, ax  
mov ds:[0], ax  
inc ax  
add ax, 1000

例如以下指令,就指明了要进行字节操作:

mov al, 1  
mov al, bl  
mov al, ds:[0]  
mov ds:[0], al  
inc al  
add al, 100

用操作符X ptr指明内存单元的长度。

X在汇编指令中可以是 qword, dword, word, byte,分别是8字节、4字节、2字节、1字节
然而,对于8086CPU,仅支持 word 和 byte

例如以下指令就指明了是字操作:

mov word ptr ds:[0], 1  
inc word ptr [bx]  
inc word ptr ds:[0]  
add word ptr [bx], 2

例如以下指令就指明了是字节操作:

mov byte ptr ds:[0], 1  
inc byte ptr [bx]  
inc byte ptr ds:[0]  
add byte ptr [bx], 2

而push只进行字操作

跳转指令

可以修改ip或者同时修改csip的指令统称为转移指令。
转移指令就是可以控制cpU执行内存中某处代码的指令。
8086CPU只有两类转移行为:

  • 段内转移:只修改ip,例如jmp ax
  • 段间转移:同时修改 csip,例如:jmp 1000:0
    通过转移指令对ip的修改范围不同,段内转移又分为:
  • 短转移,ip的修改范围是-128 ~ 127
  • 近转移,ip的修改范围是-32768 ~ 32767
    8086CPU的转移指令分为以下几类:
  • 无条件转移指令(例如jmp
  • 条件转移指令
  • 循环指令(如loop
  • 过程
  • 中断
    这些转移指令的前提条件未必相同,但其原理基本相同。

offset 操作符

操作符offset在汇编语言中会由编译器处理,能够取得标号的偏移地址。
例如:

assume cs:codesg
codesg segment
start:
     mov ax, offset start ;相当于mov ax, 0
s:
     mov ax, offset s     ;相当于mov ax, 3
codesg ends
end start

例:代码复制

assume cs:codesg
codesg segment
# 把start后的第一个指令复制到s0处去。
start:
     mov ax, bx
     mov si, offset start
     mov di, offset s0
     mov ax, cs:[si]
     mov cs:[di], ax
s0:
     nop # nop的机器码占一个字节。
     nop
codesg ends
end start

jmp 指令

jmp是无条件转移的指令,可以只修改ip,也可以同时修改csip
jmp指令要给出两种信息:

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

1. 依据位移进行转移的jmp指令

jmp short 标号,转到标号处执行指令。这个实现的是段内短转移,修改范围是-128 ~ 127
注意:这种形式的jmp指令,其对应的机器码中的idata部分,是一个-128 ~ 127的数字,表示相对的偏移位置。
此外还有jmp near ptr 标号
其功能与jmp short 标号类似,但是标号的范围更长。
机器代码中,jmp指令后面对应的位移的数字,是要转移到的命令的首地址,减去jmp指令的下一条指令的首地址。

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

jmp far ptr 标号实现的是段间转移,又称远转移。
这里的标号可以写成这样:

s:  
mov ax, 1  
db 256 dup (0)  
jmp far ptr s

当然,标号也可以写成 段地址:偏移地址 的形式

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

jmp 16位寄存器
直接修改了ip的内容。

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

有两种格式:

  • jmp word ptr 内存单元地址(段内转移)
    • 这里内存单元地址指向的内容就是新的ip的值,也就是偏移地址。
  • jmp dword ptr 内存单元地址(段间转移)
    • 内存单元地址处开始存放的两个字,高地址的字是目的段地址,低地址的字是目的偏移地址
    • (cs) = (内存单元地址 + 2)
    • (ip) = (内存单元地址)

由于jmp指令在某些情况下是执行从这条指令所在的位置进行偏移
因此如果将jmp直接复制到其他位置可能会产生不符合伪代码的期望的效果。

call, ret, retf

ret指令用栈中的数据,修改ip的内容,从而实现近转移
retf指令用栈中的数据,修改csip的内容,从而实现远转移。
ret 相当于:

pop ip

retf 相当于:

pop ip
pop cs

call far ptr 标号

指令call far ptr 标号实现的是段间转移。
CPU执行此种格式的call指令时,会进行如下操作:

push cs
push ip
jmp far ptr 标号

代码示例:

assume cs:codesg, ss:data
data segment
     dw 16 dup (0)
data ends
codesg segment
start:
     mov ax, data
     mov ss, ax
     mov sp, 16
     mov ax, 0
     call far ptr s
     inc ax
s:
     pop ax
     add ax, ax
     pop bx
     add ax, bx
     mov ax, 4c00H
     int 21H
codesg ends
end start

其在 debug 中显示的指令列表如下:
3

执行到 call 指令后,内存情况如下:
在这里插入图片描述

076A:0000所占的一行是栈段,显然,076C是执行call时的cs。此时栈顶是0010,结合指令表一看,被压入栈中的是call指令的下一条指令的首地址

call 16位寄存器

相当于

push ip
jmp 16位寄存器

call word ptr 内存单元地址

相当于

push ip
jmp word ptr 内存单元地址

call dword ptr 内存单元地址

相当于

push cs
push ip
jmp dword ptr 内存单元地址

jcxz 指令

jcxz指令是有条件转移指令。所有的有条件转移指令都是短转移,对ip的修改范围都是-128 ~ 127
指令格式:jcxz + 标号
作用:执行到这条指令的时候,若cx中的内容是0,则跳转到标号所示的位置。

模块化汇编程序

通过对 call, ret, retf, 可以实现模块化程序设计。

一、callret的配合使用

callret配合使用,可以实现子程序的机制
类似于函数的机制。

其中一种代码框架如下:

assume cs:code  
code segment  
main:  
     ;  
     ;  
     call sub1  
     ;  
     ;  
     mov ax, 4c00H  
     int 21H  
sub1:  
     ;  
     ;  
     call sub2  
     ;  
     ;  
     ret  
sub2:  
     ;  
     ;  
     ;  
     ret  
code ends  
end main

二、参数与结果传递的问题

子程序一般都要根据提供的参数处理一定的事务,然后将结果(返回值)提供给调用者。
讨论参数与返回值的传递问题,实际上就是在讨论如何存储子程序需要的参数和产生的返回值。

;说明:计算N的3次方  
;参数:(bx) = N  
;结果:(dx:ax) = N^3  
cube:  
     mov ax, bx  
     mul bx  
     mul bx  
     ret

子程序最好像这样添加注释,保证其良好的复用性。

3. 批量数据的传递

设计一个将全是字母的字符串转成大写的函数

assume cs:code  
data segment  
     db 'conversation'  
data ends  
​  
code segment  
start:  
     mov ax, data  
     mvo ds, ax  
     mov si, 0  
     mov cx, 12  
     call capital  
     mov ax, 4c00H  
     mov 21H  
       
capital:  
     and byte ptr [si], 11011111B  
     inc si  
     loop capital  
     ret  
code ends  
end start

这里的capital实际上涉及到了两个参数:字符串的首地址和长度。
其中,首地址的段地址存放在ds中,偏移地址存放在si中,长度存放在cx中。

除了用寄存器传递参数,还有一种通用的方法是用栈来传递参数

4. 寄存器冲突的问题

譬如:

assume cs:code  
data segment  
     db 'word', 0  
     db 'unix', 0  
     db 'wind', 0  
     db 'good', 0  
data ends  
​  
code segment  
start:  
     mov ax, data  
     mov ds, ax  
     mov bx, 0  
       
     mov cx, 4  
s:  
     mov si, bx  
     call capital  
     add bx, 5  
     loop s  
       
     mov ax, 4c00H  
     int 21H  
capital:  
     mov cl, [si]  
     mov ch, 0  
     jcxz ok  
     and byte ptr [si], 11011111B  
     inc si  
     jmp short capital  
ok:  
     ret  
code ends  
end start

以上程序由于主程序与子程序之间发生了寄存器的冲突,可能无法正确运行。其中一种解决方案是:

  • 子程序所使用的寄存器入栈
  • 执行子程序
  • 子程序使用的内容出栈

因此可以把capital函数改成如下:

capital:  
     push cx  
     push si  
change:  
     mov cl, [si]  
     mov ch, 0  
     jcxz ok  
     and byte ptr [si], 11011111B  
     inc si  
     jmp short change  
ok:  
     pop si  
     pop cx  
     ret
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值