您的位置:首页 > 编程语言 > C语言/C++

C语言程序的编译以及库的构建与使用---查漏补缺笔记

2015-08-27 10:51 302 查看

GCC编译过程:

举例代码如下:

#include <stdio.h>
int main(int argc,char *argv)
{
printf("Hello World!\n");
}


GCC编译器驱动程序读取源程序文件HelloWorld.c,并把它翻译成一个可执行目标文件HelloWorld。这个翻译的过程可分为四个阶段完成:

预处理阶段。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。主要包含三个方面的内容:1) 宏定义与宏替换;2) 文件包含;3)条件编译。 比如HelloWorld.c中第1行的
#include <stdio.h>
命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。由于预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,故预处理是不做语法检查的。且宏定义不分配内存,变量定义才会分配内存。在gcc中可以通过参数-E来只执行到预编译阶段。比如gcc -E HelloWorld.c -o HelloWorld.i。

编译阶段。编译器(cc1)将文本文件HelloWorld.i翻译成文本文件HelloWorld.s,它包含一个汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式确切地描述了一条低级机器语言指令。汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。例如,C编译器和Fortran编译器产生的输出文件用的都是一样的汇编语言。在gcc中可以通过参数-S来只执行到源代码到汇编代码的转换。比如:gcc -S ./HelloWorld.i -o ./HelloWorld.s 或者gcc -S ./HelloWorld.c -o HelloWorld.s。

汇编阶段。接下来,汇编器(as)将HelloWorld.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件HelloWorld.o中。HelloWorld.o文件是一个二进制文件,它的字节编码是机器语言指令而不是字符。如果我们在文本编辑器中打开HelloWorld.o文件,看到的将是一堆乱码。在gcc中可以通过参数-c来执行到汇编阶段编译阶段。比如gcc -c HelloWorld.s -o HelloWorld.o 或gcc -c HelloWorld.i -o HelloWorld.o或gcc -c HelloWorld.c -o HelloWorld.o。

链接阶段。请注意,HelloWorld程序调用了printf函数,它是每个C编译器都会提供的标准C库中的一个函数。printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的HelloWorld.o程序中。链接器(ld)就负责处理这种合并。结果就得到HelloWorld文件,它是一个可执行目标文件(或者简称为可执行文件),可以被加载到内存中,由系统执行。

以上这四个阶段的程序一起构成了编译系统。

库的构建与使用

什么是库:库只不过是目标文件的一个集合(一个容器)。如果一些目标文件针对某个给定的问题有相互关联的行为,那么就可以把这些目标文件整合到一个库当中,从而简化应用程序开发人员对这些目标文件的存取和分发。

静态库由ar或者archive工具创建。在开发人员编译并与库链接后,库中被需要的部分会被整合到可执行映像中。从应用程序的观点来看,应用程序映像已经包含了库中自己所需要的部分,因此它与外部的库不再有关联。

共享库或动态库也是与应用程序映像链接在一起,但是这种链接分两个阶段完成。第一阶段(应用程序生成阶段),链接器会检查确认应用程序生成所需要的全部符号(函数或变量)在应用程序或库中可用。但是库中相应的部分并不被整合到应用程序映像中(静态库就是这样直接整合的),而是在第二阶段(运行阶段)由动态加载器将所需要的共享库中的那部分载入内存,与应用程序映像动态地链接在一起。这样可以形成更小的映像文件,因为共享库与应用程序映像是独立的。

使用共享库节省内存的代价是库必须在运行时解析。明确库中哪些部分是需要的,找到这些部分,然后将它们载入内存这个过程需要一点时间。

生成静态库:

我们用最简单的源文件addvec.c,multvec.c以及它们的同一个头文件vector.h来演示库的创建:

addvec.c:

void addvec(int *x,int *y,int *z,int n)
{
int i;

for(i=0;i<n;i++)
z[i]=x[i]+y[i];
}


multvec.c:

void multvec(int *x,int *y,int *z,int n)
{
int i;

for(i=0;i<n;i++)
{
z[i]=x[i]*y[i];
}
}


vector.h:

#ifndef __VECTOR_H
#define __VECTOR_H
void addvec(int *x,int *y,int *z,int n);
void multvec(int *x,int *y,int *z,int n);
#endif


这样就准备好了API,注意addvec.c和vector.h都使用同一个头文件vector.h来提供它们的函数原型。现在我们可以编写一个使用这个API的实验程序来了解下库是如何工作的。

#include <stdio.h>
#include "vector.h"

int x[2]={1,2};
int y[2]={3,4};
int z[2];

int main()
{
addvec(x,y,z,2);
printf("z=[%d %d]\n",z[0],z[1]);
return 0;
}


如果想生成这里讨论的全部源文件并把它们整合到一个映像,可以执行如下命令:

[Jiakun@Kunge Link]$ gcc addvec.c multvec.c test.c -o test


这样就会编译全部三个文件并把它们一起链接到一个名为test的映像中。这个gcc命令不仅仅是编译这些源文件,还完成了把它们链接为一个映像的工作。你可以不一次性生成所有源文件,而是为addvec函数和multvec函数API生成一个库。这可以由工具(archive)来完成。下面将演示静态库的生成以及最终的映像的创建。

[Jiakun@Kunge Link]$ gcc -c -Wall addvec.c
[Jiakun@Kunge Link]$ gcc -c -Wall multvec.c
[Jiakun@Kunge Link]$ ar -cru libmyvector.a addvec.o multvec.o


这个例子首先用gcc编译两个源文件。使用-c选型告诉gcc只进行编译(不进行链接),同时打开全部警告设置。接下来使用ar命令来生成这个库(libmyvector.a)。cru选项是建立存档或向存档中添加内容时的一组标准选项。c选项指明构建静态库(如果库已经存在,这个选项将被忽略)。r选项告诉ar替换静态库中已经存在的目标(如果有的话)。最后,u是一个安全选项,限定仅当将要替换进库的目标比存档中现有的目标(当然是同名目标)更新时执行替换。(也可以添加s选项:创建目标文件索引,这在创建较大的库时能加快时间)

现在有了一个名为libmyvector.a的新文件,这便是包含两个目标(addvec.o和multvec.o)的静态库。现在该如何使用静态库来生成应用程序呢?命令如下:

[Jiakun@Kunge Link]$ gcc ./test.c -L. -lmyvector -o test
[Jiakun@Kunge Link]$ ./test
z=[4 6]
[Jiakun@Kunge Link]$


这里,gcc首先编译test.c,然后链接test.o和libmyvector.a产生一个名为test的映像文件。-L.选项告诉gcc那些库可以在当前子目录中找到(这个句点就代表当前目录)。你也可以给库指定特别的子目录,如-L/usr/mylibs。-l(L,不是1)选项指定了要使用的库,注意,myvector并不是库文件名,那个文件名是libmyvector.a。使用-l选项时,它会自动在文件名的前面加上lib,后面加上.a。因此,如果你指定的是-ltest,gcc会去查找libtest.a。

知道如何创建一个库并用它来生成一个应用程序之后,再回过头看看ar命令的其他用处。可以使用-t选项来查看静态库中包含的内容:

[Jiakun@Kunge Link]$ ar -t libmyvector.a
addvec.o
multvec.o
[Jiakun@Kunge Link]$


如果需要,还可以把某个目标从静态库中移除。为此,使用-d选项即可以,如下所示:

[Jiakun@Kunge Link]$ ar -d libmyvector.a addvec.o
[Jiakun@Kunge Link]$ ar -t libmyvector.a
multvec.o
[Jiakun@Kunge Link]$


注意,如果移除目标失败,ar实际是不会显示。为了在移除失败时显示出错信息,需要像下面这样增加-v选项:

[Jiakun@Kunge Link]$ ar -d libmyvector.a addvec.o
[Jiakun@Kunge Link]$ ar -dv libmyvector.a addvec.o
No member named 'addvec.o'
[Jiakun@Kunge Link]$


还不仅仅是可以从静态库中移除目标,还可以用-x选项把它提取出来。

[Jiakun@Kunge Link]$ ls
addvec.c  libmyvector.a  multvec.c  test  test.c  vector.h
[Jiakun@Kunge Link]$ ar -xv libmyvector.a addvec.o
x - addvec.o
[Jiakun@Kunge Link]$ ls
addvec.c  addvec.o  libmyvector.a  multvec.c  test  test.c  vector.h
[Jiakun@Kunge Link]$ ar -t libmyvector.a
multvec.o
addvec.o
[Jiakun@Kunge Link]$


提取选项-x和详细选项-v连用,即可查看ar究竟在做什么。这里ar工具的响应是有一个文件正在提取(x - addvec.o),注意,现在列出的文件中仍然包含这个静态库文件,其中的addvec.o目标也仍然存在。提取命令并不会从库中移除目标,它只是把指定的目标复制出来。如果要将目标从静态库中删除,要使用删除选项(-d)。

共享库的生成

现在我们使用共享库而不是静态库再来生成这个试验程序。过程仍然和前面一样简单。首先,用addvec.o和multvec.o目标生成一个共享库。为共享库生成源文件时需要有一点改变。因为现在库和应用程序不像静态库时那样整合在一起,所以共享库并不知道应用程序的任何信息。比如,地址就必须使用相对地址(使用GOT或者叫全局偏移表)。加载共享库时,加载器会自动解析所有的GOT地址。要生成地址无关的源文件,我们使用gcc的PIC选项:

[Jiakun@Kunge Link]$ gcc -fPIC -c addvec.c
[Jiakun@Kunge Link]$ gcc -fPIC -c multvec.c


这样得到的两个目标文件中就包含地址无关的代码。可以用gcc和-shared标志在此基础上创建一个共享库。这个标志告诉gcc将要生成一个共享库。

[Jiakun@Kunge Link]$ gcc -shared addvec.o multvec.o -o libmyvector.so


这样便指定了两个目标模块和输出的共享库文件(-o)。注意用.so扩展名来标明此文件是一个共享库(shared object)。使用这个新的共享库目标来生成应用程序,就像使用静态库那样把有关部分链接起来:

[Jiakun@Kunge Link]$ gcc test.c -L. -lmyvector -o test


你可以使用ldd命令来查看这个新的映像依赖于哪些共享库。ldd命令的功能是列出指定的应用程序的共享库依赖。例如:

[Jiakun@Kunge Link]$ ldd test
linux-vdso.so.1 =>  (0x00007ffea57db000)
libmyvector.so (0x00007f0ef84f6000)
libc.so.6 => /lib64/libc.so.6 (0x0000003888000000)
/lib64/ld-linux-x86-64.so.2 (0x0000003887c00000)
[Jiakun@Kunge Link]$


这个ldd命令输出test使用的共享库。这里列出的标准C库(libc.so.6)是动态链接器和加载器(ld-linux-x86-64.so.2)。注意,libmyvector.so文件在某些系统环境下可能会显示为未能找到(此时libmyvector.so那一行将显示为libmyvector.so => not found)。它在应用程序所在的子目录中,但这个位置必须显示地告知GNU/Linux。你可以使用LD_LIBRARY_PATH环境变量来完成这一点。在指出共享库的位置被(export命令)之后,再执行一次ldd命令:

[Jiakun@Kunge Link]$ export LD_LIBRARY_PATH=./
[Jiakun@Kunge Link]$ ldd test
linux-vdso.so.1 =>  (0x00007ffe419ff000)
libmyvector.so => ./libmyvector.so (0x00007ff6ac979000)
libc.so.6 => /lib64/libc.so.6 (0x0000003888000000)
/lib64/ld-linux-x86-64.so.2 (0x0000003887c00000)
[Jiakun@Kunge Link]$


指明共享库可以在当前目录(./)中找到后。然后再次执行ldd命令,便能成功地找到那个共享库文件。注意如果没有告知共享库的位置就执行应用程序,就会产生一条错误你信息,指出共享库无法找到。

从应用程序中加载和链接共享库

我们已经讨论了在应用程序执行之前,即应用程序被加载时,动态链接器加载和链接共享库的情景,然而,应用程序还可能在它运行时要求动态链接器加载和链接任意共享库,而无需在编译时链接那些库到应用中。

Linux系统为动态链接器提供了一些简单的接口API,允许应用程序在运行时加载和链接共享库:

#include <dlfcn.h>
void *dlopen(const char *filename,int flag);    //若成功则为指向句柄的指针,若出错,则为NULL。
void *dlsym(void *handle,char *symbol);     //返回;若成功则为指向符号的指针,若出错则为NULL。
int dlclose(void *handle);          //返回:若成功则为0,若出错则为-1。
const char *dlerror(void);          //返回:如果前面对dlopen,dlsym或dlclose的调用失败,则为错误消息,如果前面的调用成功,则为NULL。


dlopen函数加载和链接共享库filename。用以前带RTLD_GLOBAL选项打开的库解析filename中的外部符号。如果当前可执行文件是带-rdynamic选项编译的,那么对符号解析而言,它的全局符号也是可用的。flag参数必须要么包括RTLD_NOW,该标志告诉链接器立即解析对外部符号的引用,要么包括RTLD_LAZY标志,该标志指示链接器推迟符号解析直到执行来自库中的代码。这两个值中的任意一个都可以和RTLD_GLOBAL标志取或。dlsym函数的输入是一个指向前面已经打开共享库的句柄和一个符号的名字,如果该符号存在,就返回符号的地址,否则返回NULL。如果没有其他共享库正在使用这个共享库,调用dlclose函数就卸载该共享库。dlerror函数返回一个字符串,它描述的是调用dlopen,dlsym或者dlclose函数时发生的最近的错误,如果没有错误发生,就返回NULL。

示例程序dll.c如下:

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

int x[2]={1,2};
int y[2]={3,4};
int z[2];

int main()
{
void *handle;
void (*addvec)(int *,int *,int *,int);
char *error;

/* Dynamically load shared library that contains addvec() */
handle=dlopen("./libmyvector.so",RTLD_LAZY);
if(!handle)
{
fprintf(stderr,"%s\n",dlerror());
exit(1);
}

/* Get a pointer to the addvec() function we just loaded */
addvec=dlsym(handle,"addvec");
if((error=dlerror())!=NULL)
{
fprintf(stderr,"%s\n",error);
exit(1);
}

/* Now we can call addvec() just like any other function */
addvec(x,y,z,2);
printf("z=[%d %d]\n",z[0],z[1]);

/* Unload the shared library */
if(dlclose(handle)<0)
{
fprintf(stderr,"%s\n",dlerror());
exit(1);
}
return 0;
}


我们通过下面的方式调用GCC

[Jiakun@Kunge Link]$ gcc -rdynamic -o test2 test2.c -ldl
[Jiakun@Kunge Link]$ ./test2
z=[4 6]
[Jiakun@Kunge Link]$


工具

现在看一看在创建静态库,共享库和动态库时会用到的其他一些工具。

* file工具:file 工具检查文件的参数,从而确定该文件到底是什么。在很多情况下,都需要用到这个工具,在这里,可以用它得到共享目标的一些信息。看这个显示其交互作用的例子:

[Jiakun@Kunge Link]$ file ./libmyvector.so
./libmyvector.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, not stripped


于是,通过使用file工具,可以看出此共享库是针对x86-64处理器系列的64位ELF目标,还说明它是”不可剥离的”(not stripped),即调试信息能够显示。

* size命令:size命令用于轻松获得目标的text节(段),data节(段)和bss节(/段,下同)的大小。对前面建立的共享库使用size命令的实例如下:

[Jiakun@Kunge Link]$ size ./libmyvector.so
text    data     bss     dec     hex filename
1640     504      16    2160     870 ./libmyvector.so
[Jiakun@Kunge Link]$


nm命令:此命令可用于获得指定的目标文件中使用的符号:

[Jiakun@Kunge Link]$ nm -n ./libmyvector.so | grep " T "
0000000000000490 T _init
0000000000000630 T addvec
00000000000006a0 T multvec
0000000000000748 T _fini
[Jiakun@Kunge Link]$


上面这个例子使用nm得到共享库中的符号,但是只把那些有”T”标签的符号发送给stdout(即只输出了.text节或者说代码节的那些符号)。还可以用-n选项要求输出按地址顺序排序,而不是默认的按符号名字母表顺序排序。这样会得到库中的相对地址信息,如果想知道.text节确切的大小,可以使用-S选项,命令如下:

[Jiakun@Kunge Link]$ nm -n -S ./libmyvector.so | grep " T "
0000000000000490 T _init
0000000000000630 0000000000000070 T addvec
00000000000006a0 000000000000006f T multvec
0000000000000748 T _fini
[Jiakun@Kunge Link]$


从本例的输出中可以看出addvec在库中的相对偏移为0-630,其大小为0-70(十进制的112)字节。nm命令还有很多可用的选项。

objdump工具:所有二进制工具之母。能够显示一个目标文件中所有的信息。它最大的作用是反汇编.text节中的二进制指。

readelf工具:显示一个目标文件的完整结构,包括ELF头中编码的所有信息。包括size和nm的功能。

ldd工具:列出一个可执行文件在运行时所需要的共享库。

strip命令:从目标文件中删除符号表信息
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息