Linux 链接详解(1)

可执行文件的生成过程:

hello.c ----预处理--->  hello.i ----编译----> hello.s -----汇编-----> hello.o -----链接----->hello -----加载---->hello进程

其中预处理器根据hello.c中的#开头的命令解析, 如将include 头文件放在此处,选择条件编译等等;  编译阶段 就是将.i 文件翻译为更低级的汇编指令; 而后这些汇编指令通过汇编器汇编为目标文件; 最后在由连接器将目标文件与库文件链接为可执行文件; 程序要运行时 再由加载器将之加载到内存运行。

对于链接的阶段又可以分为静态链接和动态链接,接下来主要讲解它们各自的特点和原理。

符号表和Linux目标文件格式ELF

首先这里先说明一下符号表和Linux目标文件格式ELF:

如代码:

//main.c
void
swap(); int buf[2] = {1,2}; int main() { swap(); return 0; }

//swap.c
extern int buf[];
int *bufp0 = &buf[0];
int *bufp1;
void swap()
{
    int temp;
    bufp1 = &buf[1];
    temp = *bufp0;
    *bufp0 = *bufp1;
    *bufp1 = temp;
}

先将文件main.c变为汇编:

    .file    "main.c"
    #表示全局的数据段
    .globl    buf
    .data
    .align 4
    .type    buf, @object
    .size    buf, 8
buf:
.long 1 .long 2

   #表示全局的代码段 .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $0, %eax call swap movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret

这里的section标记buff,main就是用来生成符号表的,.c 文件经过语法分析后变成汇编语言,再由汇编中的.text  .data 表示目标文件.o 的代码段和数据段,各个段的不同内容再用符号表来表示。目标文件可以是可重定位文件或者可执行文件, 那么为什么要是elf格式呢?  这里就牵扯到可执行文件加载的问题,加载器将可执行文件加载到到内存,其实质就是解析可执行文件的elf, 将按照其格式将代码段  数据段 只读数据段 bss段等加载到虚拟地址空间。我们可以简单看一下linux 0.12 exec.c 源代码:

//a.out.h  早期的elf
struct exec {
  unsigned long a_magic;    /* Use macros N_MAGIC, etc for access */
  unsigned a_text;        /* length of text, in bytes */
  unsigned a_data;        /* length of data, in bytes */
  unsigned a_bss;        /* length of uninitialized data area for file, in bytes */
  unsigned a_syms;        /* length of symbol table data in file, in bytes */
  unsigned a_entry;        /* start address */
  unsigned a_trsize;        /* length of relocation info for text, in bytes */
  unsigned a_drsize;        /* length of relocation info for data, in bytes */
};
再看exec.c 的exec函数
int do_execve(unsigned long * eip,long tmp,char * filename,
    char ** argv, char ** envp)
{
    struct m_inode * inode;
    struct buffer_head * bh;
    struct exec ex;//elf 格式struct
    unsigned long page[MAX_ARG_PAGES];
    int i,argc,envc;
    int e_uid, e_gid;
    int retval;
    int sh_bang = 0;
    unsigned long p=PAGE_SIZE*MAX_ARG_PAGES-4;

    if ((0xffff & eip[1]) != 0x000f)
        panic("execve called from supervisor mode");
    for (i=0 ; i<MAX_ARG_PAGES ; i++)    /* clear page-table */
        page[i]=0;
    if (!(inode=namei(filename)))        /* get executables inode */
        return -ENOENT;
    argc = count(argv);
    envc = count(envp);
    
restart_interp:
    if (!S_ISREG(inode->i_mode)) {    /* must be regular file */
        retval = -EACCES;
        goto exec_error2;
    }
    i = inode->i_mode;
    e_uid = (i & S_ISUID) ? inode->i_uid : current->euid;
    e_gid = (i & S_ISGID) ? inode->i_gid : current->egid;
    if (current->euid == inode->i_uid)
        i >>= 6;
    else if (in_group_p(inode->i_gid))
        i >>= 3;
    if (!(i & 1) &&
        !((inode->i_mode & 0111) && suser())) {
        retval = -ENOEXEC;
        goto exec_error2;
    }
    if (!(bh = bread(inode->i_dev,inode->i_zone[0]))) {
        retval = -EACCES;
        goto exec_error2;
    }
    ex = *((struct exec *) bh->b_data);    /* read exec-header  读取elf 信息*/
    if ((bh->b_data[0] == '#') && (bh->b_data[1] == '!') && (!sh_bang)) {
        /*
         * This section does the #! interpretation.
         * Sorta complicated, but hopefully it will work.  -TYT
         */

        char buf[128], *cp, *interp, *i_name, *i_arg;
        unsigned long old_fs;

        strncpy(buf, bh->b_data+2, 127);
        brelse(bh);
        iput(inode);
        buf[127] = '\0';
        if (cp = strchr(buf, '\n')) {
            *cp = '\0';
            for (cp = buf; (*cp == ' ') || (*cp == '\t'); cp++);
        }
        if (!cp || *cp == '\0') {
            retval = -ENOEXEC; /* No interpreter name found */
            goto exec_error1;
        }
        interp = i_name = cp;
        i_arg = 0;
        for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++) {
             if (*cp == '/')
                i_name = cp+1;
        }
        if (*cp) {
            *cp++ = '\0';
            i_arg = cp;
        }
        /*
         * OK, we've parsed out the interpreter name and
         * (optional) argument.
         */
        if (sh_bang++ == 0) {
            p = copy_strings(envc, envp, page, p, 0);
            p = copy_strings(--argc, argv+1, page, p, 0);
        }
        /*
         * Splice in (1) the interpreter's name for argv[0]
         *           (2) (optional) argument to interpreter
         *           (3) filename of shell script
         *
         * This is done in reverse order, because of how the
         * user environment and arguments are stored.
         */
        p = copy_strings(1, &filename, page, p, 1);
        argc++;
        if (i_arg) {
            p = copy_strings(1, &i_arg, page, p, 2);
            argc++;
        }
        p = copy_strings(1, &i_name, page, p, 2);
        argc++;
        if (!p) {
            retval = -ENOMEM;
            goto exec_error1;
        }
        /*
         * OK, now restart the process with the interpreter's inode.
         */
        old_fs = get_fs();
        set_fs(get_ds());
        if (!(inode=namei(interp))) { /* get executables inode */
            set_fs(old_fs);
            retval = -ENOENT;
            goto exec_error1;
        }
        set_fs(old_fs);
        goto restart_interp;
    }
    brelse(bh);
        //代码段   数据段 bss段检测
    if (N_MAGIC(ex) != ZMAGIC || ex.a_trsize || ex.a_drsize ||
        ex.a_text+ex.a_data+ex.a_bss>0x3000000 ||
        inode->i_size < ex.a_text+ex.a_data+ex.a_syms+N_TXTOFF(ex)) {
        retval = -ENOEXEC;
        goto exec_error2;
    }
    if (N_TXTOFF(ex) != BLOCK_SIZE) {
        printk("%s: N_TXTOFF != BLOCK_SIZE. See a.out.h.", filename);
        retval = -ENOEXEC;
        goto exec_error2;
    }
    if (!sh_bang) {
        p = copy_strings(envc,envp,page,p,0);
        p = copy_strings(argc,argv,page,p,0);
        if (!p) {
            retval = -ENOMEM;
            goto exec_error2;
        }
    }
/* OK, This is the point of no return */
/* note that current->library stays unchanged by an exec */
    if (current->executable)
        iput(current->executable);
    current->executable = inode;
    current->signal = 0;
    for (i=0 ; i<32 ; i++) {
        current->sigaction[i].sa_mask = 0;
        current->sigaction[i].sa_flags = 0;
        if (current->sigaction[i].sa_handler != SIG_IGN)
            current->sigaction[i].sa_handler = NULL;
    }
    for (i=0 ; i<NR_OPEN ; i++)
        if ((current->close_on_exec>>i)&1)
            sys_close(i);
    current->close_on_exec = 0;
    free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
    free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
    if (last_task_used_math == current)
        last_task_used_math = NULL;
    current->used_math = 0;
    p += change_ldt(ex.a_text,page);
    p -= LIBRARY_SIZE + MAX_ARG_PAGES*PAGE_SIZE;
    p = (unsigned long) create_tables((char *)p,argc,envc);
        //加载各个段 
    current->brk = ex.a_bss +
        (current->end_data = ex.a_data +
        (current->end_code = ex.a_text));
    current->start_stack = p & 0xfffff000;
    current->suid = current->euid = e_uid;
    current->sgid = current->egid = e_gid;
    eip[0] = ex.a_entry;        /* eip, magic happens :-) */
    eip[3] = p;            /* stack pointer */
    return 0;
exec_error2:
    iput(inode);
exec_error1:
    for (i=0 ; i<MAX_ARG_PAGES ; i++)
        free_page(page[i]);
    return(retval);
}
View Code

目前的可执行文件加载要复杂的多, 但是基本原理差不多。

 

x86_64的ELF 结构如下

//ELF头信息
typedef struct
{
  unsigned char    e_ident[EI_NIDENT];    /* Magic number and other info */
  Elf64_Half    e_type;            /* Object file type */
  Elf64_Half    e_machine;        /* Architecture */
  Elf64_Word    e_version;        /* Object file version */
  Elf64_Addr    e_entry;        /* Entry point virtual address */
  Elf64_Off    e_phoff;        /* Program header table file offset */
  Elf64_Off    e_shoff;        /* Section header table file offset */
  Elf64_Word    e_flags;        /* Processor-specific flags */
  Elf64_Half    e_ehsize;        /* ELF header size in bytes */
  Elf64_Half    e_phentsize;        /* Program header table entry size */
  Elf64_Half    e_phnum;        /* Program header table entry count */
  Elf64_Half    e_shentsize;        /* Section header table entry size */
  Elf64_Half    e_shnum;        /* Section header table entry count */
  Elf64_Half    e_shstrndx;        /* Section header string table index */
} Elf64_Ehdr;

//section信息
typedef struct
{
  Elf64_Word    sh_name;        /* Section name (string tbl index) */
  Elf64_Word    sh_type;        /* Section type */
  Elf64_Xword    sh_flags;        /* Section flags */
  Elf64_Addr    sh_addr;        /* Section virtual addr at execution */
  Elf64_Off    sh_offset;        /* Section file offset */
  Elf64_Xword    sh_size;        /* Section size in bytes */
  Elf64_Word    sh_link;        /* Link to another section */
  Elf64_Word    sh_info;        /* Additional section information */
  Elf64_Xword    sh_addralign;        /* Section alignment */
  Elf64_Xword    sh_entsize;        /* Entry size if section holds table */
} Elf64_Shdr;

//符号表入口
typedef struct
{
  Elf64_Word    st_name;        /* Symbol name (string tbl index) */
  unsigned char    st_info;        /* Symbol type and binding */
  unsigned char st_other;        /* Symbol visibility */
  Elf64_Section    st_shndx;        /* Section index */
  Elf64_Addr    st_value;        /* Symbol value */
  Elf64_Xword    st_size;        /* Symbol size */
} Elf64_Sym;

//重定位信息
typedef struct
{
  Elf64_Addr    r_offset;        /* Address */
  Elf64_Xword    r_info;            /* Relocation type and symbol index */
} Elf64_Rel;
View Code

使用命令readelf -a main.o 查看elf信息得(x86_64平台):

ELF 头:
  Magic:  7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (可重定位文件)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  入口点地址:              0x0
  程序头起点:              0 (bytes into file)
  Start of section headers:          296 (bytes into file)
  标志:             0x0
  本头的大小:       64 (字节)
  程序头大小:       0 (字节)
  Number of program headers:         0
  节头大小:         64 (字节)
  节头数量:         12
  字符串表索引节头: 9

节头:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040   //已编译程序的机器代码  目标文件从0开始  偏移量就是段的开始地址,size 段大小  Entsize如果段中持有表 表的入口地址
       0000000000000015  0000000000000000  AX       0     0     4      Align 对齐方式
  [ 2] .rela.text        RELA             0000000000000000  00000548
       0000000000000018  0000000000000018          10     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000058      入口地址 00000058   长度8个字节  4字节对齐
       0000000000000008  0000000000000000  WA       0     0     4
  [ 4] .bss              NOBITS           0000000000000000  00000060            未初始化段 不占字节 4字节对齐
       0000000000000000  0000000000000000  WA       0     0     4
  [ 5] .comment          PROGBITS         0000000000000000  00000060
       000000000000002d  0000000000000001  MS       0     0     1
  [ 6] .note.GNU-stack   PROGBITS         0000000000000000  0000008d
       0000000000000000  0000000000000000           0     0     1
  [ 7] .eh_frame         PROGBITS         0000000000000000  00000090
       0000000000000038  0000000000000000   A       0     0     8
  [ 8] .rela.eh_frame    RELA             0000000000000000  00000560
       0000000000000018  0000000000000018          10     7     8
  [ 9] .shstrtab         STRTAB           0000000000000000  000000c8
       0000000000000059  0000000000000000           0     0     1
  [10] .symtab           SYMTAB           0000000000000000  00000428
       0000000000000108  0000000000000018          11     8     8
  [11] .strtab           STRTAB           0000000000000000  00000530   //符号表的符号串
       0000000000000016  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

There are no section groups in this file.

本文件中没有程序头。

重定位节 '.rela.text' 位于偏移量 0x548 含有 1 个条目:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000000a  000a00000002 R_X86_64_PC32     0000000000000000 swap - 4

重定位节 '.rela.eh_frame' 位于偏移量 0x560 含有 1 个条目:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000020  000200000002 R_X86_64_PC32     0000000000000000 .text + 0

The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported.

Symbol table '.symtab' contains 11 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    5
     8: 0000000000000000     8 OBJECT  GLOBAL DEFAULT    3 buf
     9: 0000000000000000    21 FUNC    GLOBAL DEFAULT    1 main
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND swap
由elf信息我们试着在main.o 中找到buf的值。hexdump命令

偏移量58 机器为小端  4 字节对齐    我们可以看到最后8个字节 即是buf 的1 和 2.

 详细说明一下符号表, 以main.o , swap.o为例:

  Num:    Value                            Size Type         Bind                Vis      Ndx Name

 

8: 0000000000000000     8 OBJECT  GLOBAL DEFAULT    3 buf     
9: 0000000000000000    21 FUNC    GLOBAL DEFAULT    1 main    
10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND swap

 

8: 0000000000000000     8 OBJECT  GLOBAL DEFAULT    3 bufp0
9: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND buf
10: 0000000000000008     8 OBJECT  GLOBAL DEFAULT  COM bufp1
11: 0000000000000000    59 FUNC    GLOBAL DEFAULT    1 swap

Num在8之前的是本地符号供链接器使用,如static 全局变量只在本地使用。我们可以看到函数内部的局部变量不会在符号表的, 它是在栈中分配。 这里的Value是指符号的地址,对于可重定位的模块来说, value 是距离定义目标的节的起始位置的偏移:对于引用自其他模块的变量这里value 为0,将来链接时进行重定位。对于可执行文件来说该值是一个绝对运行地址。 size 是目标的大小, type 通常是数据或者函数, ABS 代表不该被重定位的符号, undef代表未定义的符号, 而common表示还未被分配位置的未初始化的数据。

链接器符号解析和重定位:

连接器将每个引用与它输入的可重定位目标文件的符号表中的一个确定符号定义联系起来, 如main中的swap引用和 swap.c 中的swap函数的定义;  swap.c 中的buf引用和main.c 中buf的定义。对那些引用和定义都在同一模块中的本地符号的引用,符号解析是十分简单的。不过这里又牵涉到一个如何解析多处定义的全局符号问题,函数和已初始化的全局变量都是强符号,未初始化的全局变量是弱符号, 弱符号可能会造成很多的问题,造成程序中的隐藏bug。

 静态库解析引用

静态库其实就是多个目标文件的打包,在符号解析阶段, 链接器从左到右按照它们在gcc 或ld 命令出现的顺序扫描可重定位文件,在扫描过程中链接器维持一个可重定位目标文件的集合E, 这个集合中的模块会被合并起来形成可执行文件(表示最终形成可执行文件的入口 即main函数所在的模块);为引用了但是模块中没有定义的符号建立集合U; 为已经定义的符号建立集合D。

例如   gcc -static main.o liba.a libb.a: 

首先解析main.o 文件 发现是入口处, 就将它加入到集合E, 然后修改引用U 和定义D集合,对于main模块应该引用较多, 那么将引用其他模块的变量和函数加入到U集合  ------------->  接下来解析第二个存档文件lib.a。在lib.a中就会寻找匹配U中未解析符号但是在lib.a中定义的符号,例如,如果lib.a中定义了U中一个未定义的符号m, 就将m 加入到E中待将来组成可执行文件并修改UD集合。 对之后的所有文件都反复执行这个过程, 直到UD都不再变化 最后U 一定为空,否则不会形成可执行文件。这里指出的是目前gcc 默认选项的话会将所有定义模块加入到E中,不管main中是否用到了这个模块,gcc 编译和链接时都相关参数控制。-ffunction-sections, -fdata-sections会使compiler为每个function和data item分配独立的section。 --gc-sections会使ld删除没有被使用的section(具体没有试验过)。

静态库重定位:

经过了符号解析,它就把符号引用和符号定义联系起来,如 main.o 中swap的引用找到了swap.o 中swap的定义, swap符号定义被放到了E集合中,重定位就是对E集合中的引用和定义重新组成可执行文件。被定义的符号大小可以存原来的符号表读取,可执行文件也包含符号表,对E集合中的符号重新分配运行地址形成新的符号表。重定位包含两步: 1. 将所有类型相同的节拼接在一起,例如 来自输入的模块的.data 节全部合并成一个节, 这个节成为输出可执行目标文件的.data 节, 形成可执行文件并将地址修改为可运行地址---虚拟地址, 这时代码中的call  jmp 等调转指令的目标地址还不是确定地址所以需要第二步。2. 重定位节中的符号引用。链接器修改代码节和数据节中对每个符号的引用, 使得它们指向正确的运行时地址。此时链接器依赖于重定位表目。例如main.o 中的swap表目:

重定位节 '.rela.text' 位于偏移量 0x548 含有 1 个条目:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000000a  000a00000002 R_X86_64_PC32     0000000000000000 swap - 4

offset是指相对可重定位text 或data段的偏移量,当汇编器生成一个目标模块时, 它并不知道数据和代码最终将存放在存储器中的什么位置, 它也不知道这个模块引用其他模块的函数或者全局变量的位置。所以就生成了一个重定位表 告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。这个重定位表就告诉在链接器需要重定位的地方。如图:

这里将最后生成的可执行文件反编译objdump -S main

00000000004004f0 <main>:
  4004f0:    55                       push   %rbp
  4004f1:    48 89 e5                 mov    %rsp,%rbp
  4004f4:    b8 00 00 00 00           mov    $0x0,%eax
  4004f9:    e8 0a 00 00 00           callq  400508 <swap>
  4004fe:    b8 00 00 00 00           mov    $0x0,%eax
  400503:    5d                       pop    %rbp
  400504:    c3                       retq   
  400505:    0f 1f 00                 nopl   (%rax)

0000000000400508 <swap>:
  400508:    55                       push   %rbp
  400509:    48 89 e5                 mov    %rsp,%rbp
  40050c:    48 c7 05 31 0b 20 00     movq   $0x601030,0x200b31(%rip)        # 601048 <bufp1>
  400513:    30 10 60 00 
  400517:    48 8b 05 1a 0b 20 00     mov    0x200b1a(%rip),%rax        # 601038 <bufp0>
  40051e:    8b 00                    mov    (%rax),%eax
  400520:    89 45 fc                 mov    %eax,-0x4(%rbp)
  400523:    48 8b 05 0e 0b 20 00     mov    0x200b0e(%rip),%rax        # 601038 <bufp0>
  40052a:    48 8b 15 17 0b 20 00     mov    0x200b17(%rip),%rdx        # 601048 <bufp1>
  400531:    8b 12                    mov    (%rdx),%edx
  400533:    89 10                    mov    %edx,(%rax)
  400535:    48 8b 05 0c 0b 20 00     mov    0x200b0c(%rip),%rax        # 601048 <bufp1>
  40053c:    8b 55 fc                 mov    -0x4(%rbp),%edx
  40053f:    89 10                    mov    %edx,(%rax)
  400541:    5d                       pop    %rbp
  400542:    c3                       retq   
  400543:    66 2e 0f 1f 84 00 00     nopw   %cs:0x0(%rax,%rax,1)
  40054a:    00 00 00 
  40054d:    0f 1f 00                 nopl   (%rax)

再比对一下main.o的反汇编

0000000000000000 <main>:
   0:    55                       push   %rbp
   1:    48 89 e5                 mov    %rsp,%rbp
   4:    b8 00 00 00 00           mov    $0x0,%eax
   9:    e8 00 00 00 00           callq  e <main+0xe>
   e:    b8 00 00 00 00           mov    $0x0,%eax
  13:    5d                       pop    %rbp
  14:    c3                       retq   

call 指令是一个跳转指令 e8 为call 指令。 这里可以看到main.o 中指令callq swap  字节码e8 00 00 00 00 这里没有swap的定义,所以这边跳转偏移量为0,重定位即是把它赋予一个真正的值。更改跳转指令偏移量和引用全局变量地址是生成运行地址之后才做的,由之前目标文件的各个符号, 段的长度,和重定位符号表,生成运行地址后就不难进行重定位。

我们可以看到main 可执行文件中 call 之后相对偏移量已经变为a值。 4004f9: e8 0a 00 00 00 callq 400508 <swap>   后面的a表示跳转位移量 即跳转到的地址是PC + a   PC(CS:IP) 是call指令的下一条指令地址即 4004fe。  4004fe + a = 400508 正是swap 函数的地址 (相对寻址修正)。 对于全局变量则是绝对地址修正,将数据引用变成数据段相应变量的绝对地址。

以上就是静态库符号解析和重定位过程,从上述过程来说整个过程并不复杂。只是这里我们可以看到链接过程中拼接和重定位,把所有模块都拼接到一个可执行文件中,那么这个可执行文件的size就会变的很大,当模块数量及复杂度再提升时那可想而知,而且带来的令一个问题是,一旦一个模块中有一个很小的改动 那么最后都要重新拼接重定位,效率会变得很低。那么这就是动态库引入的原因。

 

      

posted on 2017-11-16 14:43  沐浴凌风  阅读(1691)  评论(0编辑  收藏  举报

导航