分类 编程语言 下的文章

大意了,没有闪

起因是在VS新建了一个C++项目,然后试图引用一个用C写的函数库。结果编译运行的时候报错了,提示无法解析的外部符号。根据常识,这个问题一般与C++的函数重载有关,但是在仔细检查了库函数和调用时函数的签名之后发现并没有问题。用grepdumpbin对着编译出来的.lib静态库文件捣腾了老半天,也愣是没有找出问题所在。

左思右想,唯一的端倪在于用dumpbin观察.lib静态库文件时发现,编译出来的库函数是没有带C++后缀的,而报错的地方所调用的库函数是带了C++后缀的,这也正是导致出现函数名不匹配的地方。无奈之下只能去网上冲浪,带着我也描述不清的问题试图找出解决方法。

在万能的StackOverflow上看到了这个回答之后才意识到,函数名是在编译期确定的。由于头文件本身作为源文件参与编译,编译器只能通过源文件信息来确定这是C还是C++代码。很显然,.c后缀的被MSVC作为C代码编译,而.cpp文件则是作为C++代码编译。

而正是这细微的编译规则差异导致了本次的问题:在库函数源代码中,由于是以.c源文件的形式参与编译,遵循了C语言不允许函数重载的规则,编译出来的函数名就是头文件中所定义的函数名;而在我写的调用程序中,因为使用了.cpp格式的文件,代码使用了C++规则编译,因而给头文件中定义的函数名加上了C++的小尾巴(如@UAEXXZ这种)。在之后的链接过程中再用C++风格的函数名去匹配C风格的函数体,由于两者名字不一致,就产生了无法解析的外部符号问题。

解决方法也非常简单,在写C/C++混合调用的程序时,被调用者(主要是头文件)最好加上用#ifdef __cplusplus包装的extern "C"标记,以确保最大的兼容性

还是神奇的gem5。

这次是在学习底层实现的时候,看到的船新POSIX API:

    if (sharedBackstore.empty()) {
        shm_fd = -1;
        map_flags =  MAP_ANON | MAP_PRIVATE;
    } else {
        DPRINTF(AddrRanges, "Sharing backing store as %s\n",
                sharedBackstore.c_str());
        shm_fd = shm_open(sharedBackstore.c_str(), O_CREAT | O_RDWR, 0666);
        if (shm_fd == -1)
               panic("Shared memory failed");
        if (ftruncate(shm_fd, range.size()))
               panic("Setting size of shared memory failed");
        map_flags = MAP_SHARED;
    }

    ...

    uint8_t* pmem = (uint8_t*) mmap(NULL, range.size(),
                                    PROT_READ | PROT_WRITE,
                                    map_flags, shm_fd, 0);

由于这块代码是从gem5中内存模块复制出来的,其功能是配置内存模块维护被模拟系统数据所需的储存空间(初始化),可以预见的是单词分配的内存空间大小至少是MB,甚至GB级别的。第一眼看去有点眼花,还在想Linux的shared memory什么时候能够配置GB级别的共享内存块了?仔细一看才发现,这里用的是shm_open而不是shmget,只是名字长得像,功能有很大差别= =,前者在fnctl.h,后者在shm.h中定义。(P.S. 在APUE第三版上翻了老半天都没找到,我觉得应该不是我的问题

比较一下shmgetshm_open的函数签名:

int shmget(key_t key, size_t size, int shmflg);
int shm_open(const char *name, int oflag, mode_t mode);

不同于shmget在创建共享内存块的时候需要声明块大小,且存在比较小气的最大限制(Linux每块默认不大于4KB,MacOS和Solaris还是比较大气,大了几个数量级),shm_open的语法更偏向于open,创建的是一个默认大小为0的空文件,关于文件大小、文件内容需要开发者后续调用其它syscall来修改。

那么shm_open实际上做了什么呢?实际上是创建了一个基于内存的文件描述符。根据man7上的文档介绍,Linux上的实现方法是建立了一个基于内存的tmpfs,并挂载到/dev/shm目录下。调用shm_open的时候,会对应地在目录下用文件的形式进行相应的操作。

如上文所述,shm_open以后,文件的大小为0,用常规方法往文件写入数据并不能如我们所愿,将文件“撑”大。这时需要使用ftruncate系统调用解决该问题,由操作系统完成对文件大小的调整。

由于shm_open系统调用与进程无关,在不同程序中可以使用相同的操作获取同一块共享内存,达到无关进程内存共享的目的。这与文件系统类似,第一次调用建立文件,后面的重复调用只需要修改文件引用值即可。由shm_open创建的文件描述符多与mmap结合运用,直接将fd用于mmap中,其余按照需要配置即可,本文开头的代码段给出了较好的示范。

当一个进程不再需要使用这段共享内存的时候,使用shm_unlink函数,把获得的fd作为参数传过去即可。当最后一个引用关闭之后,共享内存对象由系统销毁。

这里顺便说一下上面的代码段中,mmap使用的一个小细节。当满足条件sharedBackstore.empty() == false的时候,fd设置为-1之后,直接执行了mmap操作。-1显然是一个无效的文件描述符,但是为什么程序还能正常运行?

在StackOverflow上搜索后,万能网友的给出了答案:此时的fd-1是允许的,而这是与mmap的参数map_flagsMAP_ANON(或MAP_ANONYMOUS)这个flag共用时的一个特殊情况。在声明了MAP_ANON/MAP_ANONYMOUS的情况下,该共享内存块仅对当前进程及其子进程有效,其他进程无法访问。某些POSIX实现要求这种情况下,fd一定要传-1,而这也只是特殊情况,并不代表在fd=-1的情况下mmap具有特殊的逻辑。

在看C4P的时候书上提到,有这么一段代码:

    char ch;
    cin >> ch;
    while(ch != '#')
    {
        ...
        cin >> ch;
    }

如果在循环中cin到一个char类型变量中,会使程序陷入死循环。当时就在想,如果拿不到合法的数据的话,不是应该丢掉非法输入,等待下一个合法输入的到来么。后来在网上查找相关信息后,发现C和C++对于这一异常情况有着不同的处理思路。

下文假设有如下定义:int var;

在C语言中,使用scanf("%d", &var);从输入流读取一个整型数据(浮点型同理)。在这种情况下,如果读取到的数据无法解析为对应类型的有效值的话,scanf的返回值为0,表示读入整型数据失败。在这种情况下,scanf不会修改对应的目标变量,下次scanf调用从当前失败的位置继续开始。

在C++中,使用的语句是cin >> var;。如果读取的数据无法转换为var的类型(这里是int)的话,根据C++规范版本,对var有两种不同的处理方式:在C++11之前,var的值不变;C++11及以后,var置零。同时,从此往后的cinfailbit置1,所有对cin的调用均失败返回,且不修改对应变量值。要解决这种情况,需要调用cin.clear();方法重置cin的异常状态,然后才可以继续正常使用cin

从此可以得知,上述代码在C++11的条件下的确会产生无限循环的问题:第一次调用失败时,ch置零,不等于#,循环继续;而后续由于cin处于fail状态,不再产生任何有效的数据读取,使得ch始终维持为0,进而导致无限循环的产生。可以用如下程序验证:

#include <iostream>

using namespace std;

int i = -1;
char ch = '?';

void cpp()
{
    cin >> i;
    cout << i << endl;

    cin >> ch;
    cout << ch << endl;
}

void c()
{
    scanf("%d", &i);
    printf("%d\n", i);

    scanf("%c", &ch);
    printf("%c\n", ch);
}

int main()
{
    // or cpp() to test the c++-style input
    c();

    return 0;
}

参考:
https://www.zhihu.com/question/54476315

最近在看gem5代码的时候发现里面一大堆神奇操作。由于并没有系统地学过C++,导致看别人代码的时候先是一头问号,好容易弄懂之后又只得拍手称绝。这里介绍一下C++11引入的模板逻辑std::enable_if

enable_if包含于type_traits头文件中,本质上是一个结构体,但是通过借用C++模板的特性实现了逻辑功能,因而我更倾向于把它按照功能称作逻辑,而非按照其在语言中的成员类型将其称为结构体/类型。

enable_if本身不算是语言特性,也不是复杂的软件实现,可以说仅仅只算一种语法糖。其功能大致可以概括为,如果模板类中的第一个参数为true的话,按照第二个参数生成一个有效的数据类型定义;为false的话则产生编译报错。这种功能可以允许在C++模板编程的时候,按照enable_if的条件变量决定所指定的函数或变量是否在指定条件的类中定义。

为了理解enable_if的工作原理,需要先了解C++模板类中bool类型参数的作用。简单来说,可以当作模式匹配的参数使用:

template<bool B>
class A
{
public:
    const bool opt = B;
};

对于类A则有两种可能的实例化情况,分别是A<true>()A<false>()。由此,可以分别为这两种模式下的范型定义不同类型的逻辑和代码:

template<>
class A<true>
{
public:
    const bool opt = true;
};

template<>
class A<false>
{
public:
    const bool opt = false;
};

这个时候访问A<true>().optA<false>().opt,则可以实例化为指定的类,读取到opt的值分别为truefalse

然后回来看enable_if的实现。cppreference上给出的参考实现如下:

template<bool B, class T = void>
struct enable_if {};

template<class T>
struct enable_if<true, T> { typedef T type; };  // 有的实现里用的是`using type = T;`,效果相同

可以看见,enable_if只需要四行代码就可以完成功能,但是如果要求要使用的人在每次使用的时候自己写,又显得太麻烦。可能就是出于代码可读性的目的放进了头文件中吧。

这里可以看见的是,与上文不同的地方在于,这里定义完enable_if<bool, T>的原型之后,只定义了enable_if<true, T>类型,而没有定义enable_if<false, T>类型。这样做的后果是,如果在后续过程中使用到了enable_if<false, T>类型的话,编译器会报出类似invalid use of incomplete type的错误,示意所使用的类型并没有完成显式声明。为了避免这个问题,enable_if巧妙地在类名后添加了一组花括号,示意默认实现是一个没有任何成员的空类。这样就确保了类是完整的,同时避免了类中夹杂不需要的属性。

那么,此时访问enable_if<true, T>::type的话,是可以获取到type所指类型T的,而访问enable_if<false>::type的话,虽然类型是完整的,但如上文所述,会因为访问没有定义的成员类型而在编译阶段被编译器当作错误报出。举一个简单的例子:

#include <type_traits>

...

    std::enable_if<false, void>::type t;

在编译阶段,编译器会指出这一行存在问题,因为在enable_if的首个参数为false的情况下,是没有定义type成员类型的:

error: 'type' is not a member of 'std::enable_if<false, void>'

但是如果放在这个模板逻辑的应用场景中,却又不会报错:

template<bool B>
class A
{
public:
    typename std::enable_if<B, bool>::type isTrue()
    {
        return true;
    }
};

使用这段代码声明模板类AA<true>()A<false>()都可以正常编译通过,但是只有A<true>()可以访问成员方法isTrue()enable_if的目的达到了,但是在实例化A<false>()的时候显然是遇到了typename std::enable_if<false, bool>::type isTrue()这个情况的。编译器并没有报错,而是选择在不实现这个函数的同时,正常完成剩余部分的编译。

编译器的这种行为与C++的一种新兴提出的C++编译思想,SFINAE有着极大的关系。SFINAE是英文短句Substitution Failure Is Not An Error(匹配失败不是(编译)错误)的缩写。SFINAE的一种体现是,在模板类中使用推导类型构建类方法的时候,如果出现了匹配错误(无符合匹配或多重匹配)的话,选择不报错,而是直接放弃对该模板函数的实例化操作。这也正是enable_if能够实现的核心原理:一般编码情况下,访问到enable_if<false, T>::type的话会报出类型没有该成员的错误;而在模板类编程中,由于找不到enable_if<false, T>::type对应的数据类型,根据SFINAE规则,编译器会选择放弃这种情况下对该函数的实现(而不是报错说找不到匹配类型),进而完成后续的编译工作,生成正常可用的程序。

参考:
https://zhuanlan.zhihu.com/p/20029261
https://ouuan.github.io/post/c-11-enable-if-%E7%9A%84%E4%BD%BF%E7%94%A8/
https://en.cppreference.com/w/cpp/types/enable_if
https://en.cppreference.com/w/cpp/language/sfinae

Esper曾说过,一千个科研民工有一千种编译报错

最近从实验室拿到了SPEC软件包用于测试软件性能,本满心欢喜地以为可以用SPEC提供的预编译文件直接跑,结果install.sh直接的当头一棒,说是预编译文件不支持当前环境,要自己从头编译。

然而谁也没想到的是,这只是噩梦的开始。

首先来到tools/src目录下,使用./buildtools命令开始编译工具链。

  1. gnu make的问题。gnu-make一上场就给我们出了一个难题:

    glob/libglob.a(glob.o): In function `glob_in_dir':
    /opt/make-3.81/glob/glob.c:1361: undefined reference to `__alloca'
    /opt/make-3.81/glob/glob.c:1336: undefined reference to `__alloca'
    /opt/make-3.81/glob/glob.c:1277: undefined reference to `__alloca'
    /opt/make-3.81/glob/glob.c:1250: undefined reference to `__alloca'
    glob/libglob.a(glob.o): In function `glob':
    /opt/make-3.81/glob/glob.c:575: undefined reference to `__alloca'
    glob/libglob.a(glob.o):/opt/make-3.81/glob/glob.c:726: more undefined references to `__alloca' follow
    collect2: error: ld returned 1 exit status
    Makefile:410: recipe for target 'make' failed
    make[2]: *** [make] Error 1
    make[2]: Leaving directory '/opt/make-3.81'
    Makefile:603: recipe for target 'all-recursive' failed
    make[1]: *** [all-recursive] Error 1
    make[1]: Leaving directory '/opt/make-3.81'
    Makefile:326: recipe for target 'all' failed
    make: *** [all] Error 2

    通过查阅资料 [提出问题] [解决问题] 发现,该问题的解决者也没能对问题的产生原因给出明确的解释,好在留下了有效的解决方案: 将make-3.81/glob/glob.c中,第54行的

    # if _GNU_GLOB_INTERFACE_VERSION == GLOB_INTERFACE_VERSION

    改成

    # if _GNU_GLOB_INTERFACE_VERSION >= GLOB_INTERFACE_VERSION

    然后重新编译解决问题。

  2. specmd5sum的问题。
    gcc -DHAVE_CONFIG_H    -I/home/gem5/cpu2006/tools/output/include   -I. -Ilib  -c -o md5sum.o md5sum.c
    In file included from md5sum.c:38:0:
    lib/getline.h:31:1: error: conflicting types for 'getline'
    getline PARAMS ((char **_lineptr, size_t *_n, FILE *_stream));
    ^
    In file included from md5sum.c:26:0:
    /usr/include/stdio.h:678:20: note: previous declaration of 'getline' was here
    extern _IO_ssize_t getline (char **__restrict __lineptr,
                    ^
    In file included from md5sum.c:38:0:
    lib/getline.h:34:1: error: conflicting types for 'getdelim'
    getdelim PARAMS ((char **_lineptr, size_t *_n, int _delimiter, FILE *_stream));
    ^
    In file included from md5sum.c:26:0:
    /usr/include/stdio.h:668:20: note: previous declaration of 'getdelim' was here
    extern _IO_ssize_t getdelim (char **__restrict __lineptr,
                     ^
    make: *** [md5sum.o] Error 1
    + testordie 'error building specmd5sum'
    + test 2 -ne 0
    + echo '!!! error building specmd5sum'
    !!! error building specmd5sum
    + kill -TERM 1299
    + exit 1
    !!!!! buildtools killed

这次的问题出在getline函数上,该函数属于gcc的插件函数,也就是说既不是ANSI C标准,也不是POSIX标准,而是因为经常被使用而作为第三方的gnu扩展加入了gcc中。由于GLIBC 1中没有实现该方法,所以旧版本的程序需要自己实现该方法;而总从GLIBC2开始,该方法就写入了stdio.h中,再用老方法编译就会产生重定义的错误了。

解决该问题的方法之一是对gcc使用-stc=c99(其它C标准,比如C89等等也可以)让它不定义getline函数,而是使用自带的定义。但是这样引入的问题是,实际上SPEC其他组件是采用了gnu扩展标准的,而buildtools脚本又不支持对不同项目给出不同的命令行选项,反而又会导致其他项目编译失败。这里采用第二种方法,不使用自带的getline,而是使用gcc提供的getline函数。具体操作就是注释掉自带的getline.h头文件,参见该博客

  1. Perl的问题。

Perl这里一共有两个问题。第一个是asm/page.h头文件缺失的问题,可以直接注释掉。或许也可以使用-I/usr/include/x86_64-linux-gnu参数将该文件的新位置提供给gcc,但是我没有进行测试。

第二个是动态链接的问题。如果没有正确配置动态链接库libdl的话,Perl就无法提供动态加载模块的功能,导致在后续的Perl模块编译与测试阶段中出现类似的提示:

t/100generic-bzip2.....Can't load module Compress::Raw::Bzip2, dynamic loading not available in this perl.
  (You may need to build a new perl executable which either supports
  dynamic loading or has the Compress::Raw::Bzip2 module statically linked into it.)
 at /home/esper/cpu2006/tools/output/lib/perl5/site_perl/5.8.8/IO/Compress/Adapter/Bzip2.pm line 10
Compilation failed in require at /home/esper/cpu2006/tools/output/lib/perl5/site_perl/5.8.8/IO/Compress/Adapter/Bzip2.pm line 10.
BEGIN failed--compilation aborted at /home/esper/cpu2006/tools/output/lib/perl5/site_perl/5.8.8/IO/Compress/Adapter/Bzip2.pm line 10.
Compilation failed in require at /home/esper/cpu2006/tools/output/lib/perl5/site_perl/5.8.8/IO/Compress/Bzip2.pm line 11.
BEGIN failed--compilation aborted at /home/esper/cpu2006/tools/output/lib/perl5/site_perl/5.8.8/IO/Compress/Bzip2.pm line 11.
Compilation failed in require at t/100generic-bzip2.t line 12.
BEGIN failed--compilation aborted at t/100generic-bzip2.t line 12.

这里第二行给出了报错的原因:编译的Perl没有动态加载的功能,换句话说,就是用于动态加载代码的libdl库没有正确配置,导致动态加载一块的代码完全没有编译。回去翻阅Configure的输出,发现早在配置阶段就出现问题了:

dlopen() NOT found.
Do you wish to use dynamic loading? [n] 

由于完全没有给出任何有用的信息,加上咕果上也查不到什么有效的解决方案,在此陷入了困境。

然而,天无绝人之路,后来在解决另一个报错,数学库libm找不到的时候,意外地有了新的进展。可能是由于Configure脚本过老,无法识别到gcc数学库参数,额外附加库这项配置变成了none

Checking for optional libraries...
What libraries to use? [none]

由于实际上是使用了数学库的,导致在后续编译中会出现找不到符号的问题:

../libperl.a(pp.o): In function `Perl_pp_pow':
pp.c:(.text+0x27b6): undefined reference to `pow'
../libperl.a(pp.o): In function `Perl_pp_modulo':
pp.c:(.text+0x366d): undefined reference to `fmod'
../libperl.a(pp.o): In function `Perl_pp_atan2':
pp.c:(.text+0x824d): undefined reference to `atan2'
../libperl.a(pp.o): In function `Perl_pp_sin':
pp.c:(.text+0x8341): undefined reference to `sin'
../libperl.a(pp.o): In function `Perl_pp_cos':
pp.c:(.text+0x8481): undefined reference to `cos'
../libperl.a(pp.o): In function `Perl_pp_exp':
pp.c:(.text+0x87a1): undefined reference to `exp'
../libperl.a(pp.o): In function `Perl_pp_log':
pp.c:(.text+0x88e8): undefined reference to `log'
../libperl.a(pp.o): In function `Perl_pp_sqrt':
pp.c:(.text+0x8b9f): undefined reference to `sqrt'
../libperl.a(pp.o): In function `Perl_pp_crypt':
pp.c:(.text+0xabee): undefined reference to `crypt'
collect2: error: ld returned 1 exit status

看到前面提到的一篇博客中,通过修改buildtools的方法解决了该问题,并且我也进行了验证,确实有效。然而总觉得不够清真,为了解决一个系统上的问题修改了全局配置。后来了解到Perl的Configure可以通过hintfile为特定平台提供默认参数,从而解决了该问题。hintfile将会被用类似source的方式将变量引入当前脚本中,可以作为Configure选项的默认值。由于没有找到详细的文档,hintfile的变量名要自己猜,但是可以根据Configure中用于用户交互的信息进行变量名的猜测。在Linux系统中,Configure大概率会选择hints/linux.sh作为hintfile进行加载。

后来发现手动添加库依赖的变量名为libs,把-lm加入该变量值即可解决问题。

而后在阅读最新版Perl的hints/linux.sh(参见 https://github.com/Perl/perl5/blob/blead/hints/linux.sh )的时候发现,针对g++和dlopen有一段硬编码的配置:

# If using g++, the Configure scan for dlopen() and (especially)
# dlerror() might fail, easier just to forcibly hint them in.
case "$cc" in
*g++*)
  d_dlopen='define'
  d_dlerror='define'
  ;;
esac

其中提到g++和dlopen具有一定的不兼容性,Configure在这种条件下可能识别不出来libdl,抱着死马当活马医的心态,把这段配置复制到SPEC2006中Perl的hintfile中,发现没有任何变化,还是报出相同的错误。(显然不会有任何变化,默认使用的是cc/gcc命令,不会显式地拿g++来编译C代码的)

然后意识到问题之后,直接把里面的两行关键代码留下来,再次编译,报错发生了变化。

首先是通过hintfile的内置变量赋值,强行绕过了Configuredlopen检测。脚本给出了如下的警告信息,意味着虽然没有检测到libdl,但是程序忠实地按照变量配置,开启了含有libdl库的代码编译:

dlopen() NOT found.
*** WHOA THERE!!! ***
    The recommended value for $d_dlopen on this machine was "define"!
    Keep the recommended value? [y]
Do you wish to use dynamic loading? [y]

然后报出了新的错误:

        Making DynaLoader (static)
Processing hints file hints/linux.pl
Writing Makefile for DynaLoader
make[1]: Entering directory `/home/esper/cpu2006/tools/src/perl-5.8.8/ext/DynaLoader'
make[1]: Leaving directory `/home/esper/cpu2006/tools/src/perl-5.8.8/ext/DynaLoader'
make[1]: Entering directory `/home/esper/cpu2006/tools/src/perl-5.8.8/ext/DynaLoader'
../../miniperl "-I../../lib" "-I../../lib" DynaLoader_pm.PL DynaLoader.pm
../../miniperl "-I../../lib" "-I../../lib" XSLoader_pm.PL XSLoader.pm
cp XSLoader.pm ../../lib/XSLoader.pm
cp DynaLoader.pm ../../lib/DynaLoader.pm
AutoSplitting ../../lib/DynaLoader.pm (../../lib/auto/DynaLoader)
rm -f DynaLoader.xs
cp dl_dlopen.xs DynaLoader.xs
../../miniperl "-I../../lib" "-I../../lib" ../../lib/ExtUtils/xsubpp -noprototypes -typemap ../../lib/ExtUtils/typemap DynaLoader.xs > DynaLoader.xsc && mv DyaLoader.xsc DynaLoader.c
cc -c   -I/home/esper/cpu2006/tools/output/include -fno-strict-aliasing -pipe -Wdeclaration-after-statement -I/usr/local/include -D_LARGEFILE_SOURCE -D_FILE_OFSET_BITS=64 -O2   -DVERSION=\"1.05\" -DXS_VERSION=\"1.05\"  "-I../.."  -DPERL_CORE -DLIBC="" DynaLoader.c
rm -rf ../../lib/auto/DynaLoader/DynaLoader.a
/usr/bin/ar cr ../../lib/auto/DynaLoader/DynaLoader.a DynaLoader.o && : ../../lib/auto/DynaLoader/DynaLoader.a
chmod 755 ../../lib/auto/DynaLoader/DynaLoader.a
make[1]: Leaving directory `/home/esper/cpu2006/tools/src/perl-5.8.8/ext/DynaLoader'
cc -o perl  -L/home/esper/cpu2006/tools/output/lib -Wl,-E perlmain.o lib/auto/DynaLoader/DynaLoader.a  libperl.a `cat ext.libs` -lm -lcrypt -lutil -lc
lib/auto/DynaLoader/DynaLoader.a(DynaLoader.o): In function `XS_DynaLoader_dl_find_symbol':
DynaLoader.c:(.text+0x3b3): undefined reference to `dlsym'
DynaLoader.c:(.text+0x441): undefined reference to `dlerror'
lib/auto/DynaLoader/DynaLoader.a(DynaLoader.o): In function `XS_DynaLoader_dl_unload_file':
DynaLoader.c:(.text+0x4ea): undefined reference to `dlclose'
DynaLoader.c:(.text+0x559): undefined reference to `dlclose'
DynaLoader.c:(.text+0x566): undefined reference to `dlerror'
lib/auto/DynaLoader/DynaLoader.a(DynaLoader.o): In function `XS_DynaLoader_dl_load_file':
DynaLoader.c:(.text+0x645): undefined reference to `dlopen'
DynaLoader.c:(.text+0x6c9): undefined reference to `dlerror'
collect2: error: ld returned 1 exit status
make: *** [perl] Error 1
+ testordie 'error building Perl'
+ test 2 -ne 0
+ echo '!!! error building Perl'
!!! error building Perl
+ kill -TERM 550221
+ exit 1
!!!!! buildtools killed

我的眼神瞬间犀利了起来.jpg

undefined reference to ...,这不是最经典的忘记加-lXXX链接参数的结果吗?

除错终了后我神速开hintfile。目的唯一libs!!!!!我爆速修改链接选项。warning!
此处省略100余字

修改文件过后,果然编译成功!删去上文根据设想添加的两行d_XXX强制配置后,编译仍然通过,这证实了我的设想。

然而,Perl的测试中仍然有几个是处于失败状态的,大约1%,十来个左右。由于测试范围往往大于使用需求,对于这几个没通过的测试我就没理它了,直接yes进行后续编译操作。

总结就是,Perl编译动态加载模块失败的原因与数学库一样,都是因为识别不到系统配置,默认配置又不提供有效值造成的。在perl-5.8.8/hints/linux.sh末尾加上如下两行解决上述编译问题:

libs='-lm -lcrypt -lutil -lc -ldl'
libpth='/usr/local/lib64 /lib64 /usr/lib64'
  1. 自此之后编译上就应该没有本质上的问题了。如果是直接从.tar.gz文件解压的话,复制出来的src文件夹及下级文件权限都是444,即只有读权限,而后续某些模块的测试涉及到对测试文件的读写操作,会产生Permission Denied一类的报错信息。如果对自己的操作自信,不会损坏文件的话,可以使用偷懒的方法,直接给所有文件/文件夹写权限:
    sudo chmod u+w -R .

    tools/src目录下执行该命令即可。

中间还有个小插曲是,一开始以为是编译器版本的问题,整了gcc 4和5的docker来编译看能不能成功。结果不但没成功,还报出了新的You haven't done a "make depend" yet!错误,后来发现原因是gcc镜像的FROM用的是Debian系的,/bin/sh默认指向了dash而不是bash。用ln -snf /bin/bash /bin/sh覆盖修改这个链接即可。更多资料可参见 https://sjp38.github.io/post/spec_cpu2006_install/

ps. 如果在后续测试阶段发现某些测试怎么跑都跑不对,可以检查一下是不是拿到的SPEC有问题。我这里用到的SPEC是被实验室重新打包过的,里面好多原始数据文件被改过了,debug了好久才发现:(