|
|
※阅读文章※ |
Unix编程/应用问答中文版 ---2.堆栈相关问题作者:不祥 [文章出自: www.fanqiang.com] 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下的这此东西,实 在缺乏兴趣 文章加入时间: 2004-11-17 14:53:10 责任编辑: w9 (2734 人次查阅) |