❤️💕💕汇编语言目前仍在发挥着不可替代的作用,在效率上无可替代,在底层,学习linux内核,计算机外围设备和驱动,都离不开汇编。Myblog:http://nsddd.top
[TOC]
CPU执行call指令时,进行两步操作:
-
将当前的IP或CS和IP压入栈中
-
转移到标号处执行指令
call指令不能实现短转移,除此之外,call指令实现转移的方法和jmp的原理相同。
压栈一次存放2个字节,也就是用2个字节来存放IP或着CS,同时存放IP和CS一共占4个字节。一般都保存在低地址位。
比如:
push IP
此时栈内ss:[0EH]保存IP的数据。
push CS
push IP
此时栈内ss:[0EH]保存IP,ss:[0CH]的数据。
call 标号 (将当前的IP(下一条指令的IP)压栈后,转到标号处执行)
1)(sp) = (sp) - 2
2)((ss) * 16 + (sp)) = IP
注意当call一开始执行时,IP就指向下一条指令了,所以IP的地址为下一条指令的地址。
3)(IP) = (IP) + 16位位移 (不能实现短转移)
- 16位位移 = 标号处的地址 - call指令后的第一个字节的地址;
- 16位位移的范围为-32768 ~ 32767,用补码表示。
- 16位位移由编译器程序在编译时算出。
CPU执行“call 标号”时,相当于:
push IP
jmp near 标号
例:ax的数值?
内存地址 机器码 汇编指令
1000:0 B8 00 00 mov ax, 0
1000:3 E8 01 00 call s //1000H
1000:6 40 inc ax
1000:7 58 s:pop ax
//注意当call一开始执行时IP就指向下一条指令了,所以IP的地址为下一条指令的地址。
ax 为 6
“call far pty 标号”实现的时段间转移。
call far pty 标号
第一步:
(sp) = (sp) - 2
((ss) * 16 + (sp)) = (CS)
(sp) = (sp) - 2
((ss) * 16 + (sp)) = (IP)
第二步:
(CS) = 标号所在的段地址
(IP) = 标号所在的偏移地址
高位存放的是段地址(CS),低位存放的是偏移地址(IP)。
CPU执行“call far ptr 标号”时,相当于进行:
push CS
push IP
jmp far ptr 标号
例:ax中的数值?
内存地址 机器码 汇编指令
1000:0 B8 00 00 mov ax, 0
1000:3 9A 09 00 00 10 call far ptr s //cs = 1000 IP = 0008 压入栈
1000:8 40 inc ax
1000:9 58 s: pop ax //0008H
add ax, ax //0010H
pop bx //1000
add ax, bx //1010H
//注意当call一开始执行时IP就指向下一条指令了,所以IP的地址为下一条指令的地址。
ax = 1010H
指令格式:
call 16位reg
功能:
(sp) = (sp) - 2
((ss) * 16 + sp) = (ip)
(ip) = 16位reg
相当于:
push ip
jmp 16位寄存器
例:ax中的数值?
内存地址 机器码 汇编指令
1000:0 B8 06 00 mov ax, 6
1000:2 FF d0 call ax ip = 0005
1000:5 40 inc ax //跳过
1000:6 mov bp, sp //此时sp指向栈顶
add ax, [bp] //栈顶位置数据 + ax
ax = 11
ret 指令用栈中的数据,修改IP的内容,从而实现近转移。
retf 指令用栈中的数据,修改CS和IP的内容,从而实现远转移。高位为CS(段地址 ),低位为IP(偏移地址)。入栈要先入CS的地址。
CPU执行ret指令时:
(IP) = ((ss) * 16 + (sp))
(sp) = (sp) + 2
相当于:
pop IP
CPU执行retf指令时:
(IP) = ((SS) * 16 + (SP))
(SP) = (SP) + 2
(CS) = ((SS) * 16 + (SP))
(SP) = (SP) + 2
相当于:
pop IP
pop CS
例:
assume cs:code, ss:stack
stack segment
db 16 dup (0)
stack ends
code segment
mov ax, 4c00H
int 21H
start: mov ax, stack
mov ss, ax ;给栈的段地址赋值
mov sp, 16 ;栈顶指针赋值
mov ax, 0 ;ax赋值给0 入栈
push ax
mov bx, 0
ret ;这个地方没有用call,也是可以用ret
code ends
end start
程序中ret拿到的IP的值为0,所以从程序的第一行开始执行。
retf指令
- 先pop IP
- 再pop CS
assume cs:code, ss:stack
stack segment
db 16 dup (0)
stack ends
code segment
mov ax, 4C00H
int 21H
start: mov ax, stack
mov ss, ax
mov sp, 16
mov ax, code
push ax ;先压入cs
mov ax, 0
push ax ;再压入ip:0
mov bx
retf
code ends
end start
程序中IP也是0,回到开头
例: 从内存1000:0000处开始执行程序
assume cs:dode, ss:stack
stack segment
db 16 dup (0)
stack ends
code segment
stack: mov ax, stack
mov ss, ax
mov sp, 16
mov ax, 1000H //设CS = 1000H
push ax
mov ax, 0 //设IP = 0000H
push ax
retf
mov ax, 4c00H
int 21H
code ends
end start
- call word ptr 内存单元地址
相当于:
push ip
jmp word ptr 内存单元地址
例:
mov sp, 10H
mov ax, 0123H
mov ds:[0], ax
call word ptr ds:[0]
(IP) = 0123H (sp) = 0EH
- call dword ptr 内存单元地址
相当于:
push cs
push ip
jmp dword ptr 内存单元
例:
mov sp, 10H
mov ax, 0123H
mov ds:[0], ax
mov word ptr ds:[2], 0
mov dword ptr ds:[0]
(cs) = 0000H (sp) = 0CH (ip) = 0123H
例1:ax的数值为多少?
assume cs:code
stack segment
dw 8 dup (0)
stack ends
code segment
start: mov ax, stack
mov ss, ax
mov sp, 16
mov ds, ax
mov ax, 0
call word ptr ds:[0EH]
inc ax
inc ax
inc ax
mov ax, 4c00H
int 21H
code ends
end start
ax = 3
执行call时,此时IP的值已经指向下一条地址了也就是,(IP)存储在栈中的位置等于下一个内存单元的地址。此时跳转到ds:[0EH]保存的偏移地址就是放在栈中的IP地址所以,call跳转到下一条指令,所以程序按顺序执行。
- CPU取指令 : (call word ptr ds:[0EH])
- ip自增上述指令的长度 , 指向了下一条指令 (inc ax)
- 开始执行该指令 3.1. push ip ; 将 ip 压入栈 , 也就是:ss:[0EH] 保存 ip 的低 8 位 , ss:[0FH] 保存高 8 位 3.2. jmp ds:[0EH] ( (ip) = ds:[0EH] , 也就是说 , 程序又从 ds:[0EH] 中取出数据赋值给 ip , 然后继续执行 )
- 现在其实就开始执行 ip 之前保存的地址的指令了 , 也就是三个 inc ax
- 因此最终 ax 值为 3
例2:ax 和 bx的数值为多少?
assume cs:code
data segment
dw 8 dup (0)
data ends
code segment
start: mov ax, data
mov ss, ax
mov sp, 16
mov word ptr ss:[0], offset s
mov ss:[2], cs
call dword ptr ss:[0] ;跳转位置 cs = 代码段开头, ip = s的位置
nop
s: mov ax, offset s
sub ax, ss:[0Ch] ;0
mov bx, cs ;0
sub bx, ss:[0EH]
mov ax, 4C00H
int 21H
code ends
end start
ss:[0CH] 处保存的是 : call dword ptr ss:[0] 这条指令的下一条指令 nop 的相对于代码段偏移地址 ax = (mov ax, offset second 的偏移地址) - ( nop 的偏移地址) 也就是 nop 指令的长度 , 也就是 1。(0CH存放的就是call位置的下一条地址的偏移位置)
将代码段的基址赋值给 bx ,bx = (bx) - (ss:[0EH]),(ss:[0EH]) = (cs)因此 bx = 0。
例:bx的值?
assume cs:code
code segment
start: mov ax, 1
mov cx, 3
call s ;到子程序s执行
mov bx, ax
mov ax, 4c00H
int 21H
s: add ax, ax
loop s
ret
code ends
end start
分析:
1)CPU将call s指令的机器码读入,IP指向call s后的指令mov bx, ax,然后CPU执行call s指令,将当前的IP值(指令mov bx, ax 的偏移地址)压栈,并将IP的值改变位标号s处的偏移地址。
2)CPU从标号s处开始执行指令,loop循环完毕后,(ax) = 8。
3)CPU将ret指令的机器码读入,IP指向了ret指令后的内存单元,然后CPU执行ret指令,从栈中弹出一个值(即 call s 先前压入的 mov bx, ax指令的偏移地址)送入IP中。则CS:IP指向 mov bx, ax。
4) CPU从mov bx, ax开始执行指令,直至完成。
例:
源程序和内存中情况(1000:0000装入)
assume cs:code
stack segment
db 8 dup (0) 1000:0000 00, 00, 00, 00, 00, 00, 00, 00
db 8 dup (0) 1000:0008 00, 00, 00, 00, 00, 00, 00, 00
stack ends
code segment
start: mov ax, stack 1001:0000 BE 00 10
mov ss, ax 1001:0003 8E D0
mov sp, 16 1001:0005 BC 10 00
mov ax, 1000 1001:0008 B8 E8 03
call s 1001:000B E8 05 00
mov ax, 4c00H 1001:000E B8 00 4C
int 21H 1001:0011 CD 21
s: add ax, ax 1001:0013 03 C0
ret 1001:0015 C3
code ends
end strat
CPU读入call s指令后,程序跳转到s处,并把mov ax, 4C00H处的指令保存到栈中,然后执行从s处开始执行,当CPU读入ret指令后,ret跳转栈中存放的IP偏移地址所指向的位置,即mov ax, 4C00H 处。
根据call和ret可以写一个具有一定功能的程序段,我们称其为子程序,需要的时候,用call指令转去执行,可以在子程序的后面使用ret指令,从而跳转到call指令后面的代码处继续执行。
利用call和ret来实现子程序的机制。子程序框架:
标号:
指令
ret
程序框架:
assume cs:code
code segment
main: :
:
call sub1
:
:
mov ax, 4C00H
int 21H
sub1: :
:
call sub2
:
:
ret
sub2: :
:
ret
code ends
end main
例:计算data段中第一组数据的3次方,结果保存在后面一组数据中。
assume cs:code
data segment
dw 1, 2, 3, 4, 5, 6, 7, 8
dd 0, 0, 0, 0, 0, 0, 0, 0
data ends
code segment
start: mov ax, data
mov ds, ax
mov si, 0
mov di, 16
mov cx, 8
s1: mov bx, ds:[si]
mov dx, ax
call s
mov ds:[di], ax //保存低位
mov ds:[di].2, dx //保存高位
add si, 2 //后移
add di, 4 //后移
loop s1
mov ax, 4C00H
int 21H
s: mov ax, bx //(不能用dx)
mul bx
mul bx
ret
code ends
end start
寄存器的数量终究有限,我们可以将批量数据放到内存中,然后将它们所在内存空间的首地址放在寄存器中,传递给需要的子程序。对于具有批量数据的返回结果,也可以用同样的方法。
例:将一个全是字母的字符串转化为大写。
assume cs:code
data segment
db 'conversation'
data ends
code segment
start: mov ax, data
mov ds, ax
mov cx, 12
mov si, 0
call s
mov ax, 4C00H
int 21H
s1: and byte ptr ds:[si]:11011111B //转换,传递内存单元的首地址
inc si
loop s1
ret
code ends
end start
除了用寄存器传递参数外,还有一种通用的方法是用栈来传递参数。
例:将一个全是字母,以0结尾的字符串,转换为大写。
assume cs:code
data segment
db 'conversation', 0
data ends
code segment
start: mov ax, data
mov ds, ax
mov si, 0
call s
mov ax, 4C00H
int 21H
s: mov cl, ds:[si]
mov ch, 0
jcxz ok
and byte ptr ds:[si], 11011111B
inc si
jmp short s //死循环
ok: ret
code ends
end start
例:
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 cx, 4
mov bx, 0
s: mov si, 0
call s1
add bx, 5
pop cx
loop s
mov ax, 4C00H
int 21H
s1: push cx
mov cl, ds:[bx + si]
mov cH, 0
jcxz s0
and byte ptr ds:[bx + si], 11011111B
inc si
jmp short s1
s0: ret
code ends
end atart
我们希望:
1)编写调用子程序的程序的时候,不必关心子程序到底使用了那些寄存器;
2)编写子程序的时候不必关系调用者使用了那些寄存器。
3)不会发生寄存器冲突。
解决这个问题的简捷方法是,在子程序的开始将子程序种所有用到的寄存器种的内容都保存起来,在子程序返回前再恢复,或不再子程序中改变寄存器的值。
例.显示字符串
在指定位置,用指定颜色,显示一个用0结束的字符串。
(dh)= 行号(0 ~ 24), dl = 列号(0 ~ 79), (cl) = 颜色。
在屏幕第8行,第3列,用绿色显示data段中的字符串。0 ~ 79 个字符,一个字符1字节加上1字节的颜色。
第8行相当于向下7个160个字节的位置,第3列相当于向右3个字符,即3 * 2个字节的位置。
显存存放,低位字符,高位颜色, 所以一共占2个字节。 字符一个字节,颜色一个字节。
assume cs:code
data segment
db 'Welcome to masm!', 0
data ends
code segment
start: mov dh, 8
mov dl, 3
mov cl, 2
mov ax, data
mov ds, ax
mov si, 0
call show_str
mov ax, 4C00H
int 21H
show_str: //保存用到的寄存器
push cx
push si
push es
push di
push bx
mov ax, 0B800H //显存的起始地址(每台电脑不一定一样)
mov es, ax
mov di, 0
//计算行开始的位置
mov al, 0A0H //一行160个字节, 80 * 2 = 160
dec dh //第8行,0-7 也就是7的位置
mul dh //一共dh行
mov bx, ax
//计算列开始的位置
mov al, 2 //一列
dec dl //列也是 从零开始
mul dl //一个字符2个字节
add bx, ax //开始的偏移位置
mov al, cl //保存颜色
s: mov cl, ds:[si]
mov ch, 0
mov es:[bx + di], ds:[si] //显示字符串
mov es:[bx + di], al //显示颜色
jcxz ok //判断是否为零
inc si
add di, 2 //移动2个字节,即一个字符
jmp short s
ok: //取回保存的值,注意出栈顺序
pop bx
pop di
pop es
pop si
pop cx
ret
code ends
end start