GCC笔记

GCC构成

一个完整的C语言编译器套件,主要包括以下部分:

  • 预处理器、编译器、汇编器、链接器
  • C标准库的实现,C标准库对应的头文件
  • 调试工具

GCC编译的第一个程序

使用vim创建一个hello.c文件
保存退出后,使用gcc命令将hello.c编译成可执行程序a.out

  # gcc hello.c
  # ls
  a.out hello.c
  # ./a.out
  hello world

如果想将源文件编译为一个指定的可执行文件:hello,可以通过 gcc -o 完成

  # gcc -o hello hello.c 
  # ls 
  hello hello.c 
  
  # ./hello 
  hello world

GCC编译过程分析

以hello.c为例:从一个源文件,到生成最后的可执行文件,GCC编译过程的基本流程如下

  • C源文件
  • 预处理:生成预处理后的源文件 hello.i
  • 编译:将源文件翻译成汇编文件 hello.s
  • 汇编:将汇编文件汇编成目标文件 hello.o
  • 链接:将目标文件链接成可执行文件

默认情况下,gcc命令会自动完成上述的整个编译过程。当然,gcc还提供了一系列参数,使用这个参数,可以让用户精准控制每一个编译过程。

   -E:只做预处理,不编译
   -S:只编译,将源文件编译为汇编文件
   -c:只汇编,不链接
   -o:指定输出的文件名

gcc -E参数

如果只对一段C语言程序做预处理操作,而不进行编译,可以通过gcc -E 参数来完成。如下面的一段程序,在程序中分别使用#include包含头文件,使用#define定义宏,使用#ifdef条件编译。

  #include <stdio.h>
  #define PI 3.14
  int main(){
	  printf("hello world\n");
	  printf("PI=%f\n", PI);
	  #ifdef DEBUG
		printf("debug mode\n");
	  #else
		 printf("release mode\n");
	  #endif
	  return 0;
  }

对上面的C源程序使用gcc -E进行预处理,就可以生成原汁原味的C程序:

  # gcc -E hello.c > hello.i 
  # ls
  hello.c hello.i 
  
  # cat hello.i 
  ...
  extern int printf (const char *__restrict __format, ...);
  ...
  int main(void){
      printf("hello world\n");
      printf("PI = %f\n", 3.14);
      printf("release mode\n");
      return 0;
  }

通过预处理后的C程序,使用#include包含的的头文件就地展开,我们可以看到stdio.h头文件中printf函数的声明。程序中使用#define定义的宏PI,也会在实际使用的地方展开为实际的值。使用#ifdef定义的条件编译,会根据条件判断,选择实际要编译的代码分支。

gcc -S参数

如果只对C源程序做编译处理,不汇编,可以使用gcc -S 参数:会gcc会将C源程序做预处理、编译操作,生成对应的汇编文件,不再做进一步的汇编和链接操作。

  # gcc -S hello.c
  # ls
  hello.c hello.s 

当然,我们也可以将上一节经过预处理后的hello.i文件编译为hello.s汇编文件:

  # gcc -S hello.i

两者生成的hello.s内容是一样的

gcc -c参数

如果只想对一个C程序做汇编操作,不进行链接,可以使用gcc -c 来完成:

  # gcc -c hello.c
  # ls 
  hello.c hello.o 

gcc -c 选项,也可以对上几节生成的 hello.i、hello.s文件直接汇编,生成对应的目标文件:

  # gcc -c hello.i
  # ls 
  hello.c hello.i hello.o hello.s 
  # rm hello.o 
  
  # gcc -c hello.s 
  # ls
  hello.c hello.i hello.o hello.s 

默认情况下,gcc会将hello.c生成对应的hello.o目标文件。当然,我们也可以通过 -o 输出选项,生成指定的目标文件:

  # ls
  hello.c
  # gcc -o world.o hello.c 
  # ls
  hello.c  world.o

GCC创建和使用静态链接库(.a文件)

Linux 下的静态链接库是以.a结尾的二进制文件,它作为程序的一个模块,在链接期间被组合到程序中。和静态链接库相对的是动态链接库(.so文件),它在程序运行阶段被加载进内存。
Linux 下静态链接库文件的命名规则为:

libxxx.a

GCC 生成静态链接库

首先使用 gcc 命令把源文件编译为目标文件,也即.o文件:

gcc -c 源文件列表

然后使用 ar 命令将.o文件打包成静态链接库,具体格式为:

ar rcs + 静态库文件的名字 + 目标文件列表

ar 是 Linux 的一个备份压缩命令,它可以将多个文件打包成一个备份文件(也叫归档文件),也可以从备份文件中提取成员文件。ar 命令最常见的用法是将目标文件打包为静态链接库。

对参数的说明:

  • 参数 r 用来替换库中已有的目标文件,或者加入新的目标文件。
  • 参数 c 表示创建一个库。不管库否存在,都将创建。
  • 参数 s用来创建目标文件索引,这在创建较大的库时能提高速度。
    例如,下面的写法表示将目标文件 a.o、b.o 和 c.o 打包成一个静态库文件 libdemo.a:
 # ar rcs libdemo.a a.o b.o c.o

实例演示
在用户主目录(home 目录)下创建一个文件夹 test,将 test 作为整个项目的基础目录。在 test 目录中再创建四个源文件,分别是 add.c、sub.c、div.c 和 test.h。

add.c 实现两个数相加

 #include “test.h”
 int add(int a,int b)
 {
     return a + b;
 }

sub.c 实现两个数相减

 #include “test.h”
 int sub(int a,int b)
 {
     return a - b;
 }

div.c 实现两个函数相除

 #include “test.h”
 int div(int a,int b)
 {
     return a / b;
 }

还有一个 test.h 头文件,用来声明三个函数

 #ifndef __TEST_H_
 #define __TEST_H_
 int add(int a,int b);
 int sub(int a,int b);
 int div(int a,int b);
 #endif

接下来,我们就将以上代码制作成静态链接库。
首先将所有源文件都编译成目标文件:

 # gcc -c *.c

然后把所有目标文件打包成静态库文件:

 # ar rcs libtest.a *.o

GCC 使用静态链接库

使用静态链接库时,除了需要库文件本身,还需要对应的头文件:库文件包含了真正的函数代码,也即函数定义部分;头文件包含了函数的调用方法,也即函数声明部分。

为了使用上面生成的静态链接库 libtest.a,我们需要启用一个新的项目。在用户主目录(home 目录)中再创建一个文件夹 math,将 math 作为新项目的基础目录。

在比较规范的项目目录中,lib 文件夹一般用来存放库文件,include 文件夹一般用来存放头文件,src 文件夹一般用来存放源文件,bin 文件夹一般用来存放可执行文件。为了规范,我们将前面生成的 libtest.a 放到 math 目录下的 lib 文件夹,将 test.h 放到 math 目录下的 include 文件夹。

在 math 目录下再创建一个 src 文件夹,在 src 中再创建一个 main.c 源文件。

此时 math 目录中文件结构如下所示:

 |-- include
 |   `-- test.h
 |-- lib
 |   `-- libtest.a
 `-- src
     `-- main.c

在 main.c 中,可以像下面这样使用 libtest.a 中的函数:

 #include <stdio.h>
 #include "test.h"  //必须引入头文件
 int main(void)
 {
     int m, n;
     printf("Input two numbers: ");
     scanf("%d %d", &m, &n);
     printf("%d+%d=%d\n", m, n, add(m, n));
     printf("%d-%d=%d\n", m, n, sub(m, n));
     printf("%d÷%d=%d\n", m, n, div(m, n));
     return 0;
 }

在编译 main.c 的时候,我们需要使用-I(大写的字母i)选项指明头文件的包含路径,使用-L选项指明静态库的包含路径,使用-l(小写字母L)选项指明静态库的名字。所以,main.c 的完整编译命令为:

 # gcc src/main.c -I include/ -L lib/ -l test -o math.out

注意,使用-l选项指明静态库的名字时,既不需要lib前缀,也不需要.a后缀,只能写 test,GCC 会自动加上前缀和后缀。

打开 math 目录,发现多了一个 math.out 可执行文件,使用./math.out命令就可以运行 math.out 进行数学计算。

GCC 动态链接库

静态库里实现的函数,可能被多个应用程序调用,那么在链接时,被调用的这个函数可能就会多次链接到不同的应用程序中。

比如C标准库的printf函数,可能被一个应用程序调用多次,被不同的应用程序调用,当这些应用程序加载到内存运行时,内存中也就存在多个printf函数代码的副本,太浪费了内存空间。而且,对于应用程序来说,每一个调用的库函数都被链接进来,自身的文件体积也会大增。

动态链接跟静态链接相比,具有以下优势

  • 库的代码不会链接到应用程序里
  • 同一份代码(如printf代码)可以被多个应用程序共享使用
  • 大大节省了内存空间

但动态库也有缺点,发布软件时,动态库需要和应用程序一起发布,否则你编译的应用程序到了一个新的平台可能就无法运行。

制作一个动态库

 # gcc -shared -fPIC -o libmymath.so add.c sub.c

其中的参数说明:

  • -shared :动态库编译,链接动态库
  • -fPIC(或fpic) :生成使用相对地址无关的目标代码
  • -Ldir :在动态库的搜索路径中增加dir目录
  • -lname :链接静态库(libname.a)或动态库(libname.so)的库文件

使用动态链接

将动态链接库拷贝到main.c的同一目录下

 # tree
 .
 ├── inc
 │   ├── add.h
 │   └── sub.h
 ├── libmymath.so
 └── main.c

在编译main.c,链接动态库libmymath.so的时候,直接指定当前目录下的libmymath.so文件:

 # gcc main.c ./libmymath.so -I inc/
 # ls
 a.out  inc  libmymath.so  main.c

在当前目录下运行生成的可执行文件a.out,可以正常运行。将a.out拷贝到其他目录,然后再运行,就会出现错误

Linux的动态链接库一般都放在/lib官方默认目录下。如果想让a.out放在任何路径下都可以运行,我们可以把libmymath.so动态库拷贝到/lib下,然后在编译应用程序时,通过-L参数指定动态链接库的搜索路径。编译生成的a.out在运行时,就会到指定的/lib目录下去加载动态库libmyamth.so:

 # cp libmymath.so /lib
 # tree 
 .
 ├── inc
 │   ├── add.h
 │   └── sub.h
 ├── libmymath.so
 └── main.c
 # gcc main.c -L/lib -lmymath -I inc
 # tree
 .
 ├── a.out
 ├── inc
 │   ├── add.h
 │   └── sub.h
 ├── libmymath.so
 └── main.c

将生成的a.out拷贝到系统的任何目录下,都可以正常运行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值