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.
一般来说,使用可替换信号栈的步骤如下:
- 在内存中分配一块区域作为可替换信号栈
- 使用sigaltstack()函数通知系统可替换信号栈的存在和内存地址
- 使用sigaction()函数建立信号处理函数的时候,通过将sa_flags设置为SA_ONSTACK来告诉系统信号处理函数将在可替换信号栈上面运行。
评论已关闭