前言
呵呵 此问题主要是来自于 帖子 月经结贴 -- 《Segmentation Fault in Linux》
这里主要也是 结合了作者的相关 case, 来做的一些 调试分享
当然 很多的情况还是 蛮有意思
本文主要问题如下
1. 访问可执行文件中的 只读数据
2. 访问不存在的虚拟地址
3. 访问内核地址
4. 访问空指针
5. 访问异常堆地址1
6. 访问异常堆地址2
7. 访问异常堆地址3
1. 访问可执行文件中的 只读数据
比如如下数据, “Hello World” 会被放到 .text 段, 该段只读, 这里程序中试图更新该内存的数据
#include <stdio.h>
#include <stdlib.h>
int main() {
char* s = "Hello World";
s[1] = 'x';
}
调试上下文如下
page fault 的时候 ip 为 4195562 = 0x4004EA
error_code 为 7, PF_PROT | PF_WRITE | PF_USER
0x4004EA 对应的信息如下, 是一段执行代码
对应于 main 中的 如下代码, 映射到业务源代码就是 “s[1] = 'x';”
校验的时候 期望写操作, 但是实际 不支持写操作
接下来就是 输出内核日志信息, 以及向目标进程发送 SIGSEGV 信号
输出内核日志信息如下
日志中输出了 进程名称, 进程编号, 访问的地址, 指令寄存器, 栈顶寄存器, 错误编码 等等信息
出现问题的异常代码为 0x4004ea, 栈顶寄存器的值为 0x7ffdc7c9b1f0
错误编码为 7 表示 PF_PROT | PF_WRITE | PF_USER
(initramfs) ./Test16SigSegvAccessConstants
[ 207.776273] Test16SigSegvAc[258]: segfault at 400585 ip 00000000004004ea sp 00007ffdc7c9b1f0 error 7 in Test16SigSegvAccessConstants[400000+1000]
0x400585 为 .rodata 中
0x4004ea 为 main 中执行出现异常的代码段
2. 访问不存在的虚拟地址
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p = (int *) 0x7ffff7a8e58f;
*p = 10;
}
这个是根据 address 查询虚拟地址, 查询不到
直接走的 bad_area, 输出日志信息, 发送 SIGSEGV 给目标进程
报错日志信息为
(initramfs) ./Test16SigSegvAccessUnknownAddr
[ 7575.969176] Test16SigSegvAc[262]: segfault at 7ffff7a8e58f ip 00000000004004ec sp 00007fffd34e74d0 error 6 in Test16SigSegvAccessUnknownAddr [400000+1000]
出现问题的进程为 262号进程, 异常访问的地址为 0x 7ffff7a8e58f
出现问题的异常代码为 0x4004ec, 栈顶寄存器的值为 0x 7fffd34e74d0
错误编码为 6 表示 PF_WRITE | PF_USER
0x7ffff7a8e58f 为 main 中定义的需要访问的异常地址
0x4004ec 为 main 中执行出现异常的代码段
3. 访问内核地址
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p = (int *) 0xffff88007fb89a80;
*p = 10;
}
如果是访问内核空间的地址
如果是普通用户程序访问, 直接发送 SIGSEGV 信号量
报错日志信息为
(initramfs) ./Test16SigSegvAccessKernelAddr
[ 1014.007466] Test16SigSegvAc[259]: segfault at ffff88007fb89a80 ip 00000000004004ec sp 00007fffc027d130 error 7 in Test16SigSegvAccessKernelAddr[400000+1000]
出现问题的进程为 259号进程, 异常访问的地址为 0x ffff88007fb89a80
出现问题的异常代码为 0x4004ec, 栈顶寄存器的值为 0x 7fffc027d130
错误编码为 7 表示 PF_PROT | PF_WRITE | PF_USER
0x ffff88007fb89a80 为 main 中定义的需要访问的异常地址
0x4004ec 为 main 中执行出现异常的代码段
4. 访问空指针
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p = NULL;
*p = 10;
}
这个是根据 address 查询虚拟地址, 查询不到
直接走的 bad_area, 输出日志信息, 发送 SIGSEGV 给目标进程
报错日志信息为
(initramfs) ./Test16SigSegvAccessNpe
[ 9696.656307] Test16SigSegvAc[264]: segfault at 0 ip 00000000004004e6 sp 00007fffd2d459c0 error 6 in Test16SigSegvAccessNpe[400000+1000]
出现问题的进程为 264号进程, 异常访问的地址为 0x 0
出现问题的异常代码为 0x4004e6, 栈顶寄存器的值为 0x 7fffd2d459c0
错误编码为 6 表示 PF_WRITE | PF_USER
0x0 为 main 中定义的需要访问的异常地址
0x4004e6 为 main 中执行出现异常的代码段
5. 访问异常堆地址1
这里调整了一下 原文档中的测试用例, 源文档中作者的理解应该是存在问题
所以 当我看到 overflow 15k 的时候很奇怪, 源文档中每次增量是 0k, 1k, 2k, 3k, …, 15k
但是 按照原作者的期望应该每次 增量是 1k, 这里我们稍微 调整了一下 测试用例
然后 原作者文档中提到当初次分配16M的时候, SISSEGV 延迟到了 180k, 这个 按道理来说作者的理解应该也是存在问题, 初次分配 16M的时候 malloc 分配的虚拟地址是在 mmap 映射区 这两种情况 得分开讨论
#include <stdio.h>
#include <stdlib.h>
#define K 1024
int main () {
char* c;
int i = 0;
c = malloc (1);
while (1) {
char* off = c + i*K;
*off = 'a';
printf ("overflow %dK\n", i);
i ++;
}
}
按照我们对于 malloc 的理解, 程序开始的时候 malloc 分配的 chunk 会在 132kb 左右
这里 malloc(1) 会暂用 32byte, printf 会占用 1kb 左右
然后 第一个循环中 操作的是 c 所在的内存空间, 第二个循环 操作的是 printf 的缓冲区
到后面 132kb 末尾, 每 4kb 会有一个缺页中断, 操作的是对应的偏移的空间
超过 132kb 之后, 会因为找不到 vma, 而发生 SIGSEGV
6. 访问异常堆地址2
#include <stdio.h>
#include <stdlib.h>
#define K 1024
int main () {
int* a;
a = malloc (sizeof(int));
*a = 100;
printf ("0x%x\n", a);
printf ("%d\n", *a);
free (a);
printf ("%d\n", *a);
}
这个测试用例不会报错很正常
因为 malloc, free 维护的空间, 不管 free 之前还是在之后, 其申请的虚拟地址空间 都属于当前进程
malloc(sizeof(int)) 会申请 132kb 的空间
然后 a 对应的地址会为 0x602010, 然后 这块地址 可读可写
不会 出现 SIGSEGV
这里 page_fault 产生的 address 为 0x602008 是因为是在 malloc 的过程中设置这块空间的头部信息, 这里会走正常的缺页中断
这里 走正常的缺页中断处理
第二次访问的时候, 地址合法, 并且 虚拟内存对应的物理内存已经加载
我们大致看一下这个过程中 glibc 的 free 的相关处理
这里两次输出之所以 第二次值为 0, 是因为 free 的时候需要在 chunkptr 中维护空闲链表信息
这里是当前区域的 第一块空闲区域, 更新 p->forward 为 NULL, 值为 0
调整一下代码, 我们从程序上面简单的验证一下 这里的 forward 的处理
#include <stdio.h>
#include <stdlib.h>
#define K 1024
int main () {
int* a = malloc (sizeof(int));
int* b = malloc (sizeof(int));
*a = 100;
printf ("0x%x\n", a);
printf ("%d\n", *a);
free (a);
printf ("%d\n", *a);
*b = 100;
printf ("0x%x\n", b);
printf ("%d\n", *b);
free (b);
printf ("%d\n", *b);
}
b 对应的 chunkptr->fd[等价于b的数据空间] 为 6299648 为 0x602000, 记录的是前一块 空闲的chunkptr 的地址
a 对应的 chunkptr->fd[等价于a的数据空间] 为 0 为 NULL, 记录的是前一块空闲的 chunkptr 的地址
root@ubuntu:~/Desktop/linux/HelloWorld# ./Test16SigSegvAccessInvalidHeapAddr02
0x602010
100
0
0x602030
100
6299648
7. 访问异常堆地址3
这个主要是在 glibc 层面的限制, 处理
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void foo () {
char c;
memset (&c, 0x55, 128);
}
int main () {
foo();
}
日志输出如下
在 glibc 层面处理如下, 输出了如上 日志信息, 然后向给定的进程 发送了 SIGABRT 信号
内核这边 调试收到 SIG_ABRT 的信号的地方如下
foo 编译结果如下
gdb 调试这个过程如下
Reading symbols from Test16SigSegvAccessInvalidHeapAddr03...
(gdb) list
1
2 #include <stdio.h>
3 #include <stdlib.h>
4 #include <string.h>
5
6 void foo () {
7 char c;
8
9 memset (&c, 0x55, 128);
10 }
(gdb) b Test16SigSegvAccessInvalidHeapAddr03.c:9
Breakpoint 1 at 0x4005ad: file Test16SigSegvAccessInvalidHeapAddr03.c, line 9.
(gdb) run
Starting program: /root/linux/tmp/Test16SigSegvAccessInvalidHeapAddr03
Breakpoint 1, foo () at Test16SigSegvAccessInvalidHeapAddr03.c:9
9 memset (&c, 0x55, 128);
(gdb) disassemble
Dump of assembler code for function foo:
0x0000000000400596 <+0>: push %rbp
0x0000000000400597 <+1>: mov %rsp,%rbp
0x000000000040059a <+4>: sub $0x10,%rsp
0x000000000040059e <+8>: mov %fs:0x28,%rax
0x00000000004005a7 <+17>: mov %rax,-0x8(%rbp)
0x00000000004005ab <+21>: xor %eax,%eax
=> 0x00000000004005ad <+23>: lea -0x9(%rbp),%rax
0x00000000004005b1 <+27>: mov $0x80,%edx
0x00000000004005b6 <+32>: mov $0x55,%esi
0x00000000004005bb <+37>: mov %rax,%rdi
0x00000000004005be <+40>: call 0x400470 <memset@plt>
0x00000000004005c3 <+45>: nop
0x00000000004005c4 <+46>: mov -0x8(%rbp),%rax
0x00000000004005c8 <+50>: xor %fs:0x28,%rax
0x00000000004005d1 <+59>: je 0x4005d8 <foo+66>
0x00000000004005d3 <+61>: call 0x400460 <__stack_chk_fail@plt>
0x00000000004005d8 <+66>: leave
0x00000000004005d9 <+67>: ret
End of assembler dump.
(gdb) stepi
0x00000000004005b1 9 memset (&c, 0x55, 128);
(gdb) stepi
0x00000000004005b6 9 memset (&c, 0x55, 128);
(gdb) stepi
0x00000000004005bb 9 memset (&c, 0x55, 128);
(gdb) stepi
0x00000000004005be 9 memset (&c, 0x55, 128);
(gdb) stepi
0x0000000000400470 in memset@plt ()
(gdb) step
Single stepping until exit from function memset@plt,
which has no line number information.
foo () at Test16SigSegvAccessInvalidHeapAddr03.c:10
10 }
(gdb) stepi
0x00000000004005c4 10 }
(gdb) stepi
0x00000000004005c8 10 }
(gdb) stepi
0x00000000004005d1 10 }
(gdb) stepi
0x00000000004005d3 10 }
(gdb) disassemble
Dump of assembler code for function foo:
0x0000000000400596 <+0>: push %rbp
0x0000000000400597 <+1>: mov %rsp,%rbp
0x000000000040059a <+4>: sub $0x10,%rsp
0x000000000040059e <+8>: mov %fs:0x28,%rax
0x00000000004005a7 <+17>: mov %rax,-0x8(%rbp)
0x00000000004005ab <+21>: xor %eax,%eax
0x00000000004005ad <+23>: lea -0x9(%rbp),%rax
0x00000000004005b1 <+27>: mov $0x80,%edx
0x00000000004005b6 <+32>: mov $0x55,%esi
0x00000000004005bb <+37>: mov %rax,%rdi
0x00000000004005be <+40>: call 0x400470 <memset@plt>
0x00000000004005c3 <+45>: nop
0x00000000004005c4 <+46>: mov -0x8(%rbp),%rax
0x00000000004005c8 <+50>: xor %fs:0x28,%rax
0x00000000004005d1 <+59>: je 0x4005d8 <foo+66>
=> 0x00000000004005d3 <+61>: call 0x400460 <__stack_chk_fail@plt>
0x00000000004005d8 <+66>: leave
0x00000000004005d9 <+67>: ret
End of assembler dump.
完