前言
在不同的语言中,
- 变量存储
- 传递参数
- 返回值
的方式有所不同,这种差异成为语言的调用约定,因为它表述了在调用函数时函数预期得到什么样的数据。 使用最广泛的是C语言调用约定,它也是Linux平台的标准。
知识点
栈
一般,栈顶的地址要小于栈底的地址,所以说栈是向下增长的(向地址减小的方向。)
- 栈寄存器
%esp 总是包含一个指向当前栈顶的指针(地址),无论栈顶在何处。
- 如果我们只想访问栈顶的值,而不想移除它,可以这样:
movl (%esp), %eax - 访问栈顶的下一个值:
movl 4(%esp), %eax
规则
执行函数之前:
- 程序将函数的所有参数按逆序压入栈中。
- 接着,程序发出一条
call 指令,表明程序希望开始执行的函数。 call 指令会做两件事:
- 首先,将下一条指令的地址,即返回地址压入栈中。
- 然后,修改指令指针(
%eip ),以指向函数起始处。
执行函数时:
- 首先,函数通过
push %ebp 指令保存当前的基址指针寄存器%ebp 。
- 基址指针寄存器时一个特殊的寄存器,用来访问函数的参数和局部变量。
- 然后,它会用
movl %esp, %ebp 将栈指针%ebp 复制到%esp
- 这使得你能够把函数参数作为相对于基址指针的固定索引进行访问。
- 接下来,函数为其所需要的局部变量预留栈空间,只需将栈指针向外移动即可实现。
- 例如需要两个字的内存,只需要将栈指针向下移动两个字即可预留空间:
subl $8, %esp ,这样,就可以将栈用于变量存储。
函数执行完毕:
- 将其返回值存储到
%eax - 将栈恢复到调用函数时的状态(移除当前栈帧,并使调用代码的栈帧重新生效)
- 将控制权交给调用它的程序。
- 这是通过
ret 指令实现的,该指令将栈顶的值弹出,并将指令寄存器%eip 设置为该弹出值。
因此,当函数返回给调用它的代码时,必须恢复前一个栈帧。 如果不这样,ret 将无法正常工作,
因此,从函数返回,你必须使用如下指令:
movl %ebp, %esp
popl %ebp
ret
至此,函数调用完成。
- 调用代码可以检查
%eax 中的返回值。 - 调用代码也需要弹出其入栈的所有参数,以将栈指针复位至其原先的位置
- 如果不再需要参数值,可以用
addl 指令将4*参数个数 加到%esp 即可。
关于更详细的C语言调用约定(也称为ABI,即应用程序二进制接口),请查询 System V Application Binary Interface — Intel386 Architecture Processor Supplement
代码示例
#目的: 展示函数如何工作的程序
# 本程序将计算 2^3 + 5^2
#
#主程序所有内容都存储在寄存器中,因此数据段不含任何内容
.section .data
.section .text
.globl _start
_start:
pushl $3 #压入第二个参数
pushl $2 #压入第一个参数
call power
addl $8, %esp #恢复栈指针
pushl %eax #在调用下一个函数前,保存第一个答案
pushl $2
pushl $5
call power
addl $8, %esp
popl %ebx
addl %eax, %ebx
movl $1, %eax
int $0x80
#目的: 用于计算一个数的幂
#
#输入: 第一个参数 底数
# 第二个参数 底数的指数
#
#输出: 以返回值的形式给出结果
#
#注意: 直属必须大于等于1
#
#变量:
# %ebx保存底数
# %ecx保存指数
# -4(%ebp)保存当前结果
# %eax用于暂时存储
.type power, @function
power:
pushl %ebp
movl %esp, %ebp
subl $4, %esp #为本地存储保留空间
movl 8(%ebp), %ebx #第一个参数放入%eax
movl 12(%ebp), %ecx #第二个参数放入%ecx
movl %ebx, -4(%ebp) #存储当前结果
power_loop_start:
cmpl $1, %ecx #如果是1次方,就已经获得结果
je end_power
movl -4(%ebp), %eax #将当前结果放入%eax
imull %ebx, %eax #将当前结果与底数相乘
movl %eax, -4(%ebp) #保存当前结果
decl %ecx
jmp power_loop_start
end_power:
movl -4(%ebp), %eax #返回值移入%eax
movl %ebp, %esp
popl %ebp
ret
|