GNU linker的undefined reference问题
一个链接的小实验
假设有两个源文件,一个main.c, 一个helloworld.c,两个文件中分别定义了:
- main.c中定义了main函数,调用了helloworld.c中的hello 函数
int main()
{
hello();
return 0;
}
- helloworld.c中定义了hello 和world两个函数,但是world函数中调用了一个未定义的函数ufunction();
#include <stdio.h>
void hello()
{
printf("hello()"\n);
}
void world()
{
ufunction();
printf("world()\n");
}
- 现在对这两个文件进行编译链接
gcc -o main.o -c main.c
gcc -o helloworld.o -c helloworld.c
gcc -o main main.o helloworld.o
- 上面的编译链接结果是,执行第三条命令时,一定会报undefined reference symbol ufuntion错误。
- 这个小实验说明,链接的时候,会解析整个.o文件中的符号,如果找不到该符号的定义,则会报undefined reference错误。
使用静态链接库(.a)文件时的链接逻辑
一个静态链接库中,通常存在多个.o文件,链接时并不是将.a文件中的所有.o文件都进行链接,而是按需进行,这里有一个简单的问题是什么交按需?笔者的实验结论是:如果需要.o中的某个符号(可能是函数也可能是全局变量),那么就需要这个.o文件。下面同样通过一个简单的小实验来验证这个结论。
- 定义三个源文件,main.c和hello.c以及world.c,其中hello.c和world.c用来生成一个静态链接库libme.a,为了说明我们的结论,做两个小实验。
- main.c中仍然定义如下
extern void hello();
int main()
{
hello();
return 0;
}
- 第一个小实验的libme.a我们这样设计,称为libmev1.a,hello.c和world.c定义如下
//以下是hello.c中的定义
#include <stdio.h>
int globalcnt = 1000;
void hello()
{
printf("hello %d\n",globalcnt++);
}
//以下是world.c中的定义
#include <stdio.h>
extern int globalcnt;
extern void ufunction();
void world()
{
ufunction();
printf("world %d\n",globalcnt++);
}
/* 实用如下命令生成libmev1.a
* gcc -o hello.o -c hello.c
* gcc -o world.o -c world.c
* ar -rcs libmev1.a hello.o world.o
*/
- 第二个小实验的libme.a的实现,仅仅是将globalcnt的定义位置从hello.c中移动到world.c中,称为libmev2.a,hello.c和world.c定义如下
//以下是hello.c中的定义
#include <stdio.h>
extern int globalcnt;
void hello()
{
printf("hello %d\n",globalcnt++);
}
//以下是world.c中的定义
#include <stdio.h>
int globalcnt = 1000;
extern void ufunction();
void world()
{
ufunction();
printf("world %d\n",globalcnt++);
}
/* 实用如下命令生成libmev2.a
* gcc -o hello.o -c hello.c
* gcc -o world.o -c world.c
* ar -rcs libmev1.a hello.o world.o
*/
- 首先生成libmev1.a和libmev2.a,过程如下
- 下面通过链接来验证之前的结论,使用libmev2.a必然会产生undefined reference symbol ufunction错误,而使用libmev1.a则可以正确链接。实验结果如下:
通过map文件验证链接过程
上面的两个小实验说明,所需要的符号所在的.o是链接所必须的,而链接就会解析.o中的所有符号,在解析这些符号的过程时,可能引入新的依赖.o文件。就上面的小实验,我们尝试说明一下对libmev1和libmev2的链接过程。
- 对 libmev1.a的链接过程
mian.o中需要符号hello,符号hello在hello.o文件中,hello.o中不存在其依赖于其它.o的符号,因此只链接hello.o一个文件。我们使用Wl,-Map参数生成map文件来检验,显然完全符合推导过程:
- 对libmev2.a的链接过程
main.o中需要符号hello,符号hello在hello.o文件中,hello.o中需要符号globalcnt,符号globalcnt在world.o中,因此需要链接hello.o和world.o两个文件。由于直接链接libmev2.a会失败,而无法产生map文件,在main.c中定义ufunction符号来满足链接需要(这并不影响链接过程)。
extern void hello();
void ufunction()
{
}
int main()
{
hello();
return 0;
}
从结果图可以看到,链接hello.o是因为main.o中需要hello,而链接world.o是因为hello.o中需要globalcnt.
对.a文件的链接逻辑的结论
通过上面的两组小实验,可以得到gnu linker的链接逻辑如下:
- 以第一个.o文件为基准,查找所需要的符号;
- 从.a文件中找到所需符号所在的.o,将这个.o中的所有符号进行解析(无论是否被引用)
- 将所有的这样的.o全部找出,进行链接。
一个潜在的undefined reference问题及其解决方法
基于这样的链接逻辑,会导致一个奇怪的问题,即我们自己的代码中明明没有引用某些符号,而使用静态链接库时,总是报错说存在未定义的符号引用(如上面的libmev2.a)。这是由于一个.a文件中定义了多个符号,而这些符号并不一定是我们都需要的,且都实现了的(详见上面的libmev2.a)。这种问题往往不容易发现,遇到奇怪的未定义问题时,可以往这方面思考。
为了解决这一问题,有几种方法可以使用:
- 将报错的undefined reference符号手动定义,但是这些符号往往时lib.a中的某个.o需要的,我们并不一定知道该如何定义;
- 如果报错的符号是在其它lib.a中,那么还要链接所需的.a文件。
- 如果确定自己的代码中一定不会引用到报错的符号,可以使用链接参数-Wl,–gc-sections,这会告诉链接器,在链接过程中不要去解析未引用的符号。
- 对于lib.a的提供者,可以采用一个源文件中只定义一个函数,全局变量不要和函数定义放在一起。
重要补充
- 连接过程并不是以整个.o文件为单位,而是以.o中的section为单位,即section中的任何一个符号被需要,则整个section都会被链接。
- -Wl,–gc-sections移除的是.o文件中没有被用到的section. 如果不加此选项,gnu linker也会移除一些明确不用的section.
- 生成的map文件中的“Archive member included to satisfy reference by file (symbol)” 给出了链接库中的.o的一个原因,并不是全部原因,也就是说,如果引用.o中的多个符号也只给出一条。
- 可以使用-ffunction-sections将文件中的所有函数都独立放入一个section.
- 可以使用-fdata-sections将文件中的所有全局或静态变量都独立放入一个section.