C/C++ 程序编译链接所遇到的奇怪问题

27 views
Skip to first unread message

Plain_Text

unread,
Dec 12, 2014, 9:10:15 PM12/12/14
to sh...@googlegroups.com


  用简单的示例来说明我遇到的情况。


$ cat common.h

void fun1();
void fun2();
void fun3();



$ cat main.c

#include "common.h"

int main()
{
fun1();
fun2();
}



$ cat fun1.c

#include "common.h"

void fun1()
{
}



$ cat fun2.c

#include "common.h"

void fun2()
{
}



$ cat fun3.c

#include "common.h"

void fun3()
{
}




编译链接:

$ gcc -c main.c -o main.o
$ gcc -c fun1.c -o fun1.o
$ gcc -c fun2.c -o fun2.o
$ gcc -c fun3.c -o fun3.o

$ gcc main.o fun1.o fun2.o fun3.o -o example
$./example
$


  这个程序自然没有任何问题,能够成功编译链接,也能够成功运行。如果将
fun3.c 删除,不再编译 fun3.c; 当然最后也不链接 fun3.o, 那么程序依然正确。
道理很简单, main() 里只用到了 fun1 和 fun2, 有没有 fun3 根本没关系。然而
如果删除的不是 fun3.c, 而是 fun2.c, 那么链接过程就会报错: fun2 没有实现。

  用这个示例就是为了说明我的思路:如果编译链接某个程序,少了一个文件,
那么链接过程就会报错;如果不报错,那么说明缺少的那个文件根本就不需要。
我的这个思路正确吗?反正现在遇到了麻烦,编译一个大的 C++ 程序,编译链接
成功,也能够运行。可是运行时逻辑不正确,但不是什么 Segfault 一类的错误。
找了好久,才现在少编译了几个文件。将那几个文件编译链接进去,就一切正常了。

  那个 C++ 程序比较大,我不可能费心思去研究它。不过从外部来看,这种情况
完全不可思议。因为 C++ 程序跟 C 程序应该只有语法层面的区别,全局函数、类的
成员函数在编译器层面应该都是一样的。为什么增加几个文件,程序逻辑就变了呢?
既然那几个文件是要用到的,为什么缺少了,链接过程却没有报错?

  那个程序说明我对编译链接过程的理解不正确。错在哪里呢?哪位高手能用简单
的示例来演示一下这种情况。





Yongwei Wu

unread,
Dec 12, 2014, 9:49:20 PM12/12/14
to sh...@googlegroups.com
我觉得你对单个可执行文件的编译链接的过程理解基本没问题吧。但SO的情况会不一样话:如果里面漏了些符号,会在其他代码用到它们的时候出问题。还有弱符号的概念,也可能导致链接新的代码会改变行为。
> --
> -- You received this message because you are subscribed to the Google Groups Shanghai Linux User Group group. To post to this group, send email to sh...@googlegroups.com. To unsubscribe from this group, send email to shlug+un...@googlegroups.com. For more options, visit this group at https://groups.google.com/d/forum/shlug?hl=zh-CN
> ---
> 您收到此邮件是因为您订阅了 Google 网上论坛的“Shanghai Linux User Group”群组。
> 要退订此群组并停止接收此群组的电子邮件,请发送电子邮件到shlug+un...@googlegroups.com
> 要查看更多选项,请访问 https://groups.google.com/d/optout



--
Wu Yongwei
URL: http://wyw.dcweb.cn/

Xidorn Quan

unread,
Dec 13, 2014, 12:38:04 AM12/13/14
to sh...@googlegroups.com
这么说的话有道理,应该是你未链接进去的代码里面有某些函数遮蔽了某个链接库内的同名函数。如果是 C 的话,只要名字相同就会发生,而且连接时应该没有签名检查,如果参数不同的话或许就可能造成奇怪的逻辑错误。C++ 的话应该要函数签名相同才会遮蔽了,大概。

YF Liu

unread,
Dec 14, 2014, 2:21:21 AM12/14/14
to sh...@googlegroups.com
我觉得你这里首先得澄清一个概念就是C/C++在编译和链接的时候是有很大的不同的, 并不是只有语法层面的区别的。 比如说生成的最终文件如果你直接看ELF的话, C生成的是     65: 00000000004004b8     6 FUNC    GLOBAL DEFAULT   12 fun1。 而C++出来就是 59: 0000000000400538     6 FUNC    GLOBAL DEFAULT   12 _Z4fun1v。 而且C/C++编译器在处理变量的方面也有区别。 C++而且还有多态的特性, 就是在执行的时候才决定调用那个函数, 这些很难在编译的时候检测到这些问题。 boost和Loki, 两个库的确是在解决这个问题方面做了解决方案, 但是使用的门槛还比较高。 如果你使用了优化选项O1,O2,O3, 每个里面都有很多选项都会在不同程度上隐藏或者曝露代码问题。



您收到此邮件是因为您订阅了Google网上论坛上的“Shanghai Linux User Group”群组。

要退订此群组并停止接收此群组的电子邮件,请发送电子邮件到shlug+un...@googlegroups.com
要查看更多选项,请访问https://groups.google.com/d/optout


--
Best regards to you and your family

Yongwei Wu

unread,
Dec 14, 2014, 10:19:29 PM12/14/14
to sh...@googlegroups.com
你说的这些都是编译的问题,和链接无关。C++使用特殊名字的原因是为了确保特殊函数和重载函数能够在目标代码中有各自的符号名称。多态只是一个函数指针表而已,用C也一样可以模拟。

YF Liu

unread,
Dec 15, 2014, 12:01:31 AM12/15/14
to sh...@googlegroups.com
的确这些看上去是编译问题, 但是其实和链接是有关系的。 链接的过程就是在使用这些symbols, 然后做地址mapping等工作。 我就用楼主的例子来说一下我提到的C/C++的一个不同。
试着把fun1.c改成

#include "common.h"
#include <stdio.h>

void fun1(int i)
{
 int a[1];
 printf("%i", a[10]);
}

C编译正常通过, 但是执行的时候就会有错误了。 C++则不然, 它在链接的时候就会报错了。

DaboD

unread,
Dec 15, 2014, 3:18:20 AM12/15/14
to sh...@googlegroups.com
Plugin 或類似的機制也是一種可能, 例如
// downloader.h
#include <map>
#include <string>

using namespace std;

class Downloader
{
public:
    typedef Downloader * (*FactoryFunc)(const string &);

    virtual char * Download(size_t &size) = 0;

    static Downloader *Create(const string &url);

    template <typename T>
    static void Register(const string &protocol);

private:
    static map<string, FactoryFunc> factFuncs;
};

template <typename T>
void Downloader::Register(const string &protocol)
{
    factFuncs[protocol] = T::Create;
}

// downloader.cpp
#include "downloader.h"

map<string, Downloader::FactoryFunc> Downloader::factFuncs;

Downloader *Downloader::Create(const string &url)
{
    Downloader *downloader = NULL;
    string prot = url.substr(0, url.find_first_of(':'));
    if(NULL == factFuncs[prot]) {
        return NULL;
    }

    return factFuncs[prot](url);
}

// main.cpp
#include "downloader.h"
#include <iostream>

using namespace std;

int main()
{
    Downloader *d = Downloader::Create(string("http://google.com"));
    if(! d) {
        cerr << "protocol not support" << endl;
        return 1;
    }

    size_t size;
    char *data = d->Download(size);
    delete data;

    cout << "data downloaded" << endl;

    return 0;
}

$ g++ -o downloader main.cpp downloader.cpp
$ ./downloader 
protocol not support

程序能 build 過, 但第 8 行無法產生出 Downloader instance, 因些提早結束了. 如果加上 http-downloader.cpp 一起 build, 在 main 不改變的情況, 因為 Downloader instance 能被產生, main() 的執行結果也會不同
// http-downloader.cpp
#include "downloader.h"

class HttpDownloader : Downloader
{
    static Downloader * Create(const string &url);

    HttpDownloader(const string &url) : url(url) { }

    virtual char * Download(size_t &size);

    friend class Downloader;

    string url;
};

Downloader * HttpDownloader::Create(const string &url)
{
    return new HttpDownloader(url);
}

char * HttpDownloader::Download(size_t &size)
{
    size = 0;
    return NULL;
}

__attribute__((constructor)) void init()
{
    Downloader::Register<HttpDownloader>(string("http"));
}

$ g++ -o downloader downloader.cpp main.cpp http-downloader.cpp
$ ./downloader 
data downloaded

Yongwei Wu

unread,
Dec 15, 2014, 6:09:07 AM12/15/14
to sh...@googlegroups.com
这是因为C的类型检查不够严格,楼主应当写:

void fun1(void);

而不是

void fun1();

大家可能会惊讶这两者并不等价。但这个问题本身不应导致他原先的问题,因为不链接fun1.c的后果仍然是找不到fun1。

无论如何,这个问题在C++里并不存在,不能解释他碰到的问题。抛开名字的特殊转化(mangling),C++的情况是main要寻找fun1(),而fun1.cc(比如有这个C++文件)只产生了fun1(int)。

再怎么说,这些情况下链接器不需要对C++作特殊处理,仍然只是普通的符号匹配而已(我记得C++对链接器仍然有一些额外的要求,如静态对象的初始化;但你说的这些恰恰链接器不需要特殊处理)。

Plain_Text

unread,
Dec 15, 2014, 6:35:24 PM12/15/14
to sh...@googlegroups.com


  "Yongwei Wu" 的分析很有道理,这个问题很可能是弱符号 __attribute__((weak))
所引发。其实就算不是弱符号,在不同的可执行文件里也可以存在重复的符号,而
链接器不会报错。在同一个可执行文件(比如 bin 文件或者 .so 共享库)里不能
有重复的符号,否则链接的时候会报错;但如果重复的符号在不同的可执行文件里,
比如一个在 bin 里,另一个在 .so 里,那么就按照链接顺序找到所需要的符号,
重复的定义被忽略掉,不报错。




  附上一个简单的示例来演示这种微妙的问题。




$ cat common.h

void fun1();
void fun2();
void fun3();



$ cat main.c

#include <unistd.h>
#include "common.h"

int main()
{
fun1();
fun2();

sleep(3);

return 0;
}



$ cat fun1.c

#include "common.h"

void fun1()
{
}



$ cat fun2.c

#include "common.h"

void fun2()
{
}



$ cat fun3.c

#include <stdio.h>
#include "common.h"

void fun3()
{
}


unsigned int sleep (unsigned int seconds)
{
printf("Sleep by myself!\n");
}







main 里调用了 sleep, 本来那是 glibc 里的函数。但在 fun3.c 里将它改成
自己的函数。那么编译链接:

$ gcc -c main.c -o main.o
$ gcc -c fun1.c -o fun1.o
$ gcc -c fun2.c -o fun2.o
$ gcc -c fun3.c -o fun3.o

$ gcc main.o fun1.o fun2.o fun3.o -o example # 不会报错
$ ./example
Sleep by myself!
$

  如果将最后的链接改成:

$ gcc main.o fun1.o fun2.o -o example # 也不会报错
$ ./example # 简单地 sleep 三秒钟
$


  很明显,当链接上 fun3.o 的时候,就找到了“自己的” sleep; 如果不链接
上,就到之后的 .so 文件(即隐含链接的 glibc)里去找,刚好也能找到。两种
情况都能运行,但逻辑明显不一样。






Plain_Text

unread,
Dec 15, 2014, 6:59:31 PM12/15/14
to sh...@googlegroups.com



  谢谢分享。不过我这里测试你的例子会出问题。这个:

g++ -o downloader downloader.cpp main.cpp http-downloader.cpp

的确可以通过,但运行立刻报错: Segmentation fault




DaboD

unread,
Dec 15, 2014, 11:35:04 PM12/15/14
to sh...@googlegroups.com
有意思, 我的 gcc 是 4.9.1, 從上面 post 的 message 中 copy 代碼及 build command 後試了一下, 目前沒碰到 segfault 問題, 方便的話可以告訴我你的 gcc 版本及發生 segfault 的行號嗎? 哈!

不過重點在舉個多 build 少 build 檔案會造成程序行為不同的例子.

DaboD

unread,
Dec 15, 2014, 11:58:06 PM12/15/14
to sh...@googlegroups.com
目前猜想應該是 init() 跟 Downloader::factFuncs 執行順序的問題. init() 被安排在 Downloader::factFuncs 初始前執行, 就有可能發生 segfault. 修改過 downloader.h & downloader.cpp 如下
// downloader.h
#include <map>
#include <string>

using namespace std;

class Downloader
{
public:
    typedef Downloader * (*FactoryFunc)(const string &);

    virtual char * Download(size_t &size) = 0;

    static Downloader *Create(const string &url);

    template <typename T>
    static void Register(const string &protocol);

    static void Prepare()
    {
        if(! factFuncs) {
            factFuncs = new map<string, FactoryFunc>;
        }
    }

private:
    static map<string, FactoryFunc> *factFuncs;
};

template <typename T>
void Downloader::Register(const string &protocol)
{
    Prepare();

    (*factFuncs)[protocol] = T::Create;
}

// downloader.cpp
#include "downloader.h"

map<string, Downloader::FactoryFunc> *Downloader::factFuncs = NULL;

Downloader *Downloader::Create(const string &url)
{
    if(! factFuncs) {
        return NULL;
    }

    Downloader *downloader = NULL;
    string prot = url.substr(0, url.find_first_of(':'));
    if(NULL == (*factFuncs)[prot]) {
        return NULL;
    }

    return (*factFuncs)[prot](url);
}

On Tuesday, December 16, 2014 7:59:31 AM UTC+8, Plain_Text wrote:

Plain_Text

unread,
Dec 16, 2014, 7:07:05 PM12/16/14
to sh...@googlegroups.com


2014-12-15(Monday) 20:58:06 -0800, DaboD <daid...@gmail.com>:


> 目前猜想應該是 init() 跟 Downloader::factFuncs 執行順序的問題. init() 被安排在 Downloader::factFuncs
> 初始前執行, 就有可能發生 segfault. 修改過 downloader.h & downloader.cpp 如下




  这样就不会有 segfault 了,测试通过。再次感谢您提供这么好的例子。




Reply all
Reply to author
Forward
0 new messages