背景

近期做Linux开发,编译环境我用的gcc7.3,而生产环境是gcc4.4,很老的版本,连C++11都不支持,这样写起代码来会很憋屈。

于是就想着怎么能让我用上C++11。

直接升级系统/编译器自然是最好的选择,然而毕竟是生产环境,不那么容易动(哪怕是仅升级C和C++库)。于是研究目标就成为了,如何在完全不动系统环境的情况下,将高版本gcc编译产物部署到低版本系统上

思路

查阅各种文献,一般有两种方案:

  1. 把C和C++库(libc.so和libstdc++.so)随程序一起发布,并配置LD_LIBRARY_PATH变量。
  2. 静态链接。

方法1的局限性比较大,只限于同进程的所有模块都在我控制下的情况。像是我实际的应用场景,生成一个so让别人加载的形式就不能使用这种方法,因为这实质上来说还是动了其他模块的“系统环境”。

方法2听起来比较完美,会把所有的依赖代码编译到bin内,从而不再需要依赖任何C/C++库——至少在MSVC上我们通常就是这么做的。

然而现实是只有C++库可以静态链接,C库是不行的,因为gcc的C库设计时就没打算让你静态链接,参考这里

尝试

总之先尝试静态链接C++库,加上-static-libstdc++参数,然后编译链接,查看符号

nm a.so | grep GLIBCXX

确认已经没有C++库的外部引用了,然后在服务器上尝试dlopen这个so,报错:

/lib64/libc.so.6: version `GLIBC_2.14' not found (required by ./a.so)

查看服务器GLIBC的最高支持版本,显示为2.12(即gcc4.4使用的版本)。

查看so所使用的C库函数,发现只有一个函数使用到了此版本的C库:

nm a.so | grep GLIBC_2.1
                 U memcpy@@GLIBC_2.14

把这个符号重定向到低版本memcpy上吧,在编译时再加一个参数:

-Wl,--wrap=memcpy

同时在工程中加入一个新的c文件:

void *__memcpy_glibc_2_2_5(void *, const void *, size_t);
asm(".symver __memcpy_glibc_2_2_5, memcpy@GLIBC_2.2.5");
void *__wrap_memcpy(void *dest, const void *src, size_t n)
{
	return __memcpy_glibc_2_2_5(dest, src, n);
}

再次编译,在服务器上执行dlopen,成功加载。

总结

通过加入-static-libstdc++参数,以及重定向memcpy的方法,我们将编译产物引用的外部符号降低到GLIBC2.12之下,从而成功在只装有gcc4.4的机器上部署。

同时,使用这种方法时有一些需要注意的点:

  1. 跨模块的函数调用就全部使用C接口吧,C++的ABI从gcc5.X开始就已经不兼容了。
  2. 指针跨模块传递时,不要释放其他模块传过来的指针,因为可能使用的不是同一个堆。
  3. 此方法将2.14版本的memcpy换成了2.2.5版本的,这个版本的memcpy有个问题是,它其实是memmove……执行效率可能低上那么一丁点儿,如果非常介意这一点的话,可以从gcc源码中抠出memcpy.o文件编译到你工程里——不过这会牵扯上GPL协议,需要自己斟酌。