转载:Segmentation fault到底是何方妖孽

2024-05-05 16:48

本文主要是介绍转载:Segmentation fault到底是何方妖孽,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

转载一篇很不错的分析Linux “Segmentation fault”报错的文章
   Linux上开发时最恼火的就是遇到“Segmetation Fault”错误。为什么这么说,很多人看到这个错误后心里第一反应是程序访问的非法的内存,导致其被操作系统强行终止。这固然没错,可这里有个比较模糊的概念了:什么叫“非法”的内存?

   程序运行时,每个进程都有自己的虚拟地址,理论上说进程应该可以随便使用才对,为什么还会出现这个错误呢?这里就涉及到程序的装载过程及原理。
   先澄清几个概念:
    程序:一般是一组CPU指令的集合构成的文件,静态存储在诸如硬盘之类的存储设备上。
    进程:当一个程序要被计算机运行时,就是在内存中产生该程序的一个运行时实例,我们就把这个实例叫做进程。
    装载:上述从硬盘上的静态“程序”到内存中动态的“进程”之间的转变过程就叫做装载。往通俗里讲,就是启动一个进程。

   本文的主要目的是在简单了解进程的内存布局的情况下,从装载的过程入手,深入了解一下Segmetation Fault在操作系统层面是如何产生的,以及程序开发过程中应该如何避免这样的错误。
   众所周知Linux中可执行文件的格式是ELF,其实编译过程中的中间文件*.o文件、动态共享库*.so文件也是ELF格式的。在链接器看来,当它通过*.o或者配合*.so文件来生成可执行文件时,它对ELF格式的文件以链接视图(Linking View)进行看待。也就是说链接器以Section的形式来对待和处理ELF文件,诸如我们常见说的代码段(.text)、数据段(.data和.bss)等待概念。当程序最终需要被装载成进程时,装载器就出场了,装载器将可执行文件以装载视图(Executive View)进行看待。装载器将以Segment的形式来处理ELF文件。网上很多教程也是这样说的,大家可能还是理解的不是很明白,后面我们通过实例的方式将进一步向大家来澄清这两者的区别。

   既然*.o、*.so和可执行文件都是ELF格式,那么链接器和装载器是如何区分它们的呢?
   看一个简单的例子:


    readelf –h命令能够可以查看一个EFL文件的头部信息。因为viewobj.o是编译时的中间临时文件,所以它的“Start of pgrogram headers”和“Number of program headers”都为0,说明他不是一个可执行文件。取而代之的是它有9个section,所以它有“Start of section headers”和“Number of section headers”都有数据。
   再看一下动态共享库:

    
   在Linux下动态共享库被当作可执行文件来处理,虽然它不能单独执行,但某些应用程序的运行离不了它。
   最后是可执行文件,这个就不用多说了,看图:

    
   所以,我们可以得到这样一个结论:一个具体的ELF文件,其文件头部中的某些属性值,指明了它到底是可执行文件还是可重定位文件(o和.so的统称)。这样,链接器和装载器通过分析ELF文件头部就可以知道它该怎么处理该文件了。用比较直观的、方便理解的图来表示它们的区别就是:

    
   也就是说链接的时候Program Header Table是可选的,但Section Header Table是必须有的。例如*.o就没有Program Header Table,而*.so就有。装载的时候Program Header Table必须有,但Section Header Table是可选的,但即使有Section Header Table,装载器也不会鸟它。

   那么,装载器为什么要采取和链接器不同的处理策略呢?最主要的原因是为了提高内存的利用率。现代操作系统在装载程序时都充分利用程序的局部性原理,那就是,当进程运行时,并不需要一下子将程序的所有代码和数据都装载到内存里,而是先装载程序的一部分到内存里运行。当进程将要执行的指令不在内存里的话,CPU便会触发一个缺页异常,操作系统捕获到这样的异常后便接管进程,然后将需要的指令“弄”到内存里,再将执行权限还给进程。

   进程运行的时候,它虚拟地址空间的布局和它所占用的物理内存到底是什么样子呢?虚拟地址空间我们还比较好理解,可实际物理地址并不是我们能直接访问到的。一般是通过一个集成在CPU内部的叫做MMU的内存管理单元完成了从进程虚拟地址到物理地址之间的映射。对这个映射过程感兴趣的童鞋可以去拜读Bean_lee兄的“Linux 从虚拟地址到物理地址”文章,那是相当之精彩。如果看不懂,就随时咨询他老人家。不过据我所知,他最近有点忙,忙得不亦乐乎,呵呵。OK,回到我们的话题上来。既然进程虚拟地址空间的任何地址,在使用前都必须通过MMU将其映射到物理内存上一个实实在在的存储单元上。那么对于任何没有经过MMU映射过的虚拟空间的地址,不管进程是执行写操作还是读操作,操作系统都会捕捉到这个错误的非法访问,然后输出一个“Segmetation Fault”的错误提示信息并强行终止进程。

   换句话说,一个进程虚拟空间里的任何地址,在进程访问它之前必须要经过MMU转换,将它映射到物理内存的某个具体的存储位置上才是合法有效的,不然操作系统就会用“Segmetation Fault”对你的进程进行宣判,然后将其kill掉。那么,问题又来了,到底哪些地址才是合法有效的呢?看一个简单的进程虚拟地址空间的布局:


   上图是很多资料上说的Linux进程虚拟地址空间的布局结构图,其中0x0804800为进程运行时的地址入口。注意,这里的入口地址是指你的程序的第一条指令的入口地址,但是当进程运行时,进程环境空间的初始化工作,包括建立程序虚拟地址空间和物理内存的映射、加载动态库等等操作都已经完成了。当所有准备工作就绪之后才会跳到这个地址执行我们程序里的第一条指令。这个0x0804800一般由链接器在生成可执行文件时就已经固定了,通常无需我们来更改。如果你对链接的过程和原理了如指掌,那么你肯定也知道如何修改它了。上图中,当用户的程序直接访问0x084800以前的地址、0xC0000000以后的地址或者free空间里的地址都会触发“Segmetation Fault”。原因如下:

1、0x084800以前的地址、0xC0000000以后的地址:由于权限的问题,不允许进程直接访问,操作系统对其进行保护。所以用户进程如何访问它们的话就会触发“Segmetation Fault”的错误。前面几篇博文有如何访问0xC0000000以后地址的博文,也就是用户空间和内核空间的通信问题。

2、free地址段的空间就是前面说的,由于没有经过MMU将其映射到物理内存的实际存储单元上,当程序访问System break(也就是常说的brk)之后的地址就出引发段错误。brk一般是进程堆空间结束的地方。那么,我们如何知道当前进程的brk在什么地方呢?答案就是通过一个C库函数sbrk()来获取。另外还有一个系统调用brk()用来设置System break的位置,其实sbrk()也可以设置,它只不是对brk()系统调用的一个封装而已。关于这两个函数的更多用法可以参考man手册。

   为了不影响我们的测试效果,我们需要将内核的随机地址保护模式关掉。为了方式溢出攻击,现代很多操作系统都做了这样的随机地址保护。就是,当程序运行时,代码段、堆栈段的装载起始地址并不是固定不变的,而是每次运行进程时都会加上一个随机的偏移量,这会影响我们的测试效果。关闭它的方法很简单:

    [root@localhost ~]#echo “0” > /proc/sys/kernel/randomize_va_space

    如果/proc/sys/kernel/randomize_va_space为0则表示,进程每次启动运行时,其虚拟地址空间里的值就是它在ELF文件里所指定的值;如果为1,则每次启动时只有栈的装载地址做随机保护;如果为2,表示进程每次启动时,进程的装载地址、brk和堆栈地址都会随机变化。看个例子,这是网上流传比较多的一段代码,很具有代表性,这里我又站在前人的肩膀上了:


   由于全局变量bssvar未初始化,所以当程序运行时它会被放置在.bss段,占4字节。sbrk(0)会返回当前brk的值。为了便于观察,我们用了sleep(8)。下面用readelf看一下可执行文件被装载时,Segement的情况将会是什么样子:


    另一方面,内存分配时是以页为单位,一般页大小为4096字节,所以从0x08048000开始是代码段,共占内存0x00628,即1576个字节,不足一个页,但必须以页为单位,所以下一个页,也就是数据页必须从0x0804900开始。但上面显示却说数据页从0x08049628开始,但注意最后一列Allign,指明了对其方式,正好是4096字节。验证一下:



   这里我们看到操作系统确实是以页(4096字节)为单位进行内存分配。有些人可能觉得奇怪,既然stack都已经有了,为什么没有heap呢?原因是,默认情况,.bss段结束地址就是heap的开始地址。当源代码中没有诸如malloc()之类的动态内存分配函数时,在查看进程的内存映射时是看不到heap的。此时的进程空间的布局应该如下:


    我们可以知道,当程序访问0x0848000~0x0849FFF之间的所有数据都是OK的,当访问到0x084A000及其之后的地址就会报“Segmetation Fault”,因为我们的brk刚好到这里。不信??好吧,把上面程序简单调整一下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int bssvar;int main(int argc, char* argv[])
{void *ptr = NULL;printf("main start = %p\n", main);printf("bss end =  %p\n", (long)&bssvar+4);ptr = sbrk(0);printf("current brk = %p\n", (long*)ptr);sleep(8);int i=0x08049628;for(;;i++)printf("At:0x%x-0x%x\n",i,*((char*)i));return 0;
}
    重新编译运行memlayout,最后出现“Segmetation Fault”时应该是下面这个样子:


    当你的源代码中有用到诸如malloc()之类的动态内存申请函数时,brk的值会被相应的往高端内存的位置进行调整,这样调整出来的一段内存就被所谓的内存管理器,也就是著名的buddy system纳入管理范围了。这样当我们再访问这些地址时,就不会报“Segmetation Fault”了。其实如果你看过Glibc源码你就会惊奇的发现,malloc()最终也是通过调用brk()系统掉用来实现堆的管理。所以,如果我们把上述代码再做一下简单修改:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int bssvar;int main(int argc, char* argv[])
{void *ptr = NULL;printf("main start = %p\n", main);printf("bss end =  %p\n", (long)&bssvar+4);ptr = sbrk(0);printf("current brk = %p\n", (long*)ptr);sleep(8);int i=0x08049628;brk((char*)0x804A123); //注意这行代码for(;;i++)printf("At:0x%x-0x%x\n",i,*((char*)i));return 0;
}
   我们用brk()系统调用,手动把brk调整到0x804A123处,再编译运行,你就会得到下面这样的结果:

    
   至于是为什么不在0x804A123处报“Segmetation Fault”而是要跑到0x804B000处才报,原因已经不止一次的强调了,脑袋犯迷糊的童鞋还是从头再认真看一遍吧。

   又到了该总结的时候了,可能有些童鞋都忘了这篇博文是要讨论什么话题了:
   程序之所以会时不时的出现“Segmetation Fault”的根本原因是进程访问到了没有访问权限的地方,诸如内核区域或者其0x08048000之前的地方,或者由于要访问的内存没有经MMU进行映射所导致。而这种问题比较多的是出在malloc()之类的动态内存申请函数申请完内存,释放后,没有将指针设置为NULL,而其他地方在继续用先前申请的那块内存时,由于内存管理系统已经将其收回,所以才会出现这样的问题。良好的关于指针的使用习惯是,使用之前先判断其是否为NULL,所有已经归还给操作系统的内存,其访问指针都要及时置为NULL,防止所谓的“野指针”到处飞的情况,不然在大型项目里,光是围剿“Segmetation Fault”就要耗费不少兵力。

原文链接: http://m.blog.csdn.net/article/details?id=51550147

这篇关于转载:Segmentation fault到底是何方妖孽的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/962180

相关文章

【H2O2|全栈】Markdown | Md 笔记到底如何使用?【前端 · HTML前置知识】

Markdown的一些杂谈 目录 Markdown的一些杂谈 前言 准备工作 认识.Md文件 为什么使用Md? 怎么使用Md? ​编辑 怎么看别人给我的Md文件? Md文件命令 切换模式 粗体、倾斜、下划线、删除线和荧光标记 分级标题 水平线 引用 无序和有序列表 ​编辑 任务清单 插入链接和图片 内嵌代码和代码块 表格 公式 其他 源代码 预

提问的智慧(转载)

此文让我受益良多。值得一读,大家如果也觉得不错就一起来推~~~   ---------------------------------      在黑客世界里,当提出一个技术问题时,你能得到怎样的回答?这取决于挖出答案的难度,同样取决于你提问的方法。本指南旨在帮助你提高发问技巧,以获取你最想要的答案。       首先你必须明白,黑客们只偏爱艰巨的任务,或者能激发他们

Struts2常用标签总结--转载

Struts2常用标签总结 一 介绍 1.Struts2的作用 Struts2标签库提供了主题、模板支持,极大地简化了视图页面的编写,而且,struts2的主题、模板都提供了很好的扩展性。实现了更好的代码复用。Struts2允许在页面中使用自定义组件,这完全能满足项目中页面显示复杂,多变的需求。 Struts2的标签库有一个巨大的改进之处,struts2标签库的标签不依赖于

【转载】ACM感悟

今天看了一篇我们学校前辈的ACM的感悟,觉得写的十分有道理,这里转载,文章还会不断的改进和更新。 原文链接:http://www.cnblogs.com/Chierush/p/3760870.html?ADUIN=1339764596&ADSESSION=1401536826&ADTAG=CLIENT.QQ.5329_.0&ADPUBNO=26349 声明:本文是写给弱校ACM新手的一点

2024 年,数据中台引领企业走向何方?

2024 年,数据中台引领企业走向何方? 前言数据中台引领企业走向何方 前言 在当今数字化时代,数据已成为企业发展的核心资产。随着企业业务的不断扩展和数据量的急剧增长,如何有效地管理和利用数据,成为企业面临的重要挑战。数据中台作为一种新兴的技术解决方案,应运而生,为企业提供了数据整合、管理和分析的一体化平台,帮助企业实现数据驱动的决策和业务创新。 深入探讨了数据中台的概念、功能

Linux block_device gendisk和hd_struct到底是个啥关系

本文的源码版本是Linux 5.15版本,有图有真相: 1.先从块设备驱动说起 安卓平台有一个非常典型和重要的块设备驱动:zram,我们来看一下zram这个块设备驱动加载初始化和swapon的逻辑,完整梳理完这个逻辑将对Linux块设备驱动模型有深入的理解。 zram驱动加载的时候会调用zram_add函数,源码如下: 1887/*1888 * Allocate and initia

MVVM到底是什么

MVVM到底是什么 文章目录 MVVM到底是什么一、MVVM是什么二、为什么这么定义1. 分离关注点2. 提高可维护性3. 数据绑定和事件驱动4. 支持前端框架的发展 三、底层逻辑1. ViewModel层2. 数据绑定3. 事件驱动4. 响应式系统 四、扩展与高级技巧1. 组件化开发2. 双向数据绑定3. 计算属性和侦听器4. 插槽

我们在学习Spark的时候,到底在学习什么?

我必须要说,Spark这个框架出现之前,我对很多大数据领域的框架源码甚至都是嗤之以鼻的。 很多小伙伴在群里或者私信留言问我关于Spark的学习路径问题。 Spark发展至今,应该说已经非常成熟了。是大数据计算领域不得不学习的框架。尤其是Spark在稳定性和社区发展的成熟度方面,基本可以吊打其他的大数据处理框架。 我之前发过一篇关于阅读Spark源码的文章:《Spark源码阅读的正确打开方式》

【硬刚大数据】我们在学习Flink的时候,到底在学习什么?

⭐⭐欢迎关注博客主页:https://blog.csdn.net/u013411339 ⭐⭐欢迎点赞 👍 收藏 ⭐留言 📝 ,欢迎留言交流! ⭐⭐本文由【王知无】原创,首发于 CSDN博客! ⭐⭐本文首发CSDN论坛,未经过官方和本人允许,严禁转载! 本文是对《【硬刚大数据之学习路线篇】从零到大数据专家的学习指南(全面升级版)》的面试部分补充。 这是一篇指南和大纲性质的文章。

【硬刚大数据】我们在学习Spark的时候,到底在学习什么?

欢迎关注博客主页:https://blog.csdn.net/u013411339 欢迎点赞、收藏、留言 ,欢迎留言交流!本文由【王知无】原创,首发于 CSDN博客!本文首发CSDN论坛,未经过官方和本人允许,严禁转载! 本文是对《【硬刚大数据之学习路线篇】从零到大数据专家的学习指南(全面升级版)》的面试部分补充。 很多小伙伴在群里或者私信留言问我关于Spark的学习路径问题。