编译过程
C语言是一种编译型语言,需要把源文件进行编译之后才能运行,它的编译过程如下:
- 预处理:展开头文件、宏替换,去掉注释,条件编译
- 编译:检查语法,生成汇编
- 汇编:把生成的汇编文件汇编成机器码
- 链接: 链接到一起生成可执行程序 a.out
创建hello.c文件,用于测试:
#include <stdio.h>
#define PI 3.14
int main(){
printf("Hello C %f\n",PI);
return 0;
}
预处理
预处理会做展开头文件/宏替换/去掉注释/条件编译操作, 这一步主要是做一些处理,比如把我们include的一些文件都包含进来。而且把所有的预处理命令都执行掉。比如 我们常见的#define PI 3.14
执行下面的命令进行预处理:
gcc -E hello.c -o hello.i
-E :Preprocess only; do not compile, assemble or link.
可以看一下hello.i文件:
extern int sys_nerr;
extern const char *const sys_errlist[];
# 854 "/usr/include/stdio.h" 2 3 4
extern int fileno (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
extern int fileno_unlocked (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
# 873 "/usr/include/stdio.h" 3 4
extern FILE *popen (const char *__command, const char *__modes) ;
extern int pclose (FILE *__stream);
extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__));
# 913 "/usr/include/stdio.h" 3 4
extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 943 "/usr/include/stdio.h" 3 4
# 2 "hello.c" 2
# 3 "hello.c"
int main(){
printf("Hello C %f\n",3.14);
return 0;
}
可以看到main函数上面把原来include<stdio.h>文件给包含了进来,且我们定义的宏#define PI 3.14也被进行了预处理
编译(生成汇编)
这一步会先检查语法,然后生成汇编。
命令如下:
gcc -S hello.c -o hello.s
-S: Compile only; do not assemble or link.
生成的hello.s汇编文件如下:
.file "hello.c"
.text
.section .rodata
.LC1:
.string "Hello C %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
movq .LC0(%rip), %rax
movq %rax, %xmm0
movl $.LC1, %edi
movl $1, %eax
call printf
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.section .rodata
.align 8
.LC0:
.long 1374389535
.long 1074339512
.ident "GCC: (GNU) 9.3.1 20200408 (Red Hat 9.3.1-2)"
.section .note.GNU-stack,"",@progbits
汇编
把汇编代码汇编成机器码
命令如下:
gcc -c hello.s -o hello.o
-c Compile and assemble, but do not link.
这一步生成机器码,自己可以查看下是一堆二进制数据。
链接
将二进制机器码,转换成二进制语言的可执行程序
gcc hello.o -o a.out
上面所有的步骤都可以一条命令执行完成:
gcc hello.c -o a.out
这里贴上一段gcc/g++编译器的描述:
C 和 C++ 编译器 是 集成的. 他们 都要 用 四个步骤 中的 一个 或 多个 处理 输入文件: 预处理(preprocessing), 编译(compilation), 汇编(assem‐
bly) 和 连接(linking). 源文件后缀名 标识 源文件 的 语言, 但是 对 编译器 来说, 后缀名 控制着 缺省设定:
gcc 认为 预处理后的 文件 (.i) 是 C 文件, 并且 设定 C 形式 的 连接.
g++ 认为 预处理后的 文件 (.i) 是 C++ 文件, 并且 设定 C++ 形式 的 连接.
源文件后缀名 指出 语言种类 以及 后期 的 操作:
.c C 源程序; 预处理, 编译, 汇编
.C C++ 源程序; 预处理, 编译, 汇编
.cc C++ 源程序; 预处理, 编译, 汇编
.cxx C++ 源程序; 预处理, 编译, 汇编
.m Objective-C 源程序; 预处理, 编译, 汇编
.i 预处理后的 C 文件; 编译, 汇编
.ii 预处理后的 C++ 文件; 编译, 汇编
.s 汇编语言源程序; 汇编
.S 汇编语言源程序; 预处理, 汇编
.h 预处理器文件; 通常 不出现在 命令行 上
其他 后缀名 的 文件 被传递 给 连接器(linker). 通常 包括:
.o 目标文件 (Object file)
.a 归档库文件 (Archive file)
除非 使用了 -c, -S, 或 -E 选项 (或者 编译错误 阻止了 完整 的 过程), 否则 连接 总是 最后的步骤. 在 连接阶段 中, 所有 对应于 源程序 的 .o
文件, -l 库文件, 无法 识别 的 文件名 (包括 指定的 .o 目标文件 和 .a 库文件) 按 命令行中 的 顺序 传递给 连接器.
制作静态链接库
先创建c文件目录,目录结构如下
-CLearn
-lib 存放我们生成的静态链接库文件
-include 存放我们的库文件源码和头文件
-selflib.h 头文件
-selflib.c 库文件源码
-hello.c 示例程序
selflib.h内容:
#ifndef __SELFLIB_H__
#define __SELFLIB_H__
int a=20;
extern void test();
#endif
selflib.c内容:
#include <stdio.h>
int test(){
printf("test\n");
return 0;
}
hello.c内容:
#include <stdio.h>
#include "libself.h"
#define PI 3.14
int main(){
printf("hello C %2f \n",PI);
printf("a:%d \n",a);
test();
return 0;
}
编译libself.c
gcc -c include/libself.c -o libself.o
-c:仅编译,不执行链接,必须加这个参数
把libself.o转为静态链接库
ar -r lib/libself.a include/libself.o
说明:
-r: 把目标文件(libself.o)插入到库文件(libself.a)中
使用库文件编译hello.c
gcc hello.c -I include -lself -L ./lib -o hello
说明:
-I(大写i) 头文件搜索目录
-l(小写L):指定库文件名,注意-l后面直接添加库名省去“lib”和“.so”或“.a”
-L:静态链接库或者动态链接库搜索目录
制作动态链接库
编译libself.c
gcc -c include/libself.c -fPIC -o include/libself.o
-fPIC : 生成 位置无关目标码. 作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的
生成共享库
gcc -shared include/libself.o -o lib/libself.so
-shared 生成 一个 共享目标文件, 他 可以 和 其他 目标文件 连接 产生 可执行文件
将hello.c编译链接为可执行程序
gcc hello.c lib/libself.so -I include -o hello
或者
gcc hello.c -lself -L ./lib -I include -o hello
上面第二个使用方法和连接静态库是一样的。也就是说-l(小写L)选项也会查找.so文件,如果.so和.a文件同时存在,则优先连接.so文件
编译链接生成的hello文件还不能执行,上面我们使用的链接库是连接时用的链接库地址,真正执行时系统是不知道我们的链接库地址的,所以要把so文件copy到/usr/lib或者/usr/local/lib下,也可以在LD_LIBRARY_PATH环境变量上追加我们的库路径
cp lib/libself.so /usr/local/lib/
或者
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./lib