源文件需要经过编译才能生成可执行文件。GCC是一款强大的程序编译软件,能够在多个平台中使用。
主要处理源代码文件中以#开头的预编译指令。 处理规则有:
(1)删除所有的 #define,展开所有的宏定义。
(2)处理所有的条件预编译指令,如#if、#endif、#ifdef、#elif、和#else。
(3)处理 #include 预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含了其他文件。
(4)删除所有的注释。
(5)保留所有 #pragma 编译器指令,编译器需要用到它们。比如,#pragme once 是为了防止有文件被重复引用。
(6)添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告时,能够显示行号。
把预编译之后生成的.i或.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件。
(1)词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。
(2)语法分析:语法分析器对扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的语法树是一种以表达式为节点的树。
(3)语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义,也就是在编译期能分析的语义。相对应的动态语义是在运行期才能确定的语义。
(4)优化:源代码级别的一个优化过程。
(5)目标代码生成:由代码生成器 将中间代码转换成目标机器代码,生成一系列由汇编语义表示的代码序列。
(6)目标代码优化:目标代码优化器 对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移来代替乘法运算、删除多余的指令等。
将汇编代码转变成机器可以执行的指令(机器码文件)。汇编器的汇编过程相对于编译器来说更简单,没有复杂的语法,也没有语义,更不需要做指令优化,只有根据汇编指令和机器指令的对照表翻译过来。汇编过程是由汇编器完成。
经汇编之后,产生目标文件,Linux下是.o文件,windows下是.obj文件。
将不同的源文件产生的目标文件进行链接,从而生成一个可以执行的程序。链接分为静态链接和动态链接。
函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据,并把它们和应用程序的其他模块组合起来,创建最终的可执行文件。
缺点是空间浪费,因为每个可执行文件中对所需的目标文件都要有一个副本,所以如果多个程序对同一目标文件都有依赖,会出现同一个目标文件在内存中存在多个副本。
还有一个缺点是更新困难,每当库函数代码修改了,这个时候就需要重新进行编译链接生成可执行程序。
优点是运行速度快,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
动态链接的基本思想是把程序按照模块拆分成各个相对独立的部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有的程序模块都链接成一个单独的可执行文件。
共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多份副本,而是这多个程序在执行时共享同一份副本。
更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。
性能损耗:因为把链接推迟到了程序运行时,每次执行程序都需要进行链接,所以性能会有一定的损失。
最常用的gcc编译命令为:
# -o是输出的意思 输出名为hello的可执行程序,然后可以执行./hello
gcc -o hello hello.c
gcc使用示例:
gcc hello.c // 输出一个名为a.out的可执行程序,然后可以执行./a.out
gcc -o hello hello.c // 输出名为hello的可执行程序,然后可以执行./hello
gcc -o hello hello.c -static // 静态链接
gcc -c -o hello.o hello.c // 先编译(不链接)
gcc -o hello hello.o // 再链接
可以通过不同的gcc选项控制gcc编译过程,将gcc编译过程拆分。
# 只进行预处理,生成.i文件
gcc -E -o hello.i hello.c
# 只进行编译操作,将.i文件生成.s的汇编文件
gcc -S -o hello.s hello.i
# 只进行汇编操作,将.s文件生成.o文件
gcc -c -o hello.o hello.s
# 只进行链接操作,将.o文件进行链接,生成最终的可执行文件
gcc -o hello hello.o
执行gcc -o hello hello.c -v
时,可以查看到整个gcc编译步骤,可以结合上图进行理解。
# cc1命令完成预处理与编译操作
cc1 main.c -o /tmp/ccXCx1YG.s
# as命令完成汇编操作
as -o /tmp/ccZfdaDo.o /tmp/ccXCx1YG.s
cc1 sub.c -o /tmp/ccXCx1YG.s
as -o /tmp/ccn8Cjq6.o /tmp/ccXCx1YG.s
#collect2命令将生成的所有.o文件进行链接,生成可执行文件test
collect2 -o test /tmp/ccZfdaDo.o /tmp/ccn8Cjq6.o ....
常用选项 | 描述 |
---|---|
-E | 预处理,开发过程中想快速确定某个宏可以使用“-E -dM” |
-c | 把预处理、编译、汇编都做了,但是不链接 |
-o | 指定输出文件 |
-I(i的大写) | 指定头文件目录 |
-L | 指定链接时库文件目录 |
-l(L的小写) | 指定链接哪一个库文件 |
例如,使用 -I
参数指定头文件目录
# 头文件目录为当前目录 ./
gcc -c -o main.o main.c -I ./
(1)一起编译、链接
gcc -o test main.c sub.c
(2)分开编译,统一链接
gcc -c -o main.o main.c
gcc -c -o sub.o sub.c
gcc -o test main.o sub.o
制作、编译:
# 将main.c预处理、编译、汇编成main.o
gcc -c -o main.o main.c
# 将sub.c预处理、编译、汇编成sub.o
gcc -c -o sub.o sub.c
# 使用sub.o制作动态库 libsub.so,当然可以使用多个.o文件生成动态库
gcc -shared -o libsub.so sub.o sub2.o sub3.o(可以使用多个.o生成动态库)
# 使用main.o和制作的动态库链接生成可执行程序test,
# 其中-lsub可替换为libsub.so,./lib/是动态库 libsub.so所在的目录,自己指定
gcc -o test main.o -lsub -L ./lib/
运行:
方式(1):把 libsub.so
放到Ubuntu的/lib
目录,然后就可以运行test程序。
方式(2):如果不想把libsub.so
放到/lib
,也可以放在某个目录比如 /a
,然后在终端执行:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/a
./test
注意:链接时指定的libsub.so
目录与运行时指定的libsub.so
目录是两码事,两者没有关系。
# 将main.c预处理、编译、汇编成main.o
gcc -c -o main.o main.c
# 将sub.c预处理、编译、汇编成sub.o
gcc -c -o sub.o sub.c
# 使用sub.o制作静态库 libsub.a,当然可以使用多个.o文件生成静态库
ar crs libsub.a sub.o sub2.o sub3.o(可以使用多个.o生成静态库)
# 使用main.o和制作的静态库链接生成可执行程序test,
# 如果.a不在当前目录下,需要指定它的绝对或相对路径
gcc -o test main.o libsub.a
# 指定libsub.a存放的路径
gcc -o test main.o libsub.a -L ../lib/
运行:
不需要把静态库libsub.a放到板子上。
注意:执行arm-linux-gnueabihf-gcc -c -o sub.o sub.c
交叉编译需要在最后面加上 -fPIC
参数。
# 查看预处理结果,比如头文件是哪个
gcc -E main.c
# 把所有的宏展开,存在1.txt里
gcc -E -dM main.c > 1.txt
# 生成依赖文件abc.dep,后面Makefile会用
gcc -Wp,-MD,abc.dep -c -o main.o main.c
# 它会列出头文件目录、库目录(LIBRARY_PATH)
echo 'main(){}'| gcc -E -v -
hello.c
程序如下:
#include
#define MAX 20
#define MIN 10
//#define _DEBUG
#define SetBit(x) (1<<x)
int main(int argc, char* argv[]){
printf("Hello World \n");
printf("MAX = %d,MIN = %d,MAX + MIN = %d\n",MAX,MIN,MAX + MIN);
#ifdef _DEBUG
printf("SetBit(5) = %d,SetBit(6) = %d\n",SetBit(5),SetBit(6));
printf("SetBit( SetBit(2) ) = %d\n",SetBit( SetBit(2) ));
#endif
return 0;
}
main.c
程序如下:
#include
#include "sub.h"
int main(int argc, char *argv[]){
int i;
printf("Main fun!\n");
sub_fun();
return 0;
}
sub.c
程序如下:
void sub_fun(void){
printf("Sub fun!\n");
}
sub.h
程序如下:
void sub_fun(void);
注意:
程序中头文件#include
与#include "sub.h"
的不同:
使用双引号包含的头文件表示程序在当前目录下查找对应的头文件;
使用 <> 包含的头文件表示程序去系统或工具链中指定的目录查找对应的头文件
如果想把#include "sub.h"
改为include
,则需要把sub.h
放到系统或工具链中的目录中。