前面文章中介绍了如何控制屏幕光标,如何从硬盘读取数据。这种常用的功能,我们希望将其封装成过程调用,类似于高级语言中的函数,这样当我们需要控制光标或者从硬盘读数据时,就不用写大段的重复代码了。

CPU中,执行的指令通过cs:ip来确定。过程调用实际上就是通过calllcall指令来修改ipcs:ip来达到跳转到另一段指令中执行的目的。

call指令通过修改ip来实现过程调用,因为只修改ip,所以被调例程与原例程在同一个代码段内,也称为近调用。处理器在执行call指令时先将call后面的第一条指令的偏移地址压栈,再通过操作数计算出新的ip替换当前ip

lcall指令通过修改cs:ip来实现过程调用,因为同时修改csip,所以被调例程与原例程不在同一个代码段内,也称为远调用。处理器在执行lcall指令时先将cs、ip依次压栈,再用指令中给出的段地址代替cs原有的内容,用指令中给出的偏移地址代替ip原有的内容。

从子例程返回到原例程使用retlret指令。ret指令用栈中的数据修改ip的内容,实现近转移;lret用栈中的数据修改cs:ip,实现远转移。CPU执行ret指令时相当于执行pop ip,执行lret指令时相当于执行pop ippop cs

下面我们通过一些简单的例子来学习一下如何使用这些指令。

call-ret 示例

代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 告诉编译器以16位模式编译,因为BIOS在加载并运行我们的代码时是处于16位实地址模式的
.code16

	movw	$0x7c00, %sp	# 设置堆栈栈顶指针。因为call指令和ret指令的执行依赖于堆栈。
	callw	put_char_A

	jmp	.

 
put_char_A:
	movw	$0xb800, %ax
	movw	%ax, %es
	movw	$'A' | 0x0a00, %es:0
	retw				# 使用ret指令跳回原来的执行流程。

	# .org伪指令指示编译器把位置计数器移动到操作数所指定的位置,这里是将位置计数器移动到510处
	.org	510
	# .word伪指令指示编译器在当前位置写入一个字大小的数据
	.word	0xaa55

编译、反编译

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# as --32 boot.s -o boot.o
# objcopy -O binary -j .text boot.o boot.bin
# objdump -D -b binary -m i386 -Mi8086,suffix boot.bin  > boot.asm
# cat boot.asm 

boot.bin:     file format binary


Disassembly of section .data:

00000000 <.data>:
   0:	bc 00 7c             	movw   $0x7c00,%sp
   3:	e8 02 00             	callw  0x8
   6:	eb fe                	jmp    0x6
   8:	b8 00 b8             	movw   $0xb800,%ax
   b:	8e c0                	movw   %ax,%es
   d:	26 c7 06 00 00 41 0a 	movw   $0xa41,%es:0x0
  14:	c3                   	retw   
	...
 1fd:	00 55 aa             	addb   %dl,-0x56(%di)

13行编译后的指令是e8 02 00,其中e8是操作码,02 00是操作数,转换成正常顺序即00 02。编译器在计算这个操作数的时候先使用标号的汇编地址(该例中为8)减去本指令的汇编地址(该例中为3),再减去3,作为机器指令的操作数。即8 - 3 - 3 = 2。同样,指令在执行时,CPU先用ip当前的值加上指令中的操作数,再加上3,得到偏移地址。然后将call指令之后的第一条指令的地址压入栈中,再使用刚才计算得到的ip替换当前ip,从而完成跳转。因为此时栈中压入的是call后的第一条指令的偏移地址,所以当子例程通过ret返回时,会使用这个地址替换ip。从而使调用例程继续执行后续指令。

调试

启动虚拟机:

1
$ qemu-system-i386 boota.bin -S -s

在另一个终端启动gdb,(配合前面提到的.gdbinit)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# gdb -q
0x0000fff0 in ?? ()
warning: A handler for the OS ABI "GNU/Linux" is not built into this configuration
of GDB.  Attempting to continue with the default i8086 settings.

The target architecture is assumed to be i8086
Breakpoint 1 at 0x7c00

Breakpoint 1, 0x00007c00 in ?? ()
1: x/i $cs*16+$pc
=> 0x7c00:	mov    $0x7c00,%sp
(gdb)

向后执行一条指令:

1
2
3
4
5
(gdb) si
0x00007c03 in ?? ()
1: x/i $cs*16+$pc
=> 0x7c03:	call   0x7c08
(gdb) 

可以看到这里计算出来的地址是0x7c08,当前指令的地址0x7c03,加操作数2,再加3,得到0x7c08。继续执行并查看寄存器内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) si
0x00007c08 in ?? ()
1: x/i $cs*16+$pc
=> 0x7c08:	mov    $0xb800,%ax
(gdb) info registers
eax            0xaa55	43605
ecx            0x0	0
edx            0x80	128
ebx            0x0	0
esp            0x7bfe	0x7bfe
ebp            0x0	0x0
esi            0x0	0
edi            0x0	0
eip            0x7c08	0x7c08
eflags         0x202	[ IF ]
cs             0x0	0
ss             0x0	0
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0

此时ip的内容为0x7c08sp的内容为0x7bfesp初始时我们设置成了0x7c00,在执行call指令时处理器会将call后面一条指令的偏移地址压栈,所以sp的值变成了0x7bfe。我们来查看一下栈中的内容:

1
2
(gdb) x/1xh 0x7bfe
0x7bfe:	0x7c06

0x7c06正好是后面jmp指令的偏移地址。稍后ret指令执行时会将这个偏移地址从栈中弹出到ip,来跳回到原来的执行流程。

向后执行3条指令:

1
2
3
4
5
(gdb) si 3
0x00007c14 in ?? ()
1: x/i $cs*16+$pc
=> 0x7c14:      ret
(gdb)

此时屏幕左上角会打印出字符'A',常规操作就不贴图了。观察上面的输出,下一条要执行的便是ret指令,查看一下此时的寄存器内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
(gdb) info registers
eax            0xb800	47104
ecx            0x0	0
edx            0x80	128
ebx            0x0	0
esp            0x7bfe	0x7bfe
ebp            0x0	0x0
esi            0x0	0
edi            0x0	0
eip            0x7c14	0x7c14
eflags         0x202	[ IF ]
cs             0x0	0
ss             0x0	0
ds             0x0	0
es             0xb800	47104
fs             0x0	0
gs             0x0	0

ip0x7c14,要跳转到的偏移地址还保存在0x7bfe处。执行ret指令,观察结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) si
0x00007c06 in ?? ()
1: x/i $cs*16+$pc
=> 0x7c06:	jmp    0x7c06
(gdb) info registers
eax            0xb800	47104
ecx            0x0	0
edx            0x80	128
ebx            0x0	0
esp            0x7c00	0x7c00
ebp            0x0	0x0
esi            0x0	0
edi            0x0	0
eip            0x7c06	0x7c06
eflags         0x202	[ IF ]
cs             0x0	0
ss             0x0	0
ds             0x0	0
es             0xb800	47104
fs             0x0	0
gs             0x0	0

看到了吗?ip的值已经是0x7c06了,下一条要执行的指令也如我们所愿是jmp了。

call指令的操作数还可以在寄存器或内存中,例如callw *%cxcallw *procedure_address。需要注意的是正如你看到的,寄存器或内存地址前需要加一个*,就好像指针一样。具体代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
.code16

movw $0x7c00, %sp

movw $0x7c00 + put_char_A, %cx

callw *%cx

jmp .

put_char_A:
  movw $0xb800, %ax
  movw %ax, %es
  movw $'A' | 0x0a00, %es:0
  retw

.org 510
.word 0xAA55

lcall-lret 示例

下面来看一个lcall的例子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 告诉编译器以16位模式编译,因为BIOS在加载并运行我们的代码时是处于16位实地址模式的
.code16

	movw	$0x7c00, %sp	# 设置堆栈栈顶指针。因为call指令和ret指令的执行依赖于堆栈。

	# lcall指令的格式为lcall $section, $offset。0x07d0是远调用的代码段地址,0是段内偏移。
	lcallw	$0x07d0, $0

	jmp	.

	.org 0x100	# 使用伪指令.org将位置计数器移动到了0x100处
	# 因为主引导记录是被加载到0x7c00处的,所以标号put_char_A在程序执行时的实际物理地址是0x7c00 + 0x100 = 0x7d00,对应段地址0x07d0,段内偏移0。
put_char_A:
	movw	$0xb800, %ax
	movw	%ax, %es
	movw	$'A' | 0x0a00, %es:0
	lretw				#使用lret指令将栈中保存的段内偏移和段地址依次弹出到ip、cs,恢复原来的执行流程。

	# .org伪指令指示编译器把位置计数器移动到操作数所指定的位置,这里是将位置计数器移动到510处
	.org	510
	# .word伪指令指示编译器在当前位置写入一个字大小的数据
	.word	0xaa55

编译

1
2
# as --32 boot.s -o boot.o
# objcopy -O binary -j .text boot.o boot.bin

调试

启动虚拟机:

1
$ qemu-system-i386 boot.bin -S -s

在另一个终端启动gdb,(配合前面提到的.gdbinit)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# gdb -q
0x0000fff0 in ?? ()
warning: A handler for the OS ABI "GNU/Linux" is not built into this configuration
of GDB.  Attempting to continue with the default i8086 settings.

The target architecture is assumed to be i8086
Breakpoint 1 at 0x7c00

Breakpoint 1, 0x00007c00 in ?? ()
1: x/i $cs*16+$pc
=> 0x7c00:	mov    $0x7c00,%sp

向后执行两条指令,此时已经进入到了子例程,查看寄存器状态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) si 2
0x00000000 in ?? ()
1: x/i $cs*16+$pc
   0x7d00:	mov    $0xb800,%ax
(gdb) info registers
eax            0xaa55	43605
ecx            0x0	0
edx            0x80	128
ebx            0x0	0
esp            0x7bfc	0x7bfc
ebp            0x0	0x0
esi            0x0	0
edi            0x0	0
eip            0x0	0x0
eflags         0x202	[ IF ]
cs             0x7d0	2000
ss             0x0	0
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0

此时已经跳转到了段0x7d0,段内偏移0x0处了。sp也因为cs:ip压入栈中变成了0x7bfc,查看栈中的内容:

1
2
3
(gdb) x/2xh 0x7bfc
0x7bfc:	0x7c08	0x0000
(gdb) 

低地址处是ip 0x7c08,高地址处是cs 0x0000。向后执行4条指令并查看寄存器内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) si 4
0x00007c08 in ?? ()
1: x/i $cs*16+$pc
=> 0x7c08:	jmp    0x7c08
(gdb) info registers
eax            0xb800	47104
ecx            0x0	0
edx            0x80	128
ebx            0x0	0
esp            0x7c00	0x7c00
ebp            0x0	0x0
esi            0x0	0
edi            0x0	0
eip            0x7c08	0x7c08
eflags         0x202	[ IF ]
cs             0x0	0
ss             0x0	0
ds             0x0	0
es             0xb800	47104
fs             0x0	0
gs             0x0	0

可以看到在lret指令执行后,cs恢复成了0x0ip恢复成了0x7c08sp因为ipcs的出栈恢复了初始值0x7c00

lcall的操作数也可以在内存中,例如lcallw *procedure_address。具体代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.code16

movw $0x07c0, %ax
movw %ax, %ds

movw $0x7c00, %sp

lcallw *procedure_address

jmp .

.org 0x100
put_char_A:
  movw $0xb800, %ax
  movw %ax, %es
  movw $'A' | 0x0a00, %es:0
  lretw

procedure_address:
  # 偏移地址, 段地址
  .word 0, 0x07d0

.org 510
.word 0xAA55

参数传递

我们封装过程是为了方便调用,避免写重复的代码。过程调用时通常需要通过传递参数来控制过程的执行,下面我们来讲一讲参数传递时的一些规范和需要注意的地方。

参数传递示例1

 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
# 告诉编译器以16位模式编译,因为BIOS在加载并运行我们的代码时是处于16位实地址模式的
.code16

	movw	$0x7c00, %sp	# 设置堆栈栈顶指针。因为call指令和ret指令的执行依赖于堆栈。

	callw	set_cursor

	jmp	.

# 目的: 设置光标位置为 0
#
# 输入: 无
#
# 输出: 无
set_cursor:
	movw $0x3d4, %dx
	movb $0xe, %al
	outb %al, %dx

	movw $0x3d5, %dx
	movb $0, %al
	outb %al, %dx

	movw $0x3d4, %dx
	movb $0xf, %al
	outb %al, %dx

	movw $0x3d5, %dx
	movb $0, %al
	outb %al, %dx

	retw


	# .org伪指令指示编译器把位置计数器移动到操作数所指定的位置,这里是将位置计数器移动到510处
	.org	510
	# .word伪指令指示编译器在当前位置写入一个字大小的数据
	.word	0xaa55

这个例子中的过程set_cursor,或者称为函数,没有输入,也没有输出。这个函数实际上是没有什么实际用处的,因为它只能将光标位置设置为0,即屏幕左上角。想要这个函数有实际的用处的话就需要给它传递参数,将想要设置的位置作为参数传递给它。传递参数的方式大体上来说有三种:

  1. 通过寄存器传递。即将参数预先放入寄存器中,被调用的函数执行时去这个寄存器中获取参数。
  2. 通过堆栈传递。即调用函数前,先将参数压入栈中,被调函数通过bp寄存器间接寻址,获取堆栈上放置的参数。
  3. 通过寄存器和堆栈传递。即一部分参数放在寄存器中,一部分放在堆栈上。

通过寄存器传递参数很简单,所以我们主要讲解一下通过堆栈传递参数时需要注意的地方。下面看一下改造后的可以接收参数的set_cursor

参数传递示例2

 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
# 告诉编译器以16位模式编译,因为BIOS在加载并运行我们的代码时是处于16位实地址模式的
.code16

	movw	$0x7c00, %sp	# 设置堆栈栈顶指针。因为call指令和ret指令的执行依赖于堆栈。

	pushw	$79		# 将参数79压入栈中,因为一行是80列,从0开始计数,79是第一行的最后一列。
	callw	set_cursor	# 调用set_cursor,注意这里有一个隐含的将ip压栈的操作。
	addw	$2, %sp		# 用于恢复栈顶指针。

	jmp	.

# 目的: 设置光标位置
#
# 输入:
#   参数1 光标所在位置
#
# 输出: 无
set_cursor:
	movw %sp, %bp	# 将当前栈顶指针复制给bp,因为要通过bp间接访问堆栈中的参数。

	movw $0x3d4, %dx
	movb $0xe, %al
	outb %al, %dx

	# 分别通过3(%bp)、2(%bp)访问参数的高8位和低8位。此时bp指向栈顶,从栈顶向上的两个字节保存的是ip,即偏移量为0, 1的两个内存单元,2, 3这两个单元保存的是我们压入栈中的参数。
	movw $0x3d5, %dx
	movb 3(%bp), %al
	outb %al, %dx

	movw $0x3d4, %dx
	movb $0xf, %al
	outb %al, %dx

	movw $0x3d5, %dx
	movb 2(%bp), %al
	outb %al, %dx

	retw		# 通过ret从函数返回,同时将ip出栈,此时堆栈中只剩调用函数之前压入的参数了。


	# .org伪指令指示编译器把位置计数器移动到操作数所指定的位置,这里是将位置计数器移动到510处
	.org	510
	# .word伪指令指示编译器在当前位置写入一个字大小的数据
	.word	0xaa55

编译并调试

1
2
3
# as --32 boot.s -o boot.o
# objcopy -O binary -j .text boot.o boot.bin
# qemu-system-i386 boot.bin -S -s
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# gdb -q
0x0000fff0 in ?? ()
warning: A handler for the OS ABI "GNU/Linux" is not built into this configuration
of GDB.  Attempting to continue with the default i8086 settings.

The target architecture is assumed to be i8086
Breakpoint 1 at 0x7c00

Breakpoint 1, 0x00007c00 in ?? ()
1: x/i $cs*16+$pc
=> 0x7c00:	mov    $0x7c00,%sp

设置sp并查看寄存器内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) si
0x00007c03 in ?? ()
1: x/i $cs*16+$pc
=> 0x7c03:	push   $0x4f
(gdb) info registers
eax            0xaa55	43605
ecx            0x0	0
edx            0x80	128
ebx            0x0	0
esp            0x7c00	0x7c00
ebp            0x0	0x0
esi            0x0	0
edi            0x0	0
eip            0x7c03	0x7c03
eflags         0x202	[ IF ]
cs             0x0	0
ss             0x0	0
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0

此时sp指向0x7c00,将参数压栈并查看寄存器和堆栈内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(gdb) si
0x00007c05 in ?? ()
1: x/i $cs*16+$pc
=> 0x7c05:	call   0x7c0d
(gdb) info registers
eax            0xaa55	43605
ecx            0x0	0
edx            0x80	128
ebx            0x0	0
esp            0x7bfe	0x7bfe
ebp            0x0	0x0
esi            0x0	0
edi            0x0	0
eip            0x7c05	0x7c05
eflags         0x202	[ IF ]
cs             0x0	0
ss             0x0	0
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0
(gdb) x/1dh 0x7bfe
0x7bfe:	79

此时栈中压入一个参数,sp2,指向0x7bfe。调用函数并查看寄存器中的值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(gdb) si
0x00007c0d in ?? ()
1: x/i $cs*16+$pc
=> 0x7c0d:	mov    %sp,%bp
(gdb) info registers
eax            0xaa55	43605
ecx            0x0	0
edx            0x80	128
ebx            0x0	0
esp            0x7bfc	0x7bfc
ebp            0x0	0x0
esi            0x0	0
edi            0x0	0
eip            0x7c0d	0x7c0d
eflags         0x202	[ IF ]
cs             0x0	0
ss             0x0	0
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0
(gdb) x/2xh 0x7bfc
0x7bfc:	0x7c08	0x004f

call指令隐式的将ip压栈,sp2,指向0x7bfc。执行到函数返回,查看寄存器内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) si 14
0x00007c08 in ?? ()
1: x/i $cs*16+$pc
=> 0x7c08:	add    $0x2,%sp
(gdb) info registers
eax            0xaa4f	43599
ecx            0x0	0
edx            0x3d5	981
ebx            0x0	0
esp            0x7bfe	0x7bfe
ebp            0x7bfc	0x7bfc
esi            0x0	0
edi            0x0	0
eip            0x7c08	0x7c08
eflags         0x202	[ IF ]
cs             0x0	0
ss             0x0	0
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0

此时函数已通过ret指令返回,ip被弹出,sp2,恢复到压入参数后的状态。继续执行,将sp恢复到参数压栈前:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) si
0x00007c0b in ?? ()
1: x/i $cs*16+$pc
=> 0x7c0b:	jmp    0x7c0b
(gdb) info registers
eax            0xaa4f	43599
ecx            0x0	0
edx            0x3d5	981
ebx            0x0	0
esp            0x7c00	0x7c00
ebp            0x7bfc	0x7bfc
esi            0x0	0
edi            0x0	0
eip            0x7c0b	0x7c0b
eflags         0x216	[ PF AF IF ]
cs             0x0	0
ss             0x0	0
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0

虽然我们初步实现了功能,但是可以看到有些寄存器的内容也被我们的函数更改了,例如bp。想想一下,如果我们有多个函数需要嵌套调用,每一个函数都需要通过bp访问堆栈中的参数,每一个函数执行完成之后都会修改bp,那么调用函数的过程就无法再使用bp访问自己的参数了。为了解决这个问题,我们需要将函数中被修改的寄存器先保存在堆栈中,函数返回时再恢复被修改过的寄存器。

参数传递示例3

完整的示例

 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
# 告诉编译器以16位模式编译,因为BIOS在加载并运行我们的代码时是处于16位实地址模式的
.code16

	movw	$0x7c00, %sp	# 设置堆栈栈顶指针。因为call指令和ret指令的执行依赖于堆栈。

	pushw	$79		# 将参数79压入栈中,因为一行是80列,从0开始计数,79是第一行的最后一列。
	callw	set_cursor	# 调用set_cursor,注意这里有一个隐含的将ip压栈的操作。
	addw	$2, %sp		# 用于恢复栈顶指针。

	jmp	.

# 目的: 设置光标位置
#
# 输入:
#   参数1 光标所在位置
#
# 输出: 无
set_cursor:
	pushw %bp
	movw %sp, %bp	# 将当前栈顶指针复制给bp,因为要通过bp间接访问堆栈中的参数。

	movw $0x3d4, %dx
	movb $0xe, %al
	outb %al, %dx

	# 分别通过5(%bp)、4(%bp)访问参数的高8位和低8位。偏移量分别比上一个示例中增加了2,因为多压了bp在栈中。
	movw $0x3d5, %dx
	movb 5(%bp), %al
	outb %al, %dx

	movw $0x3d4, %dx
	movb $0xf, %al
	outb %al, %dx

	movw $0x3d5, %dx
	movb 4(%bp), %al
	outb %al, %dx

	# 恢复bp
	movw %bp, %sp
	popw %bp

	retw		# 通过ret从函数返回,同时将ip出栈,此时堆栈中只剩调用函数之前压入的参数了。


	# .org伪指令指示编译器把位置计数器移动到操作数所指定的位置,这里是将位置计数器移动到510处
	.org	510
	# .word伪指令指示编译器在当前位置写入一个字大小的数据
	.word	0xaa55

通常在进入函数和离开函数时都需要保存和恢复bp,即执行下面的指令:

1
2
3
4
5
pushw %bp
movw %sp, %bp

movw %bp, %sp
popw %bp

所以处理器也为我们提供了简化的指令分别对应上面的两组指令:

1
2
3
enterw

leavew

完整的示例戳这里

关于函数调用之后的sp的恢复除了在调用函数中通过add指令恢复外还可以在被调函数中通过ret指令的操作数来恢复。戳这里

最后再给大家一个功能多一点的例子。实现了清屏,设置光标位置,获取光标位置,打印字符,打印字符串等功能。比较完整的演示了函数调用中的参数传递,返回值,嵌套调用等情况。示例的输出如下:

参考文章