——《软件调试的艺术》
1.程序崩溃:当某个错误导致程序突然和异常地停止执行。
最常见的导致程序崩溃的原因是试图未经允许的情况下访问一个内存单元;
Unix系统上,操作系统一般会宣布程序导致了段错误(segmentation fault);
Window系统上,对应的术语一般是保护错误(general protection fault);
无论哪个名称,硬件都必须支持虚拟内存,而且操作系统必须使用虚拟内存才会发生这个错误。
2.C程序内存布局
text: 文本区,由程序代码中的编译器产生的机器代码组成,还包括静态链接代码,包括做初始化工作的系统代码/usr/lib/crt0.0;
data: 初始化数据区,包含在编译时就初始化的变量,即全局变量;
bss: 未初始化数据区,包含在编译时未被初始化的变量;
heap: 堆区,运行时从操作系统中请求额外的内存,常用的如C中malloc,C++中new,如果对空间不够可通过brk()来扩展堆;
stack:用来动态分配数据的空间,如函数调用的数据(参数、局部变量和返回地址等)都在栈上,每次进行函数调用时栈增长,函数返回时栈收缩;
在Linux中可通过查看一个运行程序的maps文件来得到其精确的内存布局情况。首先让程序运行等待(等待输入),然后查看程序的进程号如xx,然后查看相应的文件/proc/xx/maps:
$ ./main__scanf //运行并等待输入
$ ps aux | grep main__scanf //在另一个终端查询其进程号为6805
$ gedit /pro/6805/maps
内存从低到高:上面三部分为:text, data, bass;
然后为,heap,stack
其中第二列,如r-xp为权限字段。
3.页的概念
虚拟地址空间概念上的范围是[0,2^n],其中n为地址位数。
虚拟地址空间是通过被称为页(page)的块来查看的。在Pentium硬件上,默认的叶大小为4096字节(4KB)。
物理内存(Ram, Rom)也都是分成页查看的,当程序加载到内存时,OS安排程序的的部分页存储在物理内存页中,这些页被称为“驻留”,其余仍在磁盘上。在执行的各个阶段,将需要当前没有驻留的页,此时OS将所需也加载到内存,若没有自由内存页,则可能替换掉当前驻留的另一个程序的驻留页。
为了管理这些页操作,OS为每个过程设立了页表(page table).每个虚拟页在表中都对应一个项(entry),包括以下信息:
* 该页当前在内存或磁盘上,及相关的位置信息;
* 该页的权限:r, w, x;
页是虚拟内存系统能够操作的最小内存单元,OS不会讲不完整的页分配给程序,这也暗示了程序的一些错误不会触发段错误。
在程序执行期间,生成的地址是虚拟的,当程序试图访问某个虚拟地址处的内存时,比如y,硬件将其转换为虚拟页号v, v=y/4096, 然后硬件检查页表中的页表项v来查看该页的权限是否与执行的操作(r,w,x)相匹配。
4.Unix信号与段错误
可通过:man 7 signal 查看完整的信号列表
man num(1-9) name
Signal, GDB中处理信号: handle SIGNAL [ACTION]
5.核心文件core
有些信号表示让某个进程继续是不妥当的,甚至是不可能的,在这些情况下,默认动作是提前终止进程,并编写核心文件(core file),俗称转储文件。可通过GDB打开core file,然后开始常规的GDB操作。
核心文件包含程序崩溃时程序的详细状态:栈的内容,CPU寄存器内容,程序的静态分配变量值等。
核心文件在错误难以重建情况下非常有用:
* 运行了很长时间才发生段错误,调试器中无法重建该错误;
* 程序的行为取决于随机环境事件,再次运行可能不会出现;
某些shell可能禁止创建核心文件,在bash中,可以使用ulimit命令控制核心文件的创建:
$ ulimit -c size__n //设置core file大小
$gdb binfile core //启动GDB分析核心文件
(gdb) where | backtrace ,根据回溯输出,查看段错误发生的地方;
(gdb) frame 1 , 切换到帧1;
(gdb) p *p , 查看变量,进行分析;
大致分析后,可以从GDB中重新运行程序。
调试技巧
(1)在调试回话期间不要退出GDB, 程序改动并重新编译后,GDB重新run的时候可以发现更改;
(2)保持文本编辑器打开,通常保持一个编辑窗口,一个编译窗口,一个调试窗口;甚至最好是通过编辑器执行命令。例如,vim编辑器可通过
: make
重新编译程序
假定在vim的启动文件中使用 set autowrite
设置了vim的autowrite功能也会将光标移到报告的第一个编译警告或错误处,可通过vim的
:cnext , :cprev
在编译错误中来回调试。