[ 永远的UNIX::UNIX技术资料的宝库 ]   GB | BIG5

首页 > 编程技术 > C/C++ > 正文
Unix编程/应用问答中文版 ---2.堆栈相关问题
本文出自:http://www.nsfocus.com 维护:小四 (2002-10-18 06:02:00)
2.    堆栈相关问题 
2.1   如何理解pstack的输出信息 
2.2   
2.3   Solaris中如何获取一个C程序的调用栈回溯 
2.4   如何编程获取栈底地址 
2.5   如何得到一个运行中进程的内存映像 
2.6   调试器如何工作的 
2.7   x86/Linux上如何处理SIGFPE信号 
-------------------------------------------------------------------------- 
2. 堆栈相关问题 

2.1 如何理解pstack的输出信息 

Q: 080603a7 main    (1, 80479b8, 80479c0)  + d53 
   结尾的d53是什么 

A: Roger A. Faulkner <raf@sunraf.Sun.COM> 

在代码段绝对地址0x080603a7处,main()调用了一个函数,0x080603a7正是 
main + 0xd53,换句话说,从main()函数开始的0xd53偏移处。 

2.3 Solaris中如何获取一个C程序的调用栈回溯 

Q: 我想在Solaris 2.6极其后续版本上获取一个C程序的调用栈回溯,类似如下输出 

   (10)  0x00045e08  integ + 0x408    [./two_brn.e] 
   (11)  0x0006468c  trajcem + 0x128  [./two_brn.e] 
   (12)  0x00055490  fly_traj + 0xf58 [./two_brn.e] 
   (13)  0x0004052c  top_level + 0x14 [./two_brn.e] 
   (14)  0x000567e4  _start + 0x34    [./two_brn.e] 

   这样我就可以知道当程序崩溃、死锁的时候代码执行到了何处。在HP-UX和IRIX上 
   可以利用U_STACK_TRACE()和trace_back_stack_and_print(),Solaris上呢? 

Q: 有没有办法显示当前堆栈中的数据(GNU/Linux系统)?我希望自己的异常处理程序 
   在进程结束前dump整个栈区(stack),以便观察到栈顶是什么函数。对于调试意想 
   不到的运行时错误而言,这很重要。 

A: Bjorn Reese <breese@mail1.stofanet.dk> 

   用/usr/proc/bin/pstack [-F] <pid ...> 

   参看这个例子代码,http://home1.stofanet.dk/breese/debug/debug.tar.gz 

Q: is there a way to access call stack information at run time from within 
   a program?  i've been maintaining my own crude stack using __FUNCTION__ 
   and linked lists but can't help but think there's gotta be a better 
   way... 

A: Nate Eldredge <neldredge@hmc.edu> 

   这依赖于你的系统,如果使用glibc 2.1或更新版本,可以使用backtrace()函数, 
   参看<execinfo.h>,其他系统可能有不同的技术支持。 

   注意,你所使用的办法可能是唯一能够保证跨平台使用的 

A: Andrew Gabriel <andrew@cucumber.demon.co.uk> Consultant Software Engineer 

   下面是一个backtrace()的应用举例,如果你使用Solaris 2.4及其后续版本,那 
   么这个例子可以很好的工作。很可能无法工作在64-bit模式下,我没有尝试过, 
   好像Solaris 7已经提供了一个类似的演示程序。还可以增加某些功能,我没有时 
   间了。 

/* 
* Produce a stack trace for Solaris systems. 

* Copyright (C) 1995-1998 Andrew Gabriel <andrew@cucumber.demon.co.uk> 
* Parts derived from Usenet postings of Bart Smaalders and Casper Dik. 

*/ 

/* ......................................................................... */ 

#include <setjmp.h> 
#include <sys/types.h> 
#include <sys/reg.h> 
#include <sys/frame.h> 
#include <dlfcn.h> 
#include <errno.h> 
#include <unistd.h> 
#include <stdio.h> 

#if defined(sparc) || defined(__sparc) 
#define FLUSHWIN() asm("ta 3"); 
#define FRAME_PTR_INDEX 1 
#define SKIP_FRAMES 0 
#endif 

#if defined(i386) || defined(__i386) 
#define FLUSHWIN() 
#define FRAME_PTR_INDEX 3 
#define SKIP_FRAMES 1 
#endif 

#if defined(ppc) || defined(__ppc) 
#define FLUSHWIN() 
#define FRAME_PTR_INDEX 0 
#define SKIP_FRAMES 2 
#endif 

/* ......................................................................... */ 

static void print_address ( void * pc ) 

    Dl_info info; 

    if ( dladdr( pc, &info ) == 0 ) 
    { 
        /* not found */ 
        fprintf( stderr, "***  %s:0x%x\n", "??", ( unsigned int )pc ); 
    } 
    else 
    { 
        /* found */ 
        fprintf( stderr, "***  %s:%s+0x%x\n", info.dli_fname, info.dli_sname, 
                 ( unsigned int )pc - ( unsigned int )info.dli_saddr ); 
    } 
    return; 
}  /* end of print_address */ 

/* ......................................................................... */ 

static int validaddr ( void * addr ) 

    static long pagemask = -1; 
    char        c; 

    if ( pagemask == -1 ) 
    { 
        pagemask = ~( sysconf( _SC_PAGESIZE ) - 1 ); 
    } 
    addr = ( void * )( ( long )addr & pagemask ); 
    if ( mincore( ( char * )addr, 1, &c ) == -1 && errno == ENOMEM ) 
    { 
        return 0;  /* invalid */ 
    } 
    else 
    { 
        return 1;  /* valid */ 
    } 
}  /* end of validaddr */ 

/* ......................................................................... */ 

/* 
* this function walks up call stack, calling print_addess 
* once for each stack frame, passing the pc as the argument. 
*/ 

static void print_stack ( void ) 

    struct frame * sp; 
    jmp_buf        env; 
    int            i; 
    int *          iptr; 

    FLUSHWIN(); 

    setjmp( env ); 
    iptr = ( int * )env; 

    sp = ( struct frame * )iptr[ FRAME_PTR_INDEX ]; 

    for ( i = 0; i < SKIP_FRAMES && sp; i++ ) 
    { 
        if ( !validaddr( sp ) || !validaddr( &sp->fr_savpc ) ) 
        { 
            fprintf( stderr, "***[stack pointer corrupt]\n" ); 
            return; 
        } 
        sp = ( struct frame * )sp->fr_savfp; 
    } 

    i = 100;  /* looping check */ 

    while ( validaddr( sp ) && validaddr( &sp->fr_savpc ) && sp->fr_savpc && --i 

    { 
         print_address( ( void * )sp->fr_savpc ); 
         sp = ( struct frame * )sp->fr_savfp; 
    } 
}  /* end of print_stack */ 

/* ......................................................................... */ 

void backtrace( void ) 

    fprintf( stderr, "***backtrace...\n" ); 
    print_stack(); 
    fprintf( stderr, "***backtrace ends\n" ); 


/* ......................................................................... */ 

2.4 如何编程获取栈底地址 

Q: 虽然很多操作系统的用户进程栈底地址固定,但是我需要写一个可广泛移植C程序 
   获取这个栈底地址。 

A: tt <warning3@nsfocus.com> 2001-06-02 19:40 

假设堆栈(stack)向低地址方向增长,则所谓栈底指堆栈(stack)最高地址 

x86/Linux         栈底是0xc0000000( 栈底往低地址的4个字节总是零 ) 
SPARC/Solaris 7/8 栈底是0xffbf0000( 栈底往低地址的4个字节总是零 ) 
SPARC/Solaris 2.6 栈底是0xf0000000( 栈底往低地址的4个字节总是零 ) 
x86/FreeBSD       栈底是0xbfc00000( 栈底往低地址的4个字节总是零 ) 
x86/NetBSD 1.5    栈底是0xbfbfe000 
x86/OpenBSD 2.8   栈底是0xdfbfe000 

D: jonah 

对于NetBSD 1.5,栈底是0xbfc00000。根据源码,最高用户地址是0xbfbfe000,因为 
最后4MB(2^22)的最后两页(0x2000字节,一页4096字节)保留用做U区,但是目前不再 
使用这块内存。因此,0xbfbfe000才是真正的栈底。 

tt在OpenBSD 2.8上测试结果,栈底是0xdfbfe000,注意和NetBSD 1.5相差很大。 

A: tt <warning3@nsfocus.com> 

-------------------------------------------------------------------------- 
/* 
* gcc -Wall -O3 -o gstack gstack.c 

* A simple example to get the current stack bottom address 
* warning3 <warning3@nsfocus.com> 
* 2001-06-01 

* Modified by scz <scz@nsfocus.com> 
* 2001-06-02 
*/ 

#include <stdio.h> 
#include <stdlib.h> 
#include <signal.h> 
#include <unistd.h> 
#include <setjmp.h> 

typedef void Sigfunc ( int );  /* for signal handlers */ 

       Sigfunc * signal           ( int signo, Sigfunc * func ); 
static Sigfunc * Signal           ( int signo, Sigfunc * func ); 
static char    * get_stack_bottom ( void ); 
static void      segfault         ( int signo ); 

static sigjmp_buf             jmpbuf; 
static volatile sig_atomic_t  canjump = 0; 
static Sigfunc               *seg_handler; 
static Sigfunc               *bus_handler;  /* for xxxBSD */ 

Sigfunc * signal ( int signo, Sigfunc * func ) 

    struct sigaction act, oact; 

    act.sa_handler = func; 
    sigemptyset( &act.sa_mask ); 
    act.sa_flags   = 0; 
    if ( sigaction( signo, &act, &oact ) < 0 ) 
    { 
        return( SIG_ERR ); 
    } 
    return( oact.sa_handler ); 
}  /* end of signal */ 

static Sigfunc * Signal ( int signo, Sigfunc * func )  /* for our signal() funct 
ion */ 

    Sigfunc * sigfunc; 

    if ( ( sigfunc = signal( signo, func ) ) == SIG_ERR ) 
    { 
        exit( EXIT_FAILURE ); 
    } 
    return( sigfunc ); 
}  /* end of Signal */ 

static char * get_stack_bottom ( void ) 

    volatile char *c;  /* for autovar, must be volatile */ 

    seg_handler = Signal( SIGSEGV, segfault ); 
    bus_handler = Signal( SIGBUS, segfault ); 
    c           = ( char * )&c; 

    if ( sigsetjmp( jmpbuf, 1 ) != 0 ) 
    { 
        Signal( SIGSEGV, seg_handler ); 
        Signal( SIGBUS, bus_handler ); 
        return( ( char * )c ); 
    } 
    canjump = 1;  /* now sigsetjump() is OK */ 
    while ( 1 ) 
    { 
        *c = *c; 
        c++; 
    } 
    return( NULL ); 
}  /* end of get_stack_bottom */ 

static void segfault ( int signo ) 

    if ( canjump == 0 ) 
    { 
        return;  /* unexpected signal, ignore */ 
    } 
    canjump = 0; 
    siglongjmp( jmpbuf, signo );  /* jump back to main, don't return */ 
}  /* end of segfault */ 

int main ( int argc, char * argv[] ) 

    fprintf( stderr, "Current stack bottom is at 0x%p\n", get_stack_bottom() ); 
    return( EXIT_SUCCESS ); 
}  /* end of main */ 
-------------------------------------------------------------------------- 

D: scz <scz@nsfocus.com> 2001-06-03 00:38 

W. Richard Stevens在<<Advanced Programming in the UNIX Environment>>中详细 
介绍了setjmp/longjmp以及sigsetjmp/siglongjmp函数。 

这个程序的原理很简单,不断向栈底方向取值,越过栈底的地址访问会导致SIGSEGV 
信号,然后利用长跳转回到主流程报告当前c值,自然对应栈底。 

tt测试表明,在x86/FreeBSD中导致SIGBUS信号。据jonah报告,不仅仅是FreeBSD, 
NetBSD 以及 OpenBSD 系统中上述程序越界访问也导致SIGBUS信号,而不是SIGSEGV 
信号。 

非局部转移,比如函数间转移的时候考虑使用setjmp/longjmp。但是如果涉及到信号 
句柄与主流程之间的转移,就不能使用longjmp了。当捕捉到信号进入信号句柄,此 
时当前信号被自动加入进程的信号屏蔽字中,阻止后来产生的这种信号干扰此信号句 
柄。如果用longjmp跳出信号句柄,此时进程的信号屏蔽字状态未知,有些系统做了 
保存恢复,有些系统没有做。根据POSIX.1,此时应该使用sigsetjmp/siglongjmp函 
数。下面来自SPARC/Solaris 7的setjmp(3C) 

-------------------------------------------------------------------------- 
#include <setjmp.h> 

int  setjmp     ( jmp_buf env ); 
int  sigsetjmp  ( sigjmp_buf env, int savemask ); 
void longjmp    ( jmp_buf env, int val ); 
void siglongjmp ( sigjmp_buf env, int val ); 
-------------------------------------------------------------------------- 

如果savemask非0,sigsetjmp在env中保存进程当前信号屏蔽字,相应siglongjmp回 
来的时候从env中恢复信号屏蔽字。 

数据类型sig_atomic_t由ANSI C定义,在写时不会被中断。它意味着这种变量在具有 
虚存的系统上不会跨越页边界,可以用一条机器指令对其存取。这种类型的变量总是 
与ANSI类型修饰符volatile一并出现,防止编译器优化带来的不确定状态。 

在longjmp/siglongjmp中,全局、静态变量保持不变,声明为volatile的自动变量也 
保持不变。 

无论是否使用了编译优化开关,为了保证广泛兼容性,都应该在get_stack_bottom() 
中声明c为volatile变量。 

注意这里,必须使用长跳转,而不能从信号句柄中直接返回。因为导致信号SIGSEGV、 
SIGBUS分发的语句始终存在,直接从信号句柄中返回主流程,将回到引发信号的原指 
令处,而不是下一条指令(把这种情况理解成异常,而不是中断),于是立即导致下一 
次信号分发,出现广义上的死循环,所谓程序僵住。可以简单修改上述程序,不利用 
长跳转,简单对一个全局变量做判断决定是否继续循环递增c,程序最终僵住;如果 
在信号句柄中输出调试信息,很容易发现这个广义上的无限循环。 

D: scz <scz@nsfocus.com> 2001-06-03 00:40 

在x86/Linux系统中用如下命令可以确定栈区所在 

# cat /proc/1/maps  <-- 观察1号进程init 
... ... 
bfffe000-c0000000 rwxp fffff000 00:00 0 


在SPARC/Solaris 7中用/usr/proc/bin/pmap命令确定栈区所在 

# /usr/proc/bin/pmap 1  <-- 观察1号进程init 
... ... 
FFBEC000     16K read/write/exec     [ stack ] 


16KB == 0x4000,0xFFBEC000 + 0x4000 == 0xFFBF0000 

与前面tt介绍的 

SPARC/Solaris 7/8 栈底是0xffbf0000( 栈底往低地址的4个字节总是零 ) 

相符合。 

此外,在SPARC/Solaris 7下,可以这样验证之 

# /usr/ccs/bin/nm -nx /dev/ksyms | grep "|_userlimit" 
[7015]  |0x0000100546f8|0x000000000008|OBJT |GLOB |0    |ABS    |_userlimit 
[8051]  |0x000010054700|0x000000000008|OBJT |GLOB |0    |ABS    |_userlimit32 
# echo "_userlimit /J" | adb -k /dev/ksyms /dev/mem 
physmem 3b72 
_userlimit: 
_userlimit:     ffffffff80000000 
# skd64 0x000010054700 8 
byteArray [ 8 bytes ] ----> 
0000000000000000  00 00 00 00 FF BF 00 00 
#                             ~~~~~~~~~~~ 对于32-bit应用程序来说,这是用户 
                                          空间上限 

如果编译64-bit应用程序,用户空间上限是_userlimit,也就是0xffffffff80000000 

# /opt/SUNWspro/SC5.0/bin/cc -xarch=v9 -O -o gstack gstack.c 
# ./gstack 
Current stack bottom is at 0xffffffff80000000 


对于SPARC/Solaris 2.6 32-bit kernel mode 

# echo "_userlimit /X" | adb -k /dev/ksyms /dev/mem 
physmem 3d24 
_userlimit: 
_userlimit:     f0000000 


2.5 如何得到一个运行中进程的内存映像 

A: Sun Microsystems 1998-03-30 

有些时候必须得到一个运行中进程的内存映像而不能停止该进程,Solaris系统了这 
样的工具,gcore为运行中进程创建一个core文件。假设我的bash进程号是5347 

# gcore 5347 
gcore: core.5347 dumped 
# file core.5347 
core.5347:      ELF 32-位 MSB core文件 SPARC 版本 1,来自'bash' 


注意,只能获取属主是你自己的进程的内存映像,除非你是root。 

2.6 调试器如何工作的 

Q: 我想在一个自己编写的程序中单步运行另外一个程序,换句话说,那是一个调试 
   器,该如何做? 

A: Erik de Castro Lopo <nospam@mega-nerd.com> 

   这是一个操作系统相关的问题。最一般的回答是使用ptrace()系统调用,尽管我 
   不确认究竟这有多么普遍。Linux man手册上说SVr4、SVID EXT、AT&T、X/OPEN 
   和BSD 4.3都支持它。 

   为了使用ptrace(),你的程序应该调用fork(),然后在子进程中做如下调用: 

   ptrace( PTRACE_TRACEME, 0, 0, 0 ); 

   接下来调用exec()家族的函数执行你最终企图跟踪的程序。 

   为了单步进入子进程,在父进程中调用: 

   ptrace( PTRACE_SINGLESTEP, 0, 0, 0 ); 

   还有一些其他函数做恢复/设置寄存器、内存变量一类的工作。 

   GDB的源代码足以回答这个问题。 

2.7 x86/Linux上如何处理SIGFPE信号 

Q: 参看如下程序 

-------------------------------------------------------------------------- 
/* 
* gcc -Wall -pipe -O3 -o sigfpe_test_0 sigfpe_test_0.c 

* 注意与下面的编译效果进行对比,去掉优化开关-O3 

* gcc -Wall -pipe -o sigfpe_test_0 sigfpe_test_0.c 
*/ 

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <signal.h> 
#include <unistd.h> 
#include <setjmp.h> 

/* 
* for signal handlers 
*/ 
typedef void Sigfunc ( int ); 

       Sigfunc * signal ( int signo, Sigfunc *func ); 
static Sigfunc * Signal ( int signo, Sigfunc *func ); 
static void      on_fpe ( int signo ); 

Sigfunc * signal ( int signo, Sigfunc *func ) 

    struct sigaction act, oact; 

    act.sa_handler = func; 
    sigemptyset( &act.sa_mask ); 
    act.sa_flags   = 0; 
    if ( signo == SIGALRM ) 
    { 
#ifdef  SA_INTERRUPT 
        act.sa_flags |= SA_INTERRUPT;  /* SunOS 4.x */ 
#endif 
    } 
    else 
    { 
#ifdef  SA_RESTART 
        act.sa_flags |= SA_RESTART;  /* SVR4, 44BSD */ 
#endif 
    } 
    if ( sigaction( signo, &act, &oact ) < 0 ) 
    { 
        return( SIG_ERR ); 
    } 
    return( oact.sa_handler ); 
}  /* end of signal */ 

static Sigfunc * Signal ( int signo, Sigfunc *func ) 

    Sigfunc *sigfunc; 

    if ( ( sigfunc = signal( signo, func ) ) == SIG_ERR ) 
    { 
        perror( "signal" ); 
        exit( EXIT_FAILURE ); 
    } 
    return( sigfunc ); 
}  /* end of Signal */ 

static void on_fpe ( int signo ) 

    fprintf( stderr, "here is on_fpe\n" ); 
    return; 
}  /* end of on_fpe */ 

int main ( int argc, char * argv[] ) 

    unsigned int i; 

    Signal( SIGFPE, on_fpe ); 
    i = 51211314 / 0; 
    /* 
     * 另外,增加这行后,再次对比有-O3和无-O3的效果 
     * 
     * fprintf( stderr, "i = %#X\n", i ); 
     */ 
    return( EXIT_SUCCESS ); 
}  /* end of main */ 
-------------------------------------------------------------------------- 

有-O3、无-O3,以及有无最后那条fprintf()语句,效果上有差别,自行对比。如果 
输出"here is on_fpe",则会发现永不停止。 

D: 小四 <scz@nsfocus.com> 2001-12-14 18:25 

为了便于讨论,约定两个名词,中断和异常。这里中断指最常规的中断,比如int指 
令带来的软中断。异常的典型代表有除0错。区别在于,发生异常时,x86架构上CPU 
将当前EIP(指向引发异常的指令)压栈,发生中断时,x86架构上CPU将当前EIP的后一 
个地址(指向引发中断的指令的后一条指令)压栈。在异常处理代码中,如果认为能够 
从灾难中恢复,可以不修改被压栈的EIP,从而返回到引发异常的指令处。更多细节 
请查看Intel手册。 

这些是从前DOS下残留的汇编知识,不过也快忘光了,刚才又找元宝宝确认了一下。 

在上述代码中,on_fpe()直接返回了,导致再次触发异常,所以无休止输出。事实上 
在所有的计算器处理程序中,都会对SIGFPE信号做相应处理,前些日子看yacc/lex的 
时候又碰上过。正确的做法是,利用远跳转转移,让开引发异常的指令。 

代码修改如下 

-------------------------------------------------------------------------- 
/* 
* gcc -Wall -pipe -O3 -o sigfpe_test_1 sigfpe_test_1.c 

* 注意与下面的编译效果进行对比,去掉优化开关-O3 

* gcc -Wall -pipe -o sigfpe_test_1 sigfpe_test_1.c 
*/ 

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <signal.h> 
#include <unistd.h> 
#include <setjmp.h> 

/* 
* for signal handlers 
*/ 
typedef void Sigfunc ( int ); 

       Sigfunc * signal ( int signo, Sigfunc *func ); 
static Sigfunc * Signal ( int signo, Sigfunc *func ); 
static void      on_fpe ( int signo ); 

static sigjmp_buf             jmpbuf; 
static volatile sig_atomic_t  canjump = 0; 

Sigfunc * signal ( int signo, Sigfunc *func ) 

    struct sigaction act, oact; 

    act.sa_handler = func; 
    sigemptyset( &act.sa_mask ); 
    act.sa_flags   = 0; 
    if ( signo == SIGALRM ) 
    { 
#ifdef  SA_INTERRUPT 
        act.sa_flags |= SA_INTERRUPT;  /* SunOS 4.x */ 
#endif 
    } 
    else 
    { 
#ifdef  SA_RESTART 
        act.sa_flags |= SA_RESTART;  /* SVR4, 44BSD */ 
#endif 
    } 
    if ( sigaction( signo, &act, &oact ) < 0 ) 
    { 
        return( SIG_ERR ); 
    } 
    return( oact.sa_handler ); 
}  /* end of signal */ 

static Sigfunc * Signal ( int signo, Sigfunc *func ) 

    Sigfunc *sigfunc; 

    if ( ( sigfunc = signal( signo, func ) ) == SIG_ERR ) 
    { 
        perror( "signal" ); 
        exit( EXIT_FAILURE ); 
    } 
    return( sigfunc ); 
}  /* end of Signal */ 

static void on_fpe ( int signo ) 

    if ( canjump == 0 ) 
    { 
        return;  /* unexpected signal, ignore */ 
    } 
    canjump = 0; 
    fprintf( stderr, "here is on_fpe\n" ); 
    siglongjmp( jmpbuf, signo );  /* jump back to main, don't return */ 
    return; 
}  /* end of on_fpe */ 

int main ( int argc, char * argv[] ) 

    unsigned int i; 

    if ( sigsetjmp( jmpbuf, 1 ) != 0 ) 
    { 
        fprintf( stderr, "c u later\n" ); 
        return( EXIT_SUCCESS ); 
    } 
    /* 
     * now sigsetjump() is OK 
     */ 
    canjump = 1; 
    Signal( SIGFPE, on_fpe ); 
    i = 51211314 / 0; 
    /* 
     * 另外,增加这行后,再次对比有-O3和无-O3的效果 
     * 
     * fprintf( stderr, "i = %#X\n", i ); 
     */ 
    return( EXIT_SUCCESS ); 
}  /* end of main */ 
-------------------------------------------------------------------------- 

关于-O3的讨论,对gcc编译器熟悉的朋友请继续,呵,我对Linux下的这此东西,实 
在缺乏兴趣。 (http://www.fanqiang.com)
    进入【UNIX论坛

相关文章
Unix编程/应用问答中文版 ---1.系统管理配置问题 (2002-10-17 06:02:00)
Unix编程/应用问答中文版 ---0.简介 Unix/C传奇问题 (2002-10-16 06:02:01)
 

★  樊强制作 欢迎分享  ★