一个C程序从代码到一个可执行程序需要精力四个阶段,分别是预处理 编译 汇编 链接
预处理
其中预处理是把代码中的#开头的命令处理一下,例如#include,就是去找到对应的头文件#define就是去替换代码中所有宏定义部分的内容,这一步没有任何程序转化发生,完全是在文本层面的,例如#define就是对宏定义的内容进行文本替换,#include就是直接把头文件的内容复制过来,它生成的结果也是一个文本文件,通常用.i作后缀
例如原本有一个hello world程序长这样
#include <stdio.h>
#define PI 3.14
int main(void)
{
printf("pi is: %f\nPI is %f\n",PI, 3.14);
return 0;
}
经过 g++ -E main.cpp -o main.i命令之后就变成了这样
...
...
extern void flockfile (FILE *__stream) throw ();
extern int ftrylockfile (FILE *__stream) throw () ;
extern void funlockfile (FILE *__stream) throw ();
# 942 "/usr/include/stdio.h" 3 4
}
# 2 "main.cpp" 2
# 3 "main.cpp"
int main(void)
{
printf("pi is: %f\nPI is %f\n",3.14, 3.14);
return 0;
}
上面是冗长的头文件的内容,我用了两行省略号省略了,可以看到最下面的那个PI已经被替换成了3.14了,而引号里面的没有被替换,因为g++的规则是不处理引号里面的内容.
编译
其实g++在执行编译命令的时候会带上预处理,然后不保存过渡用的预处理出来的.i文件,所以编译时候的输入还是你写的.c文件,如果想看一看预处理结果的话才用-E指令,所以如果我们直接用原始.c文件来编译一下也可以,用刚生成的.i文件编译也可以,命令是g++ -S main.i -o main.s,他的结果是一个汇编程序的文件,注意这里输出的仍然是一个文本文件,因为内容很短我这里粘贴完整的代码
.file "main.cpp"
.section .rodata
.LC1:
.string "pi is: %f\nPI is %f\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movsd .LC0(%rip), %xmm0
movabsq $4614253070214989087, %rax
movapd %xmm0, %xmm1
movq %rax, -8(%rbp)
movsd -8(%rbp), %xmm0
movl $.LC1, %edi
movl $2, %eax
call printf
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.section .rodata
.align 8
.LC0:
.long 1374389535
.long 1074339512
.ident "g++: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
.section .note.GNU-stack,"",@progbits
汇编
汇编就是把编译出来的汇编文件打包成一个二进制文件,里面存的都是机器指令.g++的命令是g++ -c main.s -o main.o, 得到的结果是个.o文件,这个文件是不能用人类语言解读的,打开也是一堆乱码
链接
命令为g++ main.o -o main,检查刚才的.o文件,查看它引用的其他.o文件,比方说printf函数就是C语言的库函数,她在一个单独的printf.o文件里,这个文件在g++的目录里面,和我们工程没有关系,所以看不见,但是它确实会去找这个文件,所以链接就是找到它,然后把他们集成到一个可执行程序里,再比如说你程序要是用到静态库的话,也是在这一步添加进程序里面.在.o文件里还是存在变量名称的,但是链接之后就不存在任何名称了,只有地址,也就是这个程序每个变量在逻辑内存上的地址了.注意,这句很重要,就是操作系统在内存管理的时候每个程序都是单独的一块内存空间,他的地址叫做虚拟地址,并不是真的物理地址,它只相对于这个程序内存开始段的偏移量,比如说某个变量地址是100,那他并不是真的在内存的100这个位置,而是从这个程序自己空间的首地址开始再数100,这时候每个变量都有地址了,而且地址都是固定的,所以我们同一个程序每次执行同一个变量的地址都是一样的.
动态库和静态库
最后再强调一下动态链接库和静态链接库的问题,动态库在linux系统叫.so文件,在windows叫.dll文件,静态库在linux叫.a文件,在windows叫.lib文件,它们都是底层开发者给上层提供的方法,但是在整个程序使用的阶段不同
静态库
其中静态库是程序在链接时候就把内容全部塞进去了,就和预处理时候把头文件全部塞进去一样,程序链接的时候会把用到的静态库的内容也塞到最终那个可执行程序里,然后你原本的那个静态库文件.a文件或者.lib文件就可以删掉了
动态库
动态库是为了方便大型软件更新,不需要更新整个可执行程序,所以才把库的内容分离了出来,他在链接的时候只需要留下一个引用的标记,知道运行的时候找这个文件就行了,不需要把这个文件的内容完整的拷贝到程序里去,所以你程序运行的时候还需要这个动态库在目录里能让他找到,像我们windows玩游戏经常会缺少某个dll文件,就是因为系统动态库找不到了,我们只需要网上下载一个同名的放到她能找到的目录就可以了