函数补充

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
MyStack segment stack
db 256 dup(?)
MyStack ends

MyData segment
MY_MSG1 db "Hello World1!",0dh,0ah,'$'
MY_MSG2 db "Hello World2!",0dh,0ah,'$'
MyData ends

MyCode2 segment
call far ptr MY_SUB
MY_SUB:
push bp
mov bp,sp ;保存栈底

mov ax,[bp+6]
sub ax,[bp+8]

pop bp ;恢复栈底
retf 4
MyCode2 ends

MyCode segment
MAIN:
mov ax,MyData
mov ds,ax
mov es,ax

;段间子程序调用
mov ax,1
push ax
mov ax,2
push ax
call far ptr MY_SUB

mov ax,09h ;参数2
push ax
mov ax,offset MY_MSG1 ;参数1 从右往左边入栈
push ax
call SHOW_HELLO
; add sp 4 ;stdcall调用约定

mov ax,09h
push ax
mov ax,offset MY_MSG2
push ax
call SHOW_HELLO
; add sp 4

mov ax,2
push ax
mov ax,1
push ax
call MY_ADD

mov ax,4c00h
int 21h

SHOW_HELLO:
push bp
mov bp,sp
push ax ;保存环境
push cx
push dx

mov cx,0
mov dx,word ptr [bp+4] ;访问参数1
mov ah,byte ptr [bp+6] ;访问参数2
int 21h

pop dx
pop cx
pop ax ;恢复环境
; pop ax
; jmp ax ==> pop ip

; pop ax
; add sp,4
; push ax
; ret
pop bp
retn 4 ;mov ip,[sp] add sp,4 c调用约定

MY_ADD:
push bp
mov bp,sp ;保存栈底
sub sp,4 ;申请局部变量
push bx

mov ax,[bp+4]
mov [bp-2],ax ;第1个局部变量
add ax,[bp+6]
mov [bp-4],ax ;第2个局部变量

;返回值放在寄存器 ax
pop bx
mov sp,bp ;释放局部变量空间
pop bp ;恢复栈底
retn 4 ;int 3 iret

MyCode ends

end MAIN

代码解析

子程序段

1
2
3
4
5
6
7
8
9
10
11
12
MyCode2 segment
call far ptr MY_SUB
MY_SUB:
push bp
mov bp,sp ; 保存栈底

mov ax,[bp+6] ;远调用的参数偏移从6开始
sub ax,[bp+8]

pop bp ; 恢复栈底
retf 4
MyCode2 ends
  • MyCode2 segment 声明一个代码段。
  • call far ptr MY_SUB 进行远调用,跳转到 MY_SUB 子程序。(虽然MY_SUB在同一个段,但因为其他段也要调用MY_SUB,故MY_SUB中对参数的调用按照远调用的格式来写,所以此处也采用远调用,保证格式的一致)
  • MY_SUB: 标识子程序的开始。
  • push bp 保存基指针寄存器 bp 的当前值。
  • mov bp,sp 将栈指针寄存器 sp 的值复制到基指针寄存器 bp 中,保存当前栈帧。
  • mov ax,[bp+6] 将偏移 bp 加上 6 的位置处的值移动到 ax 寄存器中。
  • sub ax,[bp+8]ax 寄存器的值减去偏移 bp 加上 8 的位置处的值。
  • pop bp 恢复栈底,弹出先前保存的 bp 值。
  • retf 4 远返回并清除4个字节的参数。
  • MyCode2 ends 表示代码段结束。

主代码段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
MyCode segment
MAIN:
mov ax,MyData
mov ds,ax
mov es,ax

; 段间子程序调用
mov ax,1
push ax
mov ax,2
push ax
call far ptr MY_SUB

mov ax,09h ; 参数2
push ax
mov ax,offset MY_MSG1 ; 参数1,从右往左边入栈
push ax
call SHOW_HELLO
; add sp 4 ; stdcall 调用约定

mov ax,09h
push ax
mov ax,offset MY_MSG2
push ax
call SHOW_HELLO
; add sp 4

mov ax,2
push ax
mov ax,1
push ax
call MY_ADD

mov ax,4c00h
int 21h
  • MyCode segment 声明代码段。
  • MAIN: 标识程序的入口点。
  • mov ax,MyData 将数据段地址加载到 ax 寄存器。
  • mov ds,ax 设置数据段寄存器 ds
  • mov es,ax 设置附加段寄存器 es

接下来的代码进行段间子程序调用:

  • mov ax,1 将数值1加载到 ax 寄存器。
  • push axax 寄存器的值压入栈。
  • mov ax,2 将数值2加载到 ax 寄存器。
  • push axax 寄存器的值压入栈。
  • call far ptr MY_SUB 调用远子程序 MY_SUB。(因为MY_SUB在另一个段,故要采用远调用)

调用显示消息的子程序:

  • mov ax,09h 将参数2加载到 ax
  • push ax 将参数2压入栈。
  • mov ax,offset MY_MSG1 将消息1的偏移地址加载到 ax
  • push ax 将参数1压入栈。
  • call SHOW_HELLO 调用 SHOW_HELLO 子程序。

重复调用 SHOW_HELLO 显示第二条消息:

  • mov ax,09h 将参数2加载到 ax
  • push ax 将参数2压入栈。
  • mov ax,offset MY_MSG2 将消息2的偏移地址加载到 ax
  • push ax 将参数1压入栈。
  • call SHOW_HELLO 调用 SHOW_HELLO 子程序。

调用加法子程序:

  • mov ax,2 将数值2加载到 ax
  • push ax 将数值2压入栈。
  • mov ax,1 将数值1加载到 ax
  • push ax 将数值1压入栈。
  • call MY_ADD 调用 MY_ADD 子程序。

结束程序:

  • mov ax,4c00h 将程序终止返回码加载到 ax
  • int 21h 调用DOS中断终止程序。

子程序 SHOW_HELLO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SHOW_HELLO:
push bp
mov bp,sp
push ax ; 保存环境
push cx
push dx

mov cx,0
mov dx,word ptr [bp+4] ; 访问参数1(参数偏移固定从4开始)
mov ah,byte ptr [bp+6] ; 访问参数2
int 21h

pop dx
pop cx
pop ax ; 恢复环境
pop bp
retn 4 ; c调用约定返回

mov bp,sp放到保护环境前,则访问参数时的偏移固定从+4开始,就不用考虑压栈了多少个寄存器

子程序 MY_ADD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
MY_ADD:
push bp
mov bp,sp ; 保存栈底
sub sp,4 ; 申请局部变量
push bx

mov ax,[bp+4]
mov [bp-2],ax ; 第1个局部变量,偏移固定从-2开始
add ax,[bp+6]
mov [bp-4],ax ; 第2个局部变量

; 返回值放在寄存器 ax
pop bx
mov sp,bp ; 释放局部变量空间
pop bp ; 恢复栈底
retn 4 ; 返回并清除4个字节的参数
  • MY_ADD: 标识子程序的开始。
  • push bp 保存基指针寄存器 bp 的当前值。
  • mov bp,sp 将栈指针寄存器 sp 的值复制到基指针寄存器 bp 中,保存当前栈帧。
  • sub sp,4 分配4个字节的局部变量空间。
  • push bx 保存 bx 寄存器的值。

处理加法运算:

  • mov ax,[bp+4] 将第一个参数(第一个加数)加载到

ax 寄存器。

  • mov [bp-2],axax 的值存储到第一个局部变量中。
  • add ax,[bp+6] 将第二个参数(第二个加数)加到 ax 寄存器中。
  • mov [bp-4],ax 将结果存储到第二个局部变量中。

恢复寄存器和返回值:

  • pop bx 恢复 bx 寄存器的值。
  • mov sp,bp 释放局部变量空间。
  • pop bp 恢复栈底。
  • retn 4 返回并清除4个字节的参数。

调试

远调用会把跳转的地址显示出来

image-20240807225345166

将要跳转回来的地址都存进堆栈

image-20240808001209747

返回后CS和IP的值都被改变,正是刚才被压入堆栈的地址

image-20240808001034583

运行完减法函数后ax会保存返回值(2-1=1)

image-20240807225521818

运行完加法函数(1+2=3)

image-20240807225629507

理论补充

子程序指令

  • 子程序是完成特定功能的一段程序
  • 当主程序(调用程序)需要执行这个功能时,采用call调用指令转移到该子程序的起始处执行
  • 当运行完子程序功能后,采用RET返回指令回到主程序继续执行

子程序调用指令

  • CALL指令分为4种类型(类似JMP)

    1
    2
    3
    4
    call label			;段内调用、直接寻址
    call r16/m16 ;段内调用、间接寻址
    call far ptr label ;段间调用、直接寻址
    call far ptr mem ;段间调用、间接寻址
  • CALL指令需要保存返回地址:

    image-20240807230427937

子程序返回指令

  • 根据段内和段间、有无参数,分为4种类型

    1
    2
    3
    4
    RET			;无参数段内返回
    RET i16 ;有参数段内返回
    RETF ;无参数段间返回
    RETF i16 ;有参数段间返回
  • 需要弹出CALL指令压入堆栈的返回地址

    image-20240807230816891

中断指令

  • 中断(Interrupt)是又一种改变程序执行顺序的方法
  • 中断具有多种中断类型
  • 中断的指令有3条:INT i8 IRET INTO
  • 本节主要掌握类似子程序调用指令的中断调用指令INT i8,进而学习使用DOS功能调用
1
2
3
4
5
INT i8	;中断调用指令:产生i8好=号中断
IRET ;中断返回指令:实现中断返回
INTO ;溢出中断指令
;若溢出标志OF=1,产生4号中断
;否则顺序执行

字符串输出的功能调用

  • DOS功能调用INT 21H

    image-20240807233434301

  • 可以输出回车(0DH)和换行(0AH)字符产生回车和换行的作用

image-20240807233659737

  • DOS功能调用INT 21H

    image-20240807234351246

  • 执行该功能调用时,用户按键,最后用回车确认

  • 本调用可执行全部标准键盘编辑命令;用户按回车键结束输入,如按Ctrl+Break或Ctrl+C则中止

字符输出的功能调用

  • 显示器功能调用INT 10H

image-20240807233843817

按键判断的功能调用

  • DOS功能调用INT 21H

image-20240807234809571

  • 键盘功能调用INT 16H

    image-20240807234922451

  • 这两个功能调用都不循环等待按键,即使有键按下,键盘缓冲区仍然保留键值并且没有被清空,必要时必须用字符输入功能取走键值清空缓冲区