跳至主要內容

汇编语言一发入魂 0x08 - 过程调用中的参数传递

未央大约 6 分钟汇编语言过程调用

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

先来看一个例子:

示例一

.code16

movw $0x7c00, %sp

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
.word 0xAA55

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

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

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

示例二

代码

.code16

movw $0x7c00, %sp

pushw $79
callw set_cursor
addw $2, %sp

jmp .

# 目的: 设置光标位置
#
# 输入:
#   参数1 光标所在位置
#
# 输出: 无
set_cursor:
  movw %sp, %bp

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

  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

.org 510
.word 0xAA55

解释

5行将参数79压入栈中,因为一行是80列,从0开始计数,79是第一行的最后一列。

6行调用set_cursor,注意这里有一个隐含的将ip压栈的操作。

7行用于恢复栈顶指针。

18行将当前栈顶指针复制给bp,因为要通过bp间接访问堆栈中的参数。

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

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

调试

$ as --32 bootb.s -o bootb.o
$ objcopy -O binary -j .text bootb.o bootb.bin
$ qemu-system-i386 bootb.bin -S -s
$ gdb -q
Breakpoint 1, 0x00007c00 in ?? ()
1: x/i $cs*16+$pc
=> 0x7c00:      mov    $0x7c00,%sp
(gdb)

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

(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,将参数压栈并查看寄存器和堆栈内容:

(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
(gdb)

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

(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
(gdb)

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

(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恢复到参数压栈前:

(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访问自己的参数了。为了解决这个问题,我们需要将函数中被修改的寄存器先保存在堆栈中,函数返回时再恢复被修改过的寄存器。

来看看完整的示例:

示例三

代码

.code16

movw $0x7c00, %sp

pushw $79
callw set_cursor
addw $2, %sp

jmp .

# 目的: 设置光标位置
#
# 输入:
#   参数1 光标所在位置
#
# 输出: 无
set_cursor:
  pushw %bp
  movw %sp, %bp

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

  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

  movw %bp, %sp
  popw %bp

  retw

.org 510
.word 0xAA55

解释

18、19行先将bp保存在栈中,然后将当前栈顶指针复制到bp

26、34行的偏移量分别比上一个示例中增加了2,因为多压了bp在栈中。

37、38行恢复bp

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

pushw %bp
movw %sp, %bp

movw %bp, %sp
popw %bp

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

enterw

leavew

完整的示例戳这里open in new window

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

最后再给大家一个功能多一点的例子open in new window。实现了清屏,设置光标位置,获取光标位置,打印字符,打印字符串等功能。比较完整的演示了函数调用中的参数传递,返回值,嵌套调用等情况。示例的输出如下,有兴趣的小朋友可以自己研究研究。

boote
boote