Jackey's 感悟

Do Research

《软件调试的艺术》学习笔记——GDB使用技巧摘要(3)——程序崩溃处理

  1. 程序为什么会崩溃
  2. 内存中的程序布局

当某个错误导致程序突然和异常地停止执行时,程序崩溃。迄今为止最为常见的导致程序崩溃的原因是试图在未经允许的情况下访问一个内存位置。硬件会感知这件事,并执行对操作系统的跳转。Unix系列的平台上,操作系统一般会宣布程序导致了段错误(seg
fault),并停止程序运行。在微软的windows系统上,对应的术语是一般保护错误(general protection
fault)。无论是哪个名称,硬件都必须支持虚拟内存,而且操作系统必须使用虚拟内存才会发生这个错误。虽然这是如今的通用计算机的标准,但是读者应记住,专用的小型计算机一般没有这种情况,比如用来控制机器的嵌入式计算机。

程序在内存中是如何分布的?

在Unix平台上,为程序分配的虚拟地址的布局通常如下:

.text

.data

.bss

未使用

env

这里虚拟地址0在最下方,箭头显示了其中两个组件(堆和栈)的增长方向,当它们增长时,消耗掉未使用的自由区域。各个部分的作用如下:

  • 文本区域(.text),由程序源代码中的编译器产生的机器指令组成。这一组件包括静态链接代码,包括做初始化工作然后调用main()的系统代码/usr/lib/crt0.o
  • .data 数据区域,包含在编译时分配的所有程序变量,即全局变量。如 int x = 5;
  • .bss 数据区域,包含的是存放未初始化数据的全局变量,如 int y;
  • 当程序在运行时从操作系统请求额外的内存时(例如,C语言中调用malloc,或者C++中的new),请求的内存在名为堆(heap)的区域中分配。如果堆空间不足,可以通过调用brk()来扩展堆(这正是malloc及相关函数所做的事情)
  • 栈区域(stack),是用来动态分配数据的空间。函数调用的数据(包括参数、局部变量和返回地址)都存储在栈上。每次进行函数调用时栈都会增长,每次函数返回到其调用者时栈都会收缩。
  • 上图没有显示程序的动态链接代码,它的位置与平台相关,但是它确实在某个地方存在,而且在每种操作系统上,虚拟内存中关于动态链接代码的地址是有规律的。
  1. int q[ 200 ] ;
  2. int main( void ) {
  3. int i,n, *p;
  4. p = malloc( sizeof ( int ) ) ;
  5. scanf( “%d” , &n) ;
  6. for (i = 0 ; i < 200 ; i++ )
  7. q[i] = i;
  8. printf ( “%x %x %x %x %x\n , main, q, p, &i, scanf) ;
  9. return 0 ;
  10. }

虽然上述程序本身作用不大,但是可以编写成一个工具来非正式地探索虚拟地址空间的布局。运行结果可能如下:

% a.out

5

80483f4 80496a0 9835008 bfb3abec 8048304

分别对应于文本区域、数据区域、堆、栈和动态链接函数位置。

另外可以通过查看这一过程的maps文件来得到程序在linux上的精确内存布局情况。加入进程号是21111,那么查看的文件应该是/proc/21111/maps

页的概念

虚拟地址空间是通过坐直成成为页(page)的块来查看的。在pentium硬件上,默认的页大小是4096字节。物理内存(包括RAM和ROM)也都是分成页来查看的。当程序被加载到内存中执行时,操作系统会安排程序的部分页存储在物理内存的页中。这些页成为被”驻留”,其余部分存储在磁盘上。

在执行期间的各个阶段,将需要一些当前没有驻留的程序页。当发生这种情况的时候,硬件会感知到,将控制权转移给操作系统。后者将所需页带到内存中,可能会替换掉当前驻留的另一个程序页(如果没有可用的自由内存页),然后将控制权返回给程序。如果有被驱逐的程序页,就会变成非驻留页,被存储在磁盘上。

为了管理所有这些操作,操作系统为每个过程设立了一个页表(page table)。(pentium的页表有一个层次结构,但是为了简单起见,假定只有一层,而且这里讨论的大多数内容都不是pentium特有的。)这一过程的每个虚拟页在表中都有对应的一个项(entry),其中包括如下信息:

  • 这个页在内存中或者磁盘上的当前物理位置。如果是在磁盘上,页表对应的项会指示页是非驻留的,可能包含一个指针,指向最终导致磁盘上的物理位置的一个列表。例如,它可能显示:程序的虚拟页12是驻留的,位于内存的物理页200中。
  • 该页的权限分为3种:读、写和执行

注意:操作系统不会将不完整的页分配给程序。例如,如果要运行的程序总共有10 000字节,如果完全加载,会占用3个内存页(3*4096),不会是占用2.5个页,因为页是虚拟内存系统能够操作的最小内存单元。这是调试时要记住的很重要的一点,因为这一点暗示了程序的一些错误内存访问不会触发段错误。换言之,在调试会话期间,不能这么想:”这行代码一定没有问题,因为它没有引起段错误。”

产生段错误的真正原因:权限不匹配

在程序的运行期间,生成的地址会是虚拟的。当程序试图访问某个虚拟地址处的内存时,比如y,硬件就会将其转化为虚拟页号v,它等于y除以4096(其中除法是整除算法,舍去余数)。然后硬件会检查页表中的页表项v来查看该页的权限是否与要执行的操作匹配。如果匹配,硬件会从这个表项中得到所需位置的实际物理页号,然后完成请求的内存。但是如果该表项显示请求的操作不具有恰当的权限,硬件就会执行内部中断。这会导致跳转到操作系统的错误处理例程。然后,操作系统一般会宣告一个内存访问违例,并停止程序的执行(即从进程表和内存中去掉程序)。

程序中的错误会导致权限不匹配,并在上面列出的某个类型的内存访问周期生成段错误。

段错误可以发生在数据区域、堆栈等位置。报告错误的地方,往往不是问题本质所在,需要在附近回溯一下。

常用的调试方法有核心文件(core file)或者signal.h文件中提供的signal()或者sigaction()两个系统调用来捕获。

Advertisements

发表评论

Fill in your details below or click an icon to log in:

WordPress.com 徽标

You are commenting using your WordPress.com account. Log Out /  更改 )

Google photo

You are commenting using your Google account. Log Out /  更改 )

Twitter picture

You are commenting using your Twitter account. Log Out /  更改 )

Facebook photo

You are commenting using your Facebook account. Log Out /  更改 )

Connecting to %s

%d 博主赞过: