(一)C语言编译的四个阶段
# 预处理
gcc -E main.c -o main.i
# 编译为汇编
gcc -S main.i -o main.s
# 汇编为目标文件
gcc -c main.s -o main.o
# 链接为可执行文件
gcc main.o -o main -lm
1.预处理(Preprocessing)(gcc -E xxx.c -o xxx.i)
处理源代码.c或者.cpp文件:
(1)宏定义替换:#define PI 3.1415926。
(2)头文件的包含和展开:搜索#include中指定的头文件,并将头文件中的内容拷贝到源文件中。
(3)条件编译:#ifdef 等内容,完成条件编译内容的替换。
(4)注释删除:清除所有注释(如 //、/* … /)等。
(5)工具:预处理器(如 cpp)完成【.c 源文件】生成【*.i文件】
注:预处理阶段不会检查c程序代码语法是否错误。
2.编译(Compilation)(gcc -S xxx.i -o xxx.s)
(1)此阶段检查语法问题;
(2)语法分析、词法分析、语义分析以及中间代码生成和优化;
(3)然后生成中间汇编代码,但是还不可执行,gcc编译的中间文件是[.s]文件。
(4)工具:编译器前端(如 clang、gcc 的 cc1)完成。
3.汇编(Assembly)(gcc -c xxx.s -o xxx.o)
(1)翻译为机器码:将汇编指令逐行转换为二进制机器码。
包含指令语句的汇编代码.s 转换成 机器码.o 指令,生成目标文件(.o或.obj)。
(2)生成符号表:记录函数、全局变量等符号的地址信息等。
(3)处理重定位信息:标记需要链接阶段处理的地址引用。
(4)工具:汇编阶段由 as汇编器 完成。
4.链接(Linking)(gcc xxx.o -o xxx) -L -lm(math数学库)
- 输入:多个目标文件(.o)和库文件(.a 或 .so)
- 输出:可执行文件(如 a.out)或动态库(.dll、.so)
- 此阶段 将多个目标文件和库文件合并成可执行文件或库,完成程序中调用的各种函数跟静态库和动态库的链接,并将它们一起打包合并成可执行文件.elf(或者.out文件)。
- 链接类型:
静态链接:将库代码直接复制到可执行文件中(如 .a 文件)。
动态链接:运行时加载共享库(如 .so、.dll 文件)。
(1)符号解析:确保所有函数和变量的引用均被定义。
(2)地址重定位:调整代码中的相对地址,合并所有目标文件。
(3)合并代码段和数据段:将不同文件中的代码和数据整合到内存布局中。
(4)链接阶段由 ld链接器 完成。
注:
在开发过程中遇到编译错误时,如何确定是哪个阶段的问题:
- 预处理:头文件路径错误、宏定义冲突。
- 编译:语法错误、类型不匹配。
- 汇编:平台不支持的汇编指令。
- 链接:未定义的符号(如函数未实现)。
(1)gcc编译用到以下几个程序:C编译器gcc、汇编器as、链接器ld、二进制转换工具objcopy。
(2)nm:
(3)file,size
(4)objdump -D xxx.elf > xxx.asm
(5)objcopy binary
(二)程序的编译后组成部分
(1)linux下程序编译后,可执行.elf文件的内存布局(段指二进制格式文件中的一块区域):
(1)文本段(.text):或称代码段,通常是用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读。某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。属于CPU执行的机器指令部分( 存放函数体的二进制代码 )。
(2)数据段(.data):通常是用来存放程序中已初始化的全局变量的一块内存区域。一般常量字符串就是放在这里的,程序结束后由系统释放。只读数据段(只读常量区) 和数据段(全局变量区)统称为 数据段。
(3)bss段:未初始化的全局变量和static修饰的局部变量(初始化默认值为0);
注:
1)对于data段,保存的是初始化的全局变量和stataic的局部变量,直接载入内存即可。
2)text段保存的是代码直接载入。
3)BSS段从目标文件中读取BSS段大小,然后在内存中紧跟data段之后分配空间,并且清零(这也是为什么全局表量和static局部变量不初始化会有0值得原因)。
(2)执行可执行文件时,将可执行.elf文件从磁盘上加载到内存中,并分配虚拟内存。linux(32位)下映射4G虚拟内存:用户空间(0 - 3G) 和内核空间(3 - 4G)。
注:
0xffff ffff :4G
0xc000 0000:3G
(3)用户空间:动态存储区和静态存储区
C语言中存储区分类:
1)栈 :由编译器自动分配释放
2)堆 :一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收
3)全局区(静态区):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域(data区),未初始化的全局变量和未初始化的静态变量,static修饰的局部变量在bss段。data和bss都属于全局区。
4)常量区(readonly):一个专门放字符串常量的地方。常量存储区里面的数据是放在代码段里的,不占内存,是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。常量字符串都存放在静态存储区,返回的是常量字符串的首地址。
C++中存储区分类:
1)栈:就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。
2)堆:就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
3)自由存储区:就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
4)全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
5)常量存储区:这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改)
(4)动态存储区:栈(stack) 和 堆(heap),存放局部变量。
a.栈区:执行代码时,局部变量只有其所在的函数调用时,局部变量才由系统自动在栈上临时开辟内存空间,局部变量的生命周期随其所在的函数的结束而被系统自动释放内存空间。
b 堆区:malloc (头文件<stdlib.h>),free§;p = NULL;
(5)静态(全局区)存储区:静态区(全局区)变量在编译时就已经分配好内存空间:链接后的文件未加载前还是存在磁盘上的,文件中的变量、函数的地址都是逻辑地址。当程序执行,即加载可执行文件到内存上时,会将逻辑地址映射到内存中,这时全局变量就会被加载到内存中的数据段(data)。
注:每次调用有全局变量的函数时,都会在上一次值的基础上累加运算,而局部变量则是重新开辟栈。
(6)备注:
(1)函数体中定义的变量通常是在栈上。
(2)用malloc, calloc, realloc等分配内存的函数分配得到的就是在堆上。
(3)在所有函数体外定义的是全局变量,在全局区。
(4)加了static修饰符后,不管变量在哪里都存放在全局区(静态区)。
(5)在所有函数体外定义的static变量,表示只在该文件中有效,不能被extern到别的文件使用。
(6)在函数体内定义的static表示只在该函数体内有效。
(7)另外,函数中的"hello world"这样的字符串常量存放在 常量区。
(8)全局变量和静态static修饰的变量,没有初始化会自动初始化为0。
(7)误区:
1)普通全局变量和静态(static)全局变量的区别
答:静态全局变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,普通的全局变量在各个源文件中都是有效的,可以extern引用。 而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效, 在其它源文件中不能使用它。
2)static 局部变量和普通局部变量的区别
答:局部变量改变为静态static局部变量后,改变了它的存储方式即改变了它的生命周期。
static 局部变量只被初始化一次,下一次是依据上一次值。
3)static 函数与普通函数的区别
答:
a)static 函数与普通函数作用域不同,static函数仅作用在本文件。
b)只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。
c)对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件。
d)static 函数在内存中只有一份(.data),普通函数在每个被调用中维持一份拷贝。
e)static修饰的函数
- 如果函数定义是在.c文件,声明是在.h文件中,则别的文件即使包含头文件也无法调用此static 函数;
但是如果static 函数的定义和声明都是在.h文件中完成的,那么别的文件只要包含该头文件就可以调用使用该static 函数; - .c文件中定义static函数,外部文件中即使包含其头文件,也无论如何都无法调用使用该static 函数;
- 函数的声明和定义分开时,形参的参数名可以不同,参数的排列顺序也可以不同,但是参数的个数和类型必须一致。