测试1:静态链接

a.c
#include <stdio.h>

void test(void) {
    printf("This is a\n");
}
b.c
#include <stdio.h>

void test(void) {
    printf("This is b\n");
}
main.c
#include <stdio.h>

extern void test(void);

int main() {
    test();
    return 0;
}
Makefile
all:
	@echo "Please specify a target, try: make [ab | ba]"
	
ab: main.o liba.a libb.a
	gcc main.o -L. -la -lb

ba: main.o liba.a libb.a
	gcc main.o -L. -lb -la

main.o: main.c 
	gcc main.c -c

liba.a: a.c
	gcc a.c -c
	ar -cr liba.a a.o

libb.a: b.c 
	gcc b.c -c
	ar -cr libb.a b.o 

clean:
	rm -fr *.o *.a *.so a.out

上面的代码执行结果如下:

$ make ab
gcc main.c -c
gcc a.c -c
ar -cr liba.a a.o
gcc b.c -c
ar -cr libb.a b.o
gcc main.o -L. -la -lb
$ ./a.out
This is a
$ make ba
gcc main.o -L. -lb -la
$ ./a.out
This is b

解释如下:

链接过程中从左向右扫描目标文件和库中的符号,并维护一个Undefined符号表。在遇到main.o时,由于test函数未定义,Undefined符号表中记录下test。接下来,如果先链接liba.a,则由liba.a提供test函数的实现,并且由于链接完liba.a后已经没有未解决的符号了,后面的libb.a不会再扫描,链接过程结束,main函数中的test函数为a.c中的实现。当链接顺序反过来时,则只会链接libb.a。

测试2:静态链接-符号冲突

a.c
#include <stdio.h>

int a = 1;

void test(void) {
    printf("This is a\n");
}
b.c
#include <stdio.h>

int b = 1;

void test(void) {
    printf("This is b\n");
}
main.c
#include <stdio.h>

extern void test(void);
extern int a;
extern ina b;

int main() {
    test();
    printf("a=%d, b=%d\n", a, b);
    return 0;
}
Makefile
all:
	@echo "Please specify a target, try: make [ab | ba]"
	
ab: main.o liba.a libb.a
	gcc main.o -L. -la -lb

ba: main.o liba.a libb.a
	gcc main.o -L. -lb -la

main.o: main.c 
	gcc main.c -c

liba.a: a.c
	gcc a.c -c
	ar -cr liba.a a.o

libb.a: b.c 
	gcc b.c -c
	ar -cr libb.a b.o 

clean:
	rm -fr *.o *.a *.so a.out

执行结果:

$ make ab
gcc main.c -c
gcc a.c -c
ar -cr liba.a a.o
gcc b.c -c
ar -cr libb.a b.o
gcc main.o -L. -la -lb
/usr/bin/ld: ./libb.a(b.o): in function `test':
b.c:(.text+0x0): multiple definition of `test'; ./liba.a(a.o):a.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
make: *** [Makefile:5: ab] Error 1
$ make ba
gcc main.o -L. -lb -la
/usr/bin/ld: ./liba.a(a.o): in function `test':
a.c:(.text+0x0): multiple definition of `test'; ./libb.a(b.o):b.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
make: *** [Makefile:8: ba] Error 1

解释如下:

链接过程同样是从左向右扫描目标文件和库中的符号,并维护Undefined符号表。在遇到main.o时,由于test函数和外部变量a、b都未定义,Undefined符号表中记录下这三项。接下来,如果先链接liba.a,则由liba.a提供test函数和变量a的定义,但此时还有变量b未解决,所以会继续链接后面的库。接下来是链接libb.a,libb.a中有变量b的定义,但还有一个test函数的定义,由于静态链接中没有全局符号介入问题,并且两个test函数的符号优先级相同,所以报重复定义错误。不管是先链接liba.a还是先链接libb.a都存在同样的问题。

测试3:静态链接-链接顺序影响链接结果

a.c
#include <stdio.h>

void testa(void) {
    printf("This is a\n");
}
b.c
#include <stdio.h>

extern void testa(void);

void testb(void) {
    testa();
}
main.c
#include <stdio.h>

extern void testb(void);

int main() {
    testb();
    return 0;
}
Makefile
all:
	@echo "Please specify a target, try: make [ab | ba]"
	
ab: main.o liba.a libb.a
	gcc main.o -L. -la -lb

ba: main.o liba.a libb.a
	gcc main.o -L. -lb -la

main.o: main.c 
	gcc main.c -c

liba.a: a.c
	gcc a.c -c
	ar -cr liba.a a.o

libb.a: b.c 
	gcc b.c -c
	ar -cr libb.a b.o 

clean:
	rm -fr *.o *.a *.so a.out

执行结果:

$ make ab
gcc main.c -c
gcc a.c -c
ar -cr liba.a a.o
gcc b.c -c
ar -cr libb.a b.o
gcc main.o -L. -la -lb
/usr/bin/ld: ./libb.a(b.o): in function `testb':
b.c:(.text+0x9): undefined reference to `testa'
collect2: error: ld returned 1 exit status
make: *** [Makefile:5: ab] Error 1
$ make ba
gcc main.o -L. -lb -la
$ ./a.out
This is a

解释如下:

链接过程同样是从左向右扫描目标文件和库中的符号,并维护Undefined符号表。在遇到main.o时,由于testb函数未定义,Undefined符号表中记录下testb。接下来,如果先链接liba.a,由于liba.a中并没有提供testb函数,而此时链接器还没遇到libb.a,不知道libb.a需testa函数,所以链接器认为liba.a对整个链接过程没作用,直接抛弃掉了。接下来遇到libb.a,libb.a提供了testb函数的实现,但需要testa函数的实现,而前面的liba.a已经被抛弃了,所以报未定义错误。

以上过程如果反过来,先链接libb.a,再链接liba.a,则不会有问题。

总结起来就是,链接器链接时如果发现一个库对当前的链接没有作用,那就会跳过这个库,不管后续的库是否对这个库有依赖。

测试4:动态链接

a.c
#include <stdio.h>

void test(void) {
    printf("This is a\n");
}
b.c
#include <stdio.h>

void test(void) {
    printf("This is b\n");
}
main.c
#include <stdio.h>

extern void test(void);

int main() {
    test();
    return 0;
}
Makefile
all:
	@echo "Please specify a target, try: make [ab | ba]"

ab: main.o liba.so libb.so
	gcc main.o -L. -la -lb -Wl,-rpath=.

ba: main.o liba.so libb.so
	gcc main.o -L. -lb -la -Wl,-rpath=.

main.o: main.c 
	gcc main.c -c

liba.so: a.c
	gcc -fPIC -shared a.c -o liba.so

libb.so: b.c 
	gcc -fPIC -shared b.c -o libb.so

clean:
	rm -fr *.o *.a *.so a.out

执行结果:

$ make ab
gcc main.c -c
gcc -fPIC -shared a.c -o liba.so
gcc -fPIC -shared b.c -o libb.so
gcc main.o -L. -la -lb -Wl,-rpath=.
$ ./a.out
This is a
$ make ba
gcc main.o -L. -lb -la -Wl,-rpath=.
$ ./a.out
This is b

结果解释:

同静态库,liba.so和libb.so只要链接一个就可以解决所有的符号冲突,整个链接过程就结束了,所以先链接哪个就用哪个的实现。

通过ldd命令查看生成的可执行文件中依赖的动态库也可以验证上面的结论,如下:

$ make ab
gcc main.o -L. -la -lb -Wl,-rpath=.
$ ldd a.out
        linux-vdso.so.1 (0x00007ffc593a0000)
        liba.so => ./liba.so (0x00007fa175d70000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa175b76000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fa175d7c000)
$ make ba
gcc main.o -L. -lb -la -Wl,-rpath=.
$ ldd a.out
        linux-vdso.so.1 (0x00007ffd41d2d000)
        libb.so => ./libb.so (0x00007fe5505b3000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe5503b9000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fe5505bf000)

可以看到,生成的可执行文件只会依赖liba.so或libb.so中的一个。

测试5:动态链接-符号冲突

a.c
#include <stdio.h>

int a = 1;

void test(void) {
    printf("This is a\n");
}
b.c
#include <stdio.h>

int b = 2;

void test(void) {
    printf("This is b\n");
}
main.c
#include <stdio.h>

extern void test(void);
extern int a;
extern int b;

int main() {
    test();
    printf("a=%d, b=%d\n", a, b);
    return 0;
}
Makefile
all:
	@echo "Please specify a target, try: make [ab | ba]"

ab: main.o liba.so libb.so
	gcc main.o -L. -la -lb -Wl,-rpath=.

ba: main.o liba.so libb.so
	gcc main.o -L. -lb -la -Wl,-rpath=.

main.o: main.c 
	gcc main.c -c

liba.so: a.c
	gcc -fPIC -shared a.c -o liba.so

libb.so: b.c 
	gcc -fPIC -shared b.c -o libb.so

clean:
	rm -fr *.o *.a *.so a.out

执行结果:

$ make ab
gcc main.c -c
gcc -fPIC -shared a.c -o liba.so
gcc -fPIC -shared b.c -o libb.so
gcc main.o -L. -la -lb -Wl,-rpath=.
$ ./a.out
This is a
a=1, b=2
$ make ba
gcc main.o -L. -lb -la -Wl,-rpath=.
$ ./a.out
This is b
a=1, b=2

解释如下:

对比静态库的版本,动态库的版本可以链接成功,这是因为动态库存在全局符号介入问题。动态链接器在加载动态库时,会维护一份所有共享对象的全局符号表,当一个符号需要被加入全局符号表时,如果相同的符号名已经存在,则后加入的符号将被忽略。

测试5:动态链接-链接顺序影响链接结果

a.c
#include <stdio.h>

void testa(void) {
    printf("This is a\n");
}
b.c
#include <stdio.h>

extern void testa(void);

void testb(void) {
    testa();
}
main.c
#include <stdio.h>

extern void testb(void);

int main() {
    testb();
    return 0;
}
Makefile
all:
	@echo "Please specify a target, try: make [ab | ba]"
	
ab: main.o liba.a libb.a
	gcc main.o -L. -la -lb

ba: main.o liba.a libb.a
	gcc main.o -L. -lb -la

main.o: main.c 
	gcc main.c -c

liba.a: a.c
	gcc a.c -c
	ar -cr liba.a a.o

libb.a: b.c 
	gcc b.c -c
	ar -cr libb.a b.o 

clean:
	rm -fr *.o *.a *.so a.out

执行结果:

$ make ab
gcc main.c -c
gcc -fPIC -shared a.c -o liba.so
gcc -fPIC -shared b.c -o libb.so
gcc main.o -L. -la -lb -Wl,-rpath=.
/usr/bin/ld: ./libb.so: undefined reference to `testa'
collect2: error: ld returned 1 exit status
make: *** [Makefile:5: ab] Error 1
$ make ba
gcc main.o -L. -lb -la -Wl,-rpath=.
$ ./a.out
This is a

与静态库的规则一样,链接过程会抛弃掉链接器认为没用的动态库。

测试7:动态链接-一种消除链接顺序影响的办法

a.c
#include <stdio.h>

void testa(void) {
    printf("This is a\n");
}
b.c
#include <stdio.h>

extern void testa(void);

void testb(void) {
    testa();
}
main.c
#include <stdio.h>

extern void testb(void);

int main() {
    testb();
    return 0;
}
Makefile
all:
	@echo "Please specify a target, try: make [ab | ba]"
	
ab: main.o liba.so libb.so
	gcc main.o -L. -la -lb -Wl,-rpath=.

ba: main.o liba.so libb.so
	gcc main.o -L. -lb -la -Wl,-rpath=.

main.o: main.c 
	gcc main.c -c

liba.so: a.c
	gcc -fPIC -shared a.c -o liba.so

libb.so: b.c liba.so
	gcc -fPIC -shared b.c liba.so -o libb.so -Wl,-rpath=.

clean:
	rm -fr *.o *.a *.so a.out

这种方式下,无论是make ab还是make ba,都可以链接通过。这里的关键是在生成libb.so时,指明libb.so依赖于liba.so,所以在即使前面liba.so被抛弃了,在加载libb.so时,仍然会把liba.so重新加载进来。

测试8:静态链接中的环形引用问题

参考:https://stackoverflow.com/questions/9380363/resolving-circular-dependencies-by-linking-the-same-library-twice

环形引用,指A依赖B,B依赖A的问题,以下是一个示例:

a.c
int a = 1;
extern int b;

int testa(void) {
    return b;
}
b.c
int b = 2;
extern int a;

int testb(void) {
    return a;
}
main.c
#include <stdio.h>

extern int testa();

int main() {
    printf("%d\n", testa());
    return 0;
}
Makefile
all:
	@echo "Please specify a target, try: make [ab | ba]"
	
ab: main.o liba.a libb.a
	gcc main.o -L. -la -lb

ba: main.o liba.a libb.a
	gcc main.o -L. -lb -la

main.o: main.c 
	gcc main.c -c

liba.a: a.c
	gcc a.c -c
	ar -cr liba.a a.o

libb.a: b.c 
	gcc b.c -c
	ar -cr libb.a b.o 

clean:
	rm -fr *.o *.a *.so a.out

上面的示例中,make ab可以编译通过,make ba则不行,按之前几个示例的分析结论可以很轻易的得出原因。这里再介绍一种新的方法,这种方法使用--start-group--end-group 两个选项来解决环形引用问题,如下:

gcc main.o -Wl,--start-group -L. -lb -la -Wl,--end-group

关于 --start-group和--end-group的描述可参考man ld,如下:

--start-group archives --end-group
           The archives should be a list of archive files.  They may be either explicit file names, or -l options.

           The specified archives are searched repeatedly until no new undefined references are created.  Normally, an archive is searched only once in
           the order that it is specified on the command line.  If a symbol in that archive is needed to resolve an undefined symbol referred to by an
           object in an archive that appears later on the command line, the linker would not be able to resolve that reference.  By grouping the
           archives, they will all be searched repeatedly until all possible references are resolved.

           Using this option has a significant performance cost.  It is best to use it only when there are unavoidable circular references between two
           or more archives.

简单来说,被--start-group和--end-group包括的库会被搜索多次,而不是只搜索一次,直到所有的未定义符号都被解决,对应的代价就是链接速度会显著降低。

一些结论

1. 无论是动态链接还是静态链接,链接过程都是从左向右扫描库文件。

2. 从左向右扫描过程中,如果发现一个同优先级的符号出现了两次,那么在静态链接中,会报重复定义错误,如果是动态链接,则会以第一次加载的符号为准,这是由于全局符号介入的影响。

3. 无论是动态链接还是静态链接,在从左向右扫描库文件过程,如果发现扫描到某个库时所有的未定义符号都解决了,那后续的库就不会再扫描了。这说明在链接时指定一大堆多余的库对链接结果没有影响。

4. 动态库的全局符号介入问题,全局符号表只会记录第一次识别到的符号,后续的同名符号都被忽略,但这并不表示同名符号所在的动态库完全不会加载,因为有可能其他的符号会用到。以libc库举例,如果用户在链接libc库之前链接了一个指定的库,并且在这个库里实现了read/write接口,那么在程序运行时,程序调用的read/write接口就是指定库里的,而不是libc库里的。libc库仍然会被加载,因为libc库是程序的运行时库,程序不可能不依赖libc里的其他接口。因为libc库也被加载了,所以,通过一定的手段,仍然可以从libc中拿到属于libc的read/write接口,这就为hook创建了条件。程序可以定义自己的read/write接口,在接口内部先实现一些相关的操作,然后再调用libc里的read/write接口。

























  • 无标签