这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

博客

这里用来记录 博客 , 用来记录一些并不是很系统的知识.

这里的文章都是按照时间倒叙排列.

awesome-cs

计算机科学相关资料汇总

Linux下C++编程 Linux环境编程:从应用到内核 Linux高性能服务器编程

cpp类内存布局

Linux OS 64 位 用gdb看一下cpp类内存布局

没有虚函数的情况

#include <iostream>

class A
{
public:
    A() : c(0)
    {
        std::cout << "A::A()" << std::endl;
        func2();
    };
    void func2() { std::cout <<"A::func2()" << std::endl; };
private:
    int c;
};

int main(void)
{
    A a1;
    A a2;
    
    return 0;
}
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./a.out...
(gdb) b 22
Breakpoint 1 at 0x11fc: file basic.cpp, line 22.
(gdb) run
Starting program: /mnt/work/cs-note-src/src/cpp/memory/a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
A::A()
A::func2()
A::A()
A::func2()

Breakpoint 1, main () at basic.cpp:22
22          return 0;
(gdb) info vtbl a1
This object does not have a virtual function table
(gdb)
This object does not have a virtual function table
(gdb) p a
$1 = {i = {0, 1045149306}, x = 1.2904777690891933e-08, d = 1.2904777690891933e-08}
(gdb) p a1
$2 = {c = 0}
(gdb) p a2
$3 = {c = 0}
(gdb) p &a1
$4 = (A *) 0x7fffffffe250
(gdb) p &a2
$5 = (A *) 0x7fffffffe254
(gdb) p typeid(a1)
could not find typeinfo symbol for 'A'
(gdb) info address A::func2
Symbol "A::func2()" is a function at address 0x5555555552da.
(gdb) info symbo A::func2
A::func2() in section .text of /mnt/work/cs-note-src/src/cpp/memory/a.out
(gdb)

存在虚函数的情况


```sh
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./virtual...
(gdb) b 24
Breakpoint 1 at 0x11fc: file virtual.cpp, line 24.
(gdb) run
Starting program: /mnt/work/cs-note-src/src/cpp/memory/virtual
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
A::A()
A::func2()
A::A()
A::func2()

Breakpoint 1, main () at virtual.cpp:24
24          return 0;
(gdb) p a1
$1 = {_vptr.A = 0x555555557d68 <vtable for A+16>, c = 0}
(gdb) p &a1
$2 = (A *) 0x7fffffffe230
(gdb) x /2xg 0x7fffffffe230
0x7fffffffe230: 0x0000555555557d68      0x0000000000000000
(gdb) info vtbl a1
vtable for 'A' @ 0x555555557d68 (subobject @ 0x7fffffffe230):
[0]: 0x5555555552ea <A::virtual_func1()>
[1]: 0x555555555366 <A::virtual_func3()>
(gdb) p typeid(a1)
$3 = {_vptr.type_info = 0x7ffff7fa2fa0 <vtable for __cxxabiv1::__class_type_info+16>, __name = 0x55555555603c <typeinfo name for A> "1A"}
(gdb) p sizeof(typeid(a1))
$4 = 16
(gdb) p &typeid(a1)
$5 = (gdb_gnu_v3_type_info *) 0x555555557d78 <typeinfo for A>
(gdb) x /4xg 0x555555557d68
0x555555557d68 <_ZTV1A+16>:     0x00005555555552ea      0x0000555555555366
0x555555557d78 <_ZTI1A>:        0x00007ffff7fa2fa0      0x000055555555603c
0x555555557d88: 0x0000000000000001      0x0000000000000169
0x555555557d98: 0x0000000000000001      0x0000000000000178
0x555555557d80        0x000055555555603c
0x555555557d78        0x00007ffff7fa2fa0
0x555555557d70        0x0000555555555366
0x555555557d68        0x00005555555552ea

虚函数继承

#include <iostream>

class A
{
public:
    A() : c(0)
    {
        std::cout << "A::A()" << std::endl;
        func2();
    };
    virtual void virtual_func1() { std::cout << "A::virtual_func1()" << std::endl;};
    void func2() { std::cout <<"A::func2()" << std::endl; };
    virtual void virtual_func3() { std::cout << "A::virtual_func3()" << std::endl;};
private:
    int c;
};

class B : public A
{
public:
    B() : a(0), b(0)
    {
        std::cout << "B::B()" << std::endl;
        func2();
    }
    B(int a, int b) : a(a), b(b)
    {
        std::cout << "B::B(int a, int b)" << std::endl;
        func2();
    }
    ~B() { std::cout << "B::~B()" << std::endl; };
    virtual void virtual_fun1() { std::cout <<"B::virtual_fun1()" << std::endl; };
    void func2() { std::cout <<"B::func2()" << std::endl; };
    virtual void virtual_func3() { std::cout << "B::virtual_func3()" << std::endl;};
private:
    int a;
    int b;
};

int main(void)

{
    A a1;
    A a2;
    B b1;
    B b2;

    return 0;

}

(gdb) b 48
Breakpoint 1 at 0x1235: file ./inheritance.cpp, line 48.
(gdb) run
Starting program: /mnt/work/cs-note-src/src/cpp/memory/inheritance
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
A::A()
A::func2()
A::A()
A::func2()
A::A()
A::func2()
B::B()
B::func2()
A::A()
A::func2()
B::B()
B::func2()

Breakpoint 1, main () at ./inheritance.cpp:48
48          return 0;
(gdb) p b1
$1 = {<A> = {_vptr.A = 0x555555557d10 <vtable for B+16>, c = 0}, a = 0, b = 0}
(gdb) p b2
$2 = {<A> = {_vptr.A = 0x555555557d10 <vtable for B+16>, c = 0}, a = 0, b = 0}
(gdb) p &b1
$3 = (B *) 0x7fffffffe210
(gdb) p &b2
$4 = (B *) 0x7fffffffe230
(gdb) p typeid(b1)
$5 = {_vptr.type_info = 0x7ffff7fa3c30 <vtable for __cxxabiv1::__si_class_type_info+16>, __name = 0x55555555607b <typeinfo name for B> "1B"}
(gdb) p typeid(b2)
$6 = {_vptr.type_info = 0x7ffff7fa3c30 <vtable for __cxxabiv1::__si_class_type_info+16>, __name = 0x55555555607b <typeinfo name for B> "1B"}
(gdb) info vtbr b1
Undefined info command: "vtbr b1".  Try "help info".
(gdb) info vtbl b1
vtable for 'B' @ 0x555555557d10 (subobject @ 0x7fffffffe210):
[0]: 0x555555555362 <A::virtual_func1()>
[1]: 0x55555555555e <B::virtual_func3()>
[2]: 0x5555555554e2 <B::virtual_fun1()>
(gdb) p &typeid(b1)
$7 = (gdb_gnu_v3_type_info *) 0x555555557d48 <typeinfo for B>
(gdb) x /16xg 0x555555557d10
0x555555557d10 <_ZTV1B+16>:     0x0000555555555362      0x000055555555555e
0x555555557d20 <_ZTV1B+32>:     0x00005555555554e2      0x0000000000000000
0x555555557d30 <_ZTV1A+8>:      0x0000555555557d60      0x0000555555555362
0x555555557d40 <_ZTV1A+24>:     0x00005555555553de      0x00007ffff7fa3c30
0x555555557d50 <_ZTI1B+8>:      0x000055555555607b      0x0000555555557d60
0x555555557d60 <_ZTI1A>:        0x00007ffff7fa2fa0      0x000055555555607e
0x555555557d70: 0x0000000000000001      0x00000000000001b6
0x555555557d80: 0x0000000000000001      0x00000000000001c5
0x555555557d68       0x000055555555607e  --> typeid(a)  __name = 1A
0x555555557d60       0x00007ffff7fa2fa0  --> typeid(a) _vptr.type_info
0x555555557d58       0x0000555555557d60
0x555555557d50       0x000055555555607b  --> typeid(b)  __name = 1B
0x555555557d48       0x00007ffff7fa3c30  --> typeid(b) _vptr.type_info
0x555555557d40       0x00005555555553de  --> A::virtual_func3()
0x555555557d38       0x0000555555555362  --> A::virtual_func1()
0x555555557d30       0x0000555555557d60
0x555555557d28       0x0000000000000000
0x555555557d20       0x00005555555554e2  --> B::virtual_fun1()
0x555555557d18       0x000055555555555e  --> B::virtual_func3()
0x555555557d10       0x0000555555555362  --> A::virtual_func1()

gdb常用命令总结

记录使用gdb调试时常用的命令

1. cheat sheet

显示类命令缩写命令说明
infoi查看断点 / 线程等信息
printp打印变量或寄存器值
displaydisplay自动显示命令
whatiswhatis查看变量类型
ptypeptype查看变量类型
listl显示源码
disassembledis查看汇编代码
backtracebt查看当前线程的调用堆栈
helphelp帮助命令
控制类型的命令缩写命令说明
runr运行一个待调试的程序
continuec让暂停的程序继续运行
nextn运行到下一行
steps单步执行,遇到函数会进入
untilu运行到指定行停下来
finishfi结束当前调用函数,回到上一层调用函数处
returnreturn结束当前调用函数并返回指定值,到上一层函数调用处
jumpj将当前程序执行流跳转到指定行或地址
断点监视点缩写命令说明
breakb添加断点
tbreaktb添加临时断点
deleted删除断点
enableenable启用某个断点
disabledisable禁用某个断点
watchwatch监视某一个变量或内存地址的值是否发生变化
名称缩写命令说明
framef切换到当前调用线程的指定堆栈
threadthread切换到指定线程
set argsset args设置程序启动命令行参数
show argsshow args查看设置的命令行参数

1.1 常用调试方式

  1. 直接调试目标程序:
  2. 附加进程 id:gdb attach pid
  3. 调试 core 文件:gdb filename corename

调试目标程序

gdb ./hello_world

调试一个正在运行的程序

# 启动时直接attach到一个进程
gdb -p PID
# 在gdb环境中 attach一个进程
(gdb) attach PID

调试 core 文件:

gdb ./program_name corename

1.2 常用命令

退出命令:q(quit)或者 Ctr + d 启动程序:r (run) 继续运行:c (continue) 查看调用栈:bt (backtrace) 进入调用栈:f (frame)堆栈编号 单步执行:n(next),遇到函数直接跳过,不进入函数内部 单步执行:s(step),会进入函数内部 退出当前函数:finish,继续执行完当前函数,正常退出 返回当前函数:return,从当前位置,从当前函数退出,剩下的代码不执行了,可以指定函数的返回值; 指定位置停下来:until,和 break 类似 查看某段代码的汇编指令:disassemble 帮助命令:help

1.3 断点

1.3.1 设置断点

程序运行到某一行

b test.cpp 100

程序运行到某一个函数

b namespace::func

其他文件设置

b a.cpp:441

其他文件的某一个函数

b a.cpp:func

设置条件断点

b if i = 8

1.3.2 查看断点

info b

1.3.3 删除断点

删除第几个断点

delete N

删除第几行的断点

clear N

1.3.4使能断点

使能第 N 个断点

enable N

不使能第 N 个断点

disable N

1.4 监视点

单个变量

watch i

指针变量,监视的是指针变量本身,

watch p

指针所指向的变量,监视指针所值的内容:

watch *p

数组,可以监视整个数组的内容

watch a

注意:当监视的变量变化时会自动停下来 当监视的变量脱离了作用域,也就失效了;

1.5 打印

调用函数

p func()

调试过程中修改变量的值

print var=value

计算表达式并打印

print a+b+c

输出当前对象的各成员变量的值

print *this

查看变量类型

whatis var

查看变量类型,可以查看复合数据类型,会打印出该类型的成员变量

ptype val

1.5.1 打印字符串

打印 std::string

打印内容

p str
p str.c_str()

打印长度

p str.length()
p str.size()

字符串太长

set print elements 0

1.5.2 打印 vector 和 map

1.6 list 命令

输出上一次list命令显示的代码后面的代码,如果是第一次执行list命令,则会显示当前正在执行代码位置附近的代码;

list

上一次 list 命令显示的代码前面的代码

list -

显示当前代码文件第 Linenumber 行附近的代码;

list Linenumber

显示 FileName 文件第 LineNo 行附近的代码

list FileName:Linenumber

显示当前文件的 FunctionName 函数附近的代码

list FunctionName

显示 FileName 文件的 FunctionName 函数附件的代码

list FileName:FunctionName

其中fromto是具体的代码位置,显示这之间的代码

list from,to

1.7 jump 命令

  1. 中间跳过的代码是不会执行的;
  2. 跳到的位置后如果没有断点,那么GDB会自动继续往后执行;

跳转到第几行

jump line_number

跳转往下 10行

jump + 10

跳转到具体的内存地址

jump *0X12345678

1.8 display 命令

基本用法

display expression
  1. 跟踪变量值: 如果您正在调试一个C程序,并且想观察变量foo的值随程序执行的变化,可以这样设置:

    (gdb) display foo
    
  2. 显示内存地址内容: 若要跟踪特定内存地址(如0x12345678)的内容:

    (gdb) display *0x12345678
    
  3. 跟踪表达式结果: 如果您关心的是某个计算结果或复杂表达式的值,比如变量ab之和:

    (gdb) display a + b
    
  4. 格式化输出: 可以使用/fmt选项来指定显示值的格式,类似于printf函数的格式化字符串。例如,以十六进制显示整数:

    (gdb) display/x foo
    

    或以浮点数的科学记数法显示:

    (gdb) display/g foo
    

    查看GDB文档或使用help format命令获取更多关于格式化选项的信息。

  5. 查看已设置的display项: 如果您设置了多个display命令,可以使用info display命令列出所有当前生效的自动显示项及其编号:

    (gdb) info display
    
  6. 取消自动显示: 要取消之前设置的某个display项,使用undisplay命令并指定其编号:

    (gdb) undisplay 1
    

    上述命令将取消编号为1的display设置。

  7. 临时禁用所有自动显示: 如果想暂时关闭所有自动显示,而不取消它们的设置,可以使用disable display命令:

    (gdb) disable display
    

    后续可以通过enable display命令重新启用所有自动显示。

  8. 查看即将执行的汇编指令: 在调试汇编程序时,使用display/i $pc命令可以显示每次程序暂停时即将执行的汇编指令:

    (gdb) display/i $pc
    

    $pc是GDB的内部变量,代表当前程序计数器的值,即下一条要执行的指令地址。

2 调试分析 core 文件

  1. 用 gdb 打开 core 文件
  2. 查看堆栈信息
  3. 进入某个栈:f N,f 是 frame 的缩写,N 是栈号,如0、1、2、3…
  4. 进入栈之后,查看变量,print
gdb ./program core
bt
where
f 3

3 多线程调试

3.1 常用的命令

查看所有线程信息

info thread

切换线程

thread thread_number

查看所有线程的栈信息

thread apply all bt

在特定线程编号上打断点

break location thread thread_number

通用的命令, 可以用 all,作用禹全部线程

thread apply thread_number1 thread_number2 ... command

3.2 模式

  • all-stop mode,全停模式,当程序由于任何原因在 GDB 下停止时,不止当前的线程停止,所有的执行线程都停止。这样允许你检查程序的整体状态,包括线程切换,不用担心当下会有什么改变。
  • non-stop mode,不停模式,调试器(如VS2008和老版本的GDB)往往只支持 all-stop 模式,但在某些场景中,我们可能需要调试个别的线程,并且不想在调试过程中影响其他线程的运行,这样可以把GDB的调式模式由 all-stop 改成 non-stop7.0 版本的GDB引入了 non-stop 模式。在 non-stop 模式下 continue、next、step 命令只针对当前线程。
  • record mode,记录模式;记录模式允许 GDB 记录程序的执行过程,包括线程的运行、变量的修改等,并将这些信息保存到记录文件中。这个功能可以帮助您分析程序的运行过程,并且可以在之后的时间点回放这些记录,从而帮助您定位问题或理解程序的执行流程。
  • replay mode,回放模式;回放模式允许您回放之前记录的执行过程。在这个模式下,您可以重现程序之前的执行状态,以便更深入地分析和调试程序。这种模式在查找程序运行中的难以复现的问题时非常有用。

non-stop 模式

gdb -exec-run-mode non-stop your_program

记录模式

gdb -record record_file_name your_program

回放模式

gdb -replay record_file_name

3.3 设置线程锁

默认的调试模式:一个线程暂停运行,其他线程也随即暂停;一个线程启动运行,其他线程也随即启动

但在一些场景中,我们希望只让特定线程运行,其他线程都维持在暂停状态,即要防止线程切换

  • set scheduler-locking on,锁定线程,只有当前或指定线程可以运行;专注于一个调试一个线程。
  • set scheduler-locking off,在单步调试当前线程时,其他线程不受影响,继续并发执行。
  • set scheduler-locking step,当单步执行某一线程时,其他线程不会执行,同时保证在调试过程中当前线程不会发生改变。但如果在该模式下执行 continue、until、finish 命令,则其他线程也会执行;
  • show scheduler-locking,查看线程锁定状态;

4 技巧

和 shell 一样,有一些简便操作:

  1. 上下方向键可以查看最近的命令
  2. 回车,执行上一次执行的命令,单步调试非常有用

Reference:

  1. https://zhuanlan.zhihu.com/p/297925056
  2. https://www.zhihu.com/question/65306462

一个奇怪问题的debug之旅

介绍了debug一次奇怪问题的过程!

问题

公司的守护进程的优雅关闭,是通过捕获 SIGTERM 实现的:

  1. 捕获 SIGTERM
  2. 退出处理(释放资源等等)
  3. 最后调用 pthread_exit 退出

出现的问题是:优雅关闭时,进程又收到了 SIGABRT ,导致进程又被异常杀死了,最后进程并没有优雅的关闭。

当然在公司定位这个问题的过程中,都是通过日志和 gdb 工具来进行的。

现在我们模拟一下解决这个问题的过程,为了方便起见,就使用打印和 gdb 来进行debug

模拟的问题代码如下:

#include <iostream>
#include <pthread.h>
#include <signal.h>

void printSigInfo(int signo, siginfo_t* info, void* context)
{
    std::cout << "Signal Info:" << std::endl;
    std::cout << "  si_signo: " << info->si_signo << " - Signal number" << std::endl;
    std::cout << "  si_code: " << info->si_code << " - Signal code" << std::endl;
    std::cout << "  si_errno: " << info->si_errno << " - Errno value associated with signal" << std::endl;
    if (info->si_code > 0 && info->si_code < NSIG) { // 可能需要针对具体情况进行检查
        std::cout << "  si_pid: " << info->si_pid << " - Sending process ID" << std::endl;
        std::cout << "  si_uid: " << info->si_uid << " - Real user ID of sending process" << std::endl;
        if (info->si_addr) {
            std::cout << "  si_addr: " << static_cast<void*>(info->si_addr) << " - Faulting address" << std::endl;
        }
    }
}

void LinuxSigHandler(int signo, siginfo_t* info, void* context)
{
    switch (signo) {
        case SIGABRT:
            std::cout << "Caught signal SIGABRT (" << signo << ")." << std::endl;
            printSigInfo (signo, info, context);
            break;
        case SIGTERM:
            std::cout << "Caught signal SIGTERM (" << signo << ")." << std::endl;
            printSigInfo (signo, info, context);
            pthread_exit(0);
            break;

        default:
            std::cout << "Caught signal " << signo << std::endl;
            break;
    }

    // 重新引发信号
    raise(signo);
}

void InitSignalHandlers(void)
{
    struct sigaction action;
    action.sa_handler = NULL;
    action.sa_sigaction = LinuxSigHandler;
    sigemptyset (&action.sa_mask);
    action.sa_flags = (typeof(action.sa_flags))(SA_RESTART | SA_SIGINFO | SA_RESETHAND);
    sigaction (SIGSEGV, &action, NULL);
    sigaction (SIGILL, &action, NULL);
    sigaction (SIGFPE, &action, NULL);
    sigaction (SIGBUS, &action, NULL);
    sigaction (SIGABRT, &action, NULL);
    sigaction (SIGTERM, &action, NULL);
    /* Ignored signals. */
    struct sigaction action_ignore;
    action_ignore.sa_handler = SIG_IGN;
    action_ignore.sa_flags = 0;
    sigemptyset (&action_ignore.sa_mask);
    sigaction (SIGPIPE, &action_ignore, NULL);
}

void main_thread()
{
    for (;;)
    {
        std::cout << "main_thread is running:" << std::endl;
        sleep(10);
    }
}

int main()
{
    InitSignalHandlers();
    try {
        main_thread();
    } catch (const std::exception& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }
    catch (...) {
        std::cerr << "Caught unknown exception" << std::endl;
    }
    std::cerr << "process close" << std::endl;
    return 0;
}

我们先来观察一段这段代码,它有如下特点:

  1. 它是一个守护进程,死循环在处理业务逻辑
  2. 它调用了 sigaction 函数修改了与指定信号的相关联的处理动作
    1. 注意 SIGTERM: 收到这个信号,做打印并调用了 pthread_exit
    2. SIGABRT: 收到这个信号,做打印;
  3. 当收到 SIGTERM 时,它应该有打印,并优雅的退出,不应该再有其他的打印。

我们如何测试呢?

  1. 我们测试这个程序现在用 kill -SIGTERM 实现。公司的系统优雅的关闭就是通过 systemd 发送 SIGTERM 实现的。

现在我们来做一个测试吧!

我们来以这段代码做个测试,编译这段代码得到可执行文件 abi

g++ abi.cpp -o abi

在窗口 1 运行它

./abi
main_thread is running:
main_thread is running:

在窗口 2 里面执行:

ps -ef | grep abi
tlj       980622  976483  0 08:36 pts/0    00:00:00 ./abi
$ kill -SIGTERM 980622

观察窗口 1:

./abi
main_thread is running:
main_thread is running:
Caught signal SIGTERM (15).
Signal Info:
  si_signo: 15 - Signal number
  si_code: 0 - Signal code
  si_errno: 0 - Errno value associated with signal
Caught unknown exception
FATAL: exception not rethrown
Caught signal SIGABRT (6).
Signal Info:
  si_signo: 6 - Signal number
  si_code: -6 - Signal code
  si_errno: 0 - Errno value associated with signal
Aborted (core dumped)

分析

??? 怎么又收到了一个 SIGABRT ,还异常关闭了?

从打印可以看到

Caught unknown exception

这里捕获了一个异常,那么按照正常的逻辑,它应该继续往下执行 std::cerr << "process close" << std::endl; 才对,结果它没有往下执行,到这里就收到了 SIGABRT.

十分怪异!

那我们先来看一下 core dump 看看发生了什么?

先启用coredump

ulimit -c unlimited
echo "/var/crash/core-%e-%p-%t" > /proc/sys/kernel/core_pattern

用 gdb 看一下 core 的情况:

root@ubuntu:/mnt/work/cs-note-src/src/cpp/debug# gdb ./abi core_abi_2262_1712044154
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./abi...
[New LWP 2262]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Core was generated by `./abi'.
Program terminated with signal SIGABRT, Aborted.
#0  __pthread_kill_implementation (no_tid=0, signo=6, threadid=139802159551424) at ./nptl/pthread_kill.c:44
44      ./nptl/pthread_kill.c: No such file or directory.
(gdb)
(gdb) bt
#0  __pthread_kill_implementation (no_tid=0, signo=6, threadid=139802159551424) at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal (signo=6, threadid=139802159551424) at ./nptl/pthread_kill.c:78
#2  __GI___pthread_kill (threadid=139802159551424, signo=signo@entry=6) at ./nptl/pthread_kill.c:89
#3  0x00007f263a21b476 in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4  0x00007f263a2017f3 in __GI_abort () at ./stdlib/abort.c:79
#5  0x00007f263a2623dc in __libc_message (action=do_abort, fmt=0x7f263a3b479c "%s", fmt=0x7f263a3b479c "%s", action=do_abort)
    at ../sysdeps/posix/libc_fatal.c:155
#6  0x00007f263a2626f0 in __GI___libc_fatal (message=<optimized out>) at ../sysdeps/posix/libc_fatal.c:164
#7  0x00007f263a2763f6 in unwind_cleanup (reason=<optimized out>, exc=<optimized out>) at ./nptl/unwind.c:114
#8  0x0000562cea54094f in main () at new_abi.cpp:85
(gdb)

从这里看出来似乎就是从 main 函数发生了错误,剩下的调用栈都是库函数了。

到这里就没有思路了,main 哪里写的有问题?

catch(...) 的时候出错了,我们去掉,重新测试,还真没有了异常!

root@ubuntu:/mnt/work/cs-note-src/src/cpp/debug# ./abi
main_thread is running:
Caught signal SIGTERM (15).
Signal Info:
  si_signo: 15 - Signal number
  si_code: 0 - Signal code
  si_errno: 0 - Errno value associated with signal

那我们基本就知道了,是 catch(...) 带来的这个问题。那么究竟是怎么带来的呢?

那我们就要了解 C++ stack unwinding

Stack unwinding 栈展开

堆栈展开是在运行时从函数调用堆栈中删除函数条目的过程。局部对象以与构建它们的相反顺序被销毁。

堆栈展开通常与异常处理有关。在 C++ 中,当发生异常时,将线性搜索函数调用堆栈以查找异常处理程序,并且从函数调用堆栈中删除具有异常处理程序的函数之前的所有条目。因此,如果在同一函数(抛出异常的位置)中未处理异常,则异常处理涉及堆栈展开。基本上,堆栈展开是为运行时构造的所有自动对象调用析构函数(每当引发异常时)的过程。

#include <iostream>
#include <string>
#include <memory>

using namespace std;

class A
{
public:
    A (int a) : a(a) { cout << "A::A(): " << a << endl; }
    virtual ~A() { cout << "A::~A(): " << a << endl; }
    void disp() { cout <<"A disp" << endl; }
private:
    int a;
};

class B : public A
{
public:
    B (int a, const string &b) : A(a), b(b) { cout << "B::B(): " << b << endl; }
    ~B() { cout << "B::~B(): " << b << endl; }
    void disp() { cout <<"B disp" << endl; }
private:
    int a;
    string b;
};

void f1()
{
    cout << "f1() Start " << endl;
    B b1(1, "stack object");
    A *P = new B(2, "heap object");
    shared_ptr<A> p2 = make_shared<B>(3, "shared ptr object");  
    throw 100;
    cout << "f1() End " << endl;
}

void f2()
{
    cout << "f2() Start " << endl;
    f1();
    cout << "f2() End " << endl;
}

void f3()
{
    cout << "f3() Start " << endl;
    try {
        f2();
    }
    catch (int i) {
        cout << "Caught Exception: " << i << endl;
    }
    cout << "f3() End" << endl;
}

int main()
{
    f3();
    return 0;
}
f3() Start
f2() Start
f1() Start
A::A(): 1
B::B(): stack object
A::A(): 2
B::B(): heap object
A::A(): 3
B::B(): shared ptr object
B::~B(): shared ptr object
A::~A(): 3
B::~B(): stack object
A::~A(): 1
Caught Exception: 100
f3() End

当异常发生的时候,如果没有异常处理程序,这些局部变量一个一个去销毁的。需要注意的是 p2 ,当异常发生时,没有发生析构,这说明,这里会有资源泄露,所以当在实际项目中,需要注意到这一点。

现在我们知道了异常处理的过程,那么我们的测试程序究竟捕获了什么异常呢?为什么又导致了 SIGABRT 呢?

从调用栈,我们看到 main 函数调用的是 nptl/unwind.c 中的 unwind_cleanup 函数,咱们先看看这个函数到底是怎么回事?

// https://github.com/lattera/glibc/blob/master/nptl/unwind.c
static void
unwind_cleanup (_Unwind_Reason_Code reason, struct _Unwind_Exception *exc)
{
  /* When we get here a C++ catch block didn't rethrow the object.  We
     cannot handle this case and therefore abort.  */
  __libc_fatal ("FATAL: exception not rethrown\n");
}

看这里的注释

“当程序运行到这里,那就是 C++ catch 块没有重新抛出对象。我们无法处理这种情况,所以触发 abort”

那么就可以明白了,我们 catch 了一个异常,但是没有重新抛出,所以 glibc 出发了 abort。

那么我们 catch 的这个异常到底是什么呢?从我们的程序可以看出,我们只是调用了 pthread_exit, pthread 本身是应该只是 C 库,并不是 C++库,它不应该抛出异常啊。

这和 glibcgcc 的实现相关!

pthread_exit 或者 pthread_cancel 都会抛出一个 ___forced_unwind 异常,用于在线程退出期间展开堆栈 (stack unwinding). 当程序捕获了这个异常,需要务必重新抛出它,以便系统可以做完整的 stack unwinding

如果捕获了这个异常,那么栈展开就没办法继续进行下去了,所以 glibc 就会调用系统调用 abort 函数来进行 SIGABRT 异常处理。

修改 并测试

知道了真正原因,那么修改我们原有的程序,让它优雅的关闭吧

int main()
{
    InitSignalHandlers();
    try {
        main_thread();
    } catch (const std::exception& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }
	catch (abi::_forced_unwind&) {
		std::cerr << "Caught forced_unwid error and retrow it" << std::endl;
		throw;
	}
    catch (...) {
        std::cerr << "Caught unknown exception" << std::endl;
    }
    std::cerr << "process close" << std::endl;
    return 0;
}

测试一下,成功了,程序可以优雅关闭了!

main_thread is running:
main_thread is running:
Caught signal SIGTERM (15).
Signal Info:
  si_signo: 15 - Signal number
  si_code: 0 - Signal code
  si_errno: 0 - Errno value associated with signal
Caught forced_unwid error and retrow it

总结:

  1. 异常处理程序,要千万注意资源泄露;
  2. 异常处理程序的 stack unwinding 的过程是怎么样的。
  3. 通过 gdb 和 core dump 来进行 debug。

Reference:

  1. https://stackoverflow.com/questions/11452546/why-does-pthread-exit-throw-something-caught-by-ellipsis
  2. https://cxx-abi-dev.codesourcery.narkive.com/VlHf6T8W/gcc-unwind-abi-change-for-forced-unwind
  3. https://www.geeksforgeeks.org/stack-unwinding-in-c/
  4. https://blog.csdn.net/Jqivin/article/details/121908435
  5. https://copyprogramming.com/howto/pthread-exit-null-not-working

abbrenglishchinese
DPDebug Port调试端口
MMUMemory Management Unit内存管理单元
EXIExternal Interface外部接口
XIFCrossbar Interface交叉开关接口
TMTraffic Manager流量管理器
TM OQTraffic Manager Output Queue流量管理器输出队列
TM POLTraffic Manager Policy流量管理器策略
TM SCHTraffic Manager Scheduler流量管理器调度器
PSTATPort Status端口状态
ETHEthernet以太网
ETH MACEthernet Media Access Control以太网介质访问控制
ETH PCSEthernet Physical Coding Sublayer以太网物理编码子层
ETH ADAPTEthernet Adaptation以太网适配
RNDRandom
intrinterrupt中断
ECCError Correcting Code纠错码
SRAMStatic Random Access Memory静态随机存取存储器
DRAMDynamic Random Access Memory动态随机存取存储器
int ioctl(int fd, unsigned long op, ...);  /* glibc, BSD */

RAII