3

GCC 基本使用教程 - part 1 | UniFreak’s blog

 2 years ago
source link: http://unifreak.github.io/tutorial/note-gnu-gcc-introduction-part-1
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Gcc 基本使用教程 - part 1

posted on 10 Nov 2021 under category tutorial

GCC (GNU Compiler Collection) 除了编译 C/C++, GCC 本身还有着这些特性:

  • 可移植: 它能运行在绝大多数平台上, 支持不仅包括个人电脑的处理器, 还支持单片机, 数字信号处理器等
  • 既支持本地编译, 又支持 交叉编译 (为不同与本地系统的的其他目标系统编译)
  • 支持多种语言, 除了最常见的 C/C++, 也支持 Object C, ADA, Fortran, Go 等等

编译器基本工作原理

gcc 把源文件转换为可执行文件, 基本要经历四个阶段, 每个阶段都会用到特定的工具.

  1. 预处理器进行预处理 (cpp): 对源文件中的宏定义进行扩展, 把源文件中引用的头文件中的代码包含进去

    这个阶段中, 使用的预处理器是 cpp. 由于 cpp 默认不保存结果, 所以我们必须使用重定向, 或者传入 -save-temps 选项以保存结果, 比如:

     $ cpp hello.c > hello.i
    

    经过 cpp 预处理后的 C 源码, 一般以后缀 .i 命名. 而 C++ 则一般以 .ii 为后缀.

  2. 编译 (gcc -S): 把源码转换为汇编代码

    使用 -S 选项, 可以让 gcc 只产生汇编代码 (文件名后缀 .s) 而不产生目标文件 (文件名后缀 .o).

     $ gcc -S hello.i
    

    运行上面结果, 会生成 hello.s 文件, 其中包含了转换后的汇编代码.

  3. 汇编 (as -o): 把汇编代码转换为机器指令

    这个阶段使用的汇编器是 as. 比如运行

     $ as hello.s -o hello.o
    

    将会产生目标文件 hello.o. 目标文件以 .o 为后缀.

    注意: 这个阶段产生的目标文件中, 对于外部函数和变量的调用地址是没有定义的. 下一步的链接器负责填充这些丢失的地址.

  4. 链接 (ld / gcc): 生成最终的可执行文件

    这个阶段使用的链接器是 ld, 调用 gcc 时它会在幕后调用 ld, 比如:

     $ gcc hello.o
    

    最终会产生默认命名为 a.out 的可执行文件.

编译 C 语言

单个文件的编译

gcc -Wall main.c -o hello
  • -Wall 选项见编译警告)
  • -o hello 指定产生的可执行文件名称. 默认为 a.out

多个文件的编译

这里我们假设 main.c 用到了 (依赖于) hello_fn.c.

一块儿编译

被依赖的源文件置于后面:

gcc -Wall main.c hello_fn.c -o newhello

单独编译:

  1. 首先使用 -c 选项单独编译每个文件. -c 选项告诉 gcc 只生成目标文件, 而不生成可执行文件:

     $ gcc -Wall -c main.c
     $ gcc -Wall -c hello_fn.c
    

    执行后会产生两个目标文件: main.o 和 hello_fn.o

  2. 然后链接刚刚产生的多个目标文件:

     $ gcc main.o hello.o -o hello
    

    gcc 会使用 ld 链接器进行链接, 生成 -o 指定的可执行文件 hello.

    注意这里 不用 再重复指定 -Wall 选项, 而且被依赖的文件在后面.

链接外部函数库

如果你不了解静态库和共享库的概念, 可以读一下我的另一篇文章 (@todo). 这篇文章里我们把焦点放到 gcc 的使用上.

我们假设 calc.c 需要链接用于数学计算的 math 静态库 (静态库文件以 .a 为后缀), 一般 math 这样的系统库相关的文件会放到这些目录中:

  • 库文件位于 /usr/lib/ 或/lib/ 目录下. 比如 /usr/lib/libm.a
  • 库文件对应的头文件位于 /usr/include/ 目录下. 比如 /usr/include/math.h

C 标准库一般位于 /usr/lib/libc.a

为了将程序链接到 libm.a, 我们可以指定静态库的完整路径:

$ gcc -Wall calc.c /usr/lib/libm.a -o calc

也可以通过 -l 选项指定:

$ gcc -Wall calc.c -lm -o calc

这里的 -lm 指示 gcc 寻找并链接 libm.a. 注意 gcc 自动补上了 lib, 而且这里指定的顺序也很重要: 被依赖的库位于右边.

库搜索路径

上文提到, gcc 需要找到指定的库文件然后链接, 那么它怎么知道库文件在哪儿呢?

gcc 会默认按照如下顺序搜索被指定的库文件, 我们把这些目录叫做库路径:

  1. /usr/local/lib/
  2. /usr/lib/

gcc 也需要知道库对应的头文件的位置. 头文件的默认搜索顺序如下, 我们把这些目录叫做包含路径:

  1. /usr/local/include/
  2. /usr/include/

这里假设我们的程序 dbmain.c 依赖于 gdbm 库. 并且它被我们安装到了非标准路径 /opt/gdbm-1.8.3/ 下面, 目录结构如下:

$ tree /opt/gdbm-1.8.3/
/opt/gdbm-1.8.3/
|-- lib/                    # 库目录
|   |-- libgdbm.a           # 静态库
|   |-- libgdbm.so          # 动态库
|-- include/                # 包含目录
|   |-- gdbm.h              # 头文件

如何让 gcc 找到它呢? 有两种方法:

我们可以使用 -I 选项指定额外的包含路径, 使用 -L 指定额外的库路径:

gcc -Wall -I/opt/gdbm-1.8.3/include dbmain.c -L/opt/gdbm-1.8.3/lib -lgdbm

或者通过设置环境变量来告知 gcc 额外的搜索路径. 使用 LIBRARY_PATH 指定额外要搜索的库路径. 使用 C_INCLUDE_PATHCPLUS_INCLUDE_PATH 分别定义 c 或 c++ 额外的包含路径:

$ C_INCLUDE_PATH=/opt/gdbm-1.8.3/include
$ export C_INCLUDE_PATH
$ LIBRARY_PATH=/opt/gdbm-1.8.3/lib
$ export LIBRARY_PATH
$ gcc -Wall dbmain.c -lgdbm

设置环境变量时, 可以使用 : 分隔符, 同时指定多个路径.

使用选项和环境变量这两种方式也可以混用. 在这种情况下, gcc 按照以下顺序搜索:

  1. 先根据选项中从左到右的顺序搜索
  2. 然后是环境变量中指定的路径
  3. 最后是我们一开始讲的默认路径

在日常使用中, 我们一般通过使用选项的方式来指定.

共享库的加载路径

按照上文所讲链接库文件之后, 虽然能编译通过了, 但是尝试 运行 可执行文件时, 依然会报 “找不到文件” 错误:

$ ./a.out
./a.out: error while loading shared libraries:
libgdbm.so.3: cannot open shared object file:
No such file or directory

这是因为, 对于使用 -l 选项链接的外部库, gcc 实际上会先尝试搜索并链接可用的共享库. 而对于链接了共享库的程序, 当它运行时, 动态链接器 ld 必须找到这个共享库, 把它加载到内存中.

所以这里又涉及一个动态链接器 ld 需要搜索的加载路径, 默认会搜索以下加载路径 (同库路径):

  • /usr/local/lib
  • /usr/lib

而这两个目录中都没有要找的 libgdbm.so, 所以报错了.

为了解决这个问题, 我们可以在编译时使用 -static 选项强制 gcc 链接静态库. 这样就不会需要运行时的动态链接了:

gcc -Wall -static -I/opt/gdbm-1.8.3/include/ -L/opt/gdbm-1.8.3/lib/ dbmain.c -lgdbm

也可以通过 LD_LIBRARY_PATH 变量指定额外的加载路径让 ld 找到对应的共享库文件.

还可以在 ld 的配置文件中定义额外的加载路径.

指定 C 标准

gcc 默认使用 GNU C 标准来编译, GNU C 在 ANSI/ISO 标准上增加了诸如嵌套函数和可变长数组等特性.

但是我们也可以通过以下选项更改编译时的标准:

  • -ansi: 禁用 GNU 扩展 (asm, inline, typeof, unix, vax…)
  • -ansi -pedantic: 禁用所有 GNU 扩展, 即使那些兼容 ANSI 标准的特性
  • -std=c89: 指定为 c89, iso9899:199409, c99, gnu89, gnu99 等

-Wall 包含了以下警告:

  • -Wcomment: 嵌套函数
  • -Wformat: 不正确的使用格式字符串 (比如在 printf() 中)
  • -Wunused: 没有用到的变量
  • -Wimplicit: 未声明的函数
  • -Wreturn-type: 没有定义返回值类型也没有指定返回类型为 void 的函数

以下没有包含在 -Wall 中:

  • -W: 常见编程错误, 如函数没有返回值, 有符号和无符号比较等. 一般和 -Wall 一块儿使用
  • -Wconversion: 隐式类型转换
  • -Wshadow: 在一个作用域中重复声明变量
  • -Wcast-qual: 指针被强转后丢掉了类型限定符
  • -Wwrite-strings: 尝试覆写字符串常量
  • -Wtraditional: ANSI 标准和传统 C 标准 (K&R) 解释不同

注意: 所有这些警告都不会终止编译, 使用 -Werror 可以转换为错误, 终止编译.


下一篇介绍如何使用预处理器, 如何为调试或性能进行编译, 以及相关的工具简介等.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK