全部博文(44)
分类:
2010-03-08 13:59:01
看了栈的例子,举一反三就能知道,SIGSEGV和堆的关系取决于你的内存分配器,通常这意味着取决于C库的实现。
1 #include
2 #include
3
4 #define K 1024
5 int main () {
6 char* c;
7 int i = 0;
8
9 c = malloc (1);
10 while (1) {
11 c += i*K;
12 *c = 'a';
13 printf ("overflow %dK\n", i);
14 i ++;
15 }
16 }
上面这个例子在笔者机器上于15K时产生SIGSEGV。让我们改变初次malloc的内存大小,当初次分配16M时,SIGSEGV推迟到了溢出180K;当初次分配160M时,SIGSEGV推迟到了溢出571K。我们知道内存分配器在分配不同大小的内存时通常有不同的机制,这个例子从某种角度证明了这点。此例SIGSEGV在图2中的流程为:
1 -à 3 -à 4 -à 5 -à 11 -à 10
用一个野指针在堆里胡乱访问很少见,更多被问起的是“为什么我访问一块free()后的内存却没发生SIGSEGV”,比如下面这个例子:
1 #include
2 #include
3
4 #define K 1024
5 int main () {
6 int* a;
7
8 a = malloc (sizeof(int));
9 *a = 100;
10 printf ("%d\n", *a);
11 free (a);
12 printf ("%d\n", *a);
13 }
SIGSEGV没有发生,但free()后a指向的内存被清零了,一个合理的解释是为了安全。相信不会再有人问SIGSEGV没发生的原因。是的,free()后的内存不一定就立即归还给了操作系统,在真正的归还发生前,它一直在那儿。
看了上面两个例子,我觉得这实在没什么好讲的。
这也是产生SIGSEGV的常见原因,来看下面的例子:
1 #include
2 #include
3 #include
4
5 void foo () {
6 char c;
7
8 memset (&c, 0x55, 128);
9 }
10
11 int main () {
12 foo();
13 }
通过栈溢出,我们将函数foo的返回地址覆盖成了0x55555555,函数跳转到了一个非法地址执行,最终引发SIGSEGV。非法地址执行,在图2中的流程中的可能性就太多了,从1-à3 -à4 -à … -à10,从4到10之间,几乎每条路径都可能出现。当然对于此例,0x55555555所指向的页面并不在内存之中,其在图2的流程为:
1-à3 -à4 -à5--à11-à10
如果非法地址对应的页面(页面属于用户态地址空间)存在于内存中,它又是可执行的[*],则程序会执行一大堆随机的指令。在这些指令执行过程中一旦访问内存,其产生SIGSEGV的流程几乎就无法追踪了(除非你用调试工具跟进)。看到这里,一个很合理的问题是:为什么程序在非法地址中执行的是随机指令,而不是非法指令呢?在一块未知的内存上执行,遇到非法指令可能性比较大吧,这样应该收到SIGILL信号啊?
[*]如果不用段寄存器的type checking,只用页表保护,传统32bit IA32可读即可执行。在NX技术出现后页级也可以控制是否可以执行。
事实并非如此,我们的IA32架构使用了如此复杂的指令集,以至于找到一条非法指令的编码还真不容易。在下例子中:
1 #include
2 #include
3
4 int main() {
5 char buf[128] = "asdfaowerqoweurqwuroahfoasdbaoseur20 234123akfhasbfqower53453";
6 sleep(1);
7 }
笔者在buf中随机的敲入了一些字符,反汇编其内容得到的结果是:
0xbffa9e00: popa
0xbffa9e01: jae 0xbffa9e67
0xbffa9e03: popaw
0xbffa9e05: outsl %ds:(%esi),(%dx)
0xbffa9e06: ja 0xbffa9e6d
0xbffa9e08: jb 0xbffa9e7b
0xbffa9e0a: outsl %ds:(%esi),(%dx)
0xbffa9e0b: ja 0xbffa9e72
0xbffa9e0d: jne 0xbffa9e81
0xbffa9e0f: jno 0xbffa9e88
0xbffa9e11: jne 0xbffa9e85
0xbffa9e13: outsl %ds:(%esi),(%dx)
0xbffa9e14: popa
0xbffa9e15: push $0x73616f66
0xbffa9e1a: bound %esp,%fs:0x6f(%ecx)
0xbffa9e1e: jae 0xbffa9e85
0xbffa9e20: jne 0xbffa9e94
0xbffa9e22: xor (%eax),%dh
0xbffa9e24: and %ah,(%eax)
0xbffa9e26: and %dh,(%edx)
0xbffa9e28: xor (%ecx,%esi,1),%esi
0xbffa9e2b: xor (%ebx),%dh
0xbffa9e2d: popa
0xbffa9e2e: imul $0x61,0x68(%esi),%esp
0xbffa9e32: jae 0xbffa9e96
0xbffa9e34: data16
0xbffa9e35: jno 0xbffa9ea6
0xbffa9e37: ja 0xbffa9e9e
0xbffa9e39: jb 0xbffa9e70
0xbffa9e3b: xor 0x33(,%esi,1),%esi
0xbffa9e42: add %al,(%eax)
0xbffa9e44: add %al,(%eax)
0xbffa9e46: add %al,(%eax)
0xbffa9e48: add %al,(%eax)
0xbffa9e4a: add %al,(%eax)
0xbffa9e4c: add %al,(%eax)
0xbffa9e4e: add %al,(%eax)
0xbffa9e50: add %al,(%eax)
0xbffa9e52: add %al,(%eax)
0xbffa9e54: add %al,(%eax)
0xbffa9e56: add %al,(%eax)
0xbffa9e58: add %al,(%eax)
0xbffa9e5a: add %al,(%eax)
0xbffa9e5c: add %al,(%eax)
0xbffa9e5e: add %al,(%eax)
…………………………………………………………………………
一条非法指令都没有!大家也可以自己构造一些随机内容试试,看能得到多少非法指令。故在实际情况中,函数跳转到非法地址执行时,遇到SIGSEGV的概率是远远大于SIGILL的。
我们来构造一个遭遇SIGILL的情况,如下例:
#include
#include
#include
#define GET_EBP(ebp) \
do { \
asm volatile ("movl %%ebp, %0\n\t" : "=m" (ebp)); \
} while (0)
char buf[128];
void foo () {
printf ("Hello world\n");
}
void build_ill_func() {
int i = 0;
memcpy (buf, foo, sizeof(buf));
while (1) {
/*
* Find *call* instruction and replace it with
* *ud2a* to generate a #UD exception
*/
if ( buf[i] == 0xffffffe8 ) {
buf[i] = 0x0f;
buf[i+1] = 0x0b;
break;
}
i ++;
}
}
void overflow_ret_address () {
unsigned long ebp;
unsigned long addr = (unsigned long)buf;
int i;
GET_EBP (ebp);
for ( i=0; i<16; i++ )
memcpy ((void*)(ebp + i*sizeof(addr)), &addr, sizeof(addr));
printf ("ebp = %#x\n", ebp);
}
int main() {
printf ("%p\n", buf);
build_ill_func ();
overflow_ret_address ();
}
我们在一块全局的buf里填充了一些指令,其中有一条是ud2a,它是IA32指令集中用来构造一个非法指令陷阱。在overflow_ret_address()中,我们通过栈溢出覆盖函数的返回地址,使得函数返回时跳转到buf执行,最终执行到ud2a指令产生一个SIGILL信号。注意此例使用了ebp框架指针寄存器,在编译时不能使用-fomit-frame-pointer参数,否则得不到期望的结果。
这是一种较为特殊的情况。特殊是指前面的例子访问非法内存都发生在用户态。而此例中,对非法内存的访问却发生在内核态。通常是执行copy_from_user()或copy_to_user()时。其流程在图2中为:
1 -à …. -à 11 -à 12 -à 13
内核使用fixup[*]的技巧来处理在处理此类错误。ULK说通常的处理是发送一个SIGSEGV信号,但实际大多数系统调用都可以返回EFAULT(bad address)码,从而避免用户态程序被终结。这种情况就不举例了,笔者一时间想不出哪个系统调用可以模拟此种情况而不返回EFAULT错误。
[*]关于fixup的技巧可以参考笔者另一篇文章《Linker Script in Linux》,
我们已经总结了产生SIGSEGV的大多数情况,在实际编程中,即使现象不一样,最终发生SIGSEGV的原因都可以归到上述几类。掌握了这些基本例子,我们可以避免大多数的SIGSEGV。