C语言编译相关知识

  1. C程序的内存分配

    参考答案

    C程序内存分配为以下五个区:

    1. text (或code): 存储可执行代码,通常是固定大小而且是只读的。
    2. data: 存储已初始化的全局变量或静态局部变量。
    3. bss(Block Started by Symbol): 存储未初始化的全局变量或静态局部变量。
    4. heap: 存储动态分配的内存段,一般从bss尾部往高地址内存区增长。
    5. stack: 用于调用栈,从高地址内存区往低地址内存区增长。

    五个区的分布图可参考:

    program memory layout

    在上面的五个区中可以发现,bss区和data区都是用于存储全局变量值或静态局部变量值的,只是前者是存储未初始化的,而后者是存储已初始化的。我们知道C语言中,未定义的static变量一般都会默认初始化为0,那么为什么还要拆成两个区来存储?

    其实之所以拆成bss区和data区主要是为了减小程序大小。因为data区和bss区有如下的处理差异:

    • text 和 data 段都在可执行文件中,由系统从可执行文件中加载
    • bss 段不在可执行文件中,由系统初始化。

    即如果我们将无需初始化的变量放在bss区,bss区只存储标识符,那么只需要在程序启动时将需要初始化为0的未初始化变量初始化为0即可。这样可以减少程序大小,从而降低ROM空间的开销(在嵌入式设备上,采用更低规格的ROM可以降低成本)。

    另外还需要注意的是,将程序划分为以上五个区只是典型的情况,不同的操作系统可能会有不同的实现。 参考资料:

  2. gcc的编译有哪几个过程,每个过程的作用是什么?

    参考答案

    gcc编译分为四个过程:预处理、编译、汇编、链接。四个过程的作用分别如下:

    1. 预处理(c pre-processing):C Pre-processing简写为cpp,在gcc编译过程中主要由cpp程序负责该过程的代码处理。该过程主要做一些文本的初始化处理(如移除注释)、源文件所需头文件(#include)内容拷贝到源文件中及将macro进行展开。预处理完后会生成*.i文件,是一下过程编译的的输入。在gcc上,可以使用如下命令对文件进行预处理:
    gcc -E input.c -o input.i
    
    1. 编译(Compilation): 编译过程将前一过程生成的*.i文件进行编译以生成特定架构上的汇编代码。生成的汇编代码文件以s作为文件名后缀。在gcc上,可以使用如下命令生成文件的汇编代码:
    gcc -S input.i
    
    1. 汇编(Assembly):汇编过程将汇编代码转化为机器代码(即二进制文件),生成的文件以o为文件名后缀。在gcc套件中,汇编过程是由as程序负责的,可以使用如下命令生成文件的机器代码:
    gcc -c input.s
    
    1. 链接(Linker): 链接过程将生成的二进制文件与依赖的库文件进行链接从而生成可执行文件。链接过程由程序ld负责。

    参考资料:

  3. make的作用?

    参考答案

    make是软件开发过程中非常常用的一个工具,它读取工程中的makefile文件以自动构建软件。makefile文件主要格式为:目标 + 依赖 + 规则,如下:

    target: dependencies
    <TAB>command-1
    <TAB>command-2
    

    参考资料:

  4. CMake的作用?

    参考答案

    CMake是一个跨平台的、开源的自动化建构系统,用于软件的自动构建、测试、打包和安装。CMake本身并不具备构建功能,而是通过读取CMakeList.txt生成其它构建系统的构建文件(如生成make系统的makefile、生成Windows MSVCprojects/workspaces)。再通过这些生成的构建文件去做软件的构建。 对C/C++程序来说,CMake的优点主要有:

    1. 支持跨平台,如Linux和Windows
    2. 脚本makefile简单易读

    CMake的缺点也很明显:强大但也很复杂,调试麻烦,对开发人员要求较高。

    参考资料:

  5. 解释编译器的前端、中端及后端的区别。

    参考答案

    编译器的编译过程分为前端(front-end)、中端(middle-end)和后端(back-end)。三个过程的作用分别如下:

    1. 前端:分析源码文件生成程序的中间表示(Intermediate representation, IR),该过程主要包括预处理、词法分析、语法分析和语义分析
    2. 中端:也被称为优化器,对前端生成的IR进行优化以提高程序的性能及质量
    3. 后端:主要处理CPU架构相关的优化及生成目标机器代码

    参考资料:

  6. 什么是交叉编译?为什么在嵌入式开发中需要使用交叉编译器?

    参考答案

    交叉编译是在一个平台(主机平台)上生成另一个平台(目标平台)上的可执行代码。执行交叉编译的编译工具链即为交叉编译器。

    在嵌入式开发中,通常需要使用交叉编译器的原因有以下几点:

    1. 同时支持不同的硬件平台:嵌入式设备通常基于特定的硬件平台,例如ARM、MIPS、PowerPC等。这些平台具有不同的指令集和体系结构。通过使用交叉编译器,可以在主机平台上编写和编译代码,然后将生成的目标代码移植到目标嵌入式设备上运行。
    2. 主机与目标平台差异:主机平台通常是通用的计算机系统,例如PC或服务器,而目标平台是嵌入式设备。它们具有不同的操作系统、库和硬件资源。使用交叉编译器可以针对目标平台生成可执行代码,以便在目标设备上运行。
    3. 开发效率:嵌入式设备通常具有有限的资源,通过使用交叉编译器,可以在开发环境中进行更快速的迭代和测试,而无需在实际的嵌入式设备上进行每一次更改的编译和部署。这加快了开发过程,并提供了更高的灵活性。

    参考资料:

  7. 什么是链接器?它的作用是什么?

    参考答案

    链接器(Linker or Link editor)是一个将编译器生成的一个或多个目标文件(object files)链接为单一可执行程序、库文件或另一目标文件的程序。 链接器的主要作用有:

    1. 符号解析(Symbol resolution):链接器负责解析目标文件中使用和定义的符号(函数、变量等)。当多个目标文件之间存在相互调用或引用的符号时,链接器会解析它们之间的关系,以确保符号在最终的可执行文件中能够正确地连接和使用。
    2. 符号重定位(Symbol relocation):目标文件中的符号通常是相对于其所在模块的位置进行编码的。链接器负责将这些相对地址转换为绝对地址,以便在最终的可执行文件中正确定位符号的位置。
    3. 合并和组织代码(Code merging and organization):链接器将多个目标文件中的代码段和数据段合并成一个单一的可执行文件。它负责处理代码段的重定位和修复,以确保各个模块之间的跳转和引用是正确的。
    4. 解决库依赖(Library dependency resolution):链接器能够解决可执行文件或共享库对外部库的依赖关系。当一个目标文件引用了外部库中的函数或变量时,链接器会定位并将相关的库文件与可执行文件进行关联,以确保在运行时可以正确地调用和使用库中的功能。
    5. 符号表生成(Symbol table generation):链接器还会生成一个符号表,其中包含所有目标文件中定义和引用的符号信息。这个符号表在调试和符号查找时非常有用。

    参考资料:

  8. 解释静态链接和动态链接的区别

    参考答案(答案来源于ChatGpt)

    静态链接(Static Linking)和动态链接(Dynamic Linking)是在编译和链接过程中使用的两种不同的方法。

    静态链接是指在编译和链接时,将目标文件和库文件的代码和数据合并到最终的可执行文件中。在静态链接的情况下,目标文件中使用的所有库函数和库文件的代码都被复制到最终的可执行文件中。这意味着可执行文件独立于系统上的任何库文件,它包含了所有运行所需的代码和数据。在运行时,可执行文件不需要额外的依赖,可以直接执行。

    动态链接是指在编译和链接时,目标文件只包含对库函数的引用,而不包含实际的库函数代码和数据。在运行时,操作系统会动态加载所需的库文件,并将其与可执行文件进行链接。这意味着可执行文件本身较小,只包含了对库函数的引用,而实际的库函数代码和数据在运行时从共享库(shared library)中加载。多个可执行文件可以共享同一个共享库,从而节省了存储空间。

    区别如下:

    1. 大小:静态链接生成的可执行文件通常比较大,因为它包含了所有所需的代码和数据。而动态链接生成的可执行文件相对较小,因为它只包含对库函数的引用。

    2. 可维护性:静态链接生成的可执行文件是独立的,不依赖于外部库文件。这样可以确保程序在不同环境中的运行一致性。但是,如果库文件有更新或修复,需要重新编译和链接可执行文件。而动态链接使得库文件可以独立于可执行文件进行更新和维护,只需要更新库文件而不需要重新编译和链接可执行文件。

    3. 内存使用:静态链接时,每个可执行文件都会包含所需的库函数代码和数据,可能导致内存占用增加。而动态链接时,多个可执行文件可以共享同一个库的实例,节省了内存占用。

    4. 运行时依赖:静态链接生成的可执行文件在运行时不需要外部库文件的存在,可以直接运行。而动态链接生成的可执行文件在运行时需要依赖库文件,如果库文件不存在或版本不兼容,程序将无法执行。

    总结:静态链接将所有的代码和数据合并到可执行文件中,使其独立运行;动态链接在运行时加载所需的库文件,使得可执行文件更小,并且可以共享库文件,提高了可维护性和内存使用效率。

    参考资料:

  9. 什么是库文件(Library)?解释静态库和动态库在嵌入式开发中的使用场景。

    参考答案(答案来源于ChatGpt)

    库文件(Library)是一组预编译的代码和数据,提供了特定功能的函数,以供其他程序在编译和链接过程中使用。库文件可以包含可重用的代码、函数、变量和其他资源,它们被设计成可供多个程序共享和重复使用。

    在嵌入式系统中,通常会使用静态库来最大限度地减少系统资源的占用,尤其是对于具有严格的资源限制的系统。动态库可能在嵌入式系统中使用较少,但在一些特定场景下仍然有用,例如共享某些通用功能的库文件,以便多个应用程序可以共享和更新这些功能。

  10. 请讨论一些常见的编译器选项和优化策略,以优化嵌入式软件的性能和大小。

    参考答案(答案来源于ChatGpt)

    常用的优化软件性能和大小的编译器选项有:

    1. -Os:此选项将编译器优化为最小化代码大小。它会执行一系列优化,包括消除未使用的代码、常量折叠和传播、简化表达式等。
    2. -O2-O3:这些选项表示编译器使用更高级别的优化来提高性能和代码大小。较高级别的优化可能会增加编译时间,但通常会提供更好的性能。
    3. -ffunction-sections -fdata-sections:这些选项将代码和数据分离成小节,以便进行进一步的优化和链接时的死代码消除。这可以减小最终的可执行文件大小。
    4. -flto:此选项启用链接时优化(Link Time Optimization),它在链接过程中对整个程序进行优化。这可以提供更高级别的优化和更好的性能。
    5. -finline-functions:此选项启用函数内联优化,将函数调用替换为函数体的副本。这减少了函数调用的开销,提高了性能。
    6. -fomit-frame-pointer:此选项告诉编译器省略函数调用时的Frame Pointer,从而减少了堆栈操作和代码大小。
    7. -march-mtune:这些选项用于指定目标处理器的体系结构和微体系结构,以便编译器能够生成针对特定处理器优化的代码。
    8. -falign-functions-falign-loops:这些选项控制函数和循环的对齐方式,以提高内存访问的效率。
    9. -fno-unroll-loops:此选项禁用循环展开优化,适用于循环次数较多或代码大小有限的情况。

    参考资料:

  11. 解释编译器标志(Compiler Flag)-nostdlib的作用,并讨论在嵌入式开发中使用它的情景。

    参考答案(答案来源于ChatGpt)

    编译器标志(Compiler Flag)-nostdlib用于告诉编译器不要使用默认的标准库(standard library)。

    默认情况下,编译器会链接标准库以提供一些常见的函数和功能,如输入/输出、内存分配等。然而,在某些嵌入式开发场景中,可能需要更加精简的运行时环境,不需要使用完整的标准库。这时可以使用-nostdlib标志来排除默认的标准库。

    使用-nostdlib标志的情景包括:

    1. 嵌入式系统的资源限制:在一些嵌入式系统中,资源有限,包括处理器性能、存储空间和内存。使用-nostdlib标志可以避免链接大型标准库,减小可执行文件的大小,节省存储空间。

    2. 替代标准库:在某些嵌入式系统中,可能有自定义的库或者第三方的轻量级库,可以替代标准库的部分功能。通过使用-nostdlib标志,可以链接这些特定的库而不链接默认的标准库。

    3. 裸机编程:在裸机编程中,可能直接访问硬件寄存器和设备,而不需要标准库提供的抽象层。使用-nostdlib标志可以避免链接标准库,确保代码直接操作硬件。

    使用-nostdlib标志后,需要手动处理输入/输出、内存分配等功能的实现,或者使用其他替代方案。此外,可能还需要指定其他必要的链接选项和库文件,以满足特定嵌入式系统的需求。

    参考资料:

  12. 什么是目标文件和可执行文件?它们的区别是什么?

    参考答案(答案来源于ChatGpt)
    1. 目标文件(Object File)是编译器在编译源代码后生成的中间文件,它包含了已编译代码的二进制表示形式、符号表和其他调试信息,但尚未进行最终的链接操作。
    2. 可执行文件(Executable File)是经过链接器将一个或多个目标文件以及所需的库文件合并后生成的最终可执行程序。可执行文件包含了完整的机器指令、数据和其他所需的资源,可以直接在操作系统上运行。

    其区别如下:

    1. 目标文件是编译的中间产物,包含了已编译代码的二进制表示形式和符号表,但它们还没有被链接起来形成可执行程序。
    2. 可执行文件是最终生成的可执行程序,它包含了所有链接的目标文件和库文件,具有完整的机器指令、数据和其他资源,可以直接运行

    参考资料:

  13. 解释链接时的符号解析过程是什么?包括全局符号和局部符号的解析。

    参考答案(答案来源于ChatGpt)

    链接时的符号解析过程是链接器在将多个目标文件或库文件合并成可执行文件或目标文件时,解决符号引用(Symbol Reference)的过程。符号解析涉及全局符号和局部符号的解析。

    1. 全局符号解析:

      1. 全局符号是在多个源文件中定义和使用的全局变量、函数和对象。它们可以在不同的源文件中定义,但在链接过程中需要确保它们的引用是正确解析的。
      2. 链接器首先会收集所有目标文件中的全局符号,并将其保存到一个符号表中。符号表记录了全局符号的名称、类型和定义位置等信息。
      3. 在解析引用时,链接器会遍历所有引用的全局符号,并在符号表中查找对应的定义。如果找到了匹配的定义,链接器将将引用的地址绑定到正确的定义位置上。
    2. 局部符号解析:

      1. 局部符号是在单个源文件中定义和使用的局部变量、函数和对象。它们的作用域限定在源文件内部,对于其他源文件是不可见的。
      2. 局部符号的解析是在单个目标文件的编译过程中进行的。编译器会为每个局部符号分配唯一的标识符或地址,并在编译过程中将符号引用绑定到相应的符号定义。
      3. 在链接过程中,局部符号的解析是通过地址绑定实现的。编译器为每个局部符号分配了唯一的地址,链接器通过地址绑定将引用的地址绑定到正确的定义位置上。
  14. 什么是位置无关代码(Position-Independent Code,PIC)?它在嵌入式系统中的作用是什么?

    参考答案(答案来源于ChatGpt)

    位置无关代码(Position-Independent Code,PIC)是一种编译和链接的方式,使得代码在内存中的位置可以灵活地确定,而不依赖于固定的绝对地址。PIC的主要目的是实现可移植性和共享代码的能力。

    在嵌入式系统中,PIC有以下几个作用:

    1. 内存布局灵活:嵌入式系统通常有限的内存资源,而且内存布局可能因为硬件限制或操作系统要求而发生变化。使用PIC可以使得代码可以加载到任意的内存地址,并且代码内部的引用和跳转也是相对的,因此不依赖于固定的绝对地址。
    2. 共享库和动态链接:嵌入式系统中,共享库(或动态链接库)的使用是常见的,可以节省存储空间并提高代码的复用性。PIC使得共享库可以加载到任意的内存地址,并且可以同时被多个程序共享。
    3. 代码保护和安全性:使用PIC可以增强代码的安全性,防止针对特定地址的攻击。由于代码不依赖于固定的绝对地址,恶意用户很难通过攻击特定地址来破坏系统。

    总而言之,位置无关代码在嵌入式系统中的作用是实现代码的灵活加载和共享,适应有限的内存资源和可移植性要求,并增强代码的安全性。

    参考资料: