1. Bug分析

在《Linux信号应用之黑匣子程序设计(上)》一文中,我们实现了一个黑匣子程序——在进程崩溃后,可以保存进程的调用栈。但是,在文章结尾我们说程序有bug,那bug是什么呢?先看下面一个程序:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <execinfo.h>

void blackbox_handler(int sig)
{
    printf("Enter blackbox_handler: ");
    printf("SIG name is %s, SIG num is %dn", strsignal(sig), sig);
    
    // 打印堆栈信息
    printf("Stack information:n");
    int j, nptrs;
#define SIZE 100
    void *buffer[100];
    char **strings;
    
    nptrs = backtrace(buffer, SIZE);
    printf("backtrace() returned %d addressesn", nptrs);
    
    strings = backtrace_symbols(buffer, nptrs);
    if (strings == NULL)
    {
        perror("backtrace_symbol");
        exit(EXIT_FAILURE);
    }
    
    for(j = 0; j < nptrs; j++)
        printf("%sn", strings[j]);
    
    free(strings);
    
    _exit(EXIT_SUCCESS);
}

long count = 0;
void bad_iter()
{
   int a, b, c, d;
   a = b = c = d = 1;
   a = b + 3;
   c = count + 4;
   d = count + 5 * c;
   count++;
   printf("count:%ldn", count);
   
   bad_iter();
}

int main()
{
    struct  sigaction   sa;
    
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = blackbox_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    
    if (sigaction(SIGSEGV, &sa, NULL) < 0)
    {
        return EXIT_FAILURE;
    }
    
    bad_iter();
    
    while(1);
    
    return EXIT_SUCCESS;
}

该程序的执行结果如下:

... ...
count:261856
count:261857
count:261858
count:261859
count:261860
count:261861
Segmentation fault (core dumped)
allan@ubuntu:temp$

该程序是一种极端情况:我们的程序中使用了无线层次的递归函数,导致栈空间被用尽,此时会产生SIGSEGV信号。但是从输出看,并没有走到我们的信号处理函数里面。这是因为但由于栈空间已经被用完,所以我们的信号处理函数是没法被调用的,这种情况下,我们的黑匣子程序是没法捕捉到异常的。

但是该问题也很好解决,我们可以为我们的信号处理函数在堆里面分配一块内存作为“可替换信号栈”。

2. 使用可替换信号栈&&sigaltstack函数

使用可替换栈优化后的程序如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <execinfo.h>

void blackbox_handler(int sig)
{
    printf("Enter blackbox_handler: ");
    printf("SIG name is %s, SIG num is %dn", strsignal(sig), sig);
    
    // 打印堆栈信息
    printf("Stack information:n");
    int j, nptrs;
#define SIZE 100
    void *buffer[100];
    char **strings;
    
    nptrs = backtrace(buffer, SIZE);
    printf("backtrace() returned %d addressesn", nptrs);
    
    strings = backtrace_symbols(buffer, nptrs);
    if (strings == NULL)
    {
        perror("backtrace_symbol");
        exit(EXIT_FAILURE);
    }
    
    for(j = 0; j < nptrs; j++)
        printf("%sn", strings[j]);
    
    free(strings);
    
    _exit(EXIT_SUCCESS);
}

long count = 0;
void bad_iter()
{
    int a, b, c, d; 
    a = b = c = d = 1;
    a = b + 3;
    c = count + 4;
    d = count + 5 * c;
    count++;
    printf("count:%ldn", count);
   
    bad_iter();
}

int main()
{
    stack_t ss;
    struct  sigaction   sa;
    
    ss.ss_sp = malloc(SIGSTKSZ);
    ss.ss_size = SIGSTKSZ;
    ss.ss_flags = 0;
    if (sigaltstack(&ss, NULL) == -1)
    {
        return EXIT_FAILURE;
    }
    
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = blackbox_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_ONSTACK;
    
    if (sigaction(SIGSEGV, &sa, NULL) < 0)
    {
        return EXIT_FAILURE;
    }
    
    bad_iter();
    
    while(1);
    
    return EXIT_SUCCESS;
}

编译gcc -rdynamic blackbox_overflow.c 后运行,输出为:

... ...
count:261989
count:261990
count:261991
count:261992
Enter blackbox_handler: SIG name is Segmentation fault, SIG num is 11
Stack information:
backtrace() returned 100 addresses
./a.out(blackbox_handler+0x63) [0x400c30]
/lib/x86_64-linux-gnu/libc.so.6(+0x36ff0) [0x7f6e68d74ff0]
/lib/x86_64-linux-gnu/libc.so.6(_IO_file_write+0xb) [0x7f6e68db7e0b]
/lib/x86_64-linux-gnu/libc.so.6(_IO_do_write+0x7c) [0x7f6e68db931c]
/lib/x86_64-linux-gnu/libc.so.6(_IO_file_xsputn+0xb1) [0x7f6e68db84e1]
/lib/x86_64-linux-gnu/libc.so.6(_IO_vfprintf+0x7fa) [0x7f6e68d8879a]
/lib/x86_64-linux-gnu/libc.so.6(_IO_printf+0x99) [0x7f6e68d92749]
./a.out(bad_iter+0x7a) [0x400d62]
./a.out(bad_iter+0x84) [0x400d6c]
./a.out(bad_iter+0x84) [0x400d6c]
./a.out(bad_iter+0x84) [0x400d6c]
./a.out(bad_iter+0x84) [0x400d6c]
./a.out(bad_iter+0x84) [0x400d6c]
... ...

可以看到,使用可替换栈以后,虽然同样栈溢出了,但是我们的黑匣子程序还是起作用了。所以这种优化是有效的。下面我们来看优化的代码。

可以看到我们的代码中使用了sigaltstack函数,该函数的作用就是在在堆中为函数分配一块区域,作为该函数的栈使用。所以,虽然递归函数将系统默认的栈空间用尽了,但是当调用我们的信号处理函数时,使用的栈是它实现在堆中分配的空间,而不是系统默认的栈,所以它仍旧可以正常工作。

该函数函数原型如下:

#include <signal.h>

int sigaltstack(const stack_t *ss, stack_t *oss);

该函数两个个参数为均为stack_t类型的结构体,先来看下这个结构体:

typedef struct {
   void  *ss_sp;     /* Base address of stack */
   int    ss_flags;  /* Flags */
   size_t ss_size;   /* Number of bytes in stack */
} stack_t;

要想创建一个新的可替换信号栈,ss_flags必须设置为0,ss_sp和ss_size分别指明可替换信号栈的起始地址和栈大小。系统定义了一个常数SIGSTKSZ,该常数对极大多数可替换信号栈来说都可以满足需求,MINSIGSTKSZ规定了可替换信号栈的最小值。

如果想要禁用已存在的一个可替换信号栈,可将ss_flags设置为SS_DISABLE

而sigaltstack第一个参数为创建的新的可替换信号栈,第二个参数可以设置为NULL,如果不为NULL的话,将会将旧的可替换信号栈的信息保存在里面。函数成功返回0,失败返回-1.

一般来说,使用可替换信号栈的步骤如下:

  1. 在内存中分配一块区域作为可替换信号栈
  2. 使用sigaltstack()函数通知系统可替换信号栈的存在和内存地址
  3. 使用sigaction()函数建立信号处理函数的时候,通过将sa_flags设置为SA_ONSTACK来告诉系统信号处理函数将在可替换信号栈上面运行。