当前位置:Linux教程 - Linux - Unix编程/应用问答中文版 ---2.堆栈相关问题

Unix编程/应用问答中文版 ---2.堆栈相关问题

本文出自:http://www.nsfocus.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

在代码段绝对地址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

用/usr/proc/bin/pstack [-F]

参看这个例子代码,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

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

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

A: Andrew Gabriel Consultant Software Engineer

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

/*
* Produce a stack trace for Solaris systems.
*
* Copyright (C) 1995-1998 Andrew Gabriel
* Parts derived from Usenet postings of Bart Smaalders and Casper Dik.
*
*/

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

#include
#include
#include
#include
#include
#include
#include
#include

#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 "", ""??"", ( unsigned int )pc );
}
else
{
/* found */
fprintf( stderr, ""*** %s:%s+0x%x "", 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] "" );
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... "" );
print_stack();
fprintf( stderr, ""***backtrace ends "" );
}

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

2.4 如何编程获取栈底地址

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

A: tt 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

--------------------------------------------------------------------------
/*
* gcc -Wall -O3 -o gstack gstack.c
*
* A simple example to get the current stack bottom address
* warning3
* 2001-06-01
*
* Modified by scz
* 2001-06-02
*/

#include
#include
#include
#include
#include

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 "", get_stack_bottom() );
return( EXIT_SUCCESS );
} /* end of main */
--------------------------------------------------------------------------

D: scz 2001-06-03 00:38

W. Richard Stevens在<>中详细
介绍了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

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 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

这是一个操作系统相关的问题。最一般的回答是使用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
#include
#include
#include
#include
#include

/*
* 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 "" );
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 "", i );
*/
return( EXIT_SUCCESS );
} /* end of main */
--------------------------------------------------------------------------

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

D: 小四 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
#include
#include
#include
#include
#include

/*
* 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 "" );
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 "" );
return( EXIT_SUCCESS );
}
/*
* now sigsetjump() is OK
*/
canjump = 1;
Signal( SIGFPE, on_fpe );
i = 51211314 / 0;
/*
* 另外,增加这行后,再次对比有-O3和无-O3的效果
*
* fprintf( stderr, ""i = %#X "", i );
*/
return( EXIT_SUCCESS );
} /* end of main */
--------------------------------------------------------------------------

关于-O3的讨论,对gcc编译器熟悉的朋友请继续,呵,我对Linux下的这此东西,实
在缺乏兴趣