链接(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:

  C语言 1

前四个记号在stack.o中贯彻了,链接生成可执行文件main时得以做标记解析,而putchar是libc的库函数,在可执行文件main中

依旧是未定义的,要在程序运维时做动态链接。

  {

nm [option(s)] [file(s)]

有用的options:

  • -A 在各样符号音讯的近来打字与印刷所在对象文件名称;
  • -C 输出demangle过了的标志名称;
  • -D 打字与印刷动态符号;
  • -l 使用对象文件中的调节和测试新闻打字与印刷出所在源文件及行号;
  • -n 依照地点/符号值来排序;
  • -u 打字与印刷出那么些未定义的记号;

常见的标记类型:

  • A
    该符号的值在其后的链接上将不再改变;
  • B
    该符号放在BSS段中,平时是那几个未初阶化的全局变量;
  • D
    该符号放在普通的多少段中,平日是那多少个曾经伊始化的全局变量;
  • T
    该符号放在代码段中,平日是那多少个全局非静态函数;
  • U 该符号未定义过,要求自别的对象文件中链接进来;
  • W
    未明朗内定的弱链接符号;同链接的其它对象文件中有它的定义就用上,不然就用3个类别越发钦定的私下认可值。

留神几点:

  • -C
    总是适用于c++编写翻译出来的对象文件。还记得c++中有重载么?为了差距重载函数,c++编写翻译器会将函数重临值/参数等音讯附加到函数名称中去形成1个mangle过的记号,这用那个选项列出符号的时候,做1个逆操作,输出那个原来的、大家可了然的号子名称。
  • 行使 -l
    时,必须保障你的对象文件中涵盖符号调式音信,那貌似须求您在编写翻译的时候钦点二个-g
    选项,见 C语言,Linux:Gcc
  • 行使nm前,最棒先用Linux:File查阅对象文件所属处理器架构,然后再用相应交叉版本的nm工具。

  }

    大家因而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段,包蕴了各函数的概念。如下图所示:

    C语言 2C语言 3

    假设在编写翻译时把stack.o放到main.o前面,即:gcc stack.o main.o -o
main,可执行文件main的种种段中来自main.o的变量或函数都排到前面去了。

   实际上链接的进程是由二个链接脚本(Linker
Script)控制的,链接脚本决定了给各个段分配什么地点,怎么样对齐,哪个段在前,哪个段在后,哪些段合并到同一个Segment,其它链接脚本还要插入一些标志到结尾生成的文件中,例如
__bss_start 、 _edata 、 _end 等。假诺用 ld 做链接风尚未用 -T
选项钦定链接脚本,则应用 ld 的暗中认可链接脚本,默许链接脚本能够用 ld
–verbose
命令查看。【结果很短间接接书上的截图,实际出口内容与下图并不壹致】:

  C语言 4

  C语言 5

  C语言 6

  ENTRY(_start)说明_start是全体程序的入口点,由此_start是入口点并不是规定,是足以改用别的函数做入口点的。

  PROVIDE(__executable_start = 0x8048000); . = 0x9048000 +
SIZEOF_HEADE奥迪Q五S;是Text
Segment的原初地址,那些Segment包蕴前边列出的那么些段,
.plt、.text、.rodata等等。每种段的叙说格式都是”段名:{组成}”,例如.plt :
{ *(.plt))
},右边表示最终生成的文件的.plt段,左边表示全部目的文件的.plt段,意思是最终身成的文本的.plt段由各指标文件的.plt段组成。

  . = ALIGN (CONSTANT (MAXPAGESIZE)) – ((CONSTANT (MAXPAGESIZE) – .) &
(CONSTANT (MAXPAGESIZE) – 1)); . = DATA_SEGMENT_ALIGN (CONSTANT
(MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));是Data
Segment的发端地址,要做一名目繁多的对齐操作,那些Segment包罗前边列出的这么些段,.got、.data、.bss等等。

二、定义和评释

    

    1、extern和static关键字

    在上边编写翻译stack.c与main.c文件时其实有一点小意思,(-沃尔选项能够看看)由于编写翻译器在处理函数调用代码时髦未找打函数原型,只能依据函数调用代码做隐式评释,把多个函数注解为

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【关于External Linkage参看《[汇编与C语言关系]3.
变量的储存布局
》】(“extern关键字表示这几个标识符具有External
Linkage”其实是不确切的,准确地说应该是Previous Linkage。Previous
Linkage的定义是:本次表明的标识符具有什么等的Linkage取决于前二次注脚,那前1次评释具有同样的标识符名,而且必须是文件成效域的评释,假设在先后文件中找不到前壹遍注解(本次注解是率先次注明),那么这几个标识符具有External
Linkage),push那一个标识符具有External
Linkage指的是:尽管把main.c和stack.c链接在联合署名,假若push在main.c和stack.c中都有扬言(在stack.c中既是声称又是概念)那么这么些注明指的是同3个函数,链接之后是同多少个GLOBAL符号,代表同2个地点。

    函数宣称中的extern也得以省略不写,不写extern仍旧表示这些函数名具有External
Linkage。C语言不允许嵌套定义函数,但1旦只是声称而不定义,那种评释时允许写在函数体里面包车型大巴,那样评释的标识符具有块功用域,例如地点的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;
}

    假若用static关键字修饰叁个函数注解,则代表该标识符具有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在里头是3个LOCAL的记号,不参预链接进度,所以在链接时,main.c中用到多个External
Linkage的foo函数,链接器却找不到它的定义在哪,不可能鲜明它的地方,也就无法做标记解析,只可以报错。凡是被反复表明的变量或函数,必须有且唯有3个声称是概念,借使有两个概念,也许2个概念都尚未,链接器就不能形成链接。

    对于变量如若在main.c中的main()函数中用extern int
top;来声称top变量能够访问到stack.c中的top变量。(变量的extern注明无法大致,而且不能够定义如:extern
int top =
-一;那样写是狼狈的)。要是不想让main.c访问到top变量可以把top用static注明,在stack.c中static
int top = -一;

 

    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合并为同八个主次文件。尽管这么也能编译通过,可是在一个规模较大的品类中不能够如此做,假若又有2个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)

    把(1)中的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, 但是这样写不能生成.so文件
gcc -o libstack.so -O2 -fPIC -shared stack/stack.c stack/push.c stack/pop.c stack/is_empty.c stack/stack.h

    -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模拟运转3回main,在运维进程中做动态链接,从而得知这一个可执行文件依赖于怎么样共享库,种种共享库都在怎么着路线下,加载到进程地址空间的哪些地方。/lib/ld-linux.so.二是动态链接器,它的门道是在编写翻译链接时钦命的,gcc在做链接时用-dynamic-linker钦命动态链接器的路子,它也像其余共享库1样加载到进程的地址空间中。libc.so.陆的路径/lib/tls/i686/cmov/libc.so.陆是由动态链接器ld-linux.so.二在做动态链接时追寻到的,而libstack.so的门径未有找到。linux-gate.so.壹那么些共享库其实并不设有于文件系统中,它是由基础虚拟出来的共享库,所以它未有相应的门道,它承担处理连串调用。总而言之,共享库的检索路径由动态链接器决定,从ld.so(捌)Man
Page能够查到共享库路径的摸索顺序:

    1. 率先在环境变量LD_LIBRARY_PATH所记录的途径中摸索。

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

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

    三.
万一上述手续都找不到,则到暗中认可的系统路径中追寻,先是/usr/lib然后是/lib。

    

    消除:能够把libstack.so所在的目录的相对路径添加到/etc/ld.so.conf中,然后运行ldconfig

 

    二、共享库的命名惯例

    根据共享库的命名惯例,种种共享库有多少个文本名:real
name、soname和linker name。真正的库文件(而不是标志链接)的名字是real
name,包涵完整的共享库版本号。例如地点的libcap.so.壹.十、libc-二.八.90.so等。soname是二个标记链接的名字,只含有共享库的主版本号,主版本号1致即可有限辅助库函数的接口一致,由此应用程序的.dynamic段只记录共享库的soname,只要soname壹致,这些共享库就能够用。例如地点的libcap.so.1和libcap.so.贰是三个主版本号分裂的libcap,有个别应用程序

依傍于libcap.so.1,有些应用程序正视于libcap.so.二,但对此注重libcap.so.壹的应用程序来说,真正的库文件不管是libcap.so.壹.10照旧libcap.so.壹.11都得以用,所以选择共享库能够很有益于地升级库文件而不供给再行编写翻译应用程序,那是静态库所未有的独到之处。注意libc的本子编号有有个别例外,libc-二.8.90.so的主版本号是陆而不是二或贰.捌。

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

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