问题发现
背景
在学习TCP/IP socket编程的时候,照着书上的例子想照猫画虎写一个简易FTP程序,由于之前还有好几章的示例,每个程序都使用了一些相同的工具函数以及头文件和宏定义等,所以就想着希望能将相同的内容塞到一个头文件里面,其他示例文件include使用。下图左边红框是所有示例源文件,右边红框是所有源文件基本都会重复的部分。
于是我把这部分内容都扔到了 common.h 头文件里面:
#ifndef COMMON_H
#define COMMON_H
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
typedef struct sockaddr_in sockaddr_in;
typedef struct sockaddr sockaddr;
#define true 1
#define false 0
#define nullptr NULL
const int BUF_SIZE = 256;
#define INFO 0
#define GETF 1
#define PUTF 2
#define CDIR 3
#define ERRO -1
void error_handling(char* msg);
#endif
我还很聪明地运用了从 《Effective C++》 中学到的,尽量把宏定义使用常量定义替换掉这条 “建议”,虽然我并不知道在这么一个小示例里面做有什么用,但是跟着 dalao 的做法做总让我以为可以避免一些坑和弯路,实际上就是这行宏定义替换导致了 多重定义 的错误。
const int BUF_SIZE = 256; // 这里应该写宏定义才对 #define BUF_SIZE 256
错误描述
-- Configuring done
-- Generating done
-- Build files have been written to: /home/fredom/WorkSpace/cppPlayground/socket_programming/chapter1/build
Consolidate compiler generated dependencies of target mysrc
[ 6%] Building C object src/CMakeFiles/mysrc.dir/common.c.o
[ 12%] Linking C static library ../../runtimelib/libmysrc.a
[ 12%] Built target mysrc
Consolidate compiler generated dependencies of target calc_clnt
[ 18%] Building C object app/CMakeFiles/calc_clnt.dir/calc_clnt.c.o
[ 25%] Linking C executable ../../bin/calc_clnt
/usr/bin/ld: ../../runtimelib/libmysrc.a(common.c.o):
/home/fredom/WorkSpace/cppPlayground/socket_programming/chapter1/include/common.h:20:
multiple definition of `BUF_SIZE`;
CMakeFiles/calc_clnt.dir/calc_clnt.c.o:/home/fredom/WorkSpace/cppPlayground/socket_programming/chapter1/include/common.h:20: first defined here
collect2: error: ld returned 1 exit status
make[2]: *** [app/CMakeFiles/calc_clnt.dir/build.make:98:../bin/calc_clnt] 错误 1
make[1]: *** [CMakeFiles/Makefile2:154:app/CMakeFiles/calc_clnt.dir/all] 错误 2
make: *** [Makefile:91:all] 错误 2
从上面的错误输出可以看到,是常量 BUF_SIZE 存在重复定义,可是我就纳了闷了,我所有源文件相同的这部分代码都转移到 common.h 头文件里了啊,哪里来的第二个定义,而且从错误输出来看,确实没有报出第二个 BUF_SIZE 定义到底在哪里。我又里里外外的把所有源文件认真的检查了一遍,确信是绝对只有头文件中才有 BUF_SIZE的定义了,都排查到这里了,剩下只可能是链接的时候 BUF_SIZE 这个符号有什么猫腻。
问题排查
我仔细想了一下,以前一直写的是 C++ ,多个源文件 include 一个头文件从来没有遇到过 multiple definition of
这个错误,但是socket编程使用的是 C,说不定是两者的链接有什么差异造成的。我又想到,一般在头文件中都是声明而不会写定义,但是我从来没有细究过,为什么不能写定义?而且我在 C++ 中,很多时候类的定义确实是直接内联写在声明上的。
所以我去 Google 了一下如何查看编译生成的文件的符号,得知可以使用 objdump 这样一个工具。以下截取了部分 Linux manual page 关于 objdump 的使用方法。
NAME
objdump - display information from object files
DESCRIPTION
objdump displays information about one or more object files. The options control what particular information to display. This information
is mostly useful to programmers who are working on the compilation tools, as opposed to programmers who just want their program to compile
and work.
objfile... are the object files to be examined. When you specify archives, objdump shows information on each of the member object files.
OPTIONS
The long and short forms of options, shown here as alternatives, are equivalent. At least one option from the list
-a,-d,-D,-e,-f,-g,-G,-h,-H,-p,-P,-r,-R,-s,-S,-t,-T,-V,-x must be given.
objdump 除了用来显示文件符号表,一般还用于显示生成对象的汇编指令(从二进制可执行文件反汇编?),显示文件的符号表需要使用 -t (给静态链接库 .a使用的)和 -T (给动态链接库 .so 使用的)。objdump 可以一次显示单个或多个文件的信息,上述选项控制特定信息的输出,使用时至少要指定一个选项。
下面的问题排查就使用能复现这个错误的最小工作区和文件来解释,不然以原来的文件解释有很多无关的代码干扰。实际用到的只有 main.c
,common.h
,common.c
三个文件。
fredom@scut-lab-fgd:~/WorkSpace/cppPlayground/link_test$ tree -L 2
.
├── app
│ ├── CMakeLists.txt
│ └── main.c
├── bin
│ └── main
├── build
│ ├── app
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ ├── cmake_install.cmake
│ ├── compile_commands.json
│ ├── Makefile
│ └── src
├── CMakeLists.txt
├── include
│ └── common.h
├── lib
├── remake.sh
├── runtimelib
│ └── libmysrc.a
└── src
├── CMakeLists.txt
└── common.c
10 directories, 13 files
使用 objdump -t 看看 make 对两个源文件编译之后的 .o 输出文件都包含了什么。
Consolidate compiler generated dependencies of target mysrc
[ 25%] Building C object src/CMakeFiles/mysrc.dir/common.c.o
[ 50%] Linking C static library ../../runtimelib/libmysrc.a
[ 50%] Built target mysrc
Consolidate compiler generated dependencies of target main
[ 75%] Building C object app/CMakeFiles/main.dir/main.c.o
[100%] Linking C executable ../../bin/main
/usr/bin/ld: ../../runtimelib/libmysrc.a(common.c.o):/home/fredom/WorkSpace/cppPlayground/link_test/include/common.h:13: multiple definition of `BUF_SIZE`; CMakeFiles/main.dir/main.c.o:/home/fredom/WorkSpace/cppPlayground/link_test/include/common.h:13: first defined here
collect2: error: ld returned 1 exit status
make[2]: *** [app/CMakeFiles/main.dir/build.make:98:../bin/main] 错误 1
make[1]: *** [CMakeFiles/Makefile2:142:app/CMakeFiles/main.dir/all] 错误 2
make: *** [Makefile:91:all] 错误 2
/home/fredom/WorkSpace/cppPlayground/link_test
从上述 CMake 产生的信息可以看到,对单个文件的编译都是成功的,但是一到链接阶段,链接器 /usr/bin/ld 就会报错。两个源文件对应的编译产物分别是 common.c.o 和 main.c.o,对它们使用 objdump -t有如下结果:
common.c.o
~/WorkSpace/cppPlayground/link_test$ objdump -t build/src/CMakeFiles/mysrc.dir/common.c.o
build/src/CMakeFiles/mysrc.dir/common.c.o: 文件格式 elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 common.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .debug_info 0000000000000000 .debug_info
0000000000000000 l d .debug_abbrev 0000000000000000 .debug_abbrev
0000000000000000 l d .debug_line 0000000000000000 .debug_line
0000000000000000 l d .debug_str 0000000000000000 .debug_str
0000000000000000 l d .debug_line_str 0000000000000000 .debug_line_str
0000000000000000 g O .bss 0000000000000004 BUF_SIZE
0000000000000000 g O .data 0000000000000004 num
0000000000000000 g F .text 0000000000000044 error_handling
0000000000000000 *UND* 0000000000000000 stderr
0000000000000000 *UND* 0000000000000000 fputs
0000000000000000 *UND* 0000000000000000 fputc
0000000000000000 *UND* 0000000000000000 exit
main.c.o
~/WorkSpace/cppPlayground/link_test$ objdump -t build/app/CMakeFiles/main.dir/main.c.o
build/app/CMakeFiles/main.dir/main.c.o: 文件格式 elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 main.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .rodata 0000000000000000 .rodata
0000000000000000 l d .debug_info 0000000000000000 .debug_info
0000000000000000 l d .debug_abbrev 0000000000000000 .debug_abbrev
0000000000000000 l d .debug_line 0000000000000000 .debug_line
0000000000000000 l d .debug_str 0000000000000000 .debug_str
0000000000000000 l d .debug_line_str 0000000000000000 .debug_line_str
0000000000000000 g O .bss 0000000000000004 BUF_SIZE
0000000000000000 g F .text 0000000000000045 main
0000000000000000 *UND* 0000000000000000 error_handling
0000000000000000 *UND* 0000000000000000 num
0000000000000000 *UND* 0000000000000000 printf
但是平常没怎么用过 objdump
,这些输出的信息也没有表头,man
手册里好像也翻不到,只好再 Google 一下看看这些都是什么信息。在这个StackOverflow 的帖子里找到了一些相关的信息,不过没有科学工具的小伙伴有可能打不开链接的。
objdump -t
的输出一共有五列:
- 符号symbol的值(或者说地址?)
- 由7个标志位表示的符号的属性
- 符号所属的段(section)
- 符号的大小或者说偏移(字节为单位)
- 符号的名称
一个程序(在Linux语境下)最基本的应该是 .text代码段,.data只读数据段(大概是const常量那些),.bss全局未初始化数据段 三个段组成,在程序实际被运行成为进程之后还会有 .heap 和 .stack 堆栈区域。
而7个标志位的显示方式和Linux下使用 ls
命令显示的10个权限属性位很像,这7个位置每一列可能显示的值分组如下:
列值 | 值释义 |
---|---|
l ,g , ,! | 内部链接性,外部连接性,都不是,同时具有外部和内部链接性 |
w , | 弱链接符号,强链接符号 |
C , | 构造函数的符号,普通符号 |
W , | 警告⚠符号,普通符号(我去,警告符号是什么意思???) |
I , | 该符号是对另一个符号的间接引用,普通符号 |
d ,D , | 用于调试的符号,动态符号,普通符号 |
F ,f ,O , | 函数名称的符号,文件的符号,对象的符号,普通符号 |
说实话这里面有一大堆符号的类别我从来没听说过也不知道是用来做什么的,但是符号表的符号大概分为导出符号和未知符号,简单解释的话也可以看看这篇博客。那么刚才 objdump -t
输出的信息中,我只能猜测 **UND**
是 undifine
的缩写,也就是该符号未在本翻译单元内找到定义。在编译的初期阶段,会把宏定义字面量全部替换掉,展开include
的内容,这里引用一下其他博客的话:
预处理阶段是各个源文件独自进行的,每个源文件互不干扰,预处理的作用范围仅在本文件中,在预处理完成后,会生成一个
.i
文件,而这个.i
文件就是下一阶段编译时的一个编译单元,在此之后头文件就已经没有任何的作用了
在 main.c 的符号表输出里有这样三个未定义的符号:
0000000000000000 *UND* 0000000000000000 error_handling
0000000000000000 *UND* 0000000000000000 num
0000000000000000 *UND* 0000000000000000 printf
那么在后续的链接阶段就会去其他源文件的编译产物中的符号表去寻找这些UND-undefine
未定义符号的定义了。
以下是3个文件的内容:
main.c
#include "common.h"
extern int num;
int main(int argc, char** argv)
{
error_handling("test");
printf("%d", num);
return 0;
}
common.c
#include "common.h"
int num = 10;
void error_handling(char *msg)
{
fputs(msg, stderr);
fputc('\n', stderr);
exit(1);
}
common.h
#ifndef COMMON_H
#define COMMON_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define true 1
#define false 0
#define nullptr NULL
int BUF_SIZE;
void error_handling(char* msg);
#endif
从 main.c 和 common.c 的符号表输出可以看到,common.c 刚好有 main.c 几个缺失定义的符号的入口(也就是没有显示**UND**
啦):
main.c.o
0000000000000000 g O .bss 0000000000000004 BUF_SIZE
0000000000000000 g F .text 0000000000000045 main
0000000000000000 *UND* 0000000000000000 error_handling
0000000000000000 *UND* 0000000000000000 num
0000000000000000 *UND* 0000000000000000 printf
common.c.o
0000000000000000 g O .bss 0000000000000004 BUF_SIZE
0000000000000000 g O .data 0000000000000004 num
0000000000000000 g F .text 0000000000000044 error_handling
0000000000000000 *UND* 0000000000000000 stderr
0000000000000000 *UND* 0000000000000000 fputs
0000000000000000 *UND* 0000000000000000 fputc
0000000000000000 *UND* 0000000000000000 exit
至于printf
,stderr
,fputs
这些声明在 stdio.h 的符号,他们的链接文件在/usr/lib
等环境变量路径下,链接的时候应该能自动发现它们的定义并链接相关库的(这里有点勉强哈,但是我也不知道这里怎么解释了,先把眼下的这个问题解释清楚吧)。
从上述符号表输出还可以看到几个具有外部链接性的符号(也就是标志位修饰符带g
的),其中 BUF_SIZE 这个符号在两个符号表中都显示已定义并且是导出符号,向其他源文件提供该符号的定义入口(因为 main.c 和 common.c 都include
了头文件 common.h,而 BUF_SIZE 的声明在头文件里面),链接器在链接的时候发现了两个同样的符号,它很疑惑🤔,所以给报了multiple definition
的错误。
这里的问题在于 main.c 中存在未定义的符号,所与需要向其他符号表查表。如果 main.c 没有向其他符号表查询的动作,其实编译时链接器是无法发现这个符号的重复定义的,比如我们在 main.c 把对函数error_handling
和变量num
的引用去掉,就不会引发 多重定义 错误。
问题解决
使之具有内部链接性
要让符号具有内部链接性,那么可以用static来修饰,这样生成的符号表中,该符号属性是l --local
,只限于本翻译单元使用。
#ifndef COMMON_H
#define COMMON_H
...
static int BUF_SIZE;
...
#endif
main.c.o
build/app/CMakeFiles/main.dir/main.c.o: 文件格式 elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 main.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l O .bss 0000000000000004 BUF_SIZE
common.c.o
build/src/CMakeFiles/mysrc.dir/common.c.o: 文件格式 elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 common.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l O .bss 0000000000000004 BUF_SIZE
这样一来 CMake 就可以通过编译了。
-- Configuring done
-- Generating done
-- Build files have been written to: /home/fredom/WorkSpace/cppPlayground/link_test/build
Consolidate compiler generated dependencies of target mysrc
[ 25%] Building C object src/CMakeFiles/mysrc.dir/common.c.o
[ 50%] Linking C static library ../../runtimelib/libmysrc.a
[ 50%] Built target mysrc
Consolidate compiler generated dependencies of target main
[ 75%] Building C object app/CMakeFiles/main.dir/main.c.o
[100%] Linking C executable ../../bin/main
[100%] Built target main
使用宏定义
#ifndef COMMON_H
#define COMMON_H
...
#define BUF_SIZE 256
...
#endif
这样该名称在预处理阶段就会被检测字面值并替换成相应的定义值,符号表中甚至不会有该符号。
main.c.o
build/app/CMakeFiles/main.dir/main.c.o: 文件格式 elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 main.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .rodata 0000000000000000 .rodata
0000000000000000 l d .debug_info 0000000000000000 .debug_info
0000000000000000 l d .debug_abbrev 0000000000000000 .debug_abbrev
0000000000000000 l d .debug_line 0000000000000000 .debug_line
0000000000000000 l d .debug_str 0000000000000000 .debug_str
0000000000000000 l d .debug_line_str 0000000000000000 .debug_line_str
0000000000000000 g F .text 0000000000000045 main
0000000000000000 *UND* 0000000000000000 error_handling
0000000000000000 *UND* 0000000000000000 num
0000000000000000 *UND* 0000000000000000 printf
其他探讨
C++ 的class默认是内部链接性?
如果使用以下的文件进行编译:
main.cpp
#include <iostream>
#include "common.h"
using namespace std;
extern int num;
int main(int argc, char** argv)
{
foo a_foo(25);
cout << "num: " << num << endl;
cout << "foo::static_member: " << foo::static_member << endl;
cout << "foo.get() " << a_foo.get() << endl;
cout << "foo.set(100) "; a_foo.set(100); cout << a_foo.get() << endl;
return 0;
}
common.cpp
#include "common.h"
int foo::static_member = 0;
int num = 10;
common.h
#ifndef COMMON_H
#define COMMON_H
class foo
{
public:
foo(int x) : common_member(x) {};
static int static_member;
int common_member;
int get() const { return common_member; }
void set(int x) { common_member = x; }
};
#endif
那么得到的符号表中,类的成员函数没有显示地指明内部还是外部链接性,但是会指明这是一个函数的符号。类定义本身确实是内部链接性的。
main.c.o
0000000000000000 l d .text._ZN3fooC2Ei 0000000000000000 .text._ZN3fooC2Ei
0000000000000000 l d .text._ZNK3foo3getEv 0000000000000000 .text._ZNK3foo3getEv
0000000000000000 l d .text._ZN3foo3setEi 0000000000000000 .text._ZN3foo3setEi
0000000000000000 l .group 0000000000000000 _ZN3fooC5Ei
0000000000000000 w F .text._ZN3fooC2Ei 000000000000001b _ZN3fooC2Ei
0000000000000000 w F .text._ZN3fooC2Ei 000000000000001b _ZN3fooC1Ei
0000000000000000 w F .text._ZNK3foo3getEv 0000000000000014 _ZNK3foo3getEv
0000000000000000 w F .text._ZN3foo3setEi 000000000000001b _ZN3foo3setEi
0000000000000000 g F .text 000000000000016c main
总结
引用这篇博客作为总结吧。
预处理阶段
- 执行预处理指令
- 将头文件展开 即#include 所包含的内容
- 进行宏替换
- 删除注释
- 添加行号和标识
编译阶段
- 函数和变量声明的检查
- 语法分析 语义分析 词法分析 符号汇总等
- 将代码转换为汇编语言
汇编阶段
- 转成机器指令
- 生成符号表
- 符号地址与地址重定位
链接阶段
- 扫描符号表
- 对未定义符号向其他符号表的导出符号查询定义
- 链接对应文件对应位置的符号定义