C语言静态编译和动态编译

文章目录

  • 概述
    • 可执行文件
    • 脚本文件
  • Linux应用程序目录结构
  • 编译器初探
    • 普通程序的编译
    • 链接头文件
  • 库文件
    • 静态编译
    • 创建静态库
    • 动态编译
    • 创建动态库
  • 程序的编译过程
    • 预处理
    • 编译
    • 汇编
    • 链接
  • 结语

概述

在Linux系统中,应用程序表现为两种文件,一种是可执行文件, 另一种是脚本文件

可执行文件

可执行文件是计算机可以直接执行的程序,与windows系统的.exe程序相似,它是由源代码经过一定的手段翻译成计算机能够读懂的二进制编码,由计算机直接去执行,这个翻译的过程就称之为编译

脚本文件

脚本文件是一系列指令的组合,由另一个解释器来执行,相当于windows系统的.bat文件。
与windows系统不同之处在于,windows系统通常由后缀来决定该文件是什么文件,而Linux系统则与后缀无关,而是由文件的权限来决定的。可以有后缀,也可以没有后缀。
关于文件的权限相关,会在后续的文章中展开讨论,此处就不多做说明了。

Linux应用程序目录结构

一级目录 二级目录 三级目录 说明
/bin 二进制文件目录,用于存放启动时用到的程序
/usr bin 用户二进制目录,用于存放用户使用的标准程序
/usr local bin 本地二进制目录,用于存放软件安装的程序
/usr sbin root用户登录后的PATH管理目录
/opt 第三方应用程序安装目录

编译器初探

将源代码转换成计算机能够识别的二进制编码的过程称之为编译,编译使用的工具即为编译器。
在POSIX兼容的系统中,C语言编译器被称为c89,简称为cc。在Unix系统中,普遍使用的都是cc编译器。而我们这里讨论的gcc编译器是随着Linux发行版一起提供的,全称是GNU C编译器。

普通程序的编译

关于编译器的使用,不妨通过一个简单的例子来说明。
我们简单的写出一个C程序如下,该程序即编程者的入门程序hello.c

#include 

int main(void)
{
    printf("hello world\n");
    return 0;
}

该程序的编译命令如下:

$ gcc -o hello hello.c

执行以上命令后,会在该目录下生成一个名为hello的可执行文件,使用以下命令即可执行该程序:

$ ./hello
hello world

下面来对该编译命令做一下说明:

  • -o 紧跟着的是指定编译后可执行文件的名称,上述命令中,-o hello即指定hello为可执行文件。如果不指定-o, 则默认生成一个叫做a.out的可执行文件。即:上述编译命令直接写成gcc hello.c也是没有问题的,不过可执行文件变成了a.out,执行该程序就是:
$ ./a.out
hello world
  • hello.c 是需要编译的源文件,该文件是开发者写好的代码,可以有多个,也可以只有一个。
  • 在执行应用程序中,./代表的是当前目录,如果不加./,系统会默认到PATH环境变量中去寻找,如果找不到则会报错,如果恰巧找到了一个同名的可执行程序,程序会执行,但得到的结果并不是我们所需要的,因为它执行的并不是我们编译好的可执行程序。

链接头文件

使用C语言进行程序设计时,需要链接头文件进行系统及库函数的调用,这时候就需要链接头文件。Linux系统常用的系统头文件都存放在/usr/include目录下,该目录能够被gcc编译器自行检索到并主动链接到程序里。
但对于有些用户自定义的头文件,编译器并不能搜索到其目录,这时候就需要在编译的时候去指定需要链接的头文件的路径。链接头文件的命令是在编译的时加上大写的-I,后面紧跟头文件的路径。如下所示:

$ gcc -o fred -I/usr/openwin/include fred.c

注意:-I和头文件路径之间不要有空格。

库文件

库是一组预先编译好的函数组合,其特点是可重用性好,通常由一组相互关联的函数组成,用以执行某项任务。
系统的标准库函数存放在/lib或者/usr/lib目录下,与头文件必须以.h作为后缀一样,库函数同样也需要遵循一些规范。
库函数必须以lib作为开头,以.a或者.so作为结尾。其中,.a代表静态函数库,.so代表动态函数库。比较典型的比如:libc.alibm.a即代表标准C函数库和标准数学函数库。

静态编译

由于历史原因,编译器仅能识别标准c函数库,部分系统库函数,即使已经放在/usr/lib目录下,编译器仍然不能够识别,这时候就需要在编译的时候告诉编译器使用的是哪个库函数,编译时可以通过给出完整的静态库绝对路径的方式,或使用-l标志来告诉编译器你所使用的静态库函数。如:

$ gcc -o fred fred.c /usr/lib/libm.a
$ gcc -o fred fred.c -lm

以上两个命令达到的效果是一样的,可以通过ls /usr/lib命令来查看系统的库函数。
除此之外,用户也可以自己定义库函数,链接非标准位置的自定义库函数可以通过大写的-L标志来实现,命令如下:

$ gcc -o xllfred  -L/usr/openwin/lib xllfred.c -lxll  

创建静态库

使用ar命令(archive)可以很容易地创建属于自己的静态库。ar命令一般对.o的目标文件进行操作,目标文件可以由gcc -c命令得到。
下面就以一个具体的例子来说明一下。
首先,我们有如下两个源程序文件:

$ ls
main.c  print.c test.h
$ pg print.c
#include 

int print()
{
    printf("Hello world\n");
    return 0;
}

$ pg main.c                                                        
#include "test.h"

int main()
{
    print();
    return 0;
}

$ pg test.h
int print();

先通过gcc -c命令将其编译成.o文件:

$ gcc -c *.c                                                        
$ ls 
main.c  main.o  print.c  print.o  test.h

我们可以看到两个.o的目标文件已经成功生成。
这时候,如果我们使用以下命令,是可以直接编译成功的:

$ gcc -o test *.o
$ ls
main.c  main.o  print.c  print.o  test.h test
$ ./test
Hello world

但是这里由于我们是要创建静态库,所以可以使用ar命令来创建一个归档文件:

$ ar crv libtest.a print.o
a - test2.o
$ ls
libtest.a  main.c  main.o  print.c  print.o  test  test.h

可以看到,静态库libtest.a已经成功创建,这时,还需要使用ranlib命令来生成一个内容表,这一步在Unix系统中必不可少,但在Linux中,当使用的是GUN开发工具时,这一步可以省略。以上步骤完成后,即可以使用下面的命令来编译程序:

$ ranlib libtest.a
$ gcc -o testa main.o -L./ -ltest
$ ls
libtest.a  main.c  main.o  print.c  print.o  test  testa  test.h
$ ./testa
Hello world

通过以上案例,可以发现得到的效果其实是一样的。
当然,也可以使用以下命令,得到相同的效果(这里因为没有链接头文件,会报一个错,但是结果没有影响):

$ gcc -o testb main.c -L./ -ltest  
$ ls
libtest.a  main.c  main.o  print.c  print.o  test  testa  testb  test.h
$ ./testb
Hello world

动态编译

静态库有一个局限性,当同时运行多个程序,并且这些程序都去调用同一个静态库函数时,就会出现多个函数库副本,会占用大量的虚拟内存和磁盘空间,这时候,动态库(又叫共享库)可以解决这个问题。
当程序使用动态库时,其链接方式是这样的:程序本身不包含代码,而是引用运行时可访问的共享代码。即,只有在必要的时候,才会加载到内存中。
即,可以简单的理解如下:静态库在编译时就已经把内存给分配好了,每一次调用,都会分配一份内存。而动态库是在运行时才会去访问共享代码分配的内存,永远只会存在一份,因此不会出现内存浪费。
动态库的格式是以.so结尾,比如典型的有/lib/libm.so
装载和解析动态库的工具是ld.so,如果需要搜索标准位置以外的动态库,则需要在/etc/ld.so.conf中进行配置,然后执行ldconfig来处理它。
可以使用ldd命令来查看程序所需要的动态库文件,如:

$ ldd test
        linux-vdso.so.1 =>  (0x00007ffc0e4a8000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd26176b000)
        /lib64/ld-linux-x86-64.so.2 (0x000056170c352000)

静态库与动态库的一个明显的区别在于:使用静态库编译完成后,静态库被删除,不会影响可执行文件的执行,但是如果删除了动态库,可执行文件执行就会报错。

创建动态库

仍然是看上面这个程序:

$ ls
libtest.a  main.c  main.o  print.c  print.o  test  testa  testb  test.h

动态编译的命令是针对共享代码进行操作的,命令如下:

$ gcc -shared -fPIC -o libtest.so print.c
$ ls
libtest.a  libtest.so  main.c  main.o  print.c  print.o  test  testa  testb  test.h

这样libtest.so就已经生成了,编译时链接如下:

$ gcc -o testc main.c ./libtest.so
$ ls
libtest.a  libtest.so  main.c  main.o  print.c  print.o  test  testa  testb  testc  test.h
$ ./testc
Hello world

程序的编译过程

C程序的编译共有四个步骤组成,分别为:

  • 预处理
  • 编译
  • 汇编
  • 链接
    C语言静态编译和动态编译_第1张图片

为了可以直观的说明这个过程,我们不妨使用一个案例演示一下:
假设有两个文件,头文件hello.h 和源文件 hello.c:

$ ls
hello.c  hello.h
$ pg hello.h
# include
$ pg hello.c
#include "hello.h"

int main()
{
    printf("Hello world\n");
    return 0;
}

预处理

预处理主要完成的工作是去注释、头文件包含和宏替换,该步骤并不会检查语法。预处理命令为:

$ gcc -E -I./ hello.c -o hello.i                
$ ls
hello.c  hello.h  hello.i

预处理完成后,会由.c文件生成一个.i文件。

编译

编译步骤完成的功能是将预处理之后的程序转换为汇编语言代码。编译命令如下:

$ gcc -S  hello.i -o hello.s 
$ ls
hello.c  hello.h  hello.i  hello.s

这一步会将.i文件生成为.s格式的汇编语言代码。在该步骤中会检查语法,如果语法有错误,会在这一步报出来。

汇编

汇编就是将汇编语言程序处理成二进制目标文件。其命令如下:

$ gcc -c  hello.s -o hello.o   
$ ls
hello.c  hello.h  hello.i  hello.o  hello.s

或者使用以下命令:

$ as  hello.s -o hello.o       
$ ls
hello.c  hello.h  hello.i  hello.o  hello.s

这两个命令实际是等价的。

链接

链接是最后一步,即将多个目标文件,或者静态库文件(.a)以及动态库文件(.so)链接成最后的可执行程序的过程。其命令如下:

$ gcc -o hello hello.o  
$ ls
hello  hello.c  hello.h  hello.i  hello.o  hello.s
$ ./hello
Hello world

如果使用了动态库,可能使用到ld链接命令。

通过以上分析发现,看似简单的一条编译命令,其内部完成的步骤是相当复杂的,能够理解编译器编译的原理,对编程是有相当大的帮助的。当然实际开发过程中,只需要使用以下编译命令一步到位,它完成了以上所有四步命令的所有过程:

$ gcc -o hello hello.c
$ ./hello
hello world

结语

关于gcc编译命令还有很多,这里就不多做介绍了,如果有需要,可以通过man gcc或者info gcc来获取帮助。

你可能感兴趣的:(Linux)