目标文件和可执行文件的格式?
不同的系统的可执行文件有不同的格式。在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,在堆上分配内存。
函数调用过程:
1 上下文环境(栈基指针寄存器,栈寄存器,栈指针寄存器)入栈
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 的栈帧中;
单个过程的局部变量都在自己的栈帧中。