段错误

如果程序运行时出现段错误,用gdb可以很容易定位到究竟是哪一行引发的段错误,例如这个小程序:

例 10.4. 段错误调试实例一

#include <stdio.h>

int main(void)
{
	int man = 0;
	scanf("%d", man);
	return 0;
}

调试过程如下:

$ gdb main
......
(gdb) r
Starting program: /home/akaedu/main 
123

Program received signal SIGSEGV, Segmentation fault.
0xb7e1404b in _IO_vfscanf () from /lib/tls/i686/cmov/libc.so.6
(gdb) bt
#0  0xb7e1404b in _IO_vfscanf () from /lib/tls/i686/cmov/libc.so.6
#1  0xb7e1dd2b in scanf () from /lib/tls/i686/cmov/libc.so.6
#2  0x0804839f in main () at main.c:6

在gdb中运行,遇到段错误会自动停下来,这时可以用命令查看当前执行到哪一行代码了。gdb显示段错误出现在_IO_vfscanf函数中,用bt命令可以看到这个函数是被我们的scanf函数调用的,所以是scanf这一行代码引发的段错误。仔细观察程序发现是man前面少了个&。

上一节我们调试了一个输入字符串转整数的程序,最后提出修正Bug的方法是在循环中加上判断条件,如果不是数字就报错退出,不仅输入字母可以报错退出,输入超长的字符串也会报错退出。然而真的把两个Bug一起解决了吗?这其实是一种治标不治本的办法,因为并没有制止scanf的访问越界。我说过了,使用scanf函数是非常凶险的。表面上看这个程序无论怎么运行都不出错了,但假如我们把while (1)循环去掉,每次执行程序只转换一个数:

例 10.5. 段错误调试实例二

#include <stdio.h>

int main(void)
{
	int sum = 0, i = 0;
	char input[5];

	scanf("%s", input);
	for (i = 0; input[i] != '\0'; i++) {
		if (input[i] < '0' || input[i] > '9') {
			printf("Invalid input!\n");
			sum = -1;
			break;
		}
		sum = sum*10 + input[i] - '0';
	}
	printf("input=%d\n", sum);
	return 0;
}

然后输入一个超长的字符串,看看会发生什么:

$ ./main 
1234567890
Invalid input!
input=-1

看起来正常。再来一次,这次输个更长的:

$ ./main 
1234567890abcdef
Invalid input!
input=-1
Segmentation fault

又段错误了。我们按同样的方法用gdb调试看看:

$ gdb main
......
(gdb) r
Starting program: /home/akaedu/main 
1234567890abcdef
Invalid input!
input=-1

Program received signal SIGSEGV, Segmentation fault.
0x0804848e in main () at main.c:19
19	}
(gdb) l
14			}
15			sum = sum*10 + input[i] - '0';
16		}
17		printf("input=%d\n", sum);
18		return 0;
19	}

gdb指出,段错误发生在第19行。可是这一行什么都没有啊,只有表示main函数结束的}括号。这可以算是一条规律,如果某个函数中发生访问越界,很可能并不立即产生段错误,而在函数返回时却产生段错误

想要写出Bug-free的程序是非常不容易的,即使scanf读入字符串这么一个简单的函数调用都会隐藏着各种各样的错误,有些错误现象是我们暂时没法解释的:为什么变量i的存储单元紧跟在input数组后面?为什么同样是访问越界,有时出段错误有时不出段错误?为什么访问越界的段错误在函数返回时才出现?还有最基本的问题,为什么scanf输入整型变量就必须要加&,否则就出段错误,而输入字符串就不要加&?这些问题在后续章节中都会解释清楚。其实现在讲scanf这个函数为时过早,读者还不具备充足的基础知识。但仍然是有必要讲的,学完这一阶段之后读者应该能写出有用的程序了,然而一个只有输出而没有输入的程序算不上是有用的程序,另一方面也让读者认识到,学C语言不可能不去了解底层计算机体系结构和操作系统的原理,不了解底层原理连一个scanf函数都没办法用好,更没有办法保证写出正确的程序。