计算机系统篇之链接(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(123);
    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

运行结果为:

errortype is 0not 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(123);

2)进入 Node::Node 函数

(gdb) s
core::Node::Node (this=0x7fffffffd8d0, num=1type=2, flag=3) at data.cpp:6
6    : pdata_(nullptr)
(gdb) disas 
Dump of assembler code for function core::Node::Node(intintint):
   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(intintint) 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(intintint):
   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(intintint) 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=0x7fffffffd8d0num=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 之间的区别

上一篇:计算机系统篇之链接(12):Chapter 7 Linking 章节习题与解答

首页