在实际工作中,当遇到段错误,我们会很容易的想到这是非法访问内存导致的,比如访问了已经释放的内存,访问数据越界,尝试写没有写权限的内存等。使用gdb进行调试,查看出异常的调用栈,往往可以定位到问题原因。
但是有些时候,段错误问题的定位也没这么简单。本人在工作中就遇到了一个段错误的问题,问题原因不是那么直观,出问题的代码非常简单,直接按照常规的分析方法,很难分析出原因。往内存越界、内存没有权限这个方向分析无法定位问题。
当我们遇到这种问题的时候,没有思路的时候,往往会采用一种方式,就是把编译环境清空,完全重新再编译一遍。的确,很多时候如果我们的运行环境,有些库和可执行文件,如果不是在一个统一的环境下编译出来的,版本号对应不上,也会引起这样的问题。但是对于符号覆盖的情况,即使重新编译一遍,也无法解决问题。
除了重新编译一遍,我们还可以使用LD_DEBUG来查看调试信息,LD_DEBUG可以查看共享库的搜索和链接过程,也可以看到符号的绑定过程。
问题的实际原因:动态库中有同名的符号,在执行程序的时候出现了符号覆盖。
1问题复现
可执行文件直接依赖两个动态库,两个动态库有同名符号,这样编译可执行文件的时候无法编译通过。如果要复现这个问题,需要间接依赖。
如下图所示,liba.so和libb.so中有重名符号。libaa.so依赖liba.so,libbb.so依赖libb.so,可执行文件依赖libaa.so和libbb.so。可执行文件间接依赖liba.so和libb.so。
liba.so | 对于类test::example::Hello,在构造函数中申请了8字节的内存,在函数DoSame中对内存进行了写,在析构函数中释放了内存。 |
libb.so | libb.so中的函数test::example::Hello::DoSame与liba.so中的函数重名。 |
libaa.so | 调用了liba.so中函数test::example::Hello::DoA |
libbb.so | 调用了libb.so中的test::example::Hello::DoB和和test::example::Hello::DoSame |
main | 在main函数中调用了libaa.so中的DoAA和libbb.so中的DoSame |
main函数中调用了3个函数,分别为DoAA、DoBB、DoSame,调用关系如下图所示。
1.1liba.so
liba.h
#ifndef _LIB_A_H_
#define _LIB_A_H_
namespace test {
namespace example {
class Hello {
public:
Hello();
~Hello();
void DoA();
void DoSame();
char *p = nullptr;
};
}
}
#endif
liba.cpp
#include <stdlib.h>
#include <iostream>
#include "liba.h"
namespace test {
namespace example {
Hello::Hello() {
p = (char *)malloc(8);
printf("p: %p, this: %p\n", p, this);
}
Hello::~Hello() {
if (p) {
std::cout << "free p\n";
free(p);
}
}
void Hello::DoA() {
std::cout << "do a in liba.\n";
}
void Hello::DoSame() {
std::cout << "do same in liba.\n";
p[0] = 'a';
}
}
}
1.2libb.so
libb.h
#ifndef _LIB_B_H_
#define _LIB_B_H_
namespace test {
namespace example {
class Hello {
public:
void DoB();
void DoSame();
};
};
};
#endif
libb.cpp
#include <iostream>
#include "libb.h"
namespace test {
namespace example {
void Hello::DoB() {
std::cout << "do b in libb.\n";
}
void Hello::DoSame() {
std::cout << "do same in libb.\n";
}
};
};
1.3libaa.so
libaa.h
void DoAA();
libaa.cpp
#include "libaa.h"
#include "liba.h"
#include <iostream>
void DoAA() {
std::cout <<"do aa\n";
test::example::Hello hello;
hello.DoA();
}
1.4libbb.so
libbb.h
#include "libb.h"
void DoBB();
void DoSame();
libbb.cpp
#include <iostream>
#include "libbb.h"
#include "libb.h"
void DoBB() {
std::cout << "do bb\n";
test::example::Hello hello;
hello.DoB();
}
void DoSame() {
std::cout << "do same\n";
test::example::Hello hello;
hello.DoSame();
}
1.5main函数
#include <iostream>
#include <string>
#include "libaa.h"
#include "libbb.h"
int main() {
DoAA();
DoBB();
DoSame();
return 0;
}
使用如下命令进行编译:
g++ liba.cpp -fPIC --shared -o liba.so -g
g++ libb.cpp -fPIC --shared -o libb.so -g
g++ libbb.cpp libb.so -fPIC --shared -o libbb.so -g
g++ libaa.cpp liba.so -fPIC --shared -o libaa.so -g
g++ main.cpp libaa.so libbb.so -g
运行结果,如下,可以看到,出现了段错误。
使用gdb进行定位,在p[0]='a'这行代码出现了段错误。打印p的话,发现p是一个非法地址。
可以看到,按照我们的预期,main函数中调用DoSame是调用的libbb.so中的DoSame,而libbb.so中的DoSame是调用的libb.so中的test::example::Hello::DoSame。但是根据gdb中的栈可以看到,最终调用到了liba.so中的test::example::Hello::DoSame。
调用函数出现了错乱,与预期的不一致。
编译可执行文件的时候,如果有间接调用的函数,那么最终调用的函数不是在编译的时候确定的。而是在运行的时候动态绑定的。比如libbb.so中DoSame调用的test::example::Hello::DoSame,这个函数最终调用liba.so中的还是libb.so中的,是需要动态绑定的。
在启动a.out的时候,使用LD_DEBUG进行调试,如下命令,就可以看到运行是符号绑定的信息。
LD_DEBUG=bindings ./a.out
如下所示,DoSame最终调用到了libs.so中的函数,所以调用的函数与预期的不一致。
41428: binding file /home/wangyanlong/test/symboltest/libbb.so [0] to /home/wangyanlong/test/symboltest/liba.so [0]: normal symbol `_ZN4test7example5Hello6DoSameEv'
2符号优先级
2.1动态库中重复的符号
上一节中,编译main.cpp的时候使用的是下边的命令,libaa.so在libbb.so之前,这样编译的话,查找符号会优先使用liba.so中的符号。
g++ main.cpp libaa.so libbb.so -g
如果修改两个库的顺序,libbb.so放在libss.so之前,那么就会绑定libb.so中的DoSame。
g++ main.cpp libbb.so libaa.so
2.2静态库和动态库重复符号
静态库中的符号是硬编到可执行文件中的,在绑定符号的时候肯定是可执行文件优先。如下所示,将liba.cpp编译成静态库。编译main.cpp的时候不管libaa.so、liba.a、libbb.so的先后顺序,test::example::Hello::DoSame都会绑定liba.so中的符号。
gcc liba.cpp -c
ar rcs liba.a liba.o
g++ libb.cpp -fPIC --shared -o libb.so -g
g++ libbb.cpp libb.so -fPIC --shared -o libbb.so -g
g++ libaa.cpp -fPIC --shared -o libaa.so -g
g++ main.cpp libbb.so libaa.so liba.a -g