第四章 函数的工作原理
1、函数的组成部分
函数主要由以下几个成分组成:函数名、函数参数、局部变量、静态变量、全局变量、返回地址、返回值
(1)函数参数及几个变量:这是在逻辑上对函数的涉及到的数据进行规划,实际上当前运行的指令只能通过直接、间接、立即数三种方式访问数据。
(2)返回地址:在汇编语言中,实际上是某个指令的地址,即IP寄存器、程序段标号等存储或代表的地址。
(3)返回值:程序只能有一个返回值,具体表现为该返回值:存储在某个寄存器中,存储在某个内存单元中。
2、C调用约定的汇编语言函数
1、关于栈
在内存的具体实现中,“栈底”位于“高地址”区域,“栈顶”位于“低地址”区域,其中%ess为栈的段基址,%esp为栈顶指针(注意:栈顶非空)。当进行“压栈”操作时,%esp的值由大到小变化;当进行“弹栈”操作时,%esp的值由小到大变化。
同时,由于当前机器为32 位机,因此,每次%esp都会跨过4 byte。
2、关于如何调用函数
C约定的汇编调用实际上是利用“栈”暂存信息:被调用函数的地址、被调用函数的参数、调用函数的地址。当前执行的程序调用某个函数时,进行如下操作:
(0)为了防止寄存器中的数据被破坏,在进入“被调用函数”之前,需要在当前执行的函数中,将所有寄存器的值压栈保存。待返回当前函数时,再重新加载这些值,即常说的“恢复现场”。
(1)逆序将被调用函数的“参数”压栈:如函数fun(para 1, para 2, para 3, para 4,....),则para 4最先入栈,para 1最后入栈。此时栈顶元素为para 1。
(2)将当前的IP地址压栈:该地址为返回地址,当被调用函数结束执行后,利用ret(return的缩写)指令,返回到调用函数
(3)将当前%ebp(基址指针寄存器)值压栈
(4)movl %esp , %ebp:此时,正如“基址指针寄存器”表明的,可以通过%ebp来根据“基址寻址”方式对“被调用函数”中的局部变量进行访问。如:-8(%ebp),在x86架构中,采用%ebp进行基址寻址较使用其他寄存器快。
(5)修改%esp值,为“被调用函数”开辟栈空间,存储局部变量——可以得到被调函数的局部变量空间的范围为:%ebp~%esp。
#示例:演示进入被调函数的代码
#说明: 1、被调函数的参数保存在数据域的item标签下,此处为索引,实际情况不会这样
# 2、被调函数的标签为called_func
.section .data
item: .long 1, 2, 3 #使用long类型,是为了配合%esp每次移动都为4 byte,否则需要做其他处理
#函数即为:called_func(1,2,3)
.section .text
.globl _start
_start: #(0)保存“上下文环境”
pushl %eax #假设"调用函数"只涉及到%eax和%ebx的使用
pushl %ebx
movl 3, %ebx
#(1)对参数进行逆序压栈,此处采用“索引寻址”
load_data: subl 1,%ebx #将函数参数压入栈中
pushl item(,%ebx,4)
cmpl 0,%ebx
jne load_data
#(2)将当前函数的指令地址压入栈中,并跳转
call called_func
called_func: pushl %ebp #(3)暂存%ebp的值于栈中
movl %esp , %ebp #(4)改变%ebp的值
subl 8,%ebp #(5)为“被调函数”开辟2个字的空间,存储局部变量
#如果需要对“局部变量”进行访问,则利用%ebp进行“基址访址”形式即可
movl $2, -4(%ebp)
......
......
#使用下面的指令,返回到“调用函数”中
以上即为进入一个“被调用函数”需要做的准备工作。当退出“被调函数”时,需要做如下工作:
(1)将返回值存入%eax中
(2)清除“被调函数”栈内数据
(3)返回“调用函数”。从“被调函数”返回的代码如下:
movl %ebp, %esp
popl %ebp
ret
3、对于寄存器
在进入“被调函数”前,一定要将当前的寄存器值暂存在栈中,从而保证“被调函数”有充足的寄存器可以使用。如果要在“被调函数”保存寄存器,则破坏了函数之间的封闭性。调用函数和被调函数之间,一定只能通过全局变量、被调函数的参数进行通信,否则,将不易于程序的管理。
3、程序1:
#目的:本程序计算2^3+5^2
#程序所有内容存入寄存器中,数据段无数据
#变量说明:%eax存储函数power的计算结果,并作为返回值,返回给“调用函数”
.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 #将第一个结果弹栈,存入%ebx中。第二个结果已经存入%eax中
addl %eax, %ebx #两个结果相加,作为返回给系统的状态之,存储在%ebx中
movl $1, %eax #调用中断,退出程序
int 0x80
#目的:计算一个整数的幂
#输入:参数1:底数a
# 参数2:幂b
#输出:a^b
#注意:指数为不小于1的整数
#变量:%ebx:存底数
# %ecx:存指数
# -4(%ebp):存当前结果
# %eax:临时存储
.type power, @function
power: pushl %ebp #暂存%ebp的值于栈中
movl %esp, %ebp #将%ebp指向局部变量存储区域的开始
subl $4, %esp #开辟局部不变量存储区域
#从栈中获取参数,注意4(%ebp)存储“调用函数”的地址
movl 8(%ebp), %ebx #底数
movl 12(%ebp), %ecx #指数
movl %ebx, -4(%ebp) #存储结果,注意:由于栈空间是从“高地址”区域向“低地址”区域移动
power_loop_start: cmpl $1, %ecx #如果是1次方,则结束循环乘法
je end_power
movl -4(%ebp), %eax
imull %ebx, %eax # %eax = %eax * %ebx
movl %eax, -4(%ebp) #存回栈中
decl %ecx #指数递减
jmp power_loop_start
end_power: movl -4(%ebp), %eax
movl %ebp, %esp
popl %ebp
ret
说明:
1、由于返回给程序的状态码需不大于255,所以计算的结果不能过大
2、.type power, @function指令告诉连接器:将符号power作为函数处理。
4、递归函数——程序2
问题背景:计算某个整数的阶乘。由于每个函数都有自己的“栈帧”,所以当函数调用自己的时候,使用局部数据空间不会互相干扰。
#目标:计算某个给定数字的阶乘。程序将使用递归思想
#变量:%ebx作为临时变量
.section .data
.section .text
.globl _start
.globl factorial #通过该项,可将该函数共享给其他程序调用
_start : pushl $4 #需要计算阶乘的整数
call factorial #调用函数,计算阶乘
addl $4, %esp #清空“被调函数”开辟的存储局部变量的空间
movl %eax, %ebx #将存储在%eax中的factiorial的返回值,作为状态字存储在%ebx中
movl $1, %eax
int 0x080
#此为实际的函数定义
.type factorial, @funciton
factorial: pushl %ebp #初始化局部存储空间
movl %esp, %ebp
movl 8(%ebp), %eax #4(%ebp)存返回地址,8(%ebp)存第一个参数,即某个整数
cmpl $1, %eax #为1,则退出阶乘的计算
je end_factorial
decl %eax #大于1,则递归调用该函数
pushl %eax #与上面的指令——pushl $4相呼应
call factorial
#核心计算代码
movl 8(%ebp), %ebx
imull %ebx, %eax #%ebx存储上一次运算的结果,%eax存储当前整数,
end_factorial: movl %ebp, %esp
popl %ebp
ret
程序说明:
.type指令告诉链接器factorial为一个函数。