`
haoningabc
  • 浏览: 1476688 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

segment fault in linux (转)

阅读更多
参考 《Segmentation Fault in Linux》Author: ZX_WING(xing5820@163.com)

参考http://duartes.org/gustavo/blog/post/anatomy-of-a-program-in-memory

2.1错误的访问类型引起的SIGSEGV
 #include <stdio.h>
 #include <stdlib.h>

 int main() {
 char* s = "hello world";

 s[1] = 'H';
 }

这是最常见的一个例子。想当年俺对C 语言懵懂的时候,也在校内的BBS 上发帖问过,
当时还以为这是指针和数组的区别。此例中,”hello world”作为一个常量字符串,在编译后
会被放在.rodata 节(GCC),最后链接生成目标程序时.rodata 节会被合并到text segment 与
代码段放在一起,故其所处内存区域是只读的。这就是错误的访问类型引起的SIGSEGV。
其在图2 中的顺序为:

1 - 3 - 4 - 6 - 8 - 11 -10


访问了不属于进程地址空间的内存
 #include <stdio.h>
 #include <stdlib.h>

 int main() {
 int* p = (int*)0xC0000fff;

 *p = 10;
 }

在这个例子中,我们访问了一个属于内核的地址(IA32,32bit)。当然,很少会有人这
样写程序,但你的程序可能在不经意的情况下做出这样的行为(这个不经意的行为在后面讨
论)。此例在图2 的流程:
1 - 2 - 11 - 10

2.3访问了不存在的内存
最常见的情况不外乎解引用空指针了,如:
 #include <stdio.h>
 #include <stdlib.h>

 int main () {
 int *a = NULL;

 *a = 1;
 }

在实际情况中,此例中的空指针可能指向用户态地址空间,但其所指向的页面实际不
存在。其产生SIGSEGV 在图2 中的流程为:
1  3 - 4 - 5 - 11 -10

栈溢出了,有时SIGSEGV,有时却啥都没发生
这也是CU 常见的一个月经贴。大部分C 语言教材都会告诉你,当从一个函数返回后,
该函数栈上的内容会被自动“释放”。“释放”给大多数初学者的印象是free(),似乎这块内
存不存在了,于是当他访问这块应该不存在的内存时,发现一切都好,便陷入了深深的疑惑。
 #include <stdio.h>
 #include <stdlib.h>

 int* foo() {
 int a = 10;

 return &a;
 }

 int main() {
 int* b;

 b = foo();
 printf ("%d\n", *b);
 }

当你编译这个程序时,会看到“warning: function returns address of local variable”,GCC
已经在警告你栈溢的可能了。实际运行结果一切正常。原因是操作系统通常以“页”的粒度
来管理内存,Linux 中典型的页大小为4K,内核为进程栈分配内存也是以4K 为粒度的。故
当栈溢的幅度小于页的大小时,不会产生SIGSEGV。那是否说栈溢出超过4K,就会产生
SIGSEGV 呢?看下面这个例子:

 #include <stdio.h>
 #include <stdlib.h>
 char* foo() {
 char buf[8192];

 memset (buf, 0x55, sizeof(buf));
 return buf;
 }

 int main() {
 char* c;

 c = foo();
 printf ("%#x\n", c[5000]);
 }

虽然我们的栈溢已经超出了4K 大小,可运行仍然正常。这是因为C 教程中提到的“栈
自动释放”实际上是改变栈指针,而其指向的内存,并不是在函数返回时就被回收了。在我
们的例子中,所访问的栈溢处内存仍然存在。无效的栈内存(即栈指针范围外未被回收的栈
内存)是由操作系统在需要时回收的,这是无法预测的,也就无法预测何时访问非法的栈内
容会引发SIGSEGV。
好了,在上面的例子中,我们的栈溢例子,无论是大于一个页尺寸还是小于一个页尺寸,
访问的都是已分配而未回收的栈内存。那么访问未分配的栈内存,是否就一定会引发
SIGSEGV 呢?答案是否定的。
 #include <stdio.h>
 #include <stdlib.h>

 int main() {
 char* c;

 c = (char*)&c – 8192 *2;
 *c = 'a';
 printf ("%c\n", *c);
 }

在IA32 平台上,栈默认是向下增长的,我们栈溢16K,访问一块未分配的栈区域(至
少从我们的程序来看,此处是未分配的)。选用16K 这个值,是要让我们的溢出范围足够大,
大过内核为进程分配的初始栈大小(初始大小为4K 或8K)。按理说,我们应该看到期望的
SIGSEGV,但结果却非如此,一切正常。
答案藏在内核的page fault 处理函数中:
if (error_code & PF_USER) {
/* Accessing the stack below %sp is always a bug.
* The large cushion allows instructions like enter
* and pusha to work. ("enter $65535,$31" pushes
* 32 pointers and then decrements %sp by 65535.)
*/
if (address + 65536 + 32 * sizeof(unsigned long) < regs->sp)
goto bad_area;
}
if (expand_stack(vma, address))
goto bad_area;

内核为enter
  • 这样的指令留下了空间,
  • 从代码来看,理论上栈溢小于64K 左右都是没
    问题的,栈会自动扩展。令人迷惑的是,笔者用下面这个例子来测试栈溢的阈值,得到的确
    是70K ~ 80K 这个区间,而不是预料中的65K ~ 66K。
  • 关于enter 指令的详细介绍,请参考《Intel(R) 64 and IA-32 Architectures Software
  • Developer Manual Volume 1》6.5 节“ PROCEDURE CALLS FOR BLOCK-STRUCTURED
    LANGUAGES”

    1 #include <stdio.h>
    2 #include <stdlib.h>
    3
    4 #define GET_ESP(esp) do { \
    5 asm volatile ("movl %%esp, %0\n\t" : "=m" (esp)); \
    6 } while (0)
    7
    8
    9 #define K 1024
    10 int main() {
    11 char* c;
    12 int i = 0;
    13 unsigned long esp;
    14
    15 GET_ESP (esp);
    16 printf ("Current stack pointer is %#x\n", esp);
    17 while (1) {
    18 c = (char*)esp - i * K;
    19 *c = 'a';
    20 GET_ESP (esp);
    21 printf ("esp = %#x, overflow %dK\n", esp, i);
    22 i ++;
    23 }
    24 }


    笔者目前也不能解释其中的魔术,这神奇的程序啊!上例中发生SIGSEGV 时,在图2
    Segmentation Fault in Linux
    12
    中的流程是:
    1 - 3 - 4 - 5 - 11 - 10 (注意,发生SIGSEGV 时,该地址已经不属于用户态
    栈了,所以是5  11 而不是5 - 6)
    到这里,我们至少能够知道SIGSEGV 和操作系统(栈的分配和回收),编译器(谁知
    道它会不会使用enter 这样的指令呢)有着密切的联系,而不像教科书中“函数返回后其使
    用的栈自动回收”那样简单。

    我们知道栈了,那么堆呢?
    看了栈的例子,举一反三就能知道,SIGSEGV 和堆的关系取决于你的内存分配器,通
    常这意味着取决于C 库的实现。

    1 #include <stdio.h>
    2 #include <stdlib.h>
    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 <stdio.h>
    2 #include <stdlib.h>
    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()后的内存不一定就立即归还给了操
    作系统,在真正的归还发生前,它一直在那儿。

    2.6如果是指向全局区的野指针呢?
    看了上面两个例子,我觉得这实在没什么好讲的。
    2.7 函数跳转到了一个非法的地址上执行
    这也是产生SIGSEGV 的常见原因,来看下面的例子:

    1 #include <stdio.h>
    2 #include <stdlib.h>
    3 #include <string.h>
    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 <stdio.h>
    2 #include <stdlib.h>
    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 <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #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 参数,否则得不到期
    望的结果。
    原书在附件中
    • 大小: 121.9 KB
    分享到:
    评论

    相关推荐

      Linux环境下段错误(Segmentation fault)的产生原因及调试方法

      在Linux环境下,编程时经常会遇到一个让开发者头疼的问题——段错误(Segmentation fault)。这个问题通常是由于程序尝试访问其不应该访问或者无法访问的内存区域而触发的。本文将深入探讨段错误的原因以及如何在...

      使用valgrind 检测qt程序代码泄漏时,程序直接segment fault

      写的qt程序为hello,然后运行命令为:valgrind ./hello,然后就报了如下错误: ==32140==  ... (in /usr/lib/x86_64-linux-gnu/dri/vmwgfx_dri.so) ==32140== by 0x222DEBD5: ??? (in /usr/lib/x86_6

      STACK1_SEGMENT_STACK.rar_STACK1 SEGMENT_stack segment stack

      在汇编语言的学习中,"STACK1_SEGMENT_STACK.rar_STACK1 SEGMENT_stack segment stack"这个标题提到了两个关键概念:栈段(Stack Segment)和栈(Stack)。栈在计算机科学中扮演着至关重要的角色,尤其是在汇编语言...

      Fatal Error[e72]: Segment FIQ_STACK must be defined in a segment definition option (-Z, -b or -P)

      使用IAR开发ADI的ADUC70XX系列单片机时,编译出现下面错误提示: Fatal Error[e72]: Segment FIQ_STACK must b

      AttackLab实验-计算机系统基础-gddrxy

      “AttackLab”是一个Linux下的可执行C程序,包含了5个阶段(phase1~phase5)的不同内容。程序运行过程中,要求学生能够根据缓冲区的工作方式和程序的反汇编代码来确定攻击字符串长度和字符串中的关键内容。每次成功...

      Segment Routing 特性微图

      3. 路径计算:节点利用SPF算法,根据收集的拓扑信息生成标签转发路径,建立转发表项。 SR-BE(Best Effort)是基于Prefix Segment的SR实现,它使用SPF算法计算最短路径。SR-TE则利用Adjacency Segment或Node ...

      Segment Routing

      标题:Segment Routing(段路由) 描述:比MPLS TE更好的技术 MPLS -SR PPT 标签:Segment Routing MPLS SR,MPLS TE 基于提供的文件内容,我们可以深入探讨Segment Routing(SR)这一概念,它被视为MPLS Traffic...

      Learning to Segment Moving Objects in Videos - CVPR2015.pdf

      根据提供的文件内容,这篇文章主要讨论了在视频中分割移动物体的方法,并详细介绍了相关技术细节和实验结果。以下是文章中涉及的知识点: 1. 视频中移动物体的分割问题:文章主要解决的是如何从视频中准确地分割出...

      Laravel开发-segment

      在命令行中运行`composer require spatie/laravel-segment`,这将把Segment的包添加到项目的依赖列表并下载相关文件。 2. **配置服务**:在`config/app.php`中注册服务提供者,将`Spatie\Segment\...

      3588交叉编译ffmpeg

      3588交叉编译ffmpeg

      libsigar-amd64-linux.so以及libsigar-x86-linux.so

      标题中的"libsigar-amd64-linux.so"和"libsigar-x86-linux.so"是两个重要的库文件,它们属于Sigar(System Information Gatherer and Reporter)项目,一个跨平台的系统性能监控工具集。Sigar由Hewlett Packard ...

      swift中实现segment在导航栏控制页面之间的转换

      在Swift编程中,Segment Control是一种常见的用户界面元素,它允许用户在多个选项之间进行选择,类似于iOS中的Tab Bar。在导航栏(NavigationBar)上使用Segment Control可以为用户提供一种直观的方式来切换不同的...

      Segment Routing培训PPT

      Segment Routing 思科原厂培训PPT L3/L2 , Traffic Engineering (TE) / Fast Reroute (FRR) services are offered over the MPLS backbone Complex protocol stacks Complex troubleshooting & operation

      linux-uvc_0.1.0.svn54.orig.tar.gz_UVC驱动_genetic_segment.rar_linu

      Linux UVC(Universal Video Class)驱动是用于支持USB视频设备的开源驱动程序,适用于Linux操作系统。这个"linux-uvc_0.1.0.svn54.orig.tar.gz"是一个源代码压缩包,版本号为0.1.0,并且基于Subversion版本控制系统...

      segment-anything

      "Segment Anything"是Facebook AI团队开源的一个先进图像处理工具,主要专注于图像分割任务。这个工具的目的是为了帮助研究人员和开发者更高效地实现对图像中特定对象的精确识别和分离,从而进行深度学习模型的训练...

      android segment

      在Android开发中,"Segment"通常指的是视图组件或者布局管理方式的一种,它允许开发者将多个组件或视图组织在一个可滚动的容器中,类似Tab布局。本篇将深入探讨如何在Android中自定义Segment,并使其能正常运行。 ...

      linux c段错误处理

      什么是“Segmentation fault in Linux”? 段错误(Segmentation fault),通常简称为SIGSEGV,是计算机软件运行过程中的一种特定错误条件。当一个程序尝试访问其无权访问的内存位置,或者以不被允许的方式访问...

      ios swift写的自定义segment

      最后,为了方便在其他地方使用这个自定义Segment,我们提供一些公共方法,比如`addSegment(withTitle:)`用于添加新的Segment,以及`setSelectedSegment(at:animated:)`用于程序逻辑中改变选中的Segment。 总的来说...

      ios 自定义Segment 使用代理模式完成

      在iOS开发中,Segment Control是一种常见的用户界面组件,用于在多个视图或内容之间进行切换。自定义Segment Control可以提供更个性化的用户体验,使应用程序更具吸引力。本篇将详细介绍如何在iOS中使用代理模式来...

    Global site tag (gtag.js) - Google Analytics