原生程序的错误

如果学习编程的人像我以及大部分中国大陆的计算机系学生一样,从C语言开始学习编程的话,那么,他从一开始就处于了【真实世界】。

但是,在我的观念中,从C语言学习编程是很不正确的决定。

不过从C语言开始学习编程也不是没有一点好处。如果某人从scheme等等有GC的语言开始学习编程的话的话,那么他到了某些不得不用C/C写程序的时候,会陷入一些很麻烦的问题之中,其中之一,就是C/C都可能会出现【裸错误】,或者说,在硬件层面,操作系统层面的错误,会毫无保留地送到程序员面前。

比如说,考虑这样的C代码:

#include <stdio.h>

int main()
{
	int a[10];
	a[10000] = 0;
}

编译运行之后,如果不出意外的话,shell会告诉你:

➜  ~ ./a            
[1]    398 segmentation fault  ./a

这是什么意思?考虑一下scheme里类似的操作:

(vector-ref (vector 1 2) 10000)

scheme会告诉你:

> (vector-ref (vector 1 2) 10000)
Exception in vector-ref: 10000 is not a valid index for #(1 2)
Type (debug) to enter the debugger.

很显然,scheme在抱怨程序访问了(vector 1 2)这个只有两个元素的向量的第10000个元素,以一个异常(Exception)的形式

在scheme、java、C#等等几乎所有比较高级的(非纯函数式)语言中,任何的错误(这里指类似于访问两个元素是数组的10000个元素的错误)都会产生一个异常(Exception),不会产生segmentation fault.

然而,C语言没有这种能力。这来源于C语言的本质属性–原生。

什么是原生呢?原生,即是说,C语言里的数据类型都是没有怎么封装的、和硬件紧密结合的。

比如说一个Int,在大多数情况下,它就是32位的整数,在内存里占4个字节。

再比如说一个数组,(一般来说)它就是一段连续的内存,访问数组就是寻址。

特别地,C语言里的【指针】,指针的值就是内存地址。

比如说,

int a = 10;
int *b = &a;
printf("%llx", b);

运行编译好的程序会在不同时候打出不同的值,但它们的本质都是一样的 – a这个变量的内存地址。

0

到了这里,我们终于能够回答segmentation fault究竟是个什么东西了。实际上,这是所谓的【内存访问错误】,比如说这样的代码:

int *a = NULL;
*a = 10;

会出现segmentation fault,是因为现代计算机大都不允许使用0这一内存地址,访问它直接会引起操作系统层面的错误。

至于它为什么叫这个名字,这和历史有关,不在此赘述。

原生程序的调试

关于如何调试原生程序,我想,首先我们要明确【为什么要进行调试】。

进行调试,首先就是要确定错误位置。比如这样的代码:

#include <stdio.h>

void a()
{
}

void b()
{
}

void c()
{
	int *a = NULL;
	*a = 0;
}

int main()
{
	a();
	b();
	c();
}

现在程序出了错误,你如何判断这个错误是在a(),还是在b(),抑或是在c()里触发的呢?

有人会说,这不简单?看一眼就知道了。呵呵,这确实没有那么简单,如果a(),b(),c()每一个都有1000行呢?

这个时候,我们必须借助调试器的力量。

tips: 在编译程序时, 加入 -g 选项

首先,用gdb <要调试的程序>进入调试环境:

1

然后使用run或者r,运行程序:

2

可见,它也出现了错误,并停了下来。

其实,这里我们已经可以知道错在哪里了。因为输出已经明确告诉了我们,错误在c()这个函数里,a.c文件的第14行。不过,有些时候这些信息还是不够的,必须要知道谁调用了c()这个函数

使用bt或者backtrace,我们就可以知道这个信息:

3

非常明显,这个函数是main()调用的。

这些信息是非常宝贵的定位信息,我们有时还想知道一些运行时信息,比如在这个例子里,我很想知道,为什么*a会引起错误。

要知道这个问题,就不得不问问gdba这个变量里究竟装了什么,而这是通过print或者p命令实现的:

4

哈哈,原来是因为a这个变量里装的是0x0,所以才会引起错误。

用这几招,我们可以解决大部分的segmentation fault问题。不过有些时候,我们还需要更进一步,比如下面的程序:

#include <stdio.h>

int div(int a, int b)
{
	int c = a / b;
	return c;
}

int main()
{
	int a = 1000 - 1000;
	int b = 10;
	printf("%d", div(b, a));
}

这会造成:

~ ./a            
[1]    567 floating point exception  ./a

用gdb调试,会出现:

5

我们可以知道是因为b=0所以出了问题, 但我们更想知道, 这问题到底是怎么发生的, 或者说, 为什么b=0了呢?

这时, 可以到调用它的函数里去看看:

6

f表示frame,这个命令是说,到1号函数帧,也就是调用div(b, a)的函数现场去看看,这里,我们可以知道,因为main函数中的a变量是0,所以调用div(b,a)时,div函数的第二个参数为0.

有些时候,程序没有出现Segment Fault,但是出现了一些别的错误,那就可以用breakcontinuestep这三个组合来进行所谓的断点调试了。不过断点调试和java、C#中的调试没有太大区别,如果想要继续了解,可以看看这两个页面: