AT&T 汇编-10: 使用函数(1)

很多时候一段实现特定功能的代码可以复用,循环就是一个例子,代码复用更常见的形式是函数。

# func.s

.section .rodata
msg:
   .ascii   "This is a message.\n"

.section .text

.type print_msg, @function
print_msg:
   pushl %eax
   pushl %ebx
   pushl %ecx
   pushl %edx

   movl  $4,    %eax
   movl  $1,    %ebx
   movl  $msg,  %ecx
   movl  $19,   %edx
   int   $0x80

   popl  %edx
   popl  %ecx
   popl  %ebx
   popl  %eax

   ret

.globl _start
_start:
   call  print_msg

   movl  $1,   %eax
   movl  $0,   %ebx
   int   $0x80

第 9 行使用 .type 声明了一个标签 print_msg,说明该标签是函数的开始。11-27 行是函数主体,使用 4 号系统调用 write() 打印字符串,最后使用指令 ret 表示函数结束。ret 指令告诉程序返回调用该函数的位置,并从该位置的下一条指令继续执行。

第 11-14 行把 eax 等几个寄存器的值压到栈里保存起来,打印完字符串后在第 22-25 行再按照入栈的相反次序恢复它们原来的值。因为寄存器是全局可见的,在 16-20 行打印字符串的过程中对这几个寄存器的值进行了修改,而调用者可能使用了这些寄存器中的一个或几个,如果不先保存这些值,那么执行完函数后再回到原来的函数就有可能出错。

第 21 行使用 call 指令调用函数 print_msg。call 指令的一般格式是:

call function

执行到 call 指令时,系统把当前位置压入栈中,然后转到 function 所在的位置继续执行,遇到 ret 指令后返回调用的位置,然后从下一条指令继续往下执行。要注意的是虽然定义了 print_msg 是一个函数标签,但是系统不知道函数到底到哪里结束,只有遇到 ret 指令才表示函数结束,返回调用的位置,否则会一直往下执行,如果把 17 行的 ret 指令去掉,程序会继续往下执行进入 _start 区域,然后又执行 call 指令跳到 print_msg……这样会形成死循环。

函数可以放在被调用部分之前或之后,当执行 call 指令时连接器会查找对应函数标签的定义。如果把函数定义放在不同的文件内,则需要把函数标签生命为 .globl,并且最终编译时需要链接由使用到的函数所在的文件生成的 .o 文件。

源文件 print_msg.s 定义了一个函数 print,使用系统调用 write() 打印一个字符串:

# print_msg.s

.section .rodata
msg:
   .ascii   "This is a message.\n"

.section .text
.type print, @function
.globl print
print:
   movl  $4,    %eax
   movl  $1,    %ebx
   movl  $msg,  %ecx
   movl  $19,   %edx
   int   $0x80

   ret

在另一个文件 main.s 中调用了在 print_msg.s 中定义的函数 print:

# main.s

.section .text
.globl _start
_start:
   call  print

   movl  $1,   %eax
   movl  $0,   %ebx
   int   $0x80

编译的时候可以分别汇编两个源文件,生成 .o 文件后再链接生成可执行文件 a.out:

as print_msg.s -o print_msg.o
as main.s -o main.o
ld print_msg.o main.o

也可以把多个文件汇编成一个 .o 文件,再由该 .o 文件生成可执行文件:

as print_msg.s main.s -o all.o
ld all.o

上面的程序是一种比较简单的情况:没有参数也没有返回值。给函数传递参数一般有 3 种方法:全局变量,寄存器和栈。相应地,返回值也可以使用这 3 种方法。

使用全局变量

# func-global-arg.s

.section .rodata
msg:
   .asciz   "The result is: %d.\n"

.section .data
result:
   .int  0

.section .text
.globl main
main:
   movl  $5,   result
   call  add5

   pushl result
   pushl $msg
   call  printf
   addl  $8,   %esp

   movl  $16,  result
   call  add5

   pushl result
   pushl $msg
   call  printf
   addl  $8,   %esp

   pushl $0
   call  exit

.type add5, @function
add5:
   addl  $5,   result
   ret

第 33 行开始定义了一个函数 add5,功能是对全局变量的值加 5。第 14 行和 22 行分别给 result 赋值,接下来再调用 add5 将全局变量的值加 5,最后在主程序打印结果。全局变量比寄存器和栈要节省开销,但是不利于函数的独立性,而且难以维护。

使用寄存器

# func-register-arg.s

.section .rodata
msg:
   .asciz   "The value is: %d.\n"

.section .text
.globl main
main:
   movl  $5,   %eax
   call  pfunc

   movl  $12,  %eax
   call  pfunc

   pushl $0
   call  exit

.type pfunc, @function
pfunc:
   # TODO save the value of register(s) that may be changed in this function

   pushl %eax
   pushl $msg
   call  printf
   addl  $8,   %esp

   # TODO restore the original value(s)

   ret

在主程序里给 eax 赋不同的值,然后调用函数 pfunc 打印。

和全局变量相似,函数也可以访问 eax 等寄存器,所以在进入函数前需要先把会改变的寄存器的值保存起来,在退出函数时再恢复,就像开头介绍的那样。

使用栈

自己定义的函数可以使用任何方式使用栈的参数,调用函数时只要按照函数定义的入栈顺序传递参数就可以了。

# func-stack-arg.s

.section .rodata
msg:
   .asciz   "The value is: %d.\n"

.section .text
.globl main
main:

   pushl $5
   call  sfunc
   addl  $4,   %esp

   pushl $0
   call  exit

.type sfunc, @function
sfunc:
   pushl %ebp
   movl  %esp, %ebp

   # TODO save the value of register(s) that may be changed in this function

   pushl 8(%ebp)
   pushl $msg
   call  printf
   # addl  $8,   %esp

   # TODO restore the original value(s)

   movl  %ebp, %esp
   popl  %ebp

   ret

执行 call 指令之后,栈的情况如图所示:

              +----------------+
              |      ...       |
              +----------------+
              |      argn      |
              +----------------+
              |      ...       |
              +----------------+
              |      arg2      |
              +----------------+
     ^        |      arg1      |
     |        +----------------+
   memory     | return address |  <-- esp
  addresses   +----------------+

在函数中还有可能把参数压入栈中的情况,esp 的值会随之改变。为了避免这个问题,可以把 esp 的值赋给寄存器 ebp(在赋值前先把 ebp 的值也保存在栈中),后续对参数的访问可以通过 ebp 来完成,而不受 esp 的影响。一般可以使用下面的函数模板:

func:
   pushl %ebp
   movl  %esp, %ebp

   ...

   movl  %ebp, %esp
   popl  %ebp
   ret

执行完开头两条指令后栈的状态为:

              +----------------+
              |      ...       |
              +----------------+
              |      argn      |
              +----------------+
              |      ...       |
              +----------------+
              |      arg2      |  <-- 12(%ebp)
              +----------------+
              |      arg1      |  <-- 8(%ebp)
              +----------------+
      ^       | return address |  <-- 4(%ebp)
      |       +----------------+
    memory    |    old  ebp    |  <-- esp
   addresses  +----------------+

当前的 esp 值保存在 ebp 中,8(%ebp) 为第一个参数的位置……以此类推。

第 25-27 行打印了主程序通过栈传递进来的值。对于这个函数来说,第 26 行恢复 esp 的位置可有可无,因为在接着的第 32 行会把 esp 恢复到执行完第 21 行指令的位置。当然如果打印完之后还有别的压栈操作,最好还是把 esp 恢复到打印前的位置。

函数自己的局部变量也可以使用栈来保存,并通过 ebp 加上相应的偏移量来访问。

发表于 2011年6月23日
  1. solq
    2012年6月17日 01:46 | #1

    哥们,写得不错啊。。。请问你用的是什么工具写的??

发表评论

XHTML: 您可以使用这些标签: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>