[庖丁解牛]GCC编译过程详析

本文详细介绍了GCC编译C/C++程序的四个关键步骤:预处理、编译、汇编和链接,并讲解了静态库与动态库的区别,以及如何设置头文件和库路径。通过实例演示了整个流程,包括使用verbose模式观察详细过程和链接器警告处理。
摘要由CSDN通过智能技术生成

GCC编译过程


C程序编译过程

GCC编译一个C/C++ 程序到可执行程序可分成以上4个步骤。例如,"gcc-o hello.exe hello.c"执行如下:

1. 预处理: 预处理器(cpp)来展开包含的头文件(#include)和宏定义(#define)。
# cpp hello.c > hello.i

生成的文件"hello.i"包含了展开扩展后的源码。

2. 编译: 编译器将预处理的源代码编译成特定程序集的汇编代码。
# gcc -S hello.i

选项 -S 指定生成汇编代码,而不是目标代码。生成的汇编文件为"hello.s"。

3. 汇编: 汇编器(as)将汇编代码转换为目标文件中的机器代码"hello.o"。
# as -o hello.o hello.s
4. 链接器: 链接器(ld)将目标代码与库代码链接起来,生成可执行文件"hello.exe"。
# ld -o hello.exe hello.o ...libraries...

Verbose Mode (-v)
可以通过启用-v (verbose)选项查看详细的编译过程。

# gcc -v -o hello.exe hello.c

Defining Macro (-D)
可以使用-Dname选项定义宏,或者使用-Dname=value定义带有值的宏。如果值包含空格,则应该用双引号括起来。

头文件(.h),静态库(.a, .lib)和动态库(.so, .dll)


静态库 VS 动态库

库是可以通过链接器链接到程序中的预编译过的目标文件的集合,例如系统函数printf()和sqrt()。

外部库有两种类型:静态库和共享库。

  1. 静态库的文件扩展名在Unix中为".a"(归档文件)在Windows中为".lib"。当链接静态库到程序时,程序中使用的外部函数的机器码将复制到可执行文件中。静态库可以通过"ar"存档程序创建。
  2. 共享库的文件扩展名在Unix中为".lib"(共享对象)在Windows中为"dll"(动态链接库)。当链接共享库到程序时,只会在可执行文件中创建一个小表。在可执行文件开始运行之前,操作系统加载外部函数所需的机器码——这个过程称为动态链接。动态链接使可执行文件更小并节省磁盘空间,因为库的一个副本可以在多个程序之间共享。此外,大多数操作系统允许内存中共享库的一个副本被所有正在运行的程序使用,从而节省内存。可以升级共享库代码,而不需要重新编译程序。

由于动态链接的优点,默认情况下,如果共享库可用,GCC会链接到共享库。

您可以通过 nm file 列出库的内容。

搜索头文件和库(-I, -L and -l)

在编译程序时,编译器需要头文件来编译源代码;链接器需要这些库来解析来自其他对象文件或库的外部引用。除非您设置了适当的选项,否则编译器和链接器将无法找到头文件/库。

对于源代码中使用的每个头文件(通过#include指令),编译器会搜索这些头文件的包含路径。包含路径是通过-I<dir>选项(或环境变量CPATH)指定的。因为头文件的文件名是已知的(例如iostream)。,编译器只需要目录。

链接器在库路径中搜索将可执行文件所需链接到的库。库路径是通过-Ldir选项(大写’L’后跟目录路径)(或环境变量LIBRARY_PATH)指定的。此外,还必须指定库名。在Unix中,库libxxx.a是通过-lxxx选项(小写字母’l’,没有前缀"lib"和".so"扩展)。在Windows中,提供全名,如-lxxx.lib。链接器需要知道目录和库的名称。因此,需要指定两个选项。

默认的头文件路径,库路径和库
# cpp -v
......
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc/x86_64-linux-gnu/5/include
 /usr/local/include
 /usr/lib/gcc/x86_64-linux-gnu/5/include-fixed
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.

以详细模式(-v)运行编译,学习使用系统中使用的库路径(-L)和库(-L):

# gcc -v -o hello.exe hello.c
......
-L/usr/lib/gcc/x86_64-pc-cygwin/6.4.0
-L/usr/x86_64-pc-cygwin/lib
-L/usr/lib
-L/lib
-lgcc_s     // libgcc_s.a
-lgcc       // libgcc.a
-lcygwin    // libcygwin.a
-ladvapi32  // libadvapi32.a
-lshell32   // libshell32.a
-luser32    // libuser32.a
-lkernel32  // libkernel32.a

GCC 环境变量


GCC使用下面这些环境变量:

  • PATH: 用于搜索可执行程序和运行时共享库(.so, .dll)。
  • CPATH: 用于搜索头文件的包含路径。它在-I 选项中指定的路径被搜索后搜索。如果在预处理中指定了特定的语言,那么 C_INCLUDE_PATHCPLUS_INCLUDE_PATH可以用来指定C和C++头文件。
  • LIBRARY_PATH: 用于搜索链接库的库路径。。它在-L 选项中指定的路径被搜索后搜索。

编译过程实操


file #显示文件类型

//hello.c
#include <stdio.h>

#define MUL(x, y) ((x)*(y))

int add(int, int);

int main() {
	int x = 3, y = 4;
	int z;
	z = add(x, y);
	printf("%d + %d = %d\n", x, y, add(x, y));
	printf("%d * %d = %d\n", x, y, MUL(x, y));
	return 0;
}
//add.c
int add(int a, int b) {
	return a + b;
}
# file hello.c 
hello.c: C source, ASCII text
#cpp hello.c > hello.i
# cpp add.c > add.i
# file hello.c 
hello.c: C source, ASCII text
# cat hello.i
...//此处省略若干行
# 5 "hello.c"
int add(int, int);

int main() {
 int x = 3, y = 4;
 printf("%d + %d = %d\n", x, y, add(x, y));
 printf("%d * %d = %d\n", x, y, ((x)*(y)));
 return 0;
}
# #可以看出MUL宏被展开了
# gcc -S hello.i 
# gcc -S add.i 
# file hello.s 
hello.s: assembler source, ASCII text
# cat hello.s 
	.file	"hello.c"
	.section	.rodata
.LC0:
	.string	"%d + %d = %d\n"
.LC1:
	.string	"%d * %d = %d\n"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushq	%rbp
...//此处省略若干行,展开后的源码在这一步被编译成对应平台的汇编代码
.LFE0:
	.size	add, .-add
	.ident	"GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
	.section	.note.GNU-stack,"",@progbits
# as -o hello.o hello.s 
# as -o add.o add.s
# file hello.o 
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
# ld -o hello hello.o add.o -dynamic-linker /lib64/ld-linux-x86-64.so.2 -lc
ld: warning: cannot find entry symbol _start; defaulting to 0000000000400280
# #上面这个警告不会影响程序运行,但是会在return的时候导致段错误
# file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, not stripped
# ./hello 
3 + 4 = 7
3 * 4 = 12
Segmentation fault
# #ld那一步的警告改用gcc命令可以避免<br>
# gcc -o hello hello.o add.o -dynamic-linker /lib64/ld-linux-x86-64.so.2 -lc

nm #通常用于检查对象文件中是否定义了特定的函数。"T"表示已定义的函数,"U"表示未定义的函数,应由链接器解析。

# nm -C hello.o 
                 U add      #可以看到目标文件hello.o里没有add函数的定义 
0000000000000000 T main
                 U printf
# nm add.o 
0000000000000000 T add      #add的定义在目标文件add.o中

nm #用来显示它所需要的共享库的列表。

# ldd hello
	linux-vdso.so.1 =>  (0x00007ffe8c1f0000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3d1b6bd000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f3d1ba87000)

参考链接 https://www3.ntu.edu.sg/home/ehchua/programming/cpp/gcc_make.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值