文章目录
- 简介
- C 程序编译
- 单个源文件生成可执行程序
- 源文件生成对象文件
- 多个源文件生成可执行程序
- 编译预处理
- 生成汇编代码
- 构建静态库
- 构建共享库
简介
在 Linux 操作系统中,GCC 是一种现代化的编译器集合,它可用于编译多种程序设计语言,包括 C 语言 和 C++ 语言。
通过使用GCC 编译器,可以编写高效、安全和可靠的程序,对于 C 程序,推荐使用 gcc 进行编译,但对于 C++ 程序,建议使用 g++ 。如果需要构建大规模的 C 程序并管理源文件,可以使用 makefile 和 make 工具 来完成这项任务。
-
Makefile 是一个文本文件,其中包含目标、依赖关系和要执行的命令,可以根据程序复杂性创建多个目标并指定依赖关系和命令序列。
-
Make 命令可以自动构建程序,它会阅读 Makefile 并生成必要的命令,以确保目标依赖关系被遵循而不必手动输入一系列编译命令。
因此,在面对大型 C 程序时,使用 makefile 和 make 工具 可以显著提高编译的效率和便利性,并减少常见错误和问题的出现。
编译 C 程序的过程主要包括四个阶段:编译预处理、编译优化、汇编、链接。
-
在编译预处理阶段,编译器会读取源程序中的预处理命令(例如,宏定义命令,条件编译指令,头文件包含指令等)和特殊符号(例如,在源程序中出现的 LINE 则被解释为十进制表示的当前行号;FILE 则被解释为当前编译的源程序的文件名,并将其替换为对应的内容,同时加入头文件中的定义等)。
-
在编译优化阶段,编译程序通过词法分析、语法分析,在确认所有的指令都符合语法规则之后将代码翻译成对应的汇编代码,并进行优化处理。优化有两种方式,一种是针对代码本身的优化(例如,删除公共表达式、循环优化、代码外提、无用代码赋值的删除等),另一种是针对计算机硬件的指令优化(例如,根据机器硬件执行指令的特点对指令进行调整优化,减少目标代码长度,提高执行效率等)。
-
在汇编阶段,编译程序将产出汇编文件,再通过汇编程序将其转换为目标机器指令的序列,生成对应的目标文件,目标文件一般至少包含代码段和数据段。
-
在链接阶段,连接程序将把多个目标文件连接在一起,解决符号引用问题,生成可执行文件。根据连接的方式不同,链接库可以分为静态链接和动态链接。在静态链接中,函数的代码会被直接拷贝到最终的可执行文件中;在动态链接中,函数的代码被放到共享对象中,在生成的可执行文件中记录下共享对象的名字和少量关键信息。
C 程序编译
单个源文件生成可执行程序
通过以下一个简单的 C 程序为例,该程序的代码如下:
/* hello.c */
#include <stdio.h>int main(int argc,char *argv[])
{printf("Hello cqupthao!\n");return 0;
}
最简单直接的编译以上代码为可执行程序的方法是将该代码保存为文件 hello.c
,并在相应目录下执行如下指令:
gcc -Wall hello.c
GCC 编译器通过检查命令行中指定的文件的后缀名可识别其为 C 源代码文件( GCC 默认的动作:编译源代码文件生成对象文件,链接对象文件得到可执行程序,删除对象文件,编译器默认的可执行程序的文件名 a.out
),在终端中输入以下形式便可使其运行并显示结果:
./a.out## result
Hello cqupthao!
可以通过选项 -o
来指定所生成指定文件名的可执行程序,例如,输入以下的命令生成名为 hello
的可执行程序:
gcc -Wall helloubuntu.c -o hello
在终端中输入以下的命令格式,使其运行并显示结果:
./hello## result
Hello cqupthao!
注意
:如果编译需要用到math.h
库等非 GCC 默认调用的标准库,应使用选项-lm
。
源文件生成对象文件
当编译过程中使用选项 -c
时,GCC 会进行编译源代码文件的操作,但不会链接对象文件生成可执行文件。与此同时,程序可以保留所生成的目标文件到磁盘中,方便后续的操作和调试。
在这种情况下,GCC 默认的输出文件名与源代码文件名相同,只不过后缀变为了 .o
,表示这是一个目标文件,即未被链接的二进制文件。因此,在编译大型 C 程序时,多个源文件可以分别编译成目标文件,然后在链接阶段将它们合并起来,以生成最终的可执行文件。
例如,生成名为 hello.o
的对象文件,输入以下格式的命令:
gcc -Wall -c hello.c
选项 -o
可用来生成指定文件名的对象文件。例如,输入以下命令将产生名为 sayhello.o
的对象文件:
gcc -Wall -c hello.c -o sayhello.o
当构建对象库或者生成一系列对象文件以备稍后链接用时,可以从多个源码文件(如,hello.c callhello.c sayhello.c)生成对应的对象文件名hello.o
、callhello.o
和 sayhello.o
对象文件,输入以下格式的命令:
gcc -Wall -c hello.c callhello.c sayhello.c ## result
hello.o callhello.o sayhello.o
多个源文件生成可执行程序
当多个源码文件被编译时,GCC 编译器会自动进行链接操作。例如,一个名为 hellomain.c
文件程序调用一个名 sayhello.c
文件程序的 sayhello() 函数,该程序的代码如下:
/* hellomain.c */
void sayhello(void);int main(int argc,char *argv[])
{sayhello();return 0;
}
以下的程序代码保存在名为 sayhello.c
文件程序定义的 sayhello() 函数:
/* sayhello.c */
#include <stdio.h>void sayhello()
{printf("Hello cqupthao!\n");
}
将两个文件分别编译为对象文件且将其链接为可执行程序 hello,并删除对象文件,输入以下格式的命令:
gcc -Wall hellomain.c sayhello.c -o hello
在终端中输入以下的命令格式,使其运行并显示结果:
./hello## result
Hello cqupthao!
编译预处理
选项 -E
指示编译器只进行编译预处理。例如,将预处理源码文件 hello.c
并将结果在标准输出中列出,输入以下格式的命令:
gcc -E hello.c
选项 -o
用来将预处理过的代码定向到一个不需经过预处理且后缀为 .i
的 C 源码文件,例如,输入以下格式的命令:
gcc -E hello.c -o hello.i
生成汇编代码
选项 -S
指示编译器生成汇编语言代码然后结束。例如,将由 C 源码文件 hello.c
生成汇编语言文件 hello.s
,输入以下格式的命令:
gcc -S hello.c
汇编语言的形式依赖于编译器的目标平台,如果多个源码文件被编译,每个文件将分别产生对应的汇编代码模块。
构建静态库
静态库是编译器生成的一组以 .o
为后缀的文件集合,它们由同一个源代码库编译而来。与动态库相比,代码静态链接意味着当程序运行时,完成了所有依赖项的链接以及复制,因此可执行文件中包含了所有必需代码。
这样可以避免在程序运行时出现动态链接的错误,并且消除了动态链接库的性能损失。另外,静态库具有可移植性和独立性,因为它们不依赖于操作系统或者计算机硬件的特定版本。
静态库的另一个名字叫归档文件(archive),因为它实际上是一系列的 .o
文件的归档,构建一个库,首先要编译出库中需要的对象模块。例如,下面的两个程序文件名分别为 firsthello.c
和 secondhello.c
,其源码如下:
/* firsthello.c */
#include <stdio.h>void firsthello()
{printf("This is the first hello!\n");
}
/* secondhello.c */
#include <stdio.h>void secondhello()
{printf("This is the second hello!\n");
}
使用静态库之前,需要将需要编译的源代码文件分别编译成目标文件(后缀为 .o
),然后才能使用 ar
命令将它们打包成一个归档文件(也叫静态库文件,后缀为 .a
)。例如,将这两个源码文件编译成对象文件,输入以下格式的命令:
gcc -Wall -c firsthello.c secondhello.c
在 Linux 系统下,通常使用 ar
工具管理和创建归档文件,ar
配合参数 -r
可以创建一个新库并将对象文件插入。如果库不存在的话,参数 -r
将创建一个新的,并将对象模块添加(如有必要,通过替换)到归档文件中。例如,将创建一个包含本例中两个对象模块的名为 libhello.a
的静态库,输入以下格式的命令:
ar -r libhello.a firsthello.o secondhello.o
调用该库中的这两个函数,编写一个名为 uselib.c
的程序,该程序的代码如下:
/* uselib.c */
void firsthello(void);
void secondhello(void);int main(int argc,char *argv[])
{firsthello();secondhello();return 0;
}
程序可以通过在命令行中指定库用一条命令来编译和链接,命令如下:
gcc -Wall uselib.c libhello.a -o uselib
在终端中输入以下的命令格式,使其运行并显示结果:
./uselib## result
This is the first hello!
This is the second hello!
静态库的命名惯例是以 lib 开头且后缀为 .a
,例如,libhello.a
,这种命名约定被广泛应用于各种操作系统和编程语言中,在Linux系统中,所有的系统库都采用这种命名规则。编译器提供了 -l
选项来指定需要链接的库文件名,并且可以通过简写方式指定命令行中的库名,类似于 -l<库名> 的形式。举个例子,在上述命令中,使用 -lhello
指向了 libhello.a
静态库,将该库与 uselib.c
文件进行链接,生成可执行文件 uselib
。
需要注意的是,编译器会从默认的系统库目录中寻找 libhello.a
库文件,如果库文件存在于某个特定的目录下,则可以使用完整的路径名或者相对路径名来指定库文件的位置。如果是指定具体路径名,可以使用绝对路径或相对路径格式来表示。例如,假设 libhello.a
库文件存储在当前目录下的 lib 目录中,则可以使用以下命令:
gcc -Wall twohellos.c -L./lib -lhello -o twohellos
该命令中,-L./lib
选项指定库文件在当前目录下的 lib 目录中,-lhello
选项指代 libhello.a
静态库文件,这样编译器就会到指定的目录中查找 libhello.a
静态库文件,从而实现链接操作。
构建共享库
共享库(shared library),也称为动态链接库(dynamic library),是编译器以一种特殊的方式生成的目标文件集合。在编译过程中,对象文件模块中所有地址(包括变量引用和函数调用)都是相对的而不是绝对的,这使得共享库可以在程序运行时被动态地加载,并支持多个进程同时使用,从而节省内存空间并实现代码复用。
与静态库不同,共享库并不会被链接到最终的执行文件中,而是在程序运行时才会被加载到内存中。因此,共享库具有更好的可移植性和重用性,能够在不同的应用程序中被共享使用,从而更好地实现代码复用。
构建一个共享库,首先需要编译目标文件(.o 文件)。例如,在以下示例中我们将两个源码文件 sharedfirst.c
和 sharedsecond.c
进行编译,并分别生成目标文件 sharedfirst.o
和 sharedsecond.o
:
/* sharedfirst.c */
#include <stdio.h>void sharedfirst()
{printf("This is the first hello from a shared library!\n");
}
/* sharedsecond.c */
#include <stdio.h>void sharedsecond()
{printf("This is the second hello from a shared library!\n");
}
将以上两个源码文件编译成对象文件,输入以下格式的命令:
gcc -Wall -c -fpic sharedfirst.c sharedsecond.c
选项 -c
告诉编译器只生成 .o
的对象文件。选项 -fpic
使生成的对象模块采用浮动的(可重定位的)地址。
下面的 gcc 命令将对象文件构建成一个名为 hello.so 的共享库:
gcc -Wall -shared shellofirst.o shellosecond.o -o hello.so
在编译共享库时,没有一个明显的入口点函数,如 main() 函数,在程序运行时被调用,因此需要使用选项 -shared
告诉编译器创建一个共享库而不是可执行文件。这个选项告诉编译器不要依据操作系统约定的入口地址进行链接,而是将编译出的目标文件集合作为共享对象来处理。
由于编译器能够识别文件后缀名 .c
,并知道如何将其编译成为目标文件,可以省略编译步骤,直接通过一条以下格式的命令将直接编译和链接两个源码文件 sharedfirst.c
和 sharedsecond.c
,并将它们构建成为一个共享库 libshello.so
。在这种情况下,编译器会自动将源码文件编译为目标文件,并将这些目标文件链接为共享库:
gcc -Wall -fpic -shared sharedfirst.c sharedsecond.c -o hello.so
编写一个程序文件名为 uessharedlib.c
调用共享库中两个函数的主程序,该程序的代码如下:
/* usesharedlib.c */
void sharedfirst(void);
void sharedsecond(void);int main(int argc,char *argv[])
{sharedfirst();sharedsecond();return 0;
}
该程序可以用下面的命令编译并链接共享库:
gcc -Wall usesharedlib.c hello.so -o usesharedlib
运行它必须让其能定位到共享库 hello.so
,因为库中的函数要在程序运行时被加载。 需要注意的是,当前工作目录可能不在共享库的查找路径中,因此需要使用如下的命令行设定环境变量 LD_LIBRARY_PATH
:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./
在终端中输入以下的命令格式,使其运行并显示结果:
./usesharedlib## result
This is the first hello from a shared library!
This is the second hello from a shared library!
- 参考书籍:《Linux嵌入式C开发》(华清远见 著)