CSAPP读书笔记——程序的机器级表示之栈帧结构
来源:互联网 发布:无主之地2 mac汉化 编辑:程序博客网 时间:2024/05/22 12:48
引子
C语言的基本构成单位是函数,通过合理的组织、调用函数来完成一系列的目的。
我开始学习的时候就好奇调用函数(或者说调用过程)时到底发生了什么?
数据在内存中是如何组织的?
函数返回时如何准确到找到下一条将要执行的指令?
等等等一系列的疑问,了解了之后,豁然开朗,记录下来时常温习。
栈帧结构
IA32的程序使用堆栈支持过程的调用(函数的调用),在函数调用时会专门从堆栈中分出一块内存(称为帧)供函数使用。
传递给函数的参数由堆栈来保存,帧则负责存储寄存器的状态、局部变量的内存分配的相关任务。
如果说函数P调用函数Q,那么称P为调用者(caller),Q是被调用者(callee)。
根据上述规则, 堆栈会给Q分配帧,并且用两个指针(分别存储在
[图示caller代表P,callee代表Q]
Caller’s Framer:P是一个函数,会被其他过程调用,所以也会给它分配帧,但是我们不关心它,因为这里考虑的是P调用Q。注意P的帧分为了三个部分。
(1)自身使用区:保存P函数中的局部变量,寄存器等状态,是在Argument n
以上的蓝色部分。
(2)参数区:Argument 1--Aegument n
,这些都是将要传递给Q的参数,记得吗?参数是存储在caller的帧中的。
(3)返回地址区:即标注为Return address
的区域。它存储了当从Q返回后,P将要执行的下一条指令的地址。Callee’s Framer : Callee’s Frame一定是当前帧(Current’s Framer)。我们注意到Q的帧也是大体分为了三个部分。
(1)帧开始段 :%ebp 指向帧开始段的地址。该地址的内存单元存储的是之前%ebp 指向的地址。在P调用Q之前,%ebp 指向一个地址假如为0x88088808
(实际上这是P帧的开始地址),那么当P调用Q的时候,会将0x88088808
放到Saved %ebp
内存单元中。
(2)帧主体段:用于局部变量的创建、数据转移等
(3)帧结束段:%esp 始终指向栈顶,也是当前活动帧的结束的地方。Arugument bulid area
区是指的当Q调用其他的函数(例如Q1,Q2….),那么传递给Q1,Q2的参数是要保存在Q中的。
转移控制
了解了堆栈和栈帧结构之后,我们在看一下机器指令是如何支持过程的调用的。
我们根据图示逐条解释。
call
在P调用Q之前,内存分布是这样的。
当执行P = Q
时,假设生成了如下汇编代码:
//指令地址 //指令的二进制表示 //汇编指令0x1234567 <Q>: some instructions0x80483dc e8 b3 ff ff ff call 0x1234567<Q>// call Q0x80483e1 83 c4 14 add $0x14.%esp
当调用Q,也就是
call 0x1234567
时,首先,将call指令的下一条指令的地址压入栈中,作为Return address
。之后(还是call的操作),将之前的
%ebp 中的内容保存起来,作为Q的帧的开始段的内容,然后将%ebp 指向开始段的地址。然后(仍然是call的操作),将
%esp 移动合适的位置(分配内存)
至此,call操作完成。
leave
leave
指令目的是让栈为ret
做好准备,那么到底做好什么准备呢?
还记得吗?我们把之前的
所以,leave指令相当于:
movl %ebp,%esp //移动%esp到帧开始的地方,回收内存popl %ebp //将保存的旧的%ebp恢复(帧开始的地方存储,现在刚好被%esp指向,弹出后,%esp指向return address的地址,旧的%ebp也被重新赋值给了%ebp)
ret
当leave
指令执行后,确保栈的内存被正确回收,状态正确恢复,可以放心大胆的ret
了。 ret
后,将执行return address
处的指令。
寄存器使用习惯
在讲解C实例的时候,我们要先了解一下寄存器的使用习惯(约定俗成的),方便理解汇编代码。
我们知道,寄存器是被所有的过程(或者说函数)共享的,只不过实际上一次只有一个过程(函数)可以使用它的资源。
这实际上就会引发一个问题,如果寄存器A存储了P的一些信息,当P调用Q时,如果Q也使用A那么就会覆盖掉A存储的P的信息(这样实际上P的信息就丢失了)。
所以必须有一个原则,callee不得覆盖caller之后还会用到的寄存器的信息。(实际上,限制了callee的访问权限)
为了解决这个问题,IA32机器对寄存器加入了一些“限制”,规定了哪些寄存器的状态被caller或者callee保存。
%eax,%edx,%ecx :caller-saved寄存器。当P调用Q时,Q可以使用这些寄存器而不用担心破坏P的信息。%ebx,%esi,%edi :callee-saved寄存器。当Q需要覆盖这些寄存器的信息的时候,必须先将其copy到内存中,因为调用Q的caller可能会在今后的计算中用到这些数据。%esp,%ebp :必须要保持状态,改变时要copy出一个副本以便恢复。
举一个汇编的例子:
subl $12, %espmovl %ebx, (%esp) //movl %esi, 4(%esp)//movl %edi, 8(%esp)//想对这三个寄存器写入,那么再写入之前必须将其以前的数据备份到内存中
C实例
考虑CSAPP书上的一个例子:
int swap_add(int *xp,int *yp){ int x = *xp; int y = *yp; *xp = y; *yp = x; return x + y;}int caller(){ int arg1 = 534; int arg2 = 1057; int sum = swap_add(&arg1,&arg2); int diff = arg1 - arg2; return sum * diff;}
我们画出它的栈帧结构,是这样的:
注意这里arg1,arg2和&arg1,&arg2的相对顺序:
一会看汇编我们会发现,arg1,arg2是按照参数表的声明顺序的相反顺序添加到栈中的。
执行call汇编指令时:
我们会发现,寄存器
我们看生成的汇编代码:
caller: pushl %ebp //保存%ebp的值 movl %esp,%ebp //将%ebp设置为帧的开始地址 subl $24,%esp //给caller分配24个字节的内存 movl $534,-4(%ebp) //arg1 = 534 movl $1057,-8(%ebp)//arg2 = 1057 leal -8(%ebp),%eax //计算&arg2----先计算arg2的地址而不是arg1的 movl %eax,4(%esp) //放到栈里 leal -4(%ebp),%eax //计算arg1 movl %eax,(%esp) //放到栈里 call swap_add //调用swap_add
swap_add: pushl %ebp movl %esp,%ebp pushl %ebx //和上述caller的开始三行一模一样 movl 8(%ebp),%edx//get xp movl 12(%ebp),%ecx//get yp movl (%edx),%ebx// get x movl (%ecx),%eax//get y movl %eax,(%edx)// *xp = y movl %ebx,(%ecx)// *yp = x addl %ebx,%eax // value = x + y,%eax始终作为返回值的寄存器
递归
int rFact(int n)//递归求阶乘{ int result; if(n <= 1) result = 1; else result = n * rFact(n-1); return result;}
递归是很重要的编程思想,它的本质就是函数自己调用自己,它的汇编代码类似循环的汇编(条件跳转+标签标记),我们一起看一下。
//argument : n at %ebp+8 registers: n in %ebx.result in %eaxrfact: pushl %ebp //保存%ebp以前的状态 movl %esp,%ebp//移动栈指针,指向帧开始的地方 pushl %ebx //保存%ebx以前的状态 subl $4,%esp //分配帧空间 //以上4行都是帧创建的set-up操作, movl 8(%ebp),%ebx //get n movl $1,%eax //result = 1,%eax是用来保存返回数据的寄存器 cmpl $1,%ebx //比较n和1 jle .L53 //如果<= goto done,while循环的判断 leal -(%ebx),%eax //计算n-1 movl %eax,(%esp) //存到栈顶,%eax在返回后还要使用其中的值,所以copy一份 call rfact //递归 imull %ebx,%eax //可以理解现在%eax中存储的是递归返回后的值,也就是rFact(n-1),因为该值稍后要返回,一直保存在%eax.L3: //done,实现栈内存的回收,%ebx,%ebp,%esp的状态返回 addl $4,%esp //回收栈内存 popl %ebx //恢复%ebx的内容 popl %ebp //恢复%ebp的内容 ret //所有准备操作都做好了,return
如此来看,其实递归没有那么神秘,和普通的函数调用实际上一致的。
- set-up:保存%ebp的旧状态,分配栈空间,保存callee-saved registers的状态(例如%ebx,如果用到的话)
- body:函数主体部分,完成相应操作(注意%eax总是优先存取return的变量)。
- end :回收栈分配的内存,恢复%ebp寄存器的状态。
- CSAPP读书笔记——程序的机器级表示之栈帧结构
- CSAPP读书笔记——程序的机器级表示之寄存器分布与mov指令集
- CSAPP读书笔记——程序的机器级表示之条件跳转与循环
- csapp读书笔记 chapter 3 程序的机器级表示
- 程序的机器级表示<CSAPP>
- CSAPP第三章:程序的机器级表示 小结
- 《CSAPP》程序的机器表示——汇编代码与C的联系
- [CSAPP] 程序的机器级表示(32位机器)(一)
- [CSAPP] 程序的机器级表示(32位机器)(二)
- [CSAPP] 程序的机器级表示(32位机器)(三)
- [CSAPP] 程序的机器级表示(32位机器)(四)
- [CSAPP-I] 过程(函数栈帧) C语句的机器级表示(gcc -S)
- CSAPP读书笔记——结构体的内存对齐
- 六星经典CSAPP-笔记(3)程序的机器级表示
- 读书笔记——《深入理解计算机系统》第三章_程序的机器级表示(一)
- 读书笔记——《深入理解计算机系统》第三章_程序的机器级表示(二)
- 程序优化方法——CSAPP 读书笔记
- CSAPP之栈帧结构理解
- 19岁程序员在谷歌学到的5条经验教训
- 【JavaScript】focus()方法
- CCF 201403-4无线网络 (二维最短路)
- pthread 编程:互斥锁属性
- UGUI的Slider与Gameobject的结合使用
- CSAPP读书笔记——程序的机器级表示之栈帧结构
- web前端-资源网站
- 视频压缩:I帧、P帧、B帧
- 构造方法调用的具体过程
- iServer 缓存介绍
- C#使用LitJSON操作json数据
- iphone蓝牙
- 我的网络时代
- 51信用卡到底安全吗?