• C运行时库- CRT(C Runtime)


    基本概念

    CRT(C Runtime)是指C运行时库,它为C和C++程序提供了一组初始化和终止程序的基本构建块。这些构建块确保在main()函数执行之前和之后进行适当的初始化和清理。

    CRT的主要任务包括:

    1. 初始化静态数据:分配和初始化静态和全局变量。
    2. 调用全局构造函数:在C++中,全局或静态对象的构造函数需要在main()之前被调用。
    3. 设置堆:对于动态内存分配(如mallocnew)。
    4. 处理程序终止:当main()函数退出或调用exit()时,确保适当地调用全局和静态对象的析构函数(在C++中)。

    当编译和链接一个程序时,链接器将自动选择正确的CRT文件,以确保程序的生命周期管理正确。如果使用特定的编译和链接选项,如-fPIC-pie,链接器可能会选择不同的CRT文件,如Scrt1.o而不是crt1.o,以支持这些选项。

    为了更好地理解这些文件是如何工作的,可以考虑它们为程序的生命周期提供了一个框架:从程序的开始,到 main 函数的执行,再到程序的结束,每个阶段都有相应的初始化和清理工作需要完成。这些 crt 文件就是为此目的而存在的。

    C运行时组件在链接过程中的一般顺序

    当使用C或C++编译器(如 gccg++)来编译和链接一个程序时,C运行时的这些组件会按照特定的顺序被包含在生成的可执行文件中,以确保全局对象、构造函数、析构函数和程序的main()函数在正确的顺序中执行。

    以下是这些组件在链接过程中的一般顺序:

    1. crti.o:

      • 定义初始化(如全局构造函数)的开始部分。
      • 设置.init段的开头。
    2. crt1.o/Scrt1.o:

      • 定义程序的实际入口点_start
      • _start是程序的启动点,它会进行一些基本的初始化,然后调用全局的构造函数,接着调用main(),最后调用全局的析构函数。
      • Scrt1.o是用于位置无关代码的版本,通常在动态共享库中使用。
    3. crtbegin.o/crtbeginS.o:

      • 插入构造函数列表的开始部分,这些构造函数是由程序的不同部分(如不同的编译单元或链接的库)提供的。
      • crtbeginS.o是用于位置无关代码的版本。
    4. 你的代码:

      • 这是你实际编写的程序代码。
      • main()函数和其他全局/静态对象的定义都在这里。
    5. crtend.o/crtendS.o:

      • 插入析构函数列表的结束部分。
      • 就像crtbegin.o有一个与之相对应的版本crtbeginS.ocrtendS.ocrtend.o的位置无关代码版本。
    6. crtn.o:

      • 定义初始化和清理函数的结束部分。
      • 设置.fini段的结尾。

    在链接过程中,链接器确保按照这个顺序包含这些组件,从而确保程序在运行时具有正确的初始化和清理顺序。

    注意: 在不同的系统、编译器版本和配置中,具体的文件名和顺序可能会有所不同。上述描述是基于通用的和常见的行为,但具体细节可能会根据环境而有所变化。

    crt1.o 和 Scrt1.o

    crt1.oScrt1.o 是C运行时(CRT, C Runtime)的一部分,它们定义了程序的真正的入口点——_start。尽管当我们编写C程序时通常会以 main() 作为起点,但实际上在进入 main() 之前还会执行很多初始化操作,这些操作由这些运行时对象文件中的代码进行。

    下面是一个简化的、高层次的说明:

    1. 编写一个简单的C程序,例如:

      #include 
      
      int main() {
          printf("Hello, World!\n");
          return 0;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
    2. 当编译并链接此程序时,链接器除了链接我们的代码外,还会链接C运行时的一部分。这确保了 _start 是实际的程序入口点。

    3. 当程序开始执行时,它首先进入 _start

      • _start 负责执行多种初始化任务,例如设置堆栈,初始化全局变量,调用全局构造函数等。
      • 一旦完成所有的初始化,_start 调用 main() 函数。
      • main() 函数执行,打印 “Hello, World!”。
      • main() 返回后,控制权返回给 _start,它接着负责调用全局析构函数并执行其他清理任务。
      • 最后,_start 调用系统调用以退出程序。

    这就是为什么,如果我们使用工具(如 objdumpnm)查看一个编译好的可执行文件,会看到除了 main 之外还有其他符号和入口点,如 _start

    请注意,crt1.oScrt1.o(及其相关的CRT文件)的具体行为和内容可能会根据操作系统、编译器和系统架构而异。

    crti.o 和 crtn.o

    crti.ocrtn.o 是 C 运行时的组件,它们为全局构造函数和析构函数的初始化和清理提供所需的框架。这些构造函数和析构函数不应与 C++ 的类构造函数和析构函数混淆;在这里,我们指的是全局和静态对象的初始化和终止函数。

    在 ELF(可执行和链接格式)系统上,例如大多数 Unix-like 系统,crti.ocrtn.o 提供了 .init.fini 段的前导和后继代码。它们确保正确地设置和执行构造函数和析构函数。

    如何工作?

    1. crti.o 提供 .init.fini 段的开头部分。
    2. 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这个示例中,我们定义了一个构造函数 my_constructor 和一个析构函数 my_destructor。这些函数分别在 main() 函数之前和之后执行。

    当编译并运行此程序时,输出应如下:

    Before main()
    Inside main()
    After main()
    
    • 1
    • 2
    • 3

    这就是 crti.ocrtn.o 起作用的地方:

    1. crti.o 提供了 .init 段的开头,这段代码确保 my_constructormain() 之前执行。
    2. crtn.o 提供了 .fini 段的结束部分,这段代码确保 my_destructormain() 之后执行。

    在实际的系统中,还有其他机制和细节确保了构造函数和析构函数的正确执行顺序,以及与其他库和组件的互操作性。但从高层次来看,crti.ocrtn.o 提供了为这些函数设置和执行所需的基础框架。

    crtbegin.o / crtbeginS.o 和 crtend.o / crtendS.o

    当在C++中使用静态对象或全局对象,这些对象的构造函数和析构函数需要在程序的main()函数执行前后被调用。crtbegin.o, crtbeginS.o, crtend.o, 和 crtendS.o 这些文件正是负责这些操作。

    crtbegin.o 和 crtbeginS.o

    • 这些文件的主要目的是收集程序中所有静态或全局C++对象的构造函数,并将它们放在一个列表中。
    • crtbeginS.o 特别用于生成位置无关代码(PIC, Position-Independent Code),这在动态共享对象(如.so文件)中是必需的。

    crtend.o 和 crtendS.o

    • 这些文件负责收集所有静态或全局C++对象的析构函数。
    • 同样,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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    当这个程序启动时,我们希望首先看到“Constructor called!”,接着是“Inside main()”,最后是“Destructor called!”。

    这正是 crtbegin.ocrtend.o(或它们的PIC版本)的作用:它们确保在进入main()之前调用全局对象的构造函数,而在main()之后调用全局对象的析构函数。

    为了达到这一目的,crtbegin.o 创建了一个指向所有构造函数的列表,并确保在main()前调用它们;而 crtend.o 则为析构函数做了同样的事情,但是在main()后。

    这是为什么当我们使用g++或其他C++编译器链接C++程序时,这些CRT对象文件会自动被包括在内,以确保正确的程序初始化和终止顺序。

  • 相关阅读:
    看完通辽可汗小约翰之后应该掌握的英语词汇 01 外交类
    OSI七层模型
    详解 SpringMVC 中获取请求参数
    Graph Data Augmentation for GraphMachine Learning: A Survey
    自然语言处理(NLP)—— 语言学、结构的主要任务
    使用image-map编写校区平面示意图
    mybatis使用多参数查询
    纯CSS3实现萧瑟深秋中律动的音乐之火
    Ansible运行临时命令及常用模块介绍
    VMTK环境配置记录
  • 原文地址:https://blog.csdn.net/weixin_43844521/article/details/132851667