一、知识点复习
1.CPU执行`call 标号`时相当于进行:push ip
jmp near ptr 标号
2.三个ret指令:
1)iret指令的功能用汇编语法描述为:
pop ip
pop cs
popf
2)ret指令:
pop ip
3)retf指令:
pop ip
pop cs
3.pushf
的功能是将标志寄存器的值压栈,POPF是从栈中弹出数据,送入标志寄存器中
4.中断过程:发生中断时,CPU的硬件自动利用中断类型码找到中断向量,并用它设置CS和IP。中断过程中的操作有:
1)从中断信息中取得中断类型码
2)标志寄存器的值入栈(因为中断过程中要改变标志寄存器的值,所以先将其保存在栈中)
3)设置标志寄存器的TF和IF的值为0
4)CS的内容入栈
5)IP的内容入栈
6)从内存地址为中断类型码*
4和终端类型码*
4+2的两个字单元中读取中断处理程序的入口地址设置IP和CS
题目及代码
实验一: 实验代码在second.asm中,请补全缺少的代码。最终效果是程序运行,进入work函数的死循环,按下1和2(位于字母区上方,扫描码是2和3)的时候,程序会在某个地方打印按下1和2的次数(至少2位,即显示到99),按下4(扫描码是5)则结束程序。assume cs:code
;函数应该保存用到的寄存器,我为了简化代码并没有这样做,这样做写代码时需要很小心安排寄存器使用
;大家在写的时候请尽量保存使用到的寄存器,简化编程;
;大项目请务必保存,C语言编译完在函数入口也是会保存使用到的寄存器的
;观察程序,体会程序在死循环中是怎么"抽空"执行其他代码的
;操作键为1和2,3是退出
;===========================================
;请补充init、restore、int9、work
;==========================================
stack segment
db 128 dup (0)
stack ends
data segment
dw 0, 0
db 0 ;A 计数器
db 0 ;B 计数器
data ends
code segment
start:
mov ax, stack
mov ss, ax
mov sp, 128;使用申请的栈
call init ;安装新的中断例程
call work ;假装在工作,体会"代码执行地址即cs:ip"切换的基本方式
call restore ;恢复默认中断例程
mov ax, 4c00h
int 21h
;死循环,模拟在干活
work:
mov dh,0
begin:cmp dh,1
jne begin
ret
;es:si目标显存地址,十位在ch,个位在cl
print:
add ch, '0'
add cl, '0'
mov byte ptr es:[si], ch
mov byte ptr es:[si + 1], 21h
mov byte ptr es:[si + 2], cl
mov byte ptr es:[si + 3], 21h
ret
;参数在ax,把十位放到ch,个位放到cl
; ch = (ax / 10) % 10, cl = ax % 10
calculate:
push bx
mov bl, 10
div bl
mov cl, ah
xor ah, ah
div bl
mov ch, ah
pop bx
ret
;按下A键,运行A函数
func1:
inc byte ptr ds:[4]
xor ah, ah
mov al, ds:[4]
call calculate
mov si, 5 * 160 + 40 * 2 ;初始化 指向屏幕第5行第40列
call print
ret
;按下B键,运行B函数
func2:
inc byte ptr ds:[5]
xor ah, ah
mov al, ds:[5]
call calculate
mov si, 6 * 160 + 40 * 2 ;初始化 指向屏幕第6行第40列
call print
ret
;初始化中断例程
init:
mov ax, data
mov ds, ax
mov ax, 0
mov es, ax
cli ;关中断修改中断处理程序
;保存原来的9号中断例程地址
push es:[9*4]
pop ds:[0]
push es:[9*4+2]
pop ds:[2]
;将键盘中断处理程序指向自己编写的例程
mov word ptr es:[9*4],offset int9
mov word ptr es:[9*4+2],cs
sti
mov ax, 0b800h ;指向显存
mov es, ax
ret
;恢复之前的中断例程
restore:
mov ax, 0
mov es, ax
mov ax, data
mov ds, ax
cli
;恢复原来的中断
push ds:[0]
pop es:[9*4]
push ds:[2]
pop es:[9*4+2]
sti
ret
;根据中断引发的键位,暂停work函数,执行对应的操作,然后继续执行work函数
int9:
cli
push ax ;使用了al 所以要保存ax
push bx
push es
in al, 60h
pushf
pushf
pop bx
and bh,11111100b
push bx
popf
call dword ptr ds:[0]
pushf
call dword ptr ds:[0];调用之前的中断处理程序处理一些硬件的问题
;根据按键123来执行操作
cmp al,2
jne s1
call func1
jmp int9ret
s1:cmp al,3
jne s2
call func2
jmp int9ret
s2:cmp al,4
jne int9ret
mov dh,1
int9ret:
pop es
pop bx
pop ax
sti
iret
code ends
end start
实验二:
有A B C三个函数,逻辑大体一样,都是死循环,然后每次循环递增一次计数器并打印,然后调用delay函数延时一秒。现在要求,运行程序后,按下A键,执行A函数,A计数器逐渐递增;按下B则是执行B,B对应的计数器递增;C同样。程序机制大致同实验一,也是通过修改键盘中断例程完成。
额外约束:三个函数用同一个寄存器作为计数器,更进一步除了打印位置不一样,三个函数用的寄存器一致,不得错开使用不同的寄存器。下面是代码:
;示例代码
assume cs:code
stack segment
StackA db 128 dup (0)
StackB db 128 dup (0)
StackC db 128 dup (0)
UniStack db 128 dup (0) ;不能够共用栈
stack ends
data segment
dw 0, 0 ;保存原来的中断例程地址
Current db 0;记录当前时谁正在运行,1A,2B,3C
A_SS_SP dw 0, 0
B_SS_SP dw 0, 0
C_SS_SP dw 0, 0
data ends
code segment
start:
mov ax, stack
mov ss, ax
mov sp, offset UniStack
add sp, 128
call init ;初始化中断例程
;初始化3个函数运行情况,也就是将CS:IP指向对应函数位置
;以及设置正确的栈,避免共用栈
call init_state
lp:
jmp lp
;修改对应data段保存中内容,也就是程序运行状态
;主要是将cs:ip指向对应函数,然后设置正确的栈空间
; ax bx cx dx si di es ds bp ip cs psw
; 0 2 4 6 8 10 12 14 16 18 20 22
init_state:
push ax
push bx
push cx;这段push似乎用不用都无所谓?因为是初始化,所以谈不上保护寄存器的值吧
push bp
mov bp, sp
initA:
mov sp, offset StackB
mov cx, 5 * 160 + 80
mov bx, offset A_SS_SP
mov dx, 0
pushf
pop ax
push ax ;push pwd
push cs ;push cs
mov ax, offset func
push ax ; push ip
push dx; push bp
push ds
push es
push dx; push di 计数器di预设为0
push cx; push si
push dx ;dx
push dx ;cx
push dx ;bx
push dx ;ax
mov ds:[bx], ss
mov ds:[bx + 2], sp
initB:
mov sp, offset StackC
mov cx, 6 * 160 + 80
mov bx, offset B_SS_SP
;init_stack
mov dx, 0
pushf
pop ax
push ax ;push pwd
push cs ;push cs
mov ax, offset func
push ax ; push ip
push dx; push bp
push ds
push es
push dx; push di 计数器di预设为0
push cx; push si
push dx ;dx
push dx ;cx
push dx ;bx
push dx ;ax
mov ds:[bx], ss
mov ds:[bx + 2], sp
initC:
mov sp, offset StackC
add sp, 128
mov cx, 7 * 160 + 80
mov bx, offset C_SS_SP
;init_stack
mov dx, 0
pushf
pop ax
push ax ;push pwd
push cs ;push cs
mov ax, offset func
push ax ; push ip
push dx; push bp
push ds
push es
push dx; push di 计数器di预设为0
push cx; push si
push dx ;dx
push dx ;cx
push dx ;bx
push dx ;ax
mov ds:[bx], ss
mov ds:[bx + 2], sp
Back:
mov sp, bp
pop bp
pop cx
pop bx
pop ax
ret
func:
xor di, di ;di是计数器
_loop:
call delay
inc di
mov ax, di
call calculate
call print
jmp _loop
exit:
cli
mov ax, 0
mov es, ax
mov ax, data
mov ds, ax
push ds:[0]
pop es:[9 * 4]
push ds:[2]
pop es:[9 * 4 + 2]
sti
mov ax, 4c00h
int 21h
;delay函数空循环 0x7fffff 次
delay:
push ax
push cx
mov cx, 7
s1:
mov ax, 0ffffh
s2:
dec ax
jnz s2
dec cx
jnz s1
pop cx
pop ax
ret
;es:si目标显存地址,十位在ch,个位在cl
print:
add ch, '0'
add cl, '0'
mov byte ptr es:[si], ch
mov byte ptr es:[si + 1], 21h
mov byte ptr es:[si + 2], cl
mov byte ptr es:[si + 3], 21h
ret
;参数值放在ax
calculate:
push bx
mov bl, 10
div bl
mov cl, ah
xor ah, ah
div bl
mov ch, ah
pop bx
ret
;初始化中断例程
init:
push ax
mov ax, data
mov ds, ax
mov ax, 0
mov es, ax
cli ;关中断修改中断处理程序
push es:[9 * 4]
pop ds:[0]
push es:[9 * 4 + 2]
pop ds:[2] ;保存原来的9号中断例程地址
mov word ptr es:[9 * 4], offset int9 ; 将键盘中断处理程序指向自己编写的例程
mov es:[9 * 4 + 2], cs
sti
mov ax, 0b800h ;指向显存
mov es, ax
pop ax
ret
;恢复之间的中断例程
restore:
push ax
mov ax, 0
mov es, ax
mov ax, data
mov ds, ax
cli
push ds:[0]
pop es:[9 * 4]
push ds:[2]
pop es:[9 * 4 + 2]
sti
pop ax
ret
; ax bx cx dx si di es ds bp ip cs psw
; 0 2 4 6 8 10 12 14 16 18 20 22 24
; ↑sp此时指向这里
save_all macro
cli
push bp
push ds
push es
push di
push si
push dx
push cx
push bx
push ax
mov bx, offset Current
mov al, ds:[bx]
cmp al, 1
jne cmp2
mov bx, offset A_SS_SP
jmp update_ss_sp
cmp2:
cmp al, 2
jne cmp3
mov bx, offset B_SS_SP
jmp update_ss_sp
cmp3:
cmp al, 3
jne DoNotSave
mov bx, offset C_SS_SP
update_ss_sp:
mov ds:[bx], ss
mov ds:[bx + 2], sp
DoNotSave:
endm
restore_all macro
pop ax
pop bx
pop cx
pop dx
pop si
pop di
pop es
pop ds
pop bp
sti
iret
endm
switch macro
cmp al, 3 ; 3
je swC
cmp al, 2 ; 2
je swB
cmp al, 1 ; 1
je swA
jmp done
swA:
mov bx, offset A_SS_SP
jmp __switch
swB:
mov bx, offset B_SS_SP
jmp __switch
swC:
mov bx, offset C_SS_SP
__switch:
mov bp, offset Current
mov ds:[bp], al
mov ax, ds:[bx]
mov cx, ds:[bx + 2]
mov ss, ax
mov sp, cx
done:
endm
int9:
save_all
in al, 60h
dec al ;从而按下1234 获得的扫描码就是1234
pushf
call dword ptr ds:[0];调用系统自带的例程
switch
cmp al, 5 ; 按5
je stop
jmp int9ret
stop:
call exit
int9ret:
restore_all
code ends
end start
请按照题目思路,分析程序的运行,重点是如何实现栈的切换。
这是我的答案:
我用一个例子来说明一下代码的执行逻辑和流程吧:程序在执行主程序的死循环时按下1切换到A,在A运行的时候又按下2切换到B,接着在B运行的时候按下1回到A。
1) 程序先是把stack段的地址送到段寄存器ss中,然后用mov sp,offset UniStack和add sp,128把栈指针知道UniStack段的栈顶。这是stack段的栈分布图:
UniStack(128个字节) |
StackC(128个字节) |
StackB(128个字节) |
StackA(128个字节) |
2) 然后call init初始化中断例程
3) call init_state初始化StackC、StackB、StackA。这三段栈空间的初始化过程都差不多,不同之处在于sp、cx寄存器的值,cx寄存器用于表示三个函数计数器在显存中的位置。初始化后,StackA段的栈分布图为(StackB、StackC段的相同):
而且这时候data段的A_SS_SP,B_SS_SP,C_SS_SP 三个段的内容为:
4) 然后就是主程序的死循环了
5) 按下1,开始运行int9中断例程,先save_all,这时候我们保存的是主程序的运行状态,不是中断后跳转到的A函数,所以这时候就不分析UniStack段的结构了
6) mov bx,offset Current和mov al,ds:[bx]记录当前被中断的程序,这时候al为0,三个cmp条件都不符合,所以直接跳到DoNotSave,结束这段宏,回到中断例程,然后读取输入+调用例程,接着进入switch段,满足cmp al,1,可以跳到swA 中,mov bx,offset A_SS_SP把A的栈段信息送入bx中保存,然后跳转到__switch:mov bp, offset Current和mov ds:[bp], al这两条指令就是更改Current处的信息,这时候我们就把这里的值更改为1了,表示当前正在运行的函数是函数A。下面这四条指令的作用就是把ss,sp切换到A的栈中。因为这时候ds:[0]和ds:[2]两个地址存的数据就是A_SS_SP中的A栈段的SS和SP信息。
mov ax, ds:[bx]
mov cx, ds:[bx + 2]
mov ss, ax
mov sp, cx
这样我们就实现了栈的切换
7) 然后回到中断例程中执行jmp int9ret指令,也就是执行restore_all段的指令:执行9个pop指令+开中断+iret。我们回到init_state段中可以看到栈顶的第10个位置存的刚好是offset func,也就是func函数的起始地址,第11个就是cs的值,我们第12个就是程序标记字,我们用iret指令刚好能得到这三个值。在这里,A函数是初次被调用,所以它的IP值就是func的起始地址,也就是第一条指令的位置。这样我们就可以开始执行A函数。这时候stack段中,A栈段没什么内容,但是B、C栈段仍保持刚刚初始化的样子。
8) A运行的时候如果按下2,那么它会切换到B函数。切换过程先是把A 函数的PWS,CS,IP压入A的栈段中,然后把其余寄存器压入,A的栈段为:
9)执行save_all段中的mov bx, offset Current和 mov al, ds:[bx]后,al存的数据为1,然后mov bx,offset A_SS_SP 和jmp update_ss_sp,就是用来更新A_SS_SP处的栈地址以及栈顶指针,最后结束宏,回到中断例程中进行切换。从A切换到B的过程与从主函数切换到A的过程类似。
10)B函数运行时,如果按下1,会切换回A函数。B函数状态的保存与A函数的保存过程类似,主要来分析一下A函数的恢复。恢复是在restore_all段里。
11)A是被中断的函数,那么我们在从A切换到B时save_all段里已经push了9个寄存器,注意,在save_all段开始push前,已经把PWS,CS,IP依次入栈了,这时候我们就实现了保存A函数被中断时的IP值。然后我们回到A函数时就可以通过switch段切换栈段,然后在restore_all段里得到这个函数的状态。
12)综上,我们保存被中断函数的状态是在save_all段里。切换栈段是在switch段里,恢复以及运行函数是在restore_all段里。