CRT(C Runtime)是指C运行时库,它为C和C++程序提供了一组初始化和终止程序的基本构建块。这些构建块确保在main()函数执行之前和之后进行适当的初始化和清理。
CRT的主要任务包括:
main()之前被调用。malloc和new)。main()函数退出或调用exit()时,确保适当地调用全局和静态对象的析构函数(在C++中)。当编译和链接一个程序时,链接器将自动选择正确的CRT文件,以确保程序的生命周期管理正确。如果使用特定的编译和链接选项,如-fPIC和-pie,链接器可能会选择不同的CRT文件,如Scrt1.o而不是crt1.o,以支持这些选项。
为了更好地理解这些文件是如何工作的,可以考虑它们为程序的生命周期提供了一个框架:从程序的开始,到 main 函数的执行,再到程序的结束,每个阶段都有相应的初始化和清理工作需要完成。这些 crt 文件就是为此目的而存在的。
当使用C或C++编译器(如 gcc 或 g++)来编译和链接一个程序时,C运行时的这些组件会按照特定的顺序被包含在生成的可执行文件中,以确保全局对象、构造函数、析构函数和程序的main()函数在正确的顺序中执行。
以下是这些组件在链接过程中的一般顺序:
crti.o:
.init段的开头。crt1.o/Scrt1.o:
_start。_start是程序的启动点,它会进行一些基本的初始化,然后调用全局的构造函数,接着调用main(),最后调用全局的析构函数。Scrt1.o是用于位置无关代码的版本,通常在动态共享库中使用。crtbegin.o/crtbeginS.o:
crtbeginS.o是用于位置无关代码的版本。你的代码:
main()函数和其他全局/静态对象的定义都在这里。crtend.o/crtendS.o:
crtbegin.o有一个与之相对应的版本crtbeginS.o,crtendS.o是crtend.o的位置无关代码版本。crtn.o:
.fini段的结尾。在链接过程中,链接器确保按照这个顺序包含这些组件,从而确保程序在运行时具有正确的初始化和清理顺序。
注意: 在不同的系统、编译器版本和配置中,具体的文件名和顺序可能会有所不同。上述描述是基于通用的和常见的行为,但具体细节可能会根据环境而有所变化。
crt1.o 和 Scrt1.o 是C运行时(CRT, C Runtime)的一部分,它们定义了程序的真正的入口点——_start。尽管当我们编写C程序时通常会以 main() 作为起点,但实际上在进入 main() 之前还会执行很多初始化操作,这些操作由这些运行时对象文件中的代码进行。
下面是一个简化的、高层次的说明:
编写一个简单的C程序,例如:
#include
int main() {
printf("Hello, World!\n");
return 0;
}
当编译并链接此程序时,链接器除了链接我们的代码外,还会链接C运行时的一部分。这确保了 _start 是实际的程序入口点。
当程序开始执行时,它首先进入 _start。
_start 负责执行多种初始化任务,例如设置堆栈,初始化全局变量,调用全局构造函数等。_start 调用 main() 函数。main() 函数执行,打印 “Hello, World!”。main() 返回后,控制权返回给 _start,它接着负责调用全局析构函数并执行其他清理任务。_start 调用系统调用以退出程序。这就是为什么,如果我们使用工具(如 objdump 或 nm)查看一个编译好的可执行文件,会看到除了 main 之外还有其他符号和入口点,如 _start。
请注意,crt1.o 和 Scrt1.o(及其相关的CRT文件)的具体行为和内容可能会根据操作系统、编译器和系统架构而异。
crti.o 和 crtn.o 是 C 运行时的组件,它们为全局构造函数和析构函数的初始化和清理提供所需的框架。这些构造函数和析构函数不应与 C++ 的类构造函数和析构函数混淆;在这里,我们指的是全局和静态对象的初始化和终止函数。
在 ELF(可执行和链接格式)系统上,例如大多数 Unix-like 系统,crti.o 和 crtn.o 提供了 .init 和 .fini 段的前导和后继代码。它们确保正确地设置和执行构造函数和析构函数。
如何工作?
crti.o 提供 .init 和 .fini 段的开头部分。crtn.o 提供 .init 和 .fini 段的结尾部分。现在,我们来看一个简化的例子:
#include
void __attribute__((constructor)) my_constructor(void) {
printf("Before main()\n");
}
void __attribute__((destructor)) my_destructor(void) {
printf("After main()\n");
}
int main(void) {
printf("Inside main()\n");
return 0;
}
在这个示例中,我们定义了一个构造函数 my_constructor 和一个析构函数 my_destructor。这些函数分别在 main() 函数之前和之后执行。
当编译并运行此程序时,输出应如下:
Before main()
Inside main()
After main()
这就是 crti.o 和 crtn.o 起作用的地方:
crti.o 提供了 .init 段的开头,这段代码确保 my_constructor 在 main() 之前执行。crtn.o 提供了 .fini 段的结束部分,这段代码确保 my_destructor 在 main() 之后执行。在实际的系统中,还有其他机制和细节确保了构造函数和析构函数的正确执行顺序,以及与其他库和组件的互操作性。但从高层次来看,crti.o 和 crtn.o 提供了为这些函数设置和执行所需的基础框架。
当在C++中使用静态对象或全局对象,这些对象的构造函数和析构函数需要在程序的main()函数执行前后被调用。crtbegin.o, crtbeginS.o, crtend.o, 和 crtendS.o 这些文件正是负责这些操作。
crtbegin.o 和 crtbeginS.o
crtbeginS.o 特别用于生成位置无关代码(PIC, Position-Independent Code),这在动态共享对象(如.so文件)中是必需的。crtend.o 和 crtendS.o
crtendS.o 特别用于支持位置无关代码。示例
假设我们有一个简单的C++程序:
#include
class MyClass {
public:
MyClass() {
std::cout << "Constructor called!" << std::endl;
}
~MyClass() {
std::cout << "Destructor called!" << std::endl;
}
};
MyClass globalObject; // 全局对象
int main() {
std::cout << "Inside main()" << std::endl;
return 0;
}
当这个程序启动时,我们希望首先看到“Constructor called!”,接着是“Inside main()”,最后是“Destructor called!”。
这正是 crtbegin.o 和 crtend.o(或它们的PIC版本)的作用:它们确保在进入main()之前调用全局对象的构造函数,而在main()之后调用全局对象的析构函数。
为了达到这一目的,crtbegin.o 创建了一个指向所有构造函数的列表,并确保在main()前调用它们;而 crtend.o 则为析构函数做了同样的事情,但是在main()后。
这是为什么当我们使用g++或其他C++编译器链接C++程序时,这些CRT对象文件会自动被包括在内,以确保正确的程序初始化和终止顺序。