C程序编译过程及常见选项--静态库和动态库

本文深入剖析GCC编译器的工作流程,从预处理、编译、汇编到链接,详细解释了ELF可执行文件格式。重点探讨了静态库与动态库的区别,包括它们在链接时间、内存占用和程序更新等方面的特性。通过对gcc编译选项的介绍,帮助读者更好地理解和控制编译过程。
摘要由CSDN通过智能技术生成


注:本文系湛江市岭南师范学院物联网俱乐部原创部分训练计划,转载请保留声明。

前言

 今天是闭关的第31天,越来越接近秋招了,继续加油!今天主要给大家带来gcc编译器的编译过程详讲、静态库和动态库的学习。对于大多数程序员而言,大家都知道gcc是什么,但是如果不接触到linux平台下的开发,鲜有人真正了解gcc的编译流程,因为windows+IDE的开发模式简直是一条龙全套服务,开发者只需要关系代码逻辑与功能实现即可,但是,在享受便利的同时,必然也牺牲了一些灵活性。Linux程序员可以根据自己的需要让gcc在编译的任何阶段结束,以便检查或使用编译器在该阶段的输出信息,或者对最后生成的二进制文件进行控制,以便通过加入不同数量和种类的调试代码来为今后的调试做好准备。和其他常用的编译器一样,gcc也提供了灵活而强大的代码优化功能,利用它可以生成执行效率较高的代码。对于嵌入式ARM Linux开发和各种单片机MCU开发,这些知识也非常重要!

一、gcc详讲

1.1 编译过程

 Hello world是初学者使用任何一项编程语言最基本最简单的程序。下面是一个C语言版的"Helloworld" ,我们以该程序为例了解一下C程序的编译过程:

#include <stdio.h> 
#define TIMES 10
int main(int argc, char *argv[])
{ 
 	int i;
 	for(i=0; i<TIMES; i++)
 	{
 		printf("Hello wolrd\n"); 
 	}
 	return 0; 
}

 通常我们使用gcc来生成可执行程序,命令为:gcc hello.c,默认生成可执行文件a.out。其实编译(包括链接)的命令:gcc hello.c 可分解为如下4个大的步骤:
  预处理(Preprocessing)
  编译(Compilation)
  汇编(Assembly)
在这里插入图片描述

1.2 预处理

 预处理是读取c源程序,对其中的伪指令(以#开头的指令,也就是宏)和特殊符号
进行“替代”处理;经过此处理,生成一个没有宏定义、没有条件编译指令、没有特殊
符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,仍然是C文
件,但内容有所不同。 伪指令主要包括以下三个方面:
 (1)宏定义指令,如#define Name TokenString,#undef以及编译器内建的一些宏,  
 如__DATE__,__FILE__, __LINE__, __TIME__, __FUNCTION__ 等。 
 (2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif等。 
 (3) 头文件包含指令,如#include "FileName"或者#include <FileName>等。
 
  预处理的过程主要处理包括以下过程:
	将所有的#define删除,并且展开所有的宏定义
	处理所有的条件预编译指令,比如#if #ifdef #elif #else #endif等
	处理#include 预编译指令,将被包含的文件插入到该预编译指令的位置。
	删除所有注释 “//”和”/* */”.
	添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
	保留所有的#pragma编译器指令,因为编译器需要使用它们

  通常使用以下命令来进行预处理,参数-E表示只进行预处理:
	 gcc -E hello.c -o hello.i 

  也可以使用以下指令完成预处理过程,其中cpp是预处理器:
	 cpp hello.c > hello.i 
  
  预处理后的结果hello.i还是C语言源代码,我们可以使用cat或vim命令查看他的
  代码。
	 vim hello.i

1.3 编译(Compilation)

 编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合
语法规则之后,将其翻译成等价的中间代码表示或汇编代码。 优化处理是编译系
统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器
的硬件环境也有很大的关系。优化一部分是对中间代码的优化,这种优化不依赖于
具体的计算机。另一种优化则主要针对目标代码的生成而进行的。对于前一种优化,
主要的工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条
件、已知量的合并等)、复写传播,以及无用赋值的删除,等等。后一种类型的优
化同机器的硬件结构密切相关,最主要的是考虑是如何充分利用机器的各个硬件寄
存器存放的有关变量的值,以减少对于内存的访问次数。另外,如何根据机器硬件
执行指令的特点(如流水线、RISC、CISC等)而对指令进行一些调整使目标代码比
较短,执行的效率比较高,也是一个重要的研究课题。
 使用下面命令进行编译生成汇编文件。
	gcc -S hello.i > hello.s
	cat hello.s
或:
	arm-linux-gcc -S hello.i > hello.s
	cat hello.s
 在这里,我们使用PC的编译器gcc就会编译生成x86的汇编,而使用ARM的编译器则
生成ARM的汇编文件。同一份C代码不作任何修改,使用不同的编译器编译就生成在
不同机器上运行的程序,这就是C程序的可移植性。我们在PC上编写程序,在PC上
用ARM的交叉编译器,生成在ARM平台上的可执行程序的过程叫做交叉编译。

1.4 汇编(Assembly)

 汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统
处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标
文件中所存放的也就是与源程序等效的目标的机器语言代码。 目标文件由段组成。
 
 通常一个目标文件中至少有两个段: 
    ①代码段(文本段):该段中所包含的主要是程序的指令。该段一般是可读和可执
行的,但一般却不可写;
	②数据段:主要存放程序中要用到的各种常量、全局变量、静态的数据。一般数
据段都是可读,可写,可执行的;
	 gcc -c hello.s -o hello.o

1.5 链接(Linking

 汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问
题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如
变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有
的这些问题,都需要经链接程序的处理方能得以解决。 链接程序的主要工作就
是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另
外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系
统装入执行的统一整体,也就是可执行程序。 根据开发人员指定的库函数的链接
方式的不同,链接处理可分为两种: 
	1. 静态链接
	2. 动态链接
 对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动
态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节
约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态
链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损
害。
	gcc hello.o -o hello -static
	du -h hello 
	
显示(每个人都是不一样的):856K hello
	
	 file hello
	 
显示:hello: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), 
statically linked, for GNU/Linux 2.6.24,
BuildID[sha1]=4d58b0d381be9a829a429738361716721e0a130f, not stripped


	
	gcc hello.o -o hello
	du -h hello
	
显示(每个人都是不一样的):12K hello

	file hello
	
显示:hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
dynamically linked (uses shared libs), for GNU/Linux
2.6.24, BuildID[sha1]=3162b0cce098b0ebb49c989d8c5b40530dec09d4,
not stripped

1.6 ELF可执行文件格式

 Linux/Unix的可执行文件以及动态库都是以ELF(Executable Linkage Format)存在的。在Linux下,可以使用readelf命令查看ELF文件,关于加载过程所需要的信息都在ELF文件头里面,可以用使用readefl filename -e来查看EFL文件所有的头。我们可以先来查看下hello.c编译出来的hello可执行文件的ELF头信息:

指令:readelf hello -e

ELF Header:
 Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
 Class: ELF64
 Data: 2's complement, little endian
 Version: 1 (current)
 OS/ABI: UNIX - System V
 ABI Version: 0
 Type: EXEC (Executable file)
 Machine: Advanced Micro Devices X86-64
 Version: 0x1
 Entry point address: 0x400450
 Start of program headers: 64 (bytes into file)
 Start of section headers: 4504 (bytes into file)
 Flags: 0x0
 Size of this header: 64 (bytes)
 Size of program headers: 56 (bytes)
 Number of program headers: 9
 Size of section headers: 64 (bytes)
 Number of section headers: 30
 Section header string table index: 27

指令:readelf -d hello

Dynamic section at offset 0xe68 contains 20 entries:
 Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x4003e0
0x000000000000000d (FINI) 0x400608
0x000000006ffffef5 (GNU_HASH) 0x400298
0x0000000000000005 (STRTAB) 0x400318
0x0000000000000006 (SYMTAB) 0x4002b8
0x000000000000000a (STRSZ) 61 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x601000
0x0000000000000002 (PLTRELSZ) 72 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x400398
0x0000000000000007 (RELA) 0x400380
0x0000000000000008 (RELASZ) 24 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x400360
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x400356
0x0000000000000000 (NULL) 0x0

指令: ldd hello

 linux-vdso.so.1 => (0x00007fff191be000)
 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fbd3b089000)
 /lib64/ld-linux-x86-64.so.2 (0x000056260f532000)

1.7 gcc常见编译选项

 gcc的编译选项非常非常多,我们可以使用man手册查看,下面是我们经常使用的一些编译选项:
在这里插入图片描述

二、静态库和动态库

 在windows和Linux下都存在着大量的库,库是什么呢?本质上来说,库时一种可执行代码的二进制形式,可以被操作系统载入内存执行。我们通常将一些通用函数写成函数库,所以库是别人写好的,现有的,成熟的,可以复用的代码,你可以使用但要必须得遵守许可协议。在我们现实开发过程中,不可能每一份代码都从头编写,当我们拥有库时,我们就可以直接将我们所需要的文件链接到我们的程序中。可以为我们节省大量的时间,提高开发效率。Linux下库分为两种,静态库和动态库。

2.1静态库

  静态库文件名的命名方式是“libxxx.a”,库名前加”lib”,windows和linux下都是
后缀用”.a”,“xxx”为静态库名,windows下的静态库名也叫libxxx.a;
  链接时间: 静态库的代码是在编译过程中被载入程序中。
  链接方式:静态库的链接是将整个函数库的所有数据都整合进了目标代码。这样
做优点是在编译后的执行程序不在需要外部的函数库支持,因为所使用的函数都已
经被编进去了。缺点是,如果所使用的静态库发生更新改变,你的程序必须重新编
译。

2.2 动态库

  动态库的命名方式与静态库类似,前缀相同为“lib”,linux下后缀名为
 “.so(shared object)”即libxxx.so;而windows下后缀名为
 “.dll(dynamic link library)”即libxxx.dll;
  链接时间:动态库在编译的时候并没有被编译进目标代码,而是当你的
程序执行到相关函数时才调用该函数库里的相应函数。这样做缺点是因为
函数库并没有整合进程序,所以程序的运行环境必须提供相应的库。优点
是动态库的改变并不影响你的程序,所以动态函数库升级比较方便。

2.3 两者最大不同点

  它们两个还有很明显的不同点:当同一个程序分别使用静态库,动态库
两种方式生成两个可执行文件时,静态链接所生成的文件所占用的内存要
远远大于动态链接所生成的文件。这是因为静态链接是在编译时将所有的
函数都编译进了程序,而动态链接是在程序运行时由操作系统帮忙把动态
库调入到内存空间中使用。另外如果动态库和静态库同时存在时,链接器
优先使用动态库。

在这里插入图片描述

重要参考链接:https://www.toutiao.com/i6889283351983686158/?tt_from=mobile_qq&utm_campaign=client_share&timestamp=1627114460

三、总结

 通俗点来讲编辑器是指我用它来写程序的(编辑代码),而我们写的代码语句,电脑是不懂的,我们需要把它转成电脑能懂的语句,编译器就是这样的转化工具。就是说,我们用编辑器编写程序,由编译器编译后才可以运行!编译器是将易于编写、阅读和维护的高级计算机语言翻译为计算机能解读、运行的低级机器语言的程序。
 GCC(GNU Compiler Collection,GNU 编译器套件),是由 GNU 开发的编程语言编译器。GCC 原本作为GNU操作系统的官方编译器,现已被大多数类Unix操作系统(如 Linux、BSD、Mac OS X 等)采纳为标准的编译器,GCC 同样适用于微软的 Windows。GCC 最初用于编译 C 语言,随着项目的发展 GCC 已经成为了能够编译 C、C++、Java、Ada、fortran、Object C、Object C++、Go 语言的编译器大家族。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小贤风帆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值