函数与内存堆栈

目标文件和可执行文件的格式?

不同的系统的可执行文件有不同的格式。在SVr4实现中都采用了ELF(Extensible and Linker Format,可执行文件夹和链接格式)的格式,在其他系统中,可执行文件的格式是COFF(Common Object-File Format,普通目标文件格式,在BSD UNIX中也有自己自己的格式。可以通过命令man a.out 查看。

但是所有这些不同格式都有一个共同的概念,那就是段(segments)。它是二进制文件中简单的区域,里面保存了和某些特定类型相关的所有信息,可以通过size a.out来查看:

[root@localhost ~]# size a.out

   text    data     bss     dec     hex filename

   1197     512       8    1717     6b5 a.out

text文本段  data 数据段  bss bss段

其中文本段保存的是可执行文件的指令,数据段保存经过初始化的全局和表态变量,BSS段保存没有值的变量。但BSS段和前两个不同,它并不占据目标文件的任何空间(此段不保存在目标文件之中)。


 目标文件和可执行文件是怎么映射到内存的?


我们知道了 目标文件是以段的形式进行组织的,为什么这么做呢?那是因为段可以方便的映射到段在运行时 可以直接载入的对象中。载入器只是取文件中每个段的映像,并直接将他们放到内存,从本质来说,段在正在执行的程序中是一块内存区域,每个段都有特定的目的,下图是各个段与内存的映射关系,

     

从上图中可以看到,一个即将执行的程序可能还需要一些内存空间,用于保存局部变量、临时变量、传递到函数中的参数。堆栈段就是用于这个目的,另外还需要堆(heap)空间,用于动态分配内存


Linux 下程序运行时存储器映像:

	      ---------------------------------- \
            |         kernel memory          |  > memory invisible to user code 
0xc0000000  |--------------------------------| /
            |         user stack             |
            |     (created at runtime)       |
            |--------------------------------|<-- %rsp
            |                                |
            |--------------------------------|
            |     memory-mapped region for   |
            |     shared libraries           |
0x40000000  |--------------------------------|
            |                                |
            |--------------------------------|<-- brk
            |         runtime heap           |
            |       (created by malloc)      |
            |--------------------------------| \
            |         read-write segment     | |
            |        (.data, .bss)           | |
            |--------------------------------|  > loaded from the executable file
            |        read-only segment       | |
            |     (.init, .text, .rodata)    | |
0x08048000  |--------------------------------| /
            |                                |
        0   ----------------------------------
                       图 1




小总结:

源代码经过gcc编译生成a.out,执行a.out,程序便开始执行(进程)

操作系统为进程分配堆栈空间后,链接器把程序执行码从文件直接拷贝放入文本段(二进制,通过mmap系统调用),然后就不管它了,这个段被赋予只读和执行的属性

把程序中经过初始化的全局变量或者静态变量及他们的值放入data段,这个段被赋予读写的属性

未初始化的全局变量或静态变量放入bss段,并将bss段初始化为0.bss段大小从可执行文件中得到

cpu代码指针,指向函数入口,cpu堆栈指针指向栈顶,代码段指针从main的入口,cpu堆栈指针指向栈顶,

代码段指针从main入口地址顺序读取指令代码并执行

碰到局部变量,临时变量及函数参数,需要在栈顶开辟空间,将堆栈指针下移,碰到malloc,在堆上分配内存。



函数调用过程:

上下文环境(栈基指针寄存器,栈寄存器,栈指针寄存器)入栈

2 局部变量入栈

3  参数入栈:将函数实参参数(引用除外)赋值为副本后 从右向左一次压入系统栈中,以顶替形参来参加函数的运行

4 返回值地址入栈:将当前代码区调用指令的下一条指令的地址压栈,供函数返回时继续执行

void fun(void)

{

printf("hello world");

}

void main(void)

{

fun()

printf("函数调用结束");

}

1.EIP        Extended Instructions Pointer 指令寄存器

2.ESP     Extended Stack Pointer栈指针寄存器

3.EBP      Extended (Stack) Base Pointer 栈基指针寄存器

当调用fun函数开始时,三者的作用。

1.EIP寄存器里存储的是CPU下次要执行的指令的地址。

也就是调用完fun函数后,让CPU知道应该执行main函数中的printf("函数调用结束")语句了。

2.EBP寄存器里存储的是是栈的栈底指针,通常叫栈基址,这个是一开始进行fun()函数调用之前,由ESP传递给EBP的(在函数调用前你可以这么理解:ESP存储的是栈顶地址,也是栈底地址。)

3.ESP寄存器里存储的是在调用函数fun()之后,栈的栈顶。并且始终指向栈顶。

当调用fun函数结束后,三者的作用:

1.系统根据EIP寄存器里存储的地址,CPU就能够知道函数调用完,下一步应该做什么,也就是应该执行main函数中的printf(“函数调用结束”)。

2.EBP寄存器存储的是栈底地址,而这个地址是由ESP在函数调用前传递给EBP的。等到调用结束,EBP会把其地址再次传回给ESP。所以ESP又一次指向了函数调用结束后,栈顶的地址。

5上下文环境入栈

6 处理器从当前代码区跳转到被调用函数的入口处 执行被调函数体内语句 

7 局部变量入栈。。。。。。

8被调函数内的局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址

9 上下文环境出栈(按照返回值地址将控制权交给调用函数  如果有返回值,将返回值副本(引用除外)入栈,接着传递给调用函数的程序)

10  恢复调用函数运行状态,释放被调函数占用的栈空间


用户栈结构(图 1 中 user stack 那部分):

栈底
        --------------------- \
        |                   | |
        |         .         | |
        |         .         |  > 较早的帧
        |         .         | |
        |                   | |
        |-------------------| /
        |                   | \
        |-------------------| |
        |                   | |
        |-------------------| |
        |                   |  > 调用者的帧
        |-------------------| |
        |                   | |
        |-------------------| |
    +4  |  return address   | /
        |-------------------| \
%rbp -->|   saved %rbp      | |
        |-------------------| |
    -4  |                   | |
        |-------------------|  > 当前帧
        |                   | |
        |-------------------| |
%rsp -->|                   | |
        --------------------- / 
栈顶             图 2

每个函数调用的参数和局部变量的存储空间(上图的每个小方框)称为一个栈帧    ,

栈帧以 %rbp(帧指针) 开始,以 %rsp(栈指针)结束。因为单程序运行到某个过程时,%rsp 是会移动的(当执行一条 push 指令时),所以信息的 访问是相对于没有那么频繁移动的 %rbp 的。

假如过程 P(caller)调用过程 Q(callee),P 的返回地址会压入栈中,形成 P 的栈帧末尾;由于 Q的参数是在 P 的栈帧中的,所以在参数传递时会把 Q 的参数从 P 的栈帧中拷到 Q 的栈帧中;

单个过程的局部变量都在自己的栈帧中。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值