GCC教程

GCC教程

1.以常见的hello world程序为例,从一个C语言源文件,到生成最后的可执行文件,GCC编译过程的基本流程如下:
(1)C源文件
(2)预处理:生成预处理后的C源文件 hello.i
(3)编译:将C源文件翻译成汇编文件hello.s
(4)汇编:将汇编文件汇编成目标文件hello.o
(5)链接:将目标文件链接成可执行文件
在这里插入图片描述
gcc命令是GCC编译器里的一个前端程序,用来控制整个编译过程: 分别调用预处理器,编译器和汇编器,完成编译的每一个过程,最后调用链接器,生成可执行文件:a.out.
默认情况下,gcc命令会自动完成上述的整个编译过程,当然,gcc还提供了一系列参数,使用这个参数,可以让用户精准控制每一个编译过程,例如:
-E :只做预处理,不编译
-S :只编译,将C程序编译为汇编文件
-c :只汇编,不链接。
-o :指定输出的文件名

其他选项参数:
在这里插入图片描述
2.各阶段详解:
(1)预编译
预编译过程主要是处理那些源代码中的以"#“开始的预编译指令。比如”#include", "#define"等,主要处理规则如下:

1.将所有的“#define”删除,并且展开所有宏定义
2.处理所有条件预编译指令,比如#if,#ifdef,#elif,#else,#endif
3.处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
4.删除所有的注释“//”和“/**/”
5.添加行号和文件名标识,比如#2 “hello.c” 2,以便于编译时编译器产生调试用的行号信息以及用于编译时产生编译错误或警告时能够显示行号。
6.保留所有的#progma编译器指令,因为编译器需要使用它们。

经过预编译的i文件不包含任何宏定义,当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。
(2)编译
编译过程就是把处理完的文件进行一系列词法分析,语法分析,语义分析以及优化后生产相应的汇编代码文件,这个过程往往就是我们所说的整个程序构建的核心部分,也是最复杂的部分之一。
对于C语言来说,这个预编译和编译的程序是ccl,对于C++来说,对应的程序叫做cclplus,java则是jcl。所以实际上gcc这个命令只是这些后台程序的包装,它会根据不同参数要求去调用预编译编译程序cc1,汇编器as,链接器ld
(3)汇编
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎对应一条机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了,“汇编”这个名字也来源于此。这个过程我们可以直接调用汇编器as来完成

as hello.s -o hello.o

或者

gcc -c hello.s -o hello.o

(4)链接
简而言之:把各个目标代码连接起来组成完整代码文件

3.静态库
在 Linux 中静态库以 lib 作为前缀、以 .a 作为后缀,形如 libxxx.a(其中的 xxx 是库的名字,自己指定即可)。静态库以之所以称之为「静态库」,是因为在链接阶段,会将汇编生成的目标文件 .o 与引用的库一起链接到可执行文件中,对应的链接方式称为静态链接。
在 Linux 中静态库由程序 ar 生成。生成静态库,需要先对源文件进行汇编操作得到二进制格式的目标文件(以 .o 结尾的文件),然后再通过 ar 工具将目标文件打包就可以得到静态库文件了。
使用 ar 工具创建静态库的一般格式为:

$ ar -rcs libxxx.a 若干原材料(.o文件)

例子:
add.c


#include <stdio.h>

int add(int a, int b)
{
    return a + b;
}

sub.c


#include <stdio.h>

int subtract(int a, int b)
{
    return a - b;
}

mult.c


#include <stdio.h>

int multiply(int a, int b)
{
    return a * b;
}

#第一步:将源文件 add.c、sub.c、mult.c 进行汇编,得到二进制目标文件 add.o、sub.o、mult.o

$ gcc -c add.c sub.c mult.c

第二步:将生成的目标文件通过 ar 工具打包生成静态库

$ ar rcs libcalc.a add.o sub.o mult.o

head.h

#ifndef _HEAD_H_
#define _HEAD_H_

int add(int a, int b);

int subtract(int a, int b);

int multiply(int a, int b);

#endif

main.c

#include <stdio.h>
#include "head.h"

int main()
{
    int a = 20;
    int b = 12;

    printf("a = %d, b = %d\n", a, b);
    printf("a + b = %d\n", add(a, b));
    printf("a - b = %d\n", subtract(a, b));
    printf("a * b = %d\n", multiply(a, b));

    return 0;
}

运行指令

$ gcc main.c -o main -L ./ -l calc

编译 main.c 文件,并链接静态库 libcalc.a:

-L:指定使用的库的路径(因为在同一级目录下,所以可以直接用了./,或者使用绝对路径也是可以的)
-l:指定使用的库(库的名字一定要掐头去尾。如:libcalc.a 变为 calc)

ar 命令参数介绍
制作静态库时所使用的指令

$ ar rcs libcalc.a add.o sub.o mult.o div.o

共有三个参数:
-c:创建一个库,不管库是否存在,都将创建。这个很好理解,就不做过多的解释了。
-r:在库中插入(替换)模块 。默认新的成员添加在库的结尾处,如果模块名已经在库中存在,则替换同名的模块。
-s:创建目标文件索引,这在创建较大的库时能加快时间。
其他参数:
显示库文件中有哪些目标文件,只显示名称

$ ar t libcalc.a

显示库文件中有哪些目标文件,显示文件名、时间、大小等详细信息

$ ar tv libcalc.a

显示库文件中的索引表

$ nm -s libcalc.a

#为库文件创建索引表

$ ranlib libcalc.a

参数 -r 的详细解释
假设现在有了新的需求,需要静态库 libcalc.a 提供除法运算的功能模块,该怎么操作呢?
首先我们需要新建一个除法运算的源文件 div.c:

#include <stdio.h>
double divide(int a, int b)
{
    return (double)a / b;
}

并通过汇编操作生成目标文件 div.o。
接下来我们可以通过 -r 参数将除法运算的模块添加到静态库中:

$ ar -r libcalc.a div.o

4.动态库
(1).在 Linux 中动态库以 lib 作为前缀、以 .so 作为后缀,形如 libxxx.so(其中的 xxx 是库的名字,自己指定即可)。相比于静态库,使用动态库的程序,在程序编译时并不会链接到目标代码中,而是在运行时才被载入。不同的应用程序如果调用相同的库,那么在内存中只需要有一份该共享库的实例,避免了空间浪费问题。同时也解决了静态库对程序的更新的依赖,用户只需更新动态库即可。
(2)动态库的生成
生成动态库是直接使用 gcc 命令,并且需要添加 -fpic 以及 -shared 参数:
-fpic 参数的作用是使得 gcc 生成的代码是与位置无关的,也就是使用相对位置。
-shared 参数的作用是告诉编译器生成一个动态链接库。

(3)动态库的制作举例
还是以上述程序 add.c、sub.c、mult.c 为例:
第一步:将源文件 add.c、sub.c、mult.c 进行汇编,得到二进制目标文件 add.o、sub.o、mult.o

$ gcc -c -fpic add.c sub.c mult.c

第二步:将得到的 .o 文件打包成动态库

$ gcc -shared add.o sub.o mult.o -o libcalc.so

第三步:发布动态库和头文件

  1. 提供头文件 head.h
  2. 提供动态库 libcalc.so
    和静态库的链接方式一样,都是通过同样指令进行链接库操作
$ gcc main.c -o main -L ./ -l calc
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值