什么是现代的、可移植的、安全的等价于 C 中编译时检查的 memcpy?

Posted

技术标签:

【中文标题】什么是现代的、可移植的、安全的等价于 C 中编译时检查的 memcpy?【英文标题】:What's the modern, portable, safe equivalent of memcpy with compile-time checks in C? 【发布时间】:2019-07-04 10:33:34 【问题描述】:

我正在查看一些简洁的示例代码,这些代码展示了如何在不遇到特定硬件平台上的内存对齐问题的情况下复制浮点数。我注意到它没有使用sizeof():

uint8_t mybuffer[4];
float f;
memcpy(&f, mybuffer, 4)

下一个问题是使用什么sizeof(),这导致微软部分回答memcpy_s,但这是一种运行时方法,需要返回检查。

稍微超出我引用的代码 sn-p,对于在 编译时 可以正确确定源和目标大小的特定情况,是否有一个 compact/单行(例如,保持示例代码简短)相当于memcpy(),确保长度相同,如果不是,则以有用的消息终止编译?避免ARR01-C. Do not apply the sizeof operator to a pointer when taking the size of an array 的陷阱会很好。

这里与memcpy(), what should the value of the size parameter be?有些重叠

【问题讨论】:

如果数组mybuffer作为参数传递给函数,它将衰减为指针,并且任何大小信息都将丢失。所以我不认为有任何方法可以笼统地做到这一点。 可以使用静态断言:_Static_assert(sizeof f == sizeof mybuffer, "float is not same size as buffer."); @RishikeshRaje:这个问题并没有要求一种方法来了解仅给定指向其第一个元素的指针的缓冲区的长度。它要求一种方法来确保两个对象的大小令人满意。 @EricPostpischil - 我相信 OP 想知道 mybufer 的大小以及数组大小是否为 4。这只能通过传递给功能。 @EricPostpischil 指针讨论已经出现,因为这里的一个好的解决方案将处理上述 ARR01-C 问题。 【参考方案1】:

什么是现代的、可移植的、安全的等价于 C 中编译时检查的 memcpy?

memcpy 时尚、便携且安全。不存在您应该使用的替代品。

如果你使用微软的逻辑,那是memcpy 的错,不称职的程序员将空指针传递给它。但是此类错误应该通过在传递输入之前进行健全性检查来修复,而不是通过更改memcpy

memcpy_s 将被视为已弃用,应避免使用 C11 边界检查接口中的所有内容,因为不存在对它的编译器支持。诸如 Visual Studio 之类的危险编译器也使其不可移植,因为它们甚至不遵循 C 标准 Annex K,而是发明了自己的不兼容版本。我的建议是将每个以_s 结尾的函数都视为安全隐患。

是否存在与 memcpy() 等效的紧凑/单行(例如,为了保持示例代码简短),以确保长度相同并在不存在时以有用的消息终止编译?

是的。 memcpy(&f, mybuffer, sizeof(f))

避免了 ARR01-C 的陷阱。当获取数组的大小时不要将 sizeof 运算符应用于指针会很好

该 CERT 规则仅适用于不知道函数参数的数组衰减如何工作的程序员。解决这个问题的方法是教育人们,而不是假设每个使用你的代码库的程序员都是无能的。此外,这是静态分析器将捕获的典型错误 - 启用静态分析实际上可能正是 CERT 规则存在的原因。

如果您知道自己在做什么,您可以在函数内部使用sizeof。每个以数组为参数的函数也需要一个大小。使用那个尺寸。所以如果你有这个:void foo (size_t n, float array[n]);,那么你可以在那个函数里面做sizeof(float[n])就好了。

作为替代方案,还有另一个有点晦涩难懂的版本,有时会受到安全标准的推动。如果您将函数更改为使用数组指针void foo (size_t n, float(*array)[n]),您可以在该函数内执行sizeof(*array)。我们还可以使用数组指针来提高类型安全性:

void foo (float(*array)[5]);
...

float arr[4];
foo(&arr); // compiler error

缺点:更复杂的语法,我们失去了使const 正确参数的能力。

【讨论】:

【参考方案2】:

memcpy() 函数非常好。 C11 边界检查功能是作为减少缓冲区溢出缺陷机会的一部分而引入的。

文档 N1967,Field Experience With Annex K — Bounds Checking Interfaces,在不必要的使用部分中有这样说:

微软弃用 标准功能 [DEPR] 以增加对 API 是对标准函数的每次调用都必须 不安全,应替换为“更安全”的 API。因此, 有安全意识的团队有时会天真地开始长达数月的项目 重写他们的工作代码并尽职地替换所有实例 带有相应 API 的“已弃用”函数。这不仅 导致不必要的流失并增加注入新错误的风险 转换成正确的代码,这也降低了重写代码的效率。

sizeof 运算符是编译时运算符,这意味着大小计算是由编译时编译器可用的任何信息完成的。这就是为什么在与指针和数组一起使用时使用运算符可能会出现问题并且是运行时错误的来源。提高编译器的警告级别并使用静态代码分析工具可以帮助找到这些问题区域。

因为sizeof 运算符是一个编译时运算符,所以对于编译器错误或警告您无能为力,尽管您的特定编译器可能有一个您可以设置的警告级别来执行此类检查。

我所做的是拥有一个预处理器宏,可用于在调试构建或验证中使用的特殊发布构建中进行运行时检查,在为生产进行发布构建时将删除该宏,以消除检查开销.

因此,如果您有以下来源:

uint8_t mybuffer[4];
float f;
memcpy(&f, mybuffer, sizeof(mybuffer));

这将具有正确的字节数,因为数组 mybuffer[4] 及其实际大小可供编译器使用。

但是,我实际上更喜欢通过指定memcpy() 的目标大小来进行以下修改。这样可以确保即使源大小不正确也不会出现缓冲区溢出。

uint8_t mybuffer[4];
float f;
memcpy(&f, mybuffer, sizeof(f));

sizeof 运算符的问题是当编译器无法推断出数组的大小或地址在指针中的对象的大小时。将它与函数参数的数组声明一起使用也不安全。

如果您有一个函数int xxx (int a[5]),并且在该函数中您尝试使用sizeof 运算符来获取以字节为单位的数组大小,那么您可能会得到int * 的大小。

【讨论】:

以上是关于什么是现代的、可移植的、安全的等价于 C 中编译时检查的 memcpy?的主要内容,如果未能解决你的问题,请参考以下文章

C++最佳实践 | 5. 可移植性及多线程

C++最佳实践 | 5. 可移植性及多线程

将“通用组成的语言 X”编译成可移植 C 的编译器

C++最佳实践 | 3. 安全性

C++最佳实践 | 3. 安全性

#pragma once 等价于 c++builder