2015-04-14
关于链接器的笔记

链接时的符号冲突解决

  1. 不能允许多个强符号出现
  2. 一个强符号,多个弱符号,选强符号。
  3. 多个弱符号,则任选一个.

链接器视未初始化的符号为弱符号。

为了避免掉进规则2与规则3的坑里,强烈建议:

在生成elf后,被extern修饰的变量对应 UNDEF symobl, 而初始化的变量对应COMMON symbol.

Elf

分三种类型: 可重定向的(relocatable),可执行的(executable),动态库(shared).

内容大致分为以下几个区:

  1. header 存储该obj file的类型(relocatable, executable or shared),目标机器类型等

  2. .text 代码段

  3. .rodata Read-only data,只读数据段(比如常量string, const static 等)

  4. .data 已初始化的数据段

  5. .bss 未初始化的数据段。因为默认初始化为0指,所以这个区为空,只在运行期时赋值.

  6. .symtab 符号表,每个条目记录该符号的 段类型,大小,符号类型,名字(名字字符串存储在strtab段里,这里实际只记录在strtab段的offset)。如下所示:

  7. .rel.text 代码段里需要重定向的条目. 可执行文件此行为空.

  8. .rel.data 数据段里需要重定向的条目. 可执行文件此行为空

  9. .debug debug信息

  10. .line 代码行号与机器码的对应,debug信息。

  11. .strtab 存储常量字符串。 如.symtab里符号名字,.debug里的字符串,header里的字符串等。

  12. .dynamic 存储动态链接信息,如需要动态链接的库的名字。只有需要动态链接的文件才有此段. 例如:

    $ readelf -d /bin/bash Dynamic section at offset 0xc1ef0 contains 29 entries: 标记 类型 名称/值 0x00000001 (NEEDED) 共享库:[libtinfo.so.5] 0x00000001 (NEEDED) 共享库:[libdl.so.2] 0x00000001 (NEEDED) 共享库:[libgcc_s.so.1])))

Elf上的符号信息

Elf是*nix上可执行文件的标准格式。

elf只记录全局变量的名字、大小、位置信息,存储区(section).

read -s OBJFILE查看objfile的符号信息.例如(main.c见后面):

 $ gcc -c main.c
 $ readelf -s main.o
   Symbol table '.symtab' contains 12 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    5 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    9 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     9: 0000000000000000     8 OBJECT  GLOBAL DEFAULT    3 a
    10: 0000000000000000    21 FUNC    GLOBAL DEFAULT    1 main
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND foo

Ndx表示section. 1表示.text, 3表示.data。同时还有几个特殊的值:

elf丢掉了c的类型信息(只简单区别了函数、变量。详细信息见man elf). 丢掉了函数的参数信息.

见下面两个文件

main.c:

char *a = "hello";

int
main() {
    foo();
}

foo.c:

float* a;

void
foo() {
   return *a + 1;
}

是可以编译过的 gcc mani.c foo.c.

如果把 float *a 改成 char a, gcc会报个警告:

/usr/bin/ld: Warning: size of symbol `a’ changed from 1 in /tmp/ccpBZMHR.o to 8 in /tmp/cctBRE74.o

因为elf只记录了符号的大小.

动态链接的搜索路径

在编译期链接时,elf要在特定的路径里搜索库。 这些路径是编译期的搜索路径. 在命令行中可以通过-L PATH添加.

在加载期链接时(动态链接), elf也要在特定的路径里搜索共享库。这些路径是加载期的搜索路径。 在命令行中通过 -Wl,-rpath PATH 添加(-Wl, 表示把选项交给了linker).

所以如果你要动态链接一个位于非默认搜索路径的动态库,你需要同时在这两类路径中指定它:

gcc -L PATH -Wl,-rpath PATH foo.c -lmy

通过 -Wl,-rpath 增加的路径会记在elf文件的.dynamic节里的RPATH字段.

objdump -x OBJFILE | grep RPATH

用命令 ld --verbose | grep SEARCH_DIR 可以查看ld的默认搜索路径

与库的链接

链接时,当连接器遇到一个链接库,会在库里的所有文件中搜寻它要解决的符号,然后把不包含这些符号的文件舍弃掉(减少了最终目标文件的体积.)

因此在排列传递给连接器的文件顺序时,要按照链接库被依赖的顺序,从右到左排列.

比如 foo.c 依赖 liba.o , liba.o 依赖 libb.o, 则应该:

gcc -o out foo.c -la -lb

注意: 与库链接时并不解决多个强符号的问题。 比如 foo.c里依赖符号foo, 而libalibb都提供该符号。 那么按照上面参数的顺序, liba中的符号将被绑定,而到了libb中, foo已经不再是需要解决的符号。

举例:

main.c:

#include <stdio.h>

int
main() {
    printf("%d\n", add(3, 4));
    printf("multi: %d\n", multi(3, 4));
}

add.c:

int add(int a, int b) {
    return a + b;
}

addwrong.c:

int add(int a, int b) {
    return a * b;
}

int multi(int a, int b) {
    return a * b;
}

main.c 依赖addmulti两个符号,add.c提供了一个正确的add实现, addwrong.c提供了一个错误的add和一个正确的multi.

执行:

$ gcc -shared -fpic addwrong.c -o libaddwrong.so
$ gcc -shared -fpic add.c -o libadd.so

调整libadd.solibaddwrong.so的参数顺序,会得到不同的答案。

$ gcc -Wl,-rpath . main.c libadd.so  libaddwrong.so ;./a.out
add: 7
multi: 12

$ gcc -Wl,-rpath . main.c libaddwrong.so libadd.so   ;./a.out
add: 12
multi: 12

ldd <Share Lib> 可以查看该so文件链接了什么库.

运行期动态链接

*nix 上可以用 dlopen动态链接一个库。

系统通过引用计数来维护该库在内存里的生命期。 当没有被引用时,库会被回收。 看下面这个例子:

add1.c:

int myvar = 2;

void
add1() {
    myvar++;
}

main.c:

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

typedef void (*func_add1)() ;

void *l;

void
print_vm() {
    printf(">> :\n");
    char buf[2048];
    FILE *f = fopen("/proc/self/maps", "r");
    while (fgets(buf, sizeof(buf), f)) {
        if (strstr(buf, "libadd1.so"))
            fputs(buf, stdout);
    }
    fclose(f);
}

void
print_myvar() {
    int *myvar = dlsym(l, "myvar");
    printf(">>>> myvar:%d\n", *myvar);
}

int 
main() {
    l = dlopen("./libadd1.so", RTLD_NOW);
    func_add1 f = dlsym(l, "add1");
    f();

    print_vm();
    print_myvar();

    dlclose(l);
    
    print_vm();

    l = dlopen("./libadd1.so", RTLD_NOW);
    print_myvar();
    
}

执行:

gcc -shared -fPic add1.c -o libadd1.so
gcc -g -L. main.c -ldl -o a
./a

得到

>> vm:
7f4ed558c000-7f4ed558d000 r-xp 00000000 08:01 5901060                    /home/hqwrong/code/c/libadd1.so
7f4ed558d000-7f4ed578c000 ---p 00001000 08:01 5901060                    /home/hqwrong/code/c/libadd1.so
7f4ed578c000-7f4ed578d000 rw-p 00000000 08:01 5901060                    /home/hqwrong/code/c/libadd1.so
>>>> myvar:3
>> vm:
>>>> myvar:2

Position Independent Code(PIC)

pic代码, 即可以加载到任何地址而不需要重定位就可以运行的代码。它在.data段生成一个table保存该模块所有全局函数的绝对地址,该table被称为GOT(Global Offset Table).

索引全局函数的代码就变为间接索引GOT的一个条目, 这样加载该模块时只需重定位.data里的GOT,代码段可以保持不被更改;因此代码可以被多个程序共享. 所以在编译共享库时,-fPic是必须的。

$ gcc -shared add1.c
/usr/bin/ld: /tmp/ccosMoHf.o: relocation R_X86_64_PC32 against symbol `myvar' can not be used when making a shared object; recompile with -fPIC
/usr/bin/ld: final link failed: 错误的值
collect2: 错误:ld 返回 1

lazyrobot.me