实现动态库捕获异常信号并保存函数堆栈信息

本文档记录实现一个异常信号捕获并保存函数堆栈信息日志的动态库的过程。主程序实现主进程、子进程以及子线程打印自身进程ID和线程ID,向主进程发送信号,动态库进行捕获并记录函数堆栈信息。

一、进程的创建

1.使用fork()创建子进程

fork 函数是大多数基于 Unix 的操作系统中可用的 POSIX 兼容系统调用。该函数创建了一个新的进程,它是原始调用程序的副本。后一个进程称为 parent,新创建的进程-child。这两个进程可以看作是在不同内存空间执行的两个线程。需要注意的是,目前 Linux 的实现内部没有线程的概念,所以线程除了共享内存区域外,其他结构与进程类似。fork 函数可以实现同一程序内的并发执行,也可以从文件系统中运行一个新的可执行文件。

利用 fork 来演示一个程序内的多进程。fork 不接受参数,返回值是父进程中子进程的 PID,子进程中返回 0。如果调用失败,在父进程中返回 -1。因此,可以根据返回值的评估来构造 if 语句,每个 if 块都会被相应的进程执行,从而实现并发执行。

pid_t c_pid = fork();

if (c_pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
} else if (c_pid > 0) {        
        cout << "printed from parent process " << getpid() << endl;
        wait(nullptr);
} else {
        cout << "printed from child process " << getpid() << endl;
        exit(EXIT_SUCCESS);
}

2.使用 fork() 和 execve()创建多个进程

fork 函数调用更实际的用途是创建多个进程,并在这些进程中执行不同的程序。下面这个例子中,包括两个源代码文件:一个是父进程,另一个是子进程。子进程代码是简单的无限循环,加到单整数,可以通过发送 SIGTERM 信号来停止。

父程序声明一个需要被分叉的子进程执行的文件名,然后调用 spawnChild 函数 6 次。spawnChild 函数封装了 fork/execve 的调用,并返回新创建的进程 ID。注意,execve 需要一个程序名和参数列表作为参数,才能在子进程中启动新的程序代码。一旦 6 个子进程创建完毕,父进程继续在 while 循环中调用 wait 函数。wait 停止父进程并等待任何一个子进程终止。

注意,需要终止每个子进程,父进程才能正常退出。如果中断父进程,子进程将继续运行,其父进程成为一个系统进程。

#include <iostream>
#include <vector>
#include <sys/wait.h>
#include <unistd.h>
#include <atomic>
#include <filesystem>

using std::cout;
using std::endl;
using std::vector;
using std::string;
using std::filesystem::exists;

constexpr int FORK_NUM = 6;

pid_t spawnChild(const char* program, char** arg_list)
{
        pid_t ch_pid = fork();
        if (ch_pid == -1) {
                perror("fork");
                exit(EXIT_FAILURE);
        } else if (ch_pid > 0) {
                cout << "spawn child with pid - " << ch_pid << endl;
                return ch_pid;
        } else {
                execve(program, arg_list, nullptr);
                perror("execve");
                exit(EXIT_FAILURE);
        }
}

int main() {
        string program_name ("child");
        char *arg_list[] = {program_name.data(), nullptr};
        vector<int> children;
        children.reserve(FORK_NUM);

        if (!exists(program_name)){
                cout << "Program file 'child' does not exist in current directory!\n";
                exit(EXIT_FAILURE);
        }

        for (int i = 0; i < FORK_NUM; ++i)
                children[i] = spawnChild(program_name.c_str(), arg_list);
                

        cout << endl;

        pid_t child_pid;
        while ((child_pid = wait(nullptr)) > 0)
                cout << "child " << child_pid << " terminated" << endl;

        return EXIT_SUCCESS;
}

#include <iostream>
#include <sys/wait.h>
#include <unistd.h>

volatile sig_atomic_t shutdown_flag = 1;

void GracefulExit(int signal_number)
{
        shutdown_flag = 0;
}

int main()
{
        // Register SIGTERM handler
        signal(SIGTERM, GracefulExit);

        unsigned int tmp = 0;
        while (shutdown_flag) {
                tmp += 1;
                usleep(100);
        }

        exit(EXIT_SUCCESS);
}

二、线程的创建

多线程是实现多任务处理的一种最常用的手段,线程相比进程而言显得轻量级。核心在于 pthread 这个库,调用 pthread_create()函数就可以创建一个线程。

它的函数原型如下:

#include <pthread.h>
extern int pthread_create (pthread_t *__restrict __newthread,

                                          const pthread_attr_t *__restrict __attr,
                                          void (__start_routine) (void *),
                                          void *__restrict __arg)

参数说明:第一个参数是 pthread_t* 也就是代表线程实体的指针第二个参数为了设置线程的属性,一般为 NULL第三个参数是线程运行时的函数,这是个函数指针第四个参数也是一个指针,它是用来将数据传递进线程的运行函数

下面用一个代码来示例说明。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

//线程函数
void *test(void *arg)
{
        int i;
        char* name = (char *)arg;
        for(i=0;i<8;i++){
                printf("the pthread %s running ,count: %d\n",name,i);
                sleep(1);
        }
}
int main (int argc,char* argv)
{
        pthread_t pId;
        int i,ret;
        //创建子线程,线程id为pId
        ret = pthread_create(&pId,NULL,test,"sub pthread");
        if(ret != 0){
                printf("create pthread error!\n");
                exit(1);
        }

        for(i=0;i < 5;i++){
                printf("main thread running ,count : %d\n",i);
                sleep(1);
        }
        printf("main thread will exit when pthread is over\n");

        

        //等待线程pId的完成
        pthread_join(pId,NULL);
        

        printf("main thread exit\n");

        return 0;
}

pthread_join(),它的作用是挂起当前的线程,等待指定的线程运行完毕。在示例代码中主线程等待子线程执行完毕后才继续执行后面的代码。

线程与线程之间经常进行数据通讯,pthread_create()最后一个参数就是用来传递数据的。但需要特别注意的是它是 void * 的,也是就是无类型指针,这样做的目的是为了保证线程能够接受任意类型的参数,到时候再强制转换。

pthread 是一个动态库,编译的时候需要动态链接,不然程序会报错。

gcc -o test test.c -lpthread

三、Linux中信号机制

在终端输入kill -l命令可以查看系统支持的信号

xutianqi@xutianqi-pc:~$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

列表中,编号为1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。

下面我们对编号小于SIGRTMIN的信号进行讨论。

1) SIGHUP
本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录,wget也 能继续下载。

此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

2) SIGINT
程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

3) SIGQUIT
和SIGINT类似, 但由QUIT字符(通常是Ctrl-\)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。

4) SIGILL
执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。

5) SIGTRAP
由断点指令或其它trap指令产生. 由debugger使用。

6) SIGABRT
调用abort函数生成的信号。

7) SIGBUS
非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。

8) SIGFPE
在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。

9) SIGKILL
用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。

10) SIGUSR1
留给用户使用

11) SIGSEGV
试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.

12) SIGUSR2
留给用户使用

13) SIGPIPE
管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

14) SIGALRM
时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.

15) SIGTERM
程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。

17) SIGCHLD
子进程结束时, 父进程会收到这个信号。如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程来接管)。

18) SIGCONT
让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符...

19) SIGSTOP
停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.

20) SIGTSTP
停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号

21) SIGTTIN
当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.

22) SIGTTOU
类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.

23) SIGURG
有"紧急"数据或out-of-band数据到达socket时产生.

24) SIGXCPU
超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。

25) SIGXFSZ
当进程企图扩大文件以至于超过文件大小资源限制。

26) SIGVTALRM
虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.

27) SIGPROF
类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.

28) SIGWINCH
窗口大小改变时发出.

29) SIGIO
文件描述符准备就绪, 可以开始进行输入/输出操作.

30) SIGPWR
Power failure

31) SIGSYS
非法的系统调用。

在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP
不能恢复至默认动作的信号有:SIGILL,SIGTRAP
默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
默认会导致进程退出的信号有:SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH

此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞

四、查看程序堆栈信息

1.backtrace

int backtrace(void **buffer,int size)

该函数用于获取当前线程的调用堆栈,获取的信息将会被存放在buffer中,它是一个指针数组。参数 size 用来指定buffer中可以保存多少个void* 元素。

函数返回值是实际获取的指针个数,最大不超过size大小在buffer中的指针实际是从堆栈中获取的返回地址,每一个堆栈框架有一个返回地址。

注意某些编译器的优化选项对获取正确的调用堆栈有干扰,另外内联函数没有堆栈框架;删除框架指针也会使无法正确解析堆栈内容

2.backtrace_symbols

char ** backtrace_symbols (void *const *buffer, int size)

backtrace_symbols将从backtrace函数获取的信息转化为一个字符串数组. 参数buffer是从backtrace函数获取的数组指针,size是该数组中的元素个数(backtrace的返回值),函数返回值是一个指向字符串数组的指针,它的大小同buffer相同.每个字符串包含了一个相对于buffer中对应元素的可打印信息.它包括函数名,函数的偏移地址,和实际的返回地址。
现在,只有使用ELF二进制格式的程序和库才能获取函数名称和偏移地址。在其他系统,只有16进制的返回地址能被获取.另外,你可能需要传递相应的标志给链接器,以能支持函数名功能(比如,在使用GNU ld的系统中,你需要传递(-rdynamic))
backtrace_symbols生成的字符串都是malloc出来的,但是不要最后一个一个的free,因为backtrace_symbols是根据backtrace给出的call stack层数,一次性的malloc出来一块内存来存放结果字符串的,所以,像上面代码一样,只需要在最后,free backtrace_symbols的返回指针就OK了。这一点backtrace的manual中也是特别提到的。
注意:如果不能为字符串获取足够的空间函数的返回值将会为NULL

打印backtrace_symbols函数返回的字符串数组

[ShowStack]:[29]
0: ./test(_Z9ShowStackv+0x2e) [0x558633d33238]
1: ./test(_Z7handleri+0xb3) [0x558633d333ee]
2: /lib/x86_64-linux-gnu/libc.so.6(+0x3f040) [0x7f4cb3be6040]
3: /lib/x86_64-linux-gnu/libc.so.6(nanosleep+0x14) [0x7f4cb3c8b774]
4: /lib/x86_64-linux-gnu/libc.so.6(sleep+0x3a) [0x7f4cb3c8b67a]
5: ./test(_Z3fn3v+0x36) [0x558633d33427]
6: ./test(_Z3fn2v+0x9) [0x558633d33433]
7: ./test(_Z3fn1v+0x9) [0x558633d33467]
8: ./test(main+0x365) [0x558633d33857]
9: /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7f4cb3bc8bf7]
10: ./test(_start+0x2a) [0x558633d3312a]

其中,_Z3fn3v+字样实则是程序中的函数名,用c++flit可以解析出原函数名

$ c++filt _Z3fn3v
fn3()
-rdynamic
Pass the flag -export-dynamic to the ELF linker, on targets that support it. This instructs the linker to add all symbols, not only used ones, to the dynamic symbol table. This option is needed for some uses of "dlopen" or to allow obtaining backtraces from within a program.

五、程序异常捕获

1.signal函数

函数原型:

sighandler_t signal(int signum, sighandler_t handler);

其中,sighandler定义是这样的:typedef void (*sighandler_t)(int);

函数作用:

注册一个信号捕捉函数,也就是说,收到了某个信号,就执行它所注册的回调函数。

函数参数:

signum:信号编号,尽量用宏来写,而别用数字,这样更适合跨平台;

handler:注册的回调函数;

在handler函数接收到指定信号时,可以执行相应的操作,比如打印函数堆栈信息。

void ShowStack(){
        int i;
        void *buffer[BACKTRACE_SIZE];

        int n = backtrace(buffer, BACKTRACE_SIZE);
        printf("[%s]:[%d} n = %d\n", __func__, __LINE__, n);

        char **symbols = backtrace_symbols(buffer, n);
        if (NULL == symbols){
                perror("backtrace symbols");
                exit(EXIT_FAILURE);
        }
        printf("[%s]:[%d]\n", __func__, __LINE__);
        for (i = 0;i<n;i++)
        {
                printf("%d: %s\n", i, symbols[i]);
        }

        free(symbols);
}

void handler(int signo){
        if (signo==SIGSEGV) {
                printf("Receive SIGSEGV signal\n");
                ShowStack();
                exit(-1);
        }
        ...
}

int main(){
        signal(SIGSEGV, handler);
        ...
}

2.发送信号并捕获

为程序设置睡眠等待sleep(1800); 执行程序运行并使用kill命令发送信号。

在程序中使用getpid()打印进程pid。或者在程序运行中,ps -ef | grep [程序名]

kill -SIGNUM pid

通过kill命令为指定进程号发送信号,查看是否成功捕获。

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <thread>
#include <sys/syscall.h>
#include <execinfo.h>
#include <signal.h>
#include <vector>

#define gettid() syscall(SYS_gettid)

#define BACKTRACE_SIZE 16
using namespace std;

pthread_t subthread[2];
int tmp;

void ShowStack(){
        int i;
        void *buffer[BACKTRACE_SIZE];

        int n = backtrace(buffer, BACKTRACE_SIZE);
        printf("[%s]:[%d} n = %d\n", __func__, __LINE__, n);
        char **symbols = backtrace_symbols(buffer, n);
        if (NULL == symbols){
                perror("backtrace symbols");
                exit(EXIT_FAILURE);
        }
        printf("[%s]:[%d]\n", __func__, __LINE__);
        for (i = 0;i<n;i++)
        {
                printf("%d: %s\n", i, symbols[i]);
        }
        free(symbols);
}

void handler(int signo)
{
        switch(signo) {
        case SIGABRT:
                printf("Receive SIGABRT signal\n");
                break;
        case SIGSEGV:
                printf("Receive SIGSEGV signal\n");
                break;
        case SIGBUS:
                printf("Receive SIGBUS signal\n");
                break;
         case SIGUSR1:
                printf("Receive SIGUSR1 signal\n");
                break;
        case SIGUSR2:
                printf("Receive SIGUSR2 signal\n");
                break;
        case SIGKILL:
                printf("Receive SIGKILL signal\n");
                break;
        default:
                fprintf(stderr, "Should not be here...\n");
        break;
}
        ShowStack();
        //exit(-1);
}


void fn3()
{
        //int *p2=nullptr;
        //*p2=0;
        cout<<"running fn3 successfully!"<<endl;
        sleep(1800);
}
void fn2()
{
        fn3();
        cout<<"fn2 call fn3 successfully!"<<endl;
}
void fn1()
{
        //void *p = malloc(128);
        //free(p);
        //free(p);
        fn2();
        cout<<"fn1 call fn2 successfully!"<<endl;
}


void * test(void *arg)
{
        char *name=(char*)arg;
        printf("son process sub pthread %s pid is:%d\n", name, getpid());

        printf("son process sub pthread %s tid is:%ld\n", name, gettid());
}

int main(){
        void *p=malloc(128);
        signal(SIGSEGV, handler);
        signal(SIGABRT, handler);
        signal(SIGBUS, handler);
        signal(SIGUSR1, handler);
        signal(SIGUSR2, handler);
        cout<<"father pid="<<getpid()<<endl;
        cout<<"father tid="<<syscall(SYS_gettid)<<endl;
        int i=0,ret;
        pid_t pid=fork();
        if (pid<0){
                cout<<"error!"<<endl;
                return 1;
        } else if (pid==0){
                cout<<"fork success, son process pid:"<<getpid()<<endl;
                cout<<"son process tid:"<<gettid()<<endl;
                char *pname1=(char *)"pthread1";
                char *pname2=(char *)"pthread2";
                if ((tmp=pthread_create(&subthread[0], NULL, test, pname1))!=0)
                {
                        printf("create thread1 failed!\n");
                        exit(1);
                } else {
                        printf("create thread1 successed!\n");
                }

                if ((tmp=pthread_create(&subthread[1], NULL, test, pname2))!=0)
                {
                        printf("create thread2 failed!\n");
                        exit(1);
                } else {
                        printf("create thread2 successed!\n");
                }

                cout<<"son process tid:"<<gettid()<<endl;
                if (subthread[0]!=0)
                {
                        pthread_join(subthread[0], NULL);
                        printf("thread1 end~\n");
                }
                if (subthread[1]!=0)
                {
                        pthread_join(subthread[1], NULL);
                        printf("thread2 end!\n");
                }

        } else {
                cout<<"fork success, this is father process, son process id is:"<<pid<<endl;
                fn1();
        }

        

        return 0;

}

六、利用setjmp和longjmp做异常处理

1.Active Record

C语言中每当有一个函数调用时,就会在堆栈(Stack)上准备一个被称为AR(Active Record)的结构,那么如何来操纵AR呢,一个可能的方法是,根据局部变量的地址进行推算,例如对于上面的a函数,执行a函数时的当前AR地址就是参数i的地址偏移8个字节,也就是 ((char*)&i) - 8。然而,不同的C编译器,以及不同的硬件平台都会产生不同的AR结构布局,甚至在一些平台上,AR根本不会存放到Stack中。所以这种方式操纵AR是不通用的。

为此,C语言通过库函数的方式提供了操纵AR的统一方法,那就是setjmp和longjmp函数。

2.setjmp和longjmp

函数原型

#include <setjmp.h>
int setjmp(jmp_buf env);

setjmp 函数的功能是将函数在此处的上下文保存在 jmp_buf 结构体中,以供 longjmp 从此结构体中恢复。

  • 参数 env 即为保存上下文的 jmp_buf 结构体变量;
  • 如果直接调用该函数,返回值为 0; 若该函数从 longjmp 调用返回,返回值为非零,由 longjmp 函数提供。根据函数的返回值,我们就可以知道 setjmp 函数调用是第一次直接调用,还是由其它地方跳转过来的。
void longjmp(jmp_buf env, int val);

longjmp 函数的功能是从 jmp_buf 结构体中恢复由 setjmp 函数保存的上下文,该函数不返回,而是从 setjmp 函数中返回。

  • 参数 env 是由 setjmp 函数保存过的上下文。
  • 参数 val 表示从 longjmp 函数传递给 setjmp 函数的返回值,如果 val 值为0, setjmp 将会返回1,否则返回 val。
  • longjmp 不直接返回,而是从 setjmp 函数中返回,longjmp 执行完之后,程序就像刚从 setjmp 函数返回一样。

3.异常处理和跳转

在本例中,使用setjmp和longjmp进行捕获异常后的跳转。

jmp_buf env;
int tmp = setjmp(env);
if (tmp==0) {
        ...exception!
}

在捕获信号之后,在signal()中调用longjmp(env, signo)调回到setjmp处,此时返回值不为0,程序跳过异常部分继续执行。

参考资料:https://blog.csdn.net/smstong/article/details/50728022

七、静态库与动态库

本程序的目的是实现捕获异常信号,并记录发生异常时的函数堆栈调用信息。其中,捕获信号并打印堆栈调用信息部分可以从主程序部分抽离出来作为一个公共的工具重复使用。本小节介绍linux 下c++中的静态库与动态库,并且建立自己的动态库。

1.什么是库

库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库(.a、.lib)和动态库(.so、.dll)。

所谓静态、动态是指链接。

2.静态库

静态库是在链接阶段,将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库特点总结:

- 静态库对函数库的链接是放在编译时期完成的。

- 程序在运行时与函数库再无瓜葛,移植方便。

- 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。

linux下构建静态库的方式参考链接 https://www.cnblogs.com/johnice/archive/2013/01/17/2864319.html

3.动态库

动态库不像静态库那样,在链接阶段并没有被复制到程序中,反而是程序运行的时候由系统动态的加载到内存中供程序调用,所以这里解决了静态库早晨多份冗余拷贝的缺点,系统只需要载入一次动态库,不同的程序可以得到内存中相同的动态库副本,因此可以节省大量的内存。

在我们的代码中,log.cpp和log.h打包成动态库文件,在主程序main.cpp中调用该库文件。

如下命令是构建动态库.so文件。

g++ -fPIC log.cpp -o liblog.so -shared

动态库的命令以lib开始,.so结尾,中间部分为库名。

-shared作用就是生成共享文件(shared object)。

-fPIC 作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。

PIC使.so文件的代码段变为真正意义上的共享。如果不加-fPIC,则加载.so文件的代码段时,代码段引用的数据对象需要重定位, 重定位会修改代码段的内容,这就造成每个使用这个.so文件代码段的进程在内核里都会生成这个.so文件代码段的copy.每个copy都不一样,取决于 这个.so文件代码段和数据段内存映射的位置。

编译主程序main.cpp时命令如下:

g++ -o main main.cpp /home/newdisk/workspace/codetest/log/liblog.so -llog -lpthread -rdynamic

其中需要加上so文件的路径,-llog意为加载动态库log。

七、设置动态库自动执行

1.__attribute__介绍

  __attribute__可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。__attribute__前后都有两个下划线,并且后面会紧跟一对原括弧,括弧里面是相应的__attribute__参数

  __attribute__语法格式为:__attribute__ ( ( attribute-list ) )

2.constructor和destructor

  若函数被设定为constructor属性,则该函数会在main()函数执行之前被自动的执行。类似的,若函数被设定为destructor属性,则该函数会在main()函数执行之后或者exit()被调用后被自动的执行。

constructor属性可以使函数在main()函数之前执行,destructor属性会让函数在main()函数完成或调用exit()之后被执行。

3.动态库函数属性设置

通过__attribute__ ( ( constructor) )修饰信号注册函数,在主程序中可以不导入动态库,不执行信号注册函数。在程序执行时,在main函数执行之前会自动执行__attribute__ ( ( constructor) )修饰信号注册函数,同样可以捕获程序中的信号异常。这样主程序中没有添加任何代码,就可以通过动态库进行异常捕获和打印日志。

九、其他

1.获取当前程序名

getprogname()是Free BSD库下的函数用于获取当前函数名,在linux下使用该函数时,需要引入<bsd/stdlib.h>,并且在编译时加入-lbsd参数。

2.获取进程、线程tid

Linux中,每个进程有一个pid,类型pid_t,由getpid()取得。Linux下的POSIX线程也有一个id,类型 pthread_t,由pthread_self()取得,该id由线程库维护,其id空间是各个进程独立的(即不同进程中的线程可能有相同的id)。Linux中的POSIX线程库实现的线程其实也是一个进程(LWP),只是该进程与主进程(启动线程的进程)共享一些资源而已,比如代码段,数据段等。

有时候我们可能需要知道线程的真实pid。比如进程P1要向另外一个进程P2中的某个线程发送信号时,既不能使用P2的pid,更不能使用线程的pthread id,而只能使用该线程的真实pid,称为tid。

有一个函数gettid()可以得到tid,但glibc并没有实现该函数,只能通过Linux的系统调用syscall来获取。

#define gettid() syscall(SYS_gettid)

3.执行程序时,constructor被忽略

GCC/G++提供了 -Wl,--as-needed 和 -Wl,--no-as-needed 两个选项,这两个选项一个是开启特性,一个是取消该特性。

在生成可执行文件的时候,通过 -lxxx 选项指定需要链接的库文件。以动态库为例,如果我们指定了一个需要链接的库,则连接器会在可执行文件的文件头中会记录下该库的信息。而后,在可执行文件运行的时候,动态加载器会读取文件头信息,并加载所有的链接库。在这个过程中,如果用户指定链接了一个毫不相关的库,则这个库在最终的可执行程序运行时也会被加载,如果类似这样的不相关库很多,会明显拖慢程序启动过程。

这时,通过指定 -Wl,--as-needed 选项,链接过程中,链接器会检查所有的依赖库,没有实际被引用的库,不再写入可执行文件头。最终生成的可执行文件头中包含的都是必要的链接库信息。-Wl,--no-as-needed 选项不会做这样的检查,会把用户指定的链接库完全写入可执行文件中。

所以,在本程序中的constructor会在编译的时候被忽略掉,需要在链接动态库的时候加入-Wl, --as-needed参数。

g++ -o main main.cpp -Wl,--no-as-needed /home/newdisk/workspace/codetest/log/liblog.so -Wl,--as-needed -llog -lpthread -rdynamic
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值