程序运行过程
编译好的程序在运行时涉及多个步骤,包括加载、链接、初始化和执行。加载器负责将程序的各个部分从磁盘加载到内存中,并设置好相应的内存布局。
1. 加载(Loading)
程序在运行时首先需要被加载到内存中。这个过程由操作系统的加载器(Loader)完成。加载器负责将程序的各个部分从磁盘加载到内存中,并设置好相应的内存布局。
加载过程的详细步骤:
1.映射 ELF 文件到内存
- 操作系统加载器读取 ELF 文件的头部信息,确定各个段的位置和大小。
- 加载器将 ELF 文件中的各个段映射到内存中。每个段都有一个起始地址和长度,加载器会将这些段加载到相应的内存位置。
2.重定位(Relocation)
- 由于程序在内存中的实际地址可能与编译时预期的不同,需要进行重定位。重定位是指更新程序中的相对地址,使其指向正确的内存位置。
- 加载器会根据 ELF 文件中的重定位表(Relocation Table)更新相应的地址。
3.设置内存保护
- 加载器会根据 ELF 文件中的段属性设置内存保护。例如,.text 段通常是只读的,.data 和 .bss 段是可读写的。
2. 链接(Linking)
虽然编译后的程序已经进行了静态链接,但在加载时还需要进行动态链接。动态链接器(Dynamic Linker)负责将程序所需的动态库加载到内存中,并更新相应的符号引用。
动态链接的详细步骤:
1.查找动态库
- 动态链接器根据 ELF 文件中的动态链接表找到所需的动态库,并将它们加载到内存中。
2.重定位动态库中的符号
- 动态链接器更新程序中的符号引用,使其指向动态库中的正确位置。
3.解决依赖关系
- 动态链接器解决动态库之间的依赖关系,确保所有依赖的库都被正确加载。
3. 初始化(Initialization)
程序加载完成后,需要进行一些初始化工作,确保程序能够正常运行。
初始化的详细步骤:
1.初始化全局变量
- 操作系统加载器将 .data 段中的已初始化全局变量加载到内存中。
- 操作系统将 .bss 段中的未初始化全局变量的内存清零。
2.执行构造函数
- 如果程序中有全局构造函数(如 C++ 中的 static 构造函数),这些构造函数会在程序启动时被执行。
3.执行初始化代码
- 程序中可能有一些初始化代码块,这些代码块会在程序启动时被执行。
4. 执行(Execution)
程序加载和初始化完成后,就可以开始执行主程序了。
执行的详细步骤:
1.跳转到程序入口点
- 加载器将程序的执行指针设置到 _start 函数或其他指定的入口点。
2.执行主程序
- 程序从 _start 函数开始执行,然后进入 main 函数或其他主函数。
- 主函数中的代码开始执行。
程序加载过程中的内存分配
操作系统在加载可执行程序时,会将程序的各个段映射到内存中,并为程序分配必要的内存资源。堆和栈的大小在程序启动时由操作系统预先分配,堆的大小可以动态增长,而栈的大小可以通过编译器选项或环境变量来调整。通过这种方式,操作系统确保了程序能够正确地加载和运行。
1.映射 ELF 文件到内存
- 操作系统读取 ELF 文件的头部信息,确定各个段的位置和大小。
- 然后,操作系统会为每个段分配相应的内存,并将段的内容映射到内存中。
2.重定位(Relocation)
- 对于那些需要重定位的地址,操作系统会根据 ELF 文件中的重定位表更新相应的地址。
3.设置内存保护
- 操作系统会根据段的属性设置内存保护。例如,代码段通常是只读的,数据段是可读写的。
堆和栈的分配
堆的分配
- 堆在程序运行时动态分配,当程序调用如 malloc 函数时,请求的内存会被分配给程序。操作系统维护一个堆管理器来跟踪已分配和未分配的内存块。
- 堆的大小可以根据程序的需求动态增长。
栈的分配
- 栈在程序启动时由操作系统预先分配一定的大小。这个大小可以在编译时通过编译器选项或在运行时通过环境变量来调整。
- 栈的增长方向通常是向下增长(向更低的地址),并且当栈空间不足时,可能会导致栈溢出错误。