链接(extern、static关键词\头文件\静态库\共享库)

原文链接:http://www.orlion.ga/781/

一、 多目标文件的链接

假设有两个文件:stack.c:

/* stack.c */
char stack[512];
int top = -1;
void push(char c)
{
        stack[++top] = c;
}
char pop(void)
{
        return stack[top--];
}
int is_empty(void)
{
        return top == -1;
}

上边这个文件实现了栈。main.c:

/* main.c */
#include <stdio.h>

int main(void)
{
        push(‘a‘);
        push(‘b‘);
        push(‘c‘);
        
        while(!is_empty())
                putchar(pop());
        putchar(‘\n‘);
        return 0;
}

这个文件是使用了栈,编译:gcc main.c stack.c -o main,也可以分步编译:

gcc -c main.c
gcc -c stack.c
gcc main.o stack.o -o main

用nm命令查看目标文件的符号表,会发现main.o中有未定义的符号push、pop、is_empty、putchar,前三个符号在stack.o中实现了,链接生成可执行文件main时可以做符号解析,而putchar是libc的库函数,在可执行文件main中

仍然是未定义的,要在程序运行时做动态链接。

我们通过readelf -a main命令可以看到,main的.bss段合并了main.o和stack.o的.bss段,其中包含了变量a和stack,main的.data段也合并了main.o和stack.o的.data段,其中包含了变量b和top,main的.text段合并了main.o和stack.o的.text段,包含了各函数的定义。如下图所示:

如果在编译时把stack.o放到main.o前面,即:gcc stack.o main.o -o main,可执行文件main的每个段中来自main.o的变量或函数都排到后面去了。

二、定义和声明

1、extern和static关键字

在上面编译stack.c与main.c文件时其实有一点小问题,(-Wall选项可以看到)由于编译器在处理函数调用代码时没有找打函数原型,只好根据函数调用代码做隐式声明,把三个函数声明为

int push(char);
int pop(void);
int is_empty(void);

现在完善一中的代码:

/* main.c */
#include <stdio.h>
extern void push(char);
extern char pop(void);
extern int is_empty(void);
int main(void)
{
        push(‘a‘);
        push(‘b‘);
        push(‘c‘);
        
        while(!is_empty())
                putchar(pop());
        putchar(‘\n‘);
        return 0;
 }

这样编译器就不会报警报了,extern关键字表示这个标识符具有External Linkage(“extern关键字表示这个标识符具有External Linkage”其实是不准确的,准确地说应该是Previous Linkage。Previous Linkage的定义是:这次声明的标识符具有什么样的Linkage取决于前一次声明,这前一次声明具有相同的标识符名,而且必须是文件作用域的声明,如果在程序文件中找不到前一次声明(这次声明是第一次声明),那么这个标识符具有External Linkage),push这个标识符具有External Linkage指的是:如果把main.c和stack.c链接在一起,如果push在main.c和stack.c中都有声明(在stack.c中既是声明又是定义)那么这些声明指的是同一个函数,链接之后是同一个GLOBAL符号,代表同一个地址。

函数声明中的extern也可以省略不写,不写extern仍然表示这个函数名具有External Linkage。C语言不允许嵌套定义函数,但如果只是声明而不定义,这种声明时允许写在函数体里面的,这样声明的标识符具有块作用域,例如上面的main.c可以写成:

/* main.c */
#include <stdio.h>
int main(void)
{
        void push(char);
        char pop(void);
        int is_empty(void);
        push(‘a‘);
        push(‘b‘);
        push(‘c‘);
        
        while(!is_empty())
                putchar(pop());
        putchar(‘\n‘);
        return 0;
}

如果用stati关键字修饰一个函数声明,则表示该标识符具有Internal Linkage,例如有一下两个程序文件:

/* foo.c */
static void foo(void) {}
/* main.c */
void foo(void);
int main(void) { foo(); return 0; }

编译链接在一起会出错:

虽然在foo.c中定义了函数foo,但这个函数只具有Internal Linkage,只有在foo.c中多次声明才表示同一个函数,而在main.c中声明就不表示它了。如果把foo.c编译成目标文件,函数名foo在其中是一个LOCAL的符号,不参与链接过程,所以在链接时,main.c中用到一个External Linkage的foo函数,链接器却找不到它的定义在哪,无法确定它的地址,也就无法做符号解析,只能报错。凡是被多次声明的变量或函数,必须有且只有一个声明是定义,如果有多个定义,或者一个定义都没有,链接器就无法完成链接。

对于变量如果在main.c中的main()函数中用extern int top;来声明top变量可以访问到stack.c中的top变量。(变量的extern声明不能省略,而且不能定义如:extern int top = -1;这样写是不对的)。如果不想让main.c访问到top变量可以把top用static声明,在stack.c中static int top = -1;

2、头文件

(一)中的stack.c模块封装了top和stack两个变量导出了push\pop\is_empty三个函数接口,但是使用这个模块的每个程序文件都要写三个函数声明,假设又有一个文件也使用了这个模块,那么就要写三个函数声明。这时候可以写一个头文件stack.h来简化代码:

/* stack.h */
#ifndef STACK_H
#define STACK_H
extern void push(char);
extern char pop(void);
extern int is_empty(void);
#endif

在main.c中只需要包含就可以了:

/* main.c */
#include <stdio.h>
#include "stack.h"
int main(void)
{
        push(‘a‘);
        push(‘b‘);
        push(‘c‘);
        
        while(!is_empty())
                putchar(pop());
        putchar(‘\n‘);
        return 0;
}

对于用尖括号包含的头文件(#include <stdio.h>)gcc会首先查找-I选项指定的目录,然后查看系统的头文件目录(通常是/usr/include),而对于引号包含的头文件,gcc首先查找包含头文件的.c文件所在的目录,然后查找-I选项指定的目录,然后查找系统的头文件目录。在#include预处理中可以使用相对路劲如:#include "header/stack.h"

#idndef STACK_H和#endif是如果STACK_H这个宏没有定义过,那么从#ifndef到#endif之间的代码就包含在预处理的输出结果中,否则这一段代码就不出现在预处理的输出结果中。stack.h这个头文件的内容整个被#ifndef和#endif包含起来了,如果在包含这个头文件时STACK_H这个宏已经定义过了,则相当于这个文件中什么都没有。

还有一个问题,既然要#include头文件,那我不如直接在main.c中#include "stack.c"得了。这样把stack.c和main.c合并为同一个程序文件。虽然这样也能编译通过,但是在一个规模较大的项目中不能这么做,假如又有一个foo.c也要使用stack.c这个模块怎么办呢?如果在foo.c里面也#include "stack.c",就相当于push、pop、is_empty这三个函数在main.c和foo.c中都有定义,那么main.c和foo.c就不能链接在一起了。如果采用包含头文件的办法,那么这三个函数只在stack.c中定义了一次,最后可

以把main.c、stack.c、foo.c链接在一起。

三、静态库

有时候要把一组代码编译成一个库,这个库在很多项目中都要用到,例如libc就是这样一个库,我们在不同的程序中都会用到libc中的库函数(如printf)也会用到libc中的变量(比如environ)

把(一)中的stack.c拆成四个文件:

/* stack.c */
char stack[512];
int top = -1;
/* push.c */
extern char stack[512];
extern int top;
void push(char c)
{
        stack[++top] = c;
}
/* pop.c */
extern char stack[512];
extern int top;
char pop(void)
{
        return stack[top--];
}
/* is_empty.c */
extern int top;
int is_empty(void)
{
        return top == -1;
}
/* stack.h */
#ifndef STACK_H
#define STACK_H
extern void push(char);
extern char pop(void);
extern int is_empty(void);
#endif
/* main.c */
#include <stdio.h>
#include "stack.h"
int main(void)
{
        push(‘a‘);
        return 0;
}

目录结构为:

|–main.c

|–stack

|–is_empty.c

|–pop.c

|–push.c

|–stack.c

|–stack.h

把stack.c、push.c、pop.c、is_empty.c编译成目标文件:

gcc -c stack/stack.c stack/push.c stack/pop.c stack/is_empty.c

然后打包成一个静态库libstack.a:

ar rs libstack.a stack.o push.o pop.o is_empty.o

库文件都是以lib开头的,静态库以.a作为后缀,表示Archive。。ar命令类似于tar命令,起一个打包的作用,但是把目标文件打包成静态库只能用ar命令而不能用tar命令。选项r表示将后面的文件列表添加到文件包,如果文件包不存在就创建它,如果文件包中已有同名文件就替换成新的。s是专用于生成静态库的,表示为静态库创建索引,这个索引被链接器使用。ranlib命令也可以为静态库创建索引,以上命令等价于:

ar r libstack.a stack.o push.o pop.o is_empty.o
ranlib libstack.a

然后把libstack.a和main.c编译连接在一起:

gcc main.c -L. -l stack -I stack -o main

-L选项告诉编译器去哪里找需要的库文件,-L.表示在当前目录找。-lstack告诉编译器要链接libstack库,-I选项告诉编译器去哪里找头文件。注意,即使库文件就在当前目录,编译器默认也不会去找的,所以-L.选项不能少。编译器默认会找的目录可以用-print-search-dirs选项查看。

编译器会首先找有没有共享库libstack.so,如果有就链接它,如果没有就找有没有静态库libstack.a,如果有就链接它。所以编译器是优先考虑共享库的,如果希望编译器只链接静态库,可以指定-static选项。

在链接libc共享库时只是指定了动态链接器和该程序所需要的库文件,并没有真的做链接,可执行文件main中调用的libc库函数仍然是未定义符号,要在运行时做动态链接。而在链接静态库时,链接器会把静态库中的目标文件取出来和可执行文件真正链接在一起。

四、共享库

1、编译、链接、运行

组成共享库的目标文件和一般的目标文件有所不同,在编译时要加-fPIC选项:

gcc -c -fPIC stack/stack.c stack/push.c stack/pop.c stack/is_empty.c

-f后边跟编译选项,PIC是其中一种,表示生成位置无关代码。一般的目标文件称为Relocatable,在链接时可以把目标文件中各段的地址做重定位,重定位时需要修改指令。

现在把main.c和共享库编译链接在一起,然后运行:

$ gcc main.c -g -L. -lstack -Istack -o main
$ ./main 
./main: error while loading shared libraries: libstack.so: cannot 
open shared object file: No such file or directory

(TODO:这里本机实验时有问题)

编译的时候没问题,由于指定了-L.选项,编译器可以在当前目录下找到libstack.so,而运行时却说找不到libstack.so。

可以用ldd命令查看可执行文件依赖哪些共享库:

$ ldd main
        linux-gate.so.1 =>  (0xb7f5c000)
        libstack.so => not found
        libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7dcf000)
        /lib/ld-linux.so.2 (0xb7f42000)

ldd模拟运行一遍main,在运行过程中做动态链接,从而得知这个可执行文件依赖于哪些共享库,每个共享库都在什么路径下,加载到进程地址空间的什么地址。/lib/ld-linux.so.2是动态链接器,它的路径是在编译链接时指定的,gcc在做链接时用-dynamic-linker指定动态链接器的路径,它也像其它共享库一样加载到进程的地址空间中。libc.so.6的路径/lib/tls/i686/cmov/libc.so.6是由动态链接器ld-linux.so.2在做动态链接时搜索到的,而libstack.so的路径没有找到。linux-gate.so.1这个共享库其实并不存在于文件系统中,它是由内核虚拟出来的共享库,所以它没有对应的路径,它负责处理系统调用。总之,共享库的搜索路径由动态链接器决定,从ld.so(8)Man Page可以查到共享库路径的搜索顺序:

1. 首先在环境变量LD_LIBRARY_PATH所记录的路径中查找。

2. 然后从缓存文件/etc/ld.so.cache中查找。这个缓存文件由ldconfig命令读取配置文

件/etc/ld.so.conf之后生成,稍后详细解释。

3. 如果上述步骤都找不到,则到默认的系统路径中查找,先是/usr/lib然后是/lib。

解决:可以把libstack.so所在的目录的绝对路径添加到/etc/ld.so.conf中,然后运行ldconfig

2、共享库的命名惯例

按照共享库的命名惯例,每个共享库有三个文件名:real name、soname和linker name。真正的库文件(而不是符号链接)的名字是real name,包含完整的共享库版本号。例如上面的libcap.so.1.10、libc-2.8.90.so等。soname是一个符号链接的名字,只包含共享库的主版本号,主版本号一致即可保证库函数的接口一致,因此应用程序的.dynamic段只记录共享库的soname,只要soname一致,这个共享库就可以用。例如上面的libcap.so.1和libcap.so.2是两个主版本号不同的libcap,有些应用程序

依赖于libcap.so.1,有些应用程序依赖于libcap.so.2,但对于依赖libcap.so.1的应用程序来说,真正的库文件不管是libcap.so.1.10还是libcap.so.1.11都可以用,所以使用共享库可以很方便地升级库文件而不需要重新编译应用程序,这是静态库所没有的优点。注意libc的版本编号有一点特殊,libc-2.8.90.so的主版本号是6而不是2或2.8。

linker name仅在编译链接时使用,gcc的-L选项应该指定linker name所在的目录。有的linker

name是库文件的一个符号链接,有的linker name是一段链接脚本

时间: 2024-12-21 13:49:13

链接(extern、static关键词\头文件\静态库\共享库)的相关文章

Linux C头文件查找与动态库搜索

一.编译程序时,头文件路径搜索 本文介绍在linux中头文件的搜索路径,也就是说你通过include指定的头文件,linux下的gcc编译器它是怎么找到它的呢.在此之前,先了解一个基本概念. 头文件是一种文本文件,使用文本编辑器将代码编写好之后,以扩展名.h保存就行了.头文件中一般放一些重复使用的代码,例如函数声明.变量声明.常数定义.宏的定义等等.当使用#include语句将头文件引用时,相当于将头文件中所有内容,复制到#include处.#include有两种写法形式,分别是: #inclu

cctype头文件(字符处理库)的使用

C++ 中cctype头文件的使用 头文件cctype(字符处理库)中定义了有关字符判断与处理的库函数,使用前要包含头文件: #include <cctype> using namespace  std: cctype头文件中的常用函数列表如下: 函数名称   返回值isalnum()  如果参数是字母数字,即字母或者数字,函数返回trueisalpha()  如果参数是字母,函数返回trueiscntrl()  如果参数是控制字符,函数返回trueisdigit()  如果参数是数字(0-9

Linux 环境下 动态库(共享库) 共享路径设置

个人博客首页(点击查看详情)   1.简介     动态库 链接 后生成 可执行文件,该可执行文件 需要读取动态库文件,因此 我们需要 告知 计算机 该库文件的位置信息:本文用来 描述 如何设置 动态库文件 位置信息     备注:    系统无法 获取 动态库文件 会提示 " error while loading shared libraries: libxxx.so: cannot open shared object file: Error 40"      2.全局变量 设置

共享库

一.编译.链接.运行 /* stack.c */ char stack[512]; int top = -1; /* push.c */ extern char stack[512]; extern int top; void push(char c) {         stack[++top] = c; } /* pop.c */ extern char stack[512]; extern int top; char pop(void) {         return stack[top

linux下动态链接库.so文件 静态链接库.a文件创建及使用

转摘网址为:http://www.cnblogs.com/fengyv/archive/2012/08/10/2631313.html Linux下文件的类型是不依赖于其后缀名的,但一般来讲:    .o,是目标文件,相当于windows中的.obj文件    .so 为共享库,是shared object,用于动态连接的,和dll差不多    .a为静态库,是好多个.o合在一起,用于静态连接    .la为libtool自动生成的一些共享库,vi编辑查看,主要记录了一些配置信息.可以用如下命令

.h头文件 .lib库文件 .dll动态链接库文件关系

.h头文件是编译时必须的,lib是链接时需要的,dll是运行时需要的. 附加依赖项的是.lib不是.dll,若生成了DLL,则肯定也生成 LIB文件.如果要完成源代码的编译和链接,有头文件和lib就够了.如果也使动态连接的程序运行起来,有dll就够了.在开发和调试阶段,当然最好都有. .h .lib .dll三者的关系是: H文件作用是:声明函数接口 DLL文件作用是: 函数可执行代码 当我们在自己的程序中引用了一个H文件里的函数,编链器怎么知道该调用哪个DLL文件呢?这就是LIB文件的作用:

头文件的查找方式和库的搜索路径

  对于以压缩包发布的软件,在它的目录下通常都有一个配置脚本configure,它的作用确定编译参数(比如头文件位置.连接库位置等),然后生成Makefile以编译程序.可以进入该软件的目录,执行"./configure --help"命令查看使用帮. 一个程序能正确编译.链接.运行需要满足3个条件:预处理时能找到头文件,连接时能找到库,运行时能找到库.下面分别介绍: 1.指定头文件位置 在程序中常用两种方法来包含头文件: #include <headerfile.h>  

C语言中,头文件和源文件的关系(转)

简单的说其实要理解C文件与头文件(即.h)有什么不同之处,首先需要弄明白编译器的工作过程,一般说来编译器会做以下几个过程: 1.预处理阶段 2.词法与语法分析阶段 3.编译阶段,首先编译成纯汇编语句,再将之汇编成跟CPU相关的二进制码,生成各个目标文件 (.obj文件)4.连接阶段,将各个目标文件中的各段代码进行绝对地址定位,生成跟特定平台相关的可执行文件,当然,最后还可以用objcopy生成纯二进制码,也就是去掉了文件格式信息.(生成.exe文件) 编译器在编译时是以C文件为单位进行的,也就是

静态库和共享库制作

 1静态库和共享库 *本节就如何创建和使用程序库进行论述.所谓"程序库",简单说,就是包含了数据 和执行码的文件.其不能单独执行,可以作为其它执行程序的一部分来完成某些功能.库的 存在,可以使得程序模块化,可以加快程序的再编译,可以实现代码重用,可以使得程序便 于升级.程序库可分静态库(static library)和共享库(shared object). A:静态库 是在可执行程序运行前就已经加入到执行码中,成为执行程序的一部分:共享库,是在 执行程序启动时加载到执行程序中,可以