检查 malloc 函数返回内容的四个理由

news2024/11/26 18:21:38

写在前面:

一些开发人员可能对检查不屑一顾:他们故意不检查malloc函数是否分配了内存。他们的推理很简单——他们认为会有足够的记忆。如果没有足够的内存来完成操作,请让程序崩溃。似乎是一个糟糕的事实。

注意。在本文中,malloc 函数下将暗示问题不仅与这个特定函数有关,还与 callocrealloc_aligned_malloc_recalloc、strdup 等有关。我不想用所有这些函数名称弄乱文章。所有这些函数的共同点是它们可以返回空指针。

目录

malloc

你需要自己检查

空指针取消引用是未定义的行为

空指针取消引用是一个漏洞

在哪里保证将取消引用恰好一个空指针?

内存集以直接顺序填充内存的保证在哪里?

结论


malloc

如果 malloc 函数无法分配内存缓冲区,它将返回 NULL。任何正常的程序都应该检查 malloc 函数返回的指针,并适当地处理无法分配内存的情况。

不幸的是,许多程序员忽略了检查指针,有时他们故意不检查内存是否已分配。他们的理由如下:

如果 malloc 函数无法分配内存,则程序不太可能继续正常运行。最有可能的是,内存不足以执行其他操作,因此为什么要为内存分配错误而烦恼。空指针对内存的第一个寻址会导致在 Windows 中生成结构化异常。当涉及到类Unix系统时,该过程接收SIGSEGV [RU]信号。结果,程序崩溃,这是完全可以接受的。没有记忆,没有痛苦。或者,您可以捕获结构化异常/信号,并以更集中的方式处理空指针的取消引用。这比写数千张支票更方便。

我不是在编造这个。我和一些人交谈过,他们认为这种方法是合适的,并且有意识地从不检查malloc函数返回的结果。

顺便说一句,开发人员还有另一个借口,为什么他们不进行检查。malloc 函数只保留内存,但不保证当我们开始使用分配的内存缓冲区时会有足够的物理内存。因此,如果仍然没有保证,为什么要进行检查?例如,EFL Core 库的开发人员之一 Carsten Haitzler 解释了为什么我在库代码中没有检查的情况下计算了 500 多个片段。以下是他对文章的评论:

这是一个普遍的共识,至少在Linux上,这一直是我们的主要关注点,很长一段时间是我们的唯一目标,malloc/calloc/realloc的返回值不能被信任,特别是小的时候。Linux默认会过度使用内存。这意味着您获得了新的内存,但内核还没有实际分配实际的物理内存页给它。只有虚拟空间。除非你碰了它。如果内核不能服务这个请求,你的程序无论如何都会崩溃,试图访问一个看起来像有效指针的内存。因此,总的来说,检查allocs的返回值的价值很小,至少在Linux上是这样的。有时我们这样做……有时不是。但是一般情况下,返回值不能被信任,除非它是非常大的内存,并且你的alloc永远不会被服务——例如,你的alloc根本不适合虚拟地址空间(有时在32位上发生)。是的,过度投入是可以调整的,但它的代价是大多数人都不想付出的,甚至没有人知道他们可以调整。其次,如果分配一小块内存失败——例如一个链表节点……实际上,如果返回NULL…坠毁是你能做的最好的事情。你的内存很低,可能会崩溃,像glib对g_malloc那样调用abort(),因为如果你不能分配20-40字节…你的系统无论如何都会崩溃,因为你已经没有工作内存了。这里我说的不是微型嵌入式系统,而是具有虚拟内存和几兆字节内存等的大型机器,这一直是我们的目标。我可以理解为什么PVS-Studio不喜欢这样。严格来说,它实际上是正确的,但实际上,在实际情况下,花在处理这些东西上的代码是一种代码浪费。稍后我会详细介绍。

 

开发人员的给定推理是不正确的。下面,我将详细解释原因。

你需要自己检查

这里同时有四个原因,每个原因都足以证明在调用malloc函数后写检查。如果有人从你的团队不写检查,让他阅读本文。
在我开始之前,这里有一个小的理论参考,为什么发生空指针的解引用会发生结构性异常或信号。这对进一步讲故事很重要。

在地址空间的开头,操作系统保护一个或多个内存页。这允许通过空指针或值接近0的指针来检测内存寻址的错误。

在各种操作系统中,为这些目的保留的内存数量不同。此外,在某些操作系统中,这个值是可配置的。因此,调用特定字节数的内存预留是没有意义的。让我提醒您,在Linux系统中,标准值是64Kb。

重要的是,当向空指针添加任何足够大的数字时,您可能会“删除”控制内存页,并意外地进入不受写入保护的页。因此,可能会损坏一些数据。操作系统不会注意到这一点,也不会产生任何信号/异常。

请注意。如果我们讨论嵌入式系统,可能没有任何内存保护来防止由空地址写入。有些系统内存很低,所有的内存都用来存储数据。然而,具有少量RAM的系统很可能没有动态内存管理,因此也没有malloc函数。

准备好你的咖啡,让我们开始吧!

空指针取消引用是未定义的行为

就 C 和C++语言而言,空指针取消引用会导致未定义的行为。当调用未定义的行为时,任何事情都可能发生。不要假设您知道如果发生 nullptr 取消引用,程序将如何运行。现代编译器利用了认真的优化。因此,有时无法预测特定代码错误将如何表现。

程序的未定义行为非常令人讨厌。应避免代码中未定义的行为。

不要以为你能使用结构化异常处理程序(Windows中的SEH)或信号(在类UNIX系统中)来处理空指针取消引用。如果发生了空指针取消引用,则程序工作已经中断,任何事情都可能发生。让我们看一个抽象的例子,为什么我们不能依赖 SEH 处理程序等。

size_t *ptr = (size_t *)malloc(sizeof(size_t) * N * 2);
for (size_t i = 0; i != N; ++i)
{
  ptr[i] = i;
  ptr[N * 2 - i - 1] = i;
}

此代码填充从边缘到中心的数组。元素值向中心增加。我在 1 分钟内想出了这个例子,所以不要猜测为什么有人需要这样的数组。我什至不认识我自己。对我来说,相邻行中的记录发生在数组的开头和末尾的某个地方很重要。有时你在实际任务中需要这样的东西,当我们到达第 4 个原因时,我们将查看实际代码。

让我们再次仔细看看这两行:

ptr[i] = i;
ptr[N * 2 - i - 1] = i;

从程序员的角度来看,在循环开始时,ptr[0] 元素中会出现一条记录 — 将出现结构化异常/信号。它会被处理,一切都会好起来的。

但是,编译器可能会出于某些优化目的交换赋值。它拥有这样做的所有权利。根据编译器,如果指针被取消引用,则它不能等于 nullptr。如果指针为 null,则它是未定义的行为,编译器不需要考虑优化的后果。

因此,编译器可能会决定,出于优化目的,按如下方式执行赋值更有利可图:

ptr[N * 2 - i - 1] = i;
ptr[i] = i;

因此,在开始时,将通过 ((size_t *)nullptr)[N * 2 - 0 - 1] 地址进行记录。如果值 足够大,则内存开头的受保护页面将被“跳过”,并且 i 变量的值可以写入任何可用于写入的单元格中。总的来说,一些数据将被损坏。

只有在该地址之后,才会执行 ((size_t *)nullptr)[0] 地址的赋值。操作系统将注意到尝试写入它控制的区域,并将生成信号/异常。

程序可以处理此结构异常/信号。但为时已晚。在内存中的某个地方,有损坏的数据。此外,目前尚不清楚哪些数据已损坏以及可能产生什么后果!

编译器是否应该为交换赋值操作负责?不。程序员允许取消引用空指针,从而引导程序处于未定义的行为状态。在这种特殊情况下,程序的未定义行为将是数据在内存中的某个地方损坏。

结论:

遵循以下原则:

  • 任何空指针解引用都是程序的未定义行为。没有所谓的“无害的”未定义的行为。任何未定义的行为是不可接受的。
  • 如果没有事先检查,不允许对malloc函数及其类似物返回的指针进行解引用。不要依赖任何其他方法来拦截空指针的解引用。只使用老式的if运算符。 

空指针取消引用是一个漏洞

一些开发人员根本不认为是错误,其他人则认为是漏洞。这是空指针取消引用时发生的确切情况。

在许多项目中,如果程序由于取消引用空指针而崩溃,或者如果使用信号拦截/结构异常以某种常规方式处理错误,这是可以接受的。

在其他应用程序中,空指针取消引用表示一种可用于应用层 DoS 攻击的潜在漏洞。程序或其中一个执行线程不会正常处理内存不足,而是终止其工作。这可能会导致数据丢失、数据完整性等。

下面是一个示例。有这样一个程序,如Ytnef,用于解码TNEF线程,例如,在Outlook中创建。调用calloc后没有检查被认为是CVE-2017-6298漏洞。

所有可能包含空指针取消引用的固定片段大致相同:

vl->data = calloc(vl->size, sizeof(WORD));
temp_word = SwapWord((BYTE*)d, sizeof(WORD));
memcpy(vl->data, &temp_word, vl->size);

如果您正在开发的应用程序不是非常重要,并且在工作期间崩溃不是问题,那么是的 - 不要编写检查。

但是,如果您正在开发真正的软件项目或库,则不进行检查是不可接受的!

 如果要创建库,请注意,在某些应用程序中,取消引用空指针是一个漏洞。您必须处理内存分配错误并正确返回有关失败的信息。

在哪里保证将取消引用恰好一个空指针?

那些懒得写检查的人,出于某种原因认为取消引用正好影响空指针。是的,这种情况经常发生。但是程序员能够为整个应用程序的代码担保吗?我肯定没有。

我将用实际例子来说明我的意思。例如,让我们看看在Chromium中使用的LLVM-subzero库的代码片段。

void StringMapImpl::init(unsigned InitSize) {
  assert((InitSize & (InitSize-1)) == 0 &&
         "Init Size must be a power of 2 or zero!");
  NumBuckets = InitSize ? InitSize : 16;
  NumItems = 0;
  NumTombstones = 0;
  
  TheTable = (StringMapEntryBase **)
             calloc(NumBuckets+1,
                    sizeof(StringMapEntryBase **) + 
                    sizeof(unsigned));

  // Allocate one extra bucket, set it to look filled
  // so the iterators stop at end.
  TheTable[NumBuckets] = (StringMapEntryBase*)2;
}

 在分配内存缓冲区后,TheTable[NumBuckets] 单元格中将立即发生一条记录。如果变量 NumBuckets 的值足够大,我们将污染一些数据,带来不可预测的后果。在这种损害之后,推测程序将如何运行是没有意义的。可能会有最意想不到的后果。

当库开发人员不检查调用malloc函数的结果时,他们明白自己在做什么。恐怕他们低估了这种方法的危险性。例如,让我们看一下 EFL 库中的以下代码片段:

static void
st_collections_group_parts_part_description_filter_data(void)
{
  ....
  filter->data_count++;
  array = realloc(filter->data,
    sizeof(Edje_Part_Description_Spec_Filter_Data) *
    filter->data_count);
  array[filter->data_count - 1].name = name;
  array[filter->data_count - 1].value = value;
  filter->data = array;
}

这里我们有一个典型的情况:缓冲区中没有足够的空间来存储数据,应该增加。为了增加缓冲区的大小,使用了 realloc 函数,该函数可能会返回 NULL。

如果发生这种情况,由于空指针取消引用,不一定会发生结构化异常/信号。让我们看一下这些行:

array[filter->data_count - 1].name = name;
array[filter->data_count - 1].value = value;

 

如果 filter->data_count 变量的值足够大,则这些值将被写入一个奇怪的地址。

在内存中,某些数据将损坏,但程序仍会运行。后果是不可预测的,肯定不会有好处。

结论:

我再次问这个问题:“哪里能保证对空指针的解引用会发生?”没有这样的保证。在开发或修改代码时,不可能记住最近考虑过的细微差别。你可以很容易地在内存中弄乱一些东西,而程序将继续执行,因为什么都没有发生。 

编写可靠和正确的代码的唯一方法是始终检查malloc函数返回的结果。检查一下,少写点bug。

内存集以直接顺序填充内存的保证在哪里?

会有人说这样的话:

我非常了解 realloc 和文章中写的其他一切。但我是专业人士,在分配内存时,我只是立即使用内存集用零填充它。必要时,我使用支票。但我不会在每个 malloc 之后编写额外的检查。

通常,在缓冲区分配后立即填充内存是一个很奇怪的想法。这很奇怪,因为有一个calloc函数。然而,人们经常这样做。你不需要看很远就能找到例子,这是来自WebRTC库的代码: 

int Resampler::Reset(int inFreq, int outFreq, size_t num_channels) {
  ....
  state1_ = malloc(8 * sizeof(int32_t));
  memset(state1_, 0, 8 * sizeof(int32_t));
  ....
}

分配内存,然后用零填充缓冲区。这是一种非常普遍的做法,尽管实际上,使用calloc可以将两条线减少为一条。不过这并不重要。

最主要的是,即使是这样的代码也是不安全的!memset 函数不需要从头开始填充内存,从而导致空指针取消引用。

memset 函数有权从末尾开始填充缓冲区。如果分配了大量缓冲区,则可以清除一些有用的数据。是的,在填充内存的同时,memset 函数最终将到达受保护的页面,并且操作系统将生成结构异常/信号。但是,处理它们不再有意义。到这个时候,一大块内存将被破坏——程序的后续工作将是不可预测的。

读者可能会争辩说,这一切都纯粹是理论上的。是的,memset 函数理论上可以从缓冲区的末尾开始填充缓冲区,但实际上,没有人会以这种方式实现此函数。

void *memset(void *dest, int c, size_t n)
{
  unsigned char *s = dest;
  size_t k;
  if (!n) return dest;
  s[0] = c;
  s[n-1] = c;
  ....
}

注意以下几行:

s[0] = c;
s[n-1] = c;

在这里,我们来到原因 N1“取消引用空指针是未定义的行为”。不能保证编译器不会交换赋值。如果您的编译器这样做,并且 n 参数非常有价值,那么一开始就会损坏一个字节的内存。空指针取消引用将仅在该之后发生。

又不服气了?那么,这个实现怎么样?

void *memset(void *dest, int c, size_t n)
{
  size_t k;
  if (!n) return dest;
  s[0] = s[n-1] = c;
  if (n <= 2) return dest;
  ....
}

您甚至不能信任内存集函数。是的,这可能是一个人为和牵强附会的问题。我只是想展示如果不检查指针的值会出现多少细微差别。根本不可能考虑到所有这些。因此,您应该仔细检查 malloc 函数返回的每个指针和类似指针。这就是你成为一名专业人士并编写可靠代码的时候。

结论

始终立即检查由 malloc 函数或其类似物返回的指针。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/180915.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Opencv 之 DNN 与 CUDA综述

Opencv 之 DNN 与 CUDA 目录 Opencv官方手稿&#xff08;包含各模块API介绍及使用例程&#xff09; Opencv在github的仓库地址&#xff1a;https://github.com/opencvOpencv额外的测试数据 下载&#xff1a;https://github.com/opencv/opencv_extra #可通过git下载拉取 git c…

【算法练习】删除链表的节点

题源&#xff1a;牛客描述给定单向链表的头指针和一个要删除的节点的值&#xff0c;定义一个函数删除该节点。返回删除后的链表的头节点。1.此题对比原题有改动2.题目保证链表中节点的值互不相同3.该题只会输出返回的链表和结果做对比&#xff0c;所以若使用 C 或 C 语言&#…

pix2pix(二)训练图像尺寸及分配显卡

背景&#xff1a;新的数据集上&#xff0c;图像的大小为496496&#xff0c;与原尺寸512512不一样&#xff0c;不知道能否直接运行。另外&#xff0c;我们现在有了四张空余显卡服务器&#xff0c;并且新数据集的数据量较大&#xff0c;我们有空余的显卡资源加快训练。 目的&…

C++ 模板

在学习stl之前&#xff0c;我们就已经略微讲解了一些模板的知识&#xff0c;而现在&#xff0c;我们来进一步了解一下模板的相关知识 初步了解 目录 一. 非类型模板参数 二. 模板的特化 全特化 偏特化 三. 模板分离编译 四. 总结 一. 非类型模板参数 模板参数…

C进阶_结构体内存对齐

请看下面的代码&#xff0c;输出结果是多少&#xff1f; #include <stdio.h> int main() {struct S1{char c1;int i;char c2;};printf("%d\n", sizeof(struct S1));struct S2{char c1;char c2;int i;};printf("%d\n", sizeof(struct S2));return 0;…

Xmake v2.7.6 发布,新增 Verilog 和 C++ Modules 分发支持

Xmake 是一个基于 Lua 的轻量级跨平台构建工具。 它非常的轻量&#xff0c;没有任何依赖&#xff0c;因为它内置了 Lua 运行时。 它使用 xmake.lua 维护项目构建&#xff0c;相比 makefile/CMakeLists.txt&#xff0c;配置语法更加简洁直观&#xff0c;对新手非常友好&#x…

前端CSS学习之路-css002

&#x1f60a;博主页面&#xff1a;鱿年年 &#x1f449;博主推荐专栏&#xff1a;《WEB前端》&#x1f448; ​&#x1f493;博主格言&#xff1a;追风赶月莫停留&#xff0c;平芜尽处是春山❤️ 目录 CSS字体属性 一、字体系列 二、字体大小 三、字体粗细 四、文字样…

Docker安装nacos

首先将自己的服务器在配置上弄成docker的 然后再下方命令框中直接粘贴如下命令&#xff1a; docker run –name nacos -d -p 8848:8848 -p 9848:9848 -p 9849:9849 –restartalways -e JVM_XMS256m -e JVM_XMX256m -e MODEstandalone -v /usr/local/nacos/logs:/home…

基于多协议传感器的桥梁监测数据采集与管理系统设计

文章目录前言1、要求&#xff1a;2、系统框图2.1系统总体框图2.2、stm32通过AHT20采集温湿度框图&#xff1a;2.3、stm32通过modbus协议与上位机通信框图&#xff1a;3、ModBus协议1、协议概述2、Modbus主/从协议原理3、通用Modbus帧结构---协议数据单元(PDU)4、两种Modbus串行…

readonly与disabled对比

<!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <title>readonly与disabled</title> </head> <body> <!--readonly与disabled 都是只读不能修改…

传统推荐模型(二)协同过滤的进化——矩阵分解算法

传统推荐模型&#xff08;二&#xff09;协同过滤的进化——矩阵分解算法 针对协同过滤算法的头部效应较明显、泛化能力较弱的问题&#xff0c;矩阵分解算法被提出。矩阵分解在协同过滤算法中“共现矩阵”的基础上&#xff0c;加人了隐向量的概念&#xff0c;加强了模型处理稀…

动态顺序表——简单的增删查改

前言 &#xff1a;从这篇博客开始&#xff0c;我会进行数据结构(用C语言实现)有关内容的记录与分享。对于我们而言&#xff0c;数据结构的知识难度较大并且十分重要&#xff0c;希望我的分享给各位带来一些帮助。而今天要分享的就是数据结构中最简单的知识——顺序表的增删查改…

11.Java方法的综合练习题大全-双色球彩票系统,数字的加密和解密等试题

本篇文章是Java方法的专题练习,从第五题开始难度增大,涉及大厂真题,前四道题目是基础练习,友友们可有目的性的选择学习&#x1f618;&#x1f495; 文章目录前言一、数组的遍历1.注意点:输出语句的用法2.题目正解二、数组最大值三、判断是否存在四、复制数组五、案例一:卖飞机票…

【学习笔记之数据结构】二叉树(一)

二叉树的概念&#xff1a; 二叉树是一种树的度不大于2的树&#xff0c;也就是它的节点的度都是小于等于2的。二叉树的子树有左右之分&#xff0c;左右的次序不能颠倒&#xff0c;因此二叉树是一个有序树。任意的二叉树都由空树、只有根节点、只有左子树、只有右子树、左右子树均…

一个简单的自托管图片库HomeGallery

什么是 HomeGallery &#xff1f; HomeGallery 是一个自托管的开源 Web 图片库&#xff0c;用于浏览个人照片和视频&#xff0c;其中包括标记、对移动端友好和 AI 驱动的图像和面部发现功能。 HomeGallery 的独特功能是自动 相似图像/反向图像搜索功能 和 无数据库架构 &#x…

实验三、8人智力竞赛抢答电路设计

实验三 8人智力竞赛抢答电路设计 实验目的 设计一个能支持八路抢答的智力竞赛抢答器&#xff1b;主持人按下开始抢答的按键后&#xff0c;有短暂的报警声提示抢答人员抢答开始且指示灯亮表示抢答进行中&#xff1b;在开始抢答后数码管显8秒倒计时&#xff1b;有抢答人员按下抢…

Linux企业应用现状

一、Linux在服务器领域的发展 随着开源软件在世界范围内影响力日益增强&#xff0c;Linux服务器操作系统在整个服务器操作系统市场格局中占据了越来越多的市场份额&#xff0c;已经形成了大规模市场应用的局面。并且保持着快速的增长率。尤其在政府、金融、农业、交通、电信等国…

linux 网络编程socket

前言 socket&#xff08;套接字&#xff09;是linux下进程间通信的一种方式&#xff0c;通常使用C-S&#xff08;客户端-服务端&#xff09;的方式通信&#xff0c;它可以是同一主机下的不同进程间通信或者不同主机的进程通信。 socket是夹在应用层和TCP/UDP协议层间的软件抽象…

机器自动翻译古文拼音 - 将进酒拼音版本,译文拼音版本

写了一个程序&#xff0c;用来给佛经和古诗加上拼音&#xff0c;并处理多音字和排版&#xff0c;顺便加上翻译。 定期翻译一些&#xff0c;给老人和小孩子用。 将进酒 君不见&#xff0c;黄河之水天上来&#xff0c;奔流到海不复回。 君不见&#xff0c;高堂明镜悲白发&…

Servlet介绍及其概念

Servlet介绍及其概念一、Web基础二、编写HTTP Server&#xff0c;打印Hello,World三、Servlet的出现1. 思考上述HTTP服务器的问题2. 实现代码重用&#xff0c;简化开发过程3. 实现最简单的Servlet4. 导入依赖5. pom.xml文件6. Servlet版本问题7. 整个Servlet工程结构四、运行Se…