怎么调用计算机底层函数,笔记 | 计算机系统基础:04-函数调用时发生了什么?...

零. 课程要点:

C语言内存模型

函数调用的机器级表示

从这一章开始,我们要运用之前所学的计算机系统基础知识,来理解更复杂的C语言语句或结构。例如,在C语言中,一个函数调用时底层究竟做了哪些操作?知道细节之后我们才能更好的分析函数调用过程中有没有出错,开销大不大,有什么要注意的地方。不过,在此之前,我们需要先简单认识一下C语言的内存分配模型,在之后讲可执行程序的编译、链接、运行时会更深入探讨,这里我们需要对它有个基本的认知。

一. C语言内存模型

1b76d0047b59

可执行文件的存储器映像

我们写好一个程序,代码是存储在硬盘上的,经过编译和链接后,当运行这个程序时,需要将其载入内存,上图的右侧就是内存分配的基本模型(如何映射之后会详细介绍),从高地址到低地址依次可分为内核虚存区(内核使用),用户栈(程序运行时存放局部变量,从高地址向低地址增长),共享库区域,用户堆(程序运行时用于分配malloc和new申请的区域),读写数据段(存放全局变量和静态变量),只读代码段(存放程序和常量等),和未使用区域。

其中的栈和堆就是我们常说的堆栈,二者是不同的,不过这里不详细讨论,我们重点关注栈,是我们理解函数调用的关键区域。

二. 函数调用的机器级表示

假设有以下函数调用:

int add ( int x, int y ) {

return x+y;

}

int main ( ) {

int t1 = 125;

int t2 = 80;

int sum = add(t1, t2); /*调用add函数*/

return sum;

}

那么其调用示意图是这样的:

1b76d0047b59

函数调用示意图

在这个过程中需要做些什么呢?

首先main中的入口参数t1和t2必须先存到一个地方,并且这个地方add必须能访问到吧。

参数保存好后,需要把返回地址保存好,这样add执行完之后就能返回这继续往下执行。

保存好参数和返回地址后,就可以调用add函数啦。

add执行开始时先为自己局部变量(如果有)分配空间

然后add取出之前的参数,并执行函数过程

执行完之后,需要取之前保存的返回地址,返回继续执行main。

以上的过程就是通过栈来完成的。这样讲比较抽象,我们来看看以上代码对应的汇编指令,并结合栈和栈帧的变化情况来具体说明:

main:

pushl %ebp

movl %esp, %ebp

subl $24, %esp

movl $125, -12(%ebp)

movl $80, -8(%ebp)

movl -8(%ebp), %eax

movl %eax, 4(%esp)

movl -12(%ebp), %eax

movl %eax, (%esp)

call add

movl %eax, -4(%ebp)

movl -4(%ebp), %eax

leave

ret

add:

pushl %ebp

movl %esp, %ebp

movl 8(%ebp), %edx

movl 12(%ebp), %eax

addl %edx, %eax

leave

ret

在分析这段汇编语句之前,要先回忆下我们介绍IA-32体系结构时,提到的ebp和esp寄存器。ebp里存放的是“基址指针”,而esp里存放的是“堆栈指针”。

基址指针(Base Pointer) 指向系统栈最上面一个栈帧的底部,而堆栈指针(Stack Pointer)总是指向栈顶位置。由于ESP指针是会随时发生改变的,一般使用EBP寄存器来对堆栈进行访问。所以每个函数调用在开始时都要保存原来的EBP,然后设置自己的堆栈地址,并在函数结束返回时恢复原来的EBP,使上级函数可以正常使用EBP。(如果不太理解没关系,下面会解释)

pushl %ebp

其实main()也是个函数,只不过是主函数,同样会被调用。因此,main开始执行时,需要把ebp的内容入栈,即保存原系统栈基址。

请注意:这个时候ebp还是指向原栈基址,esp因为永远指向栈顶,所以它现在指向的是旧ebp入栈的地方。

1b76d0047b59

旧ebp入栈

movl %esp, %ebp

这条指令把esp的内容赋给ebp,也就是说ebp现在指向跟esp一样的位置,即旧ebp入栈的地方。

1b76d0047b59

形成帧底

subl $24, %esp

这条指令将esp的地址,也就是栈顶的位置减24,即开辟出了24个字节的空间。

1b76d0047b59

生成栈帧

movl $125, -12(%ebp)

movl $80, -8(%ebp)

这两条指令是main为自己的变量分配空间,第一条指令将第一个参数t1=125,存放到ebp-12的地方,占4个字节。第二条指令将第二个参数t2=80,存放到ebp-8的地方,占4个字节。(ebp-4的地方是用来存放第三个变量sum,调用函数add后的结果会更新该内容)

1b76d0047b59

分配变量

movl -8(%ebp), %eax

movl %eax, 4(%esp)

movl -12(%ebp), %eax

movl %eax, (%esp)

这四条指令是准备入口参数,为调用add作准备。

将ebp-8中的内容赋给eax,也就是eax中存放着的是80。然后将eax中的内容赋给esp-4的地方。

将ebp-12中的内容赋给eax,也就是eax中存放着的是125。然后将eax中的内容赋给esp的地方。

1b76d0047b59

准备入口参数

call add

使用call指令调用add函数,call指令会先把call语句的下一条语句(movl %eax, -4(%ebp))的地址入栈,这样当add返回后,能够继续执行main函数。

add:

pushl %ebp

movl %esp, %ebp

movl 8(%ebp), %edx

movl 12(%ebp), %eax

addl %edx, %eax

leave

ret

然后开始执行add函数。add函数开始执行时,也要先保存ebp的值,也就是同样要执行pushl %ebp和movl %esp, %ebp(参考main的做法),然后取入口参数(在哪呢?%ebp+8和%ebp+12处),进行相加后把结果存在eax中(返回参数总在eax中),然后leave并ret回main。

leave指令可以看成是movl %ebp,%esp和popl %ebp,也就是先让栈顶指针回到ebp所指向的地方,然后把保存着ebp在main中的旧值出栈给ebp,也就是ebp回到了main函数时的栈基址位置。

ret指令可以看成是popl %eip和jmp,也就是把返回地址出栈至指令指针,然后无条件跳转到相应地址,继续往下执行。

1b76d0047b59

调用add函数

1b76d0047b59

add函数返回

movl %eax, -4(%ebp)

这条指令把add返回的结果存放到变量sum里,放在ebp-4的地方。

1b76d0047b59

更新sum变量

movl -4(%ebp), %eax

为什么又要把ebp-4的内容放到eax里呢?因为main准备要返回了,所以要把返回结果sum放到eax里。(如果main直接return 0就不用这样了)

leave

跟子函数add一样,这条指令相当于movl %ebp,%esp和popl %ebp,也就是先让栈顶指针回到ebp所指向的地方,然后把保存着ebp在main中的旧值出栈给ebp,也就是ebp回到了main函数时的栈基址位置。

注意此时esp地址增4,指向的地方存放的是call调用main函数的上层函数的下一条指令的返回地址。

1b76d0047b59

movl %ebp,%esp

1b76d0047b59

popl %ebp

ret

跟子函数add一样,ret指令可以看成是popl %eip和jmp,也就是把返回地址出栈至指令指针,然后无条件跳转到相应地址,继续往下执行。

总结上面的过程,可以看出函数调用的大致结构是这样的:

准备阶段

• 形成帧底:push指令 和 mov指令

• 生成栈帧(如果需要的话):sub指令 或 and指令

• 保存现场(如果有被调用者保存寄存器) :mov指令

过程(函数)体

• 分配局部变量空间,并赋值

• 具体处理逻辑,如果遇到函数调用时

准备参数:将实参送栈帧入口参数处

CALL指令:保存返回地址并转被调用函数

• 在EAX中准备返回参数

结束阶段

• 退栈:leave指令 或 pop指令

• 取返回地址返回:ret指令

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值