嵌入式C语言面试相关知识——编译过程
一、博客声明
又是一年一度的秋招,怎么能只刷笔试题目呢,面试题目也得看,想当好厂的牛马其实也不容易呀O(∩_∩)O。注意:这篇博客大部分是来自网上的资源,通过自问或者他问,然后寻找答案,为了加深印象,总结和抄一遍。
二、自问题目
1、C语言的编译过程是什么?
C语言的编译过程分为四个主要步骤:*预处理、编译、汇编和链接。
源文件 (source.c)
↓ 预处理器 (Preprocessor)
预处理后的文件 (source.i)
↓ 编译器 (Compiler)
汇编代码文件 (source.s)
↓ 汇编器 (Assembler)
目标文件 (source.o)
↓ 链接器 (Linker)
可执行文件 (a.out 或 main.exe)
- 预处理(Preprocessing): 在这一步,预处理器将对源码进行处理。它会根据以 “#” 开头的指令,如
#include
和#define
,展开头文件、宏定义,并去除注释等。生成的结果是一个经过宏替换和条件编译后的纯C代码文件,输出的扩展名为 “.i” 。gcc -E main.c -o main.i
- 编译(Compilation): 在这一步,编译器将把预处理后的源代码转为汇编语言。它会将C语言代码翻译成相应的汇编指令,但还没有生成可以执行的代码。生成的结果是一个以 “.s” 或 “.asm” 为扩展名的汇编文件。
gcc -S main.i -o main.s
- 汇编(Assembly): 在这一步,汇编器将把上一步产生的汇编语言代码转化为机器码指令,即可执行的二进制代码文件。它会将每条汇编指令翻译成机器码,并生成目标文件(通常以 “.obj” 或 “.o” 为扩展名),其中包含了已经转换好的机器码指令。
gcc -c main.s -o main.o
- 链接(Linking): 最后一步就是链接器对目标文件进行链接操作,将所有需要用到的函数库和对象文件合并成一个可执行文件。链接器解析符号引用并确定函数和变量在内存中的地址,生成最终可执行文件(通过以 “.exe” 或 “.out” 为扩展名).
gcc main.o -o main
2、编译过程中参与的工具有哪些?
其实上面已经说了,分别是预处理器、编译器、汇编器和链接器。
- 预处理器: 预处理器会处理以 # 开头的指令(如#include、#define等)和宏定义。预处理的输出是一个纯C语言代码文件。
- 宏替换: 将所有的宏定义(如#define)替换为相应的值。
- 头文件包含: 处理#include指令,将头文件的内容插入到源文件中。
- 条件编译: 根据条件编译指令(如#ifdef、#ifndef)选择性地编译代码段。
- 删除注释: 去除源代码中的注释。
- 编译器: 编译是将预处理后的C代码转换为汇编代码的过程。编译器会进行语法检查、语义分析和代码优化,生成目标平台的汇编代码。
- 汇编器: 汇编器将编译器生成的汇编代码转换为机器码(目标代码),生成目标文件(通常是.o或.obj文件)。目标文件是二进制文件,包含机器码和相关数据。
- 链接器: 链接器将一个或多个目标文件以及所需的库文件链接在一起,生成最终的可执行文件。链接器会解析符号引用(例如函数和全局变量),并将所有目标文件和库文件中的代码和数据整合到一个可执行文件中。
- 静态链接: 将所有需要的库代码复制到可执行文件中。
- 动态链接: 可执行文件在运行时加载库代码(如动态链接库DLL或共享库SO)。
3、什么是条件编译,作用是什么?
-
条件编译(Conditional Compilation): 是指在编译过程中,根据特定的条件来决定某些代码是否被编译的技术。条件编译通过预处理指令来实现,这些指令会在预处理阶段进行解析,并根据条件来包含或排除代码块。条件编译的预处理指令主要包括:
- #if
- #ifdef
- #ifndef
- #else
- #elif
- #endif
-
作用:
- 提高代码的可移植性: 在不同的平台或环境下编译相同的代码时,可能需要包含或排除特定的代码段。通过条件编译,可以为不同的平台或环境编写特定的代码,而无需维护多个代码版本。
- 调试和开发: 在开发和调试过程中,条件编译可以用来包含或排除调试代码。例如,可以在调试版本中包含调试信息和日志记录代码,而在发布版本中排除这些代码。
- 功能开关: 通过条件编译,可以根据需要启用或禁用某些功能。例如,可以使用条件编译来启用或禁用某些模块、特性或优化选项。
- 减少代码的编译时间: 在大型项目中,条件编译可以用来只编译需要测试或开发的部分代码,从而减少编译时间。
4、链接环节中的静态链接和动态链接是什么,有何区别?
-
静态链接: 是将所有需要的库代码在编译时直接复制到可执行文件中。
- 特点:
- 独特性: 生成的可执行文件包含了所有必要的库代码,不依赖于外部库文件。这使得可执行文件可以在没有相应库文件的系统上运行。
- 文件体积较大: 由于所有的库代码都被复制到可执行文件中,生成的可执行文件体积较大。
- 更新不便: 如果库文件有更新,必须重新编译所有引用该库的可执行文件。
- 静态链接库文件后缀名: Linux/Unix系统为 “.a” ,Windows系统为 “.lib” 。
- 特点:
-
动态链接: 是在运行时,将库代码加载到内存中,由多个程序共享使用。动态链接使用动态链接库。
- 特点:
- 节省内存和磁盘空间: 多个程序可以共享相同的动态库,减少了内存和磁盘空间的使用。
- 文件体积较小: 生成的可执行文件不包含库代码,体积较小。
- 灵活性: 更新库文件不需要重新编译可执行文件,只需要保证新的库文件和旧的库文件的接口兼容。
- 动态链接库文件后缀名: Linux/Unix系统为 “.so” ,Windows系统为 “.dll” , macOs系统为 “.dylib” 。
- 特点:
-
静态链接和动态链接的区别:
特点 | 静态链接 | 动态链接 |
---|---|---|
文件独立性 | 独立运行,不依赖外部库文件 | 依赖外部库文件 |
可执行文件体积 | 大 | 小 |
内存使用 | 高 | 低(共享库代码) |
更新与维护 | 更新库文件需要重新编译 | 更新库文件不需要重新编译 |
运行效率 | 一般较高,启动速度快 | 一般较低,启动速度慢 |
共享性 | 不共享库代码,每个程序独立 | 共享库代码,多个程序可使用同一个库 |
5、什么是头文件保护符(Include Guards),为什么需要它们?
头文件保护符是一种防止头文件被重复包含的方法。通过使用#ifndef、#define和#endif预处理指令,可以确保头文件的内容只被包含一次,从而避免重复定义和编译错误。
6、如何创建和使用静态库和动态库?
- 创建静态库:
gcc -c file1.c file2.c
ar rcs libmylib.a file1.o file2.o
- 使用静态库:
gcc main.c -o main -L. -lmylib
- 创建动态库:
gcc -fPIC -c file1.c file2.c
gcc -shared -o libmylib.so file1.o file2.o
- 使用动态库:
gcc main.c -o main -L. -lmylib
export LD_LIBRARY_PATH=/path/to/library:$LD_LIBRARY_PATH