计算机系统篇之链接(13):升级共享库导致程序运行时错误的惨痛经历
Author: stormQ
Created: Saturday, 21. December 2019 11:58AM
Last Modified: Sunday, 01. November 2020 11:40AM
本文描述了一个升级共享库导致程序运行时错误的案例,并利用 gdb 详细分析了其原因,最后给出了一种可行的解决方案。
问题可以简单描述为:可执行目标文件 main 依赖共享库 libb.so 和 liba.so,而 libb.so 也依赖 liba.so。但是,可执行目标文件 main 所依赖的 liba.so 版本为 liba.so.2.0,而 libb.so 所依赖的 liba.so 版本为 liba.so.1.0。当运行 main 时,程序崩溃退出。
step 0: 目录结构如下
test
├── exe
│ ├── main
│ └── main.cpp
├── liba_v1.0
│ ├── data.cpp
│ ├── data.h
│ └── liba.so.1.0
├── liba_v2.0
│ ├── data.cpp
│ ├── data.h
│ └── liba.so.2.0
└── libb
├── init.cpp
├── init.h
└── libb.so
step 1: 生成共享库 liba.so.1.0
在 test/liba_v1.0 目录下执行如下命令:
$ g++ -o liba.so.1.0 data.cpp -shared -fpic -g
test/liba_v1.0/data.h:
#pragma once
namespace core {
struct NodeData
{
NodeData(int val1, int val2, int val3)
: num(val1), type(val2), flag(val3)
{
}
int num;
int type;
int flag;
};
class Node
{
public:
Node(int num, int type, int flag);
~Node();
int count() const;
int type() const;
int flag() const;
bool isNull() const { return pdata_->type == 0; }
private:
NodeData *pdata_;
};
} // namespace core
test/liba_v1.0/data.cpp:
#include "data.h"
namespace core {
Node::Node(int num, int type, int flag)
: pdata_(nullptr)
{
pdata_ = new NodeData(num, type, flag);
}
Node::~Node()
{
if (!pdata_)
{
delete pdata_;
}
}
int Node::count() const
{
return pdata_->num;
}
int Node::type() const
{
return pdata_->type;
}
int Node::flag() const
{
return pdata_->flag;
}
} // namespace core
step 2: 生成共享库 libb.so
在 test/libb 目录下执行如下命令:
$ g++ -o libb.so init.cpp -shared -fpic -g -I../liba_v1.0
test/libb/init.h:
#pragma once
namespace sys {
void init();
} // namespace sys
test/libb/init.cpp:
#include "init.h"
#include "data.h"
#include <iostream>
namespace sys {
void init()
{
core::Node node(1, 2, 3);
if (node.isNull())
{
std::cout << "error: type is 0, not expected" << std::endl;
}
}
} // namespace sys
step 3: 生成共享库 liba.so.2.0
在 test/liba_v2.0 目录下执行如下命令:
$ g++ -o liba.so.2.0 data.cpp -shared -fpic -g
test/liba_v2.0/data.h:
#pragma once
namespace core {
struct NodeData
{
NodeData(int val1, int val2, int val3)
: num(val1), is_valid(false), type(val2), flag(val3)
{
}
int num;
bool is_valid;
int type;
int flag;
};
class Node
{
public:
Node(int num, int type, int flag);
~Node();
int count() const;
int type() const;
int flag() const;
bool isNull() const { return pdata_->type == 0; }
private:
NodeData *pdata_;
};
} // namespace core
注意: 相比于 liba.so.1.0,liba.so.2.0 只在 NodeData 中增加了数据成员 is_valid,其余不变。
test/liba_v2.0/data.cpp:
#include "data.h"
namespace core {
Node::Node(int num, int type, int flag)
: pdata_(nullptr)
{
pdata_ = new NodeData(num, type, flag);
}
Node::~Node()
{
if (!pdata_)
{
delete pdata_;
}
}
int Node::count() const
{
return pdata_->num;
}
int Node::type() const
{
return pdata_->type;
}
int Node::flag() const
{
return pdata_->flag;
}
} // namespace core
step 4: 生成可执行目标文件 main
$ g++ -o main main.cpp ../libb/libb.so ../liba_v2.0/liba.so.2.0 -g -I../liba_v2.0 -I../libb
test/exe/main.cpp:
#include "init.h"
#include "data.h"
#include <iostream>
int main()
{
#define TYPE 5
sys::init();
core::Node node(4, TYPE, 6);
const auto node_type = node.type();
std::cout << "actual type = " << node_type << " in main()"
<< ", expected type = " << TYPE << std::endl;
#undef TYPE
return 0;
}
step 5: 运行可执行目标文件 main
在 test/exe 目录下执行如下命令:
$ ./main
运行结果为:
error: type is 0, not expected
actual type = 5 in main(), expected type = 5
注意: init() 函数中 node.pdata_->type 的应该被赋值为 2,而实际值为 0,不符合预期结果。即程序运行时发生了错误。
1)使用 gdb 运行可执行目标文件 main
$ gdb -q ./main
Reading symbols from ./main...done.
(gdb) start
Temporary breakpoint 1 at 0x400c3f: file main.cpp, line 6.
Starting program: /home/xuxiaoqiang/Desktop/test/exe/main
Temporary breakpoint 1, main () at main.cpp:6
6 {
(gdb) n
8 sys::init();
(gdb) s
sys::init () at init.cpp:8
8 {
(gdb) n
9 core::Node node(1, 2, 3);
2)进入 Node::Node 函数
(gdb) s
core::Node::Node (this=0x7fffffffd8d0, num=1, type=2, flag=3) at data.cpp:6
6 : pdata_(nullptr)
(gdb) disas
Dump of assembler code for function core::Node::Node(int, int, int):
0x00007ffff79d28e0 <+0>: push %rbp
0x00007ffff79d28e1 <+1>: mov %rsp,%rbp
0x00007ffff79d28e4 <+4>: push %rbx
0x00007ffff79d28e5 <+5>: sub $0x28,%rsp
0x00007ffff79d28e9 <+9>: mov %rdi,-0x18(%rbp)
0x00007ffff79d28ed <+13>: mov %esi,-0x1c(%rbp)
0x00007ffff79d28f0 <+16>: mov %edx,-0x20(%rbp)
0x00007ffff79d28f3 <+19>: mov %ecx,-0x24(%rbp)
=> 0x00007ffff79d28f6 <+22>: mov -0x18(%rbp),%rax
0x00007ffff79d28fa <+26>: movq $0x0,(%rax)
0x00007ffff79d2901 <+33>: mov $0x10,%edi
0x00007ffff79d2906 <+38>: callq 0x7ffff79d27c0 <_Znwm@plt>
0x00007ffff79d290b <+43>: mov %rax,%rbx
0x00007ffff79d290e <+46>: mov -0x24(%rbp),%ecx
0x00007ffff79d2911 <+49>: mov -0x20(%rbp),%edx
0x00007ffff79d2914 <+52>: mov -0x1c(%rbp),%eax
0x00007ffff79d2917 <+55>: mov %eax,%esi
0x00007ffff79d2919 <+57>: mov %rbx,%rdi
0x00007ffff79d291c <+60>: callq 0x7ffff79d27b0 <_ZN4core8NodeDataC1Eiii@plt>
0x00007ffff79d2921 <+65>: mov -0x18(%rbp),%rax
0x00007ffff79d2925 <+69>: mov %rbx,(%rax)
0x00007ffff79d2928 <+72>: nop
---Type <return> to continue, or q <return> to quit---q
Quit
(gdb) info symbol 0x00007ffff79d28e0
core::Node::Node(int, int, int) in section .text of ../liba_v2.0/liba.so.2.0
从上面看出,init() 函数实际调用的是定义在 liba.so.2.0 中 Node 构造函数,而不是 liba.so.1.0 中的。
3)进入 NodeData::NodeData
(gdb) n
8 pdata_ = new NodeData(num, type, flag);
(gdb) s
core::NodeData::NodeData (this=0x614c20, val1=1, val2=2, val3=3) at data.h:8
8 : num(val1), is_valid(false), type(val2), flag(val3)
(gdb) disas
Dump of assembler code for function core::NodeData::NodeData(int, int, int):
0x00007ffff79d299c <+0>: push %rbp
0x00007ffff79d299d <+1>: mov %rsp,%rbp
0x00007ffff79d29a0 <+4>: mov %rdi,-0x8(%rbp)
0x00007ffff79d29a4 <+8>: mov %esi,-0xc(%rbp)
0x00007ffff79d29a7 <+11>: mov %edx,-0x10(%rbp)
0x00007ffff79d29aa <+14>: mov %ecx,-0x14(%rbp)
=> 0x00007ffff79d29ad <+17>: mov -0x8(%rbp),%rax
0x00007ffff79d29b1 <+21>: mov -0xc(%rbp),%edx
0x00007ffff79d29b4 <+24>: mov %edx,(%rax)
0x00007ffff79d29b6 <+26>: mov -0x8(%rbp),%rax
0x00007ffff79d29ba <+30>: movb $0x0,0x4(%rax)
0x00007ffff79d29be <+34>: mov -0x8(%rbp),%rax
0x00007ffff79d29c2 <+38>: mov -0x10(%rbp),%edx
0x00007ffff79d29c5 <+41>: mov %edx,0x8(%rax)
0x00007ffff79d29c8 <+44>: mov -0x8(%rbp),%rax
0x00007ffff79d29cc <+48>: mov -0x14(%rbp),%edx
0x00007ffff79d29cf <+51>: mov %edx,0xc(%rax)
0x00007ffff79d29d2 <+54>: nop
0x00007ffff79d29d3 <+55>: pop %rbp
0x00007ffff79d29d4 <+56>: retq
End of assembler dump.
(gdb) info symbol 0x00007ffff79d299c
core::NodeData::NodeData(int, int, int) in section .text of ../liba_v2.0/liba.so.2.0
从上面看出,init() 函数实际调用的是定义在 liba.so.2.0 中 NodeData 构造函数,而不是 liba.so.1.0 中的。
4)打印 NodeData 的 this 指针的内容
(gdb) p *this
$1 = {num = 0, is_valid = false, type = 0, flag = 0}
(gdb) display *this
1: *this = {num = 0, is_valid = false, type = 0, flag = 0}
(gdb) ni
0x00007ffff79d29b1 8 : num(val1), is_valid(false), type(val2), flag(val3)
1: *this = {num = 0, is_valid = false, type = 0, flag = 0}
(gdb)
0x00007ffff79d29b4 8 : num(val1), is_valid(false), type(val2), flag(val3)
1: *this = {num = 0, is_valid = false, type = 0, flag = 0}
(gdb)
0x00007ffff79d29b6 8 : num(val1), is_valid(false), type(val2), flag(val3)
1: *this = {num = 1, is_valid = false, type = 0, flag = 0}
(gdb)
0x00007ffff79d29ba 8 : num(val1), is_valid(false), type(val2), flag(val3)
1: *this = {num = 1, is_valid = false, type = 0, flag = 0}
(gdb)
0x00007ffff79d29be 8 : num(val1), is_valid(false), type(val2), flag(val3)
1: *this = {num = 1, is_valid = false, type = 0, flag = 0}
(gdb)
0x00007ffff79d29c2 8 : num(val1), is_valid(false), type(val2), flag(val3)
1: *this = {num = 1, is_valid = false, type = 0, flag = 0}
(gdb)
0x00007ffff79d29c5 8 : num(val1), is_valid(false), type(val2), flag(val3)
1: *this = {num = 1, is_valid = false, type = 0, flag = 0}
(gdb)
0x00007ffff79d29c8 8 : num(val1), is_valid(false), type(val2), flag(val3)
1: *this = {num = 1, is_valid = false, type = 2, flag = 0}
(gdb)
0x00007ffff79d29cc 8 : num(val1), is_valid(false), type(val2), flag(val3)
1: *this = {num = 1, is_valid = false, type = 2, flag = 0}
(gdb)
0x00007ffff79d29cf 8 : num(val1), is_valid(false), type(val2), flag(val3)
1: *this = {num = 1, is_valid = false, type = 2, flag = 0}
(gdb)
10 }
1: *this = {num = 1, is_valid = false, type = 2, flag = 3}
(gdb) p/x this
$2 = 0x614c20
(gdb) p/x &this->type
$3 = 0x614c28
(gdb) n
core::Node::Node (this=0x7fffffffd8d0, num=1, type=2, flag=3) at data.cpp:9
9 }
从上面可以看出,init() 函数中局部变量 node 的数据成员 pdata_ 实际指向的是一个大小为 16 字节的 NodeData 对象,并且数据成员 type 的地址在对象内部的偏移量为 8。从 libb.so 的角度来看,这两点都不符合预期。
5)进入 Node::isNull() 函数
(gdb) n
sys::init () at init.cpp:10
10 if (node.isNull())
(gdb) s
core::Node::isNull (this=0x7fffffffd8d0) at ../liba_v1.0/data.h:26
26 bool isNull() const { return pdata_->type == 0; }
(gdb) disas
Dump of assembler code for function core::Node::isNull() const:
0x00007ffff7bd4c80 <+0>: push %rbp
0x00007ffff7bd4c81 <+1>: mov %rsp,%rbp
0x00007ffff7bd4c84 <+4>: mov %rdi,-0x8(%rbp)
=> 0x00007ffff7bd4c88 <+8>: mov -0x8(%rbp),%rax
0x00007ffff7bd4c8c <+12>: mov (%rax),%rax
0x00007ffff7bd4c8f <+15>: mov 0x4(%rax),%eax
0x00007ffff7bd4c92 <+18>: test %eax,%eax
0x00007ffff7bd4c94 <+20>: sete %al
0x00007ffff7bd4c97 <+23>: pop %rbp
0x00007ffff7bd4c98 <+24>: retq
End of assembler dump.
(gdb) info symbol 0x00007ffff7bd4c80
core::Node::isNull() const in section .text of ../libb/libb.so
从上面可以看出,init() 函数中实际所调用的 isNull() 函数定义在 ../liba_v1.0/data.h 中。
6)打印 pdata_ 及其数据成员
(gdb) p/x pdata_
$4 = 0x614c20
(gdb) p/x &pdata_->type
$5 = 0x614c24
(gdb) p/x pdata_->type
$6 = 0x0
(gdb) n
sys::init () at init.cpp:12
12 std::cout << "error: type is 0, not expected" << std::endl;
从上面可以看出,isNull() 函数中所访问的 pdata_->type 的地址在对象内部的偏移量为 4。而该偏移量实际对应的字段为 pdata_->is_valid,值为 0。因此,导致了 init() 函数在运行时执行 isNull() 函数的结果不符合预期。
因此,可以得出结论:导致程序运行时错误的原因为:main 所依赖的 liba.so 的版本与 libb.so 所依赖的 liba.so 的版本不兼容(对象布局不一致)。
解决方案之一:将 libb.so 所依赖的 liba.so 的版本升级为 liba.so.2.0,并重新编译 libb.so。
下一篇:计算机系统篇之链接(14):.plt、.plt.got、.got 和 .got.plt sections 之间的区别