我之前为了教育目的写的一些东西......
考虑以下 c 程序:
int q[200];
main(void) {
int i;
for(i=0;i<2000;i++) {
q[i]=i;
}
}
编译并执行后,会生成一个核心转储:
$ gcc -ggdb3 segfault.c
$ ulimit -c unlimited
$ ./a.out
Segmentation fault (core dumped)
现在使用 gdb 进行事后分析:
$ gdb -q ./a.out core
Program terminated with signal 11, Segmentation fault.
[New process 7221]
#0 0x080483b4 in main () at s.c:8
8 q[i]=i;
(gdb) p i
$1 = 1008
(gdb)
呵呵,当一个人在分配的200个项目之外写入时,程序并没有出现段错误,而是在i=1008时崩溃了,为什么呢?
输入页面。
在 UNIX/Linux 上可以通过多种方式确定页面大小,一种方法是使用系统函数 sysconf(),如下所示:
#include <stdio.h>
#include <unistd.h> // sysconf(3)
int main(void) {
printf("The page size for this system is %ld bytes.\n",
sysconf(_SC_PAGESIZE));
return 0;
}
给出输出:
该系统的页面大小为 4096 字节。
或者可以使用命令行实用程序 getconf,如下所示:
$ getconf PAGESIZE
4096
尸检
事实证明,段错误不是发生在 i=200 处,而是发生在 i=1008 处,让我们找出原因。启动 gdb 进行一些事后分析:
$gdb -q ./a.out core
Core was generated by `./a.out'.
Program terminated with signal 11, Segmentation fault.
[New process 4605]
#0 0x080483b4 in main () at seg.c:6
6 q[i]=i;
(gdb) p i
$1 = 1008
(gdb) p &q
$2 = (int (*)[200]) 0x804a040
(gdb) p &q[199]
$3 = (int *) 0x804a35c
q 结束于地址 0x804a35c,或者更确切地说,q[199] 的最后一个字节位于该位置。正如我们之前看到的,页面大小为 4096 字节,机器的 32 位字大小将虚拟地址分解为 20 位页号和 12 位偏移量。
q[] 以虚拟页号结尾:
0x804a = 32842
抵消:
0x35c = 860
所以仍然有:
4096 - 864 = 3232
分配 q[] 的内存页上剩余的字节数。该空间可以容纳:
3232 / 4 = 808
整数,并且代码将其视为包含 q 在位置 200 到 1008 处的元素。
我们都知道这些元素不存在,编译器没有抱怨,硬件也没有抱怨,因为我们对该页面有写权限。仅当 i=1008 时 q[] 引用了我们没有写入权限的不同页面上的地址,虚拟内存硬件才会检测到这一点并触发段错误。
一个整数存储在 4 个字节中,这意味着该页面包含 808 (3236/4) 个额外的假元素,这意味着从 q[200]、q[201] 一直到元素 199 访问这些元素仍然是完全合法的+808=1007 (q[1007]) 而不触发段故障。访问 q[1008] 时,您将进入一个权限不同的新页面。