"c/c++ 的编译和链接的问题"

介绍

hello world in c 中,提到了一些编译链接 的问题。在实际项目中,大家学习很多 c/c++ 的语言特性,而忽略了编译和链 接的过程。 对编译和链接过程的理解,反过来有助于我们理解 c/c++ 的很多语 言特性。

前提

为了让观点更加明显,我去除所有的库函数调用。这样就需要理解 main 函数的返回值。

我们写一个最简单的程序

// simple_main.cpp
int main(int argc, char * argv[]) {
   return 12;
}

我们看一下程序输出

% c++ simple_main.cpp && ./a.out ; echo $?
12

在 bash 中 $? 表示进程的返回值。也就是 main 函数的返回值。

编译过程

我们看下面的程序。这个程序在编译过程中不会报错的。

extern int foo(int input);
int main(int argc, char * argv[]) {
   return foo(12);
}

注意到,我为了排除噪音,没有任何 #include 语句。

% c++ -c -o simple_main.o simple_main.cpp

命令成功执行,没有任何错误。-c 表示编译。如果不带这个选项, gcc 自动 启动连接过程,会有链接错误。

-o 表明输出文件的位置。我们检查一下输出文件。

linux 下运行

% nm -C simple_main.o

OSX 下运行

% nm  simple_main.o
                 U __Z3fooi
0000000000000000 T _main

% nm  simple_main.o | c++filt
                 U foo(int)
0000000000000000 T _main

这里我们注意到, U 表示引用外部符号。这就是 extern int foo(int input) 的作用。

我们还注意到,c++ 的 mangle/demangle 的作用个, int foo 被翻译成了 __z3fooi 了,这种程序在链接过程中, C 语言的程序是无法找到这个符号的。

我们换一种方法

extern "C" int foo(int input);
int main(int argc, char * argv[]) {
   return foo(12);
}
% g++ -c -o simple_main.o simple_main.cpp
% nm simple_main.o
                 U _foo
0000000000000000 T _main

这里我们看到 extern "C" 的作用就是防止 mangle 。

参考 https://en.wikipedia.org/wiki/Name_mangling

这里不同的平台会自动添加一个下划线在符号中。我们先忽略这个。

编译成功。

链接简单的目标文件

// foo1.cpp
int foo(int input){
    return input;
}
// foo2.cpp
int foo(int input){
    return input*2;
}

我们分别编译一下。

% c++ -c -o foo1.o foo1.cpp
% c++ -c -o foo2.o foo2.cpp
% nm foo1.o
0000000000000000 T __Z3fooi
% nm foo2.o
0000000000000000 T __Z3fooi

nm 的输出中, T 表示这个函数已经在这个目标文件中定义了。

如果我们链接第一个库

% c++ -o a.out simple_main.o foo1.o && ./a.out ; echo $?

12

% c++ -o a.out simple_main.o foo2.o && ./a.out ; echo $?

24

可以看到,只有连接的时候,才能决定我们调用哪一个函数。如果我们声明的是 extern "C" int foo(int) 的话,就会有连接错误。

Undefined symbols for architecture x86_64:
  "_foo", referenced from:
      _main in simple_main.o

解决这个错误的办法就是用 C 语言写一个 foo 函数。

// foo1.c
int foo(int i) { return i*3; }
% gcc -c -o fooc.o  foo1.c
% nm fooc.o
0000000000000000 T _foo

% c++  -o a.out fooc.o simple_main.o
% ./a.out ; echo $?
36

在连接阶段,语言的特征就比较模糊了,连接器看到就是一大堆没有定义的符号,无论这些目标文件是 c, c++ 还是汇编语言。还是其他什么语言生成的目标文件。

尽管如此,符号上还是带有一些语言特征,例如 c++ 的 mangle 机制。

如果我们两个目标文件都链接的话,会产生链接错误。

5 g++ -o a.out simple_main.o foo1.o foo2.o
duplicate symbol __Z3fooi in:
    foo1.o
    foo2.o
ld: 1 duplicate symbol for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

链接静态链接库

静态链接库很简单,就是一大堆目标文件的堆叠。

创建链接库

% ar r libfoo1.a foo1.o
% ar r libfoo2.a foo2.o

连接静态链接库

% c++ -L. -o a.out simple_main.o -lfoo1

-L. 表明到哪里寻找 libfoo.a 。这里用当前目录。

-lfoo1 表示要连接 foo1 这个库。

如果我们两个库都链接的话

% g++ -o a.out simple_main.o -L. -lfoo1 -lfoo2
% ./a.out; echo $?
12
% g++ -o a.out simple_main.o -L. -lfoo2 -lfoo1
% ./a.out; echo $?
24

和链接目标文件不一样,如果出现符号重复定义,这里没有任何错误,具体调用哪一个 foo 函数,完全取决于 -l 在链接过程中出现的位置。

这是一个经常误入的一个坑点,注意,这里和编译没有关系。完全是连接的问题。

链接共享链接库

shared library 的使用,必须重新编译所有目标文件,让目标文件是位置无关的代码 PIC, Position Independent Code。

% g++ -c -fPIC -o simple_main.o  simple_main.cpp
% g++ -c -fPIC -o foo1.o foo1.cpp
% g++ -c -fPIC -o foo2.o foo2.cpp

生成动态链接库

OSX 下

% g++ -dynamiclib -o libfoo1.dylib foo1.o
% g++ -dynamiclib -o libfoo2.dylib foo2.o

linux 下

% g++ -o libfoo1.so -shared foo1.o
% g++ -o libfoo2.so -shared foo2.o

链接

% g++ -L. -o a.out simple_main.o -lfoo1
% ./a.out ; echo $?
12
% g++ -L. -o a.out simple_main.o -lfoo2
./a.out ; echo $?
24

链接不同的库,调用不同的动态链接库。

同时链接两个

% g++ -L. -o a.out simple_main.o -lfoo1 -lfoo2
./a.out; echo $?
12
% g++ -L. -o a.out simple_main.o -lfoo2 -lfoo1
./a.out; echo $?
24

和静态链接库类似,符号重复定义的时候,没有报错,取决于链接命令中,库出现的顺序。

延伸,故事才开始,共享链接和静态连接有很多很多的不同。换句话说,静态链接很简单,几乎和直接链接目标文件一样。动态链接就不一样了。还取决于生成期链接和运行期链接。 rpath, LD_LIBRARY_PATH 等等。比较复杂。这里指出其复杂性,不做展开讨论。

链接动态链接库

这里指出 windows 下的 DLL 是动态链接库,和 *unix 的共享链接库有很多类似,但也有很多不同。我在 windows 下的经验有限,不是十分了解。仅仅指出他们不同。