STD 中的动态内存分配
Dynamic memory allocation in STD
我经常使用微控制器和 C++,知道我不执行动态内存分配对我来说很重要。但是我想充分利用 STD 库。确定来自 STD 的 function/class 是否使用动态内存分配的最佳策略是什么?
到目前为止,我想到了这些选项:
- 阅读并理解 STD 代码。这当然是可能的,但老实说,这不是最容易阅读的代码,而且有很多。
- 阅读代码的一个变体可能是让脚本搜索内存分配并突出显示这些部分以使其更易于阅读。这仍然需要弄清楚分配内存的函数在哪里使用,等等。
- 只是测试我想使用的东西并用调试器观察内存。到目前为止,我一直在使用这种方法,但这是一种反应性方法。在设计代码时,我想事先知道我可以从 STD 中使用什么。还有就是在某些(边缘)情况下分配了内存。这些可能不会出现在这个有限的测试中。
- 最后可以做的是定期扫描生成的汇编代码以进行内存分配。我怀疑这可以编写脚本并包含在工具链中,但这同样是一种反应性方法。
如果您看到任何其他选项或有做类似事情的经验,请告诉我。
p.s。我目前主要使用 ARM Cortex-Mx 芯片,使用 GCC 进行编译。
你们在评论中有一些很好的建议,但没有实际答案,所以我会尝试回答。
从本质上讲,您是在暗示 C 和 C++ 之间存在一些实际上并不存在的差异。你怎么知道 stdlib 函数不分配内存?
允许一些 STL 函数分配内存,它们应该使用 allocators. For example, vectors take an template parameter for an alternative allocator (for example pool allocators are common). There is even a standard function for discovering if a type uses memory
但是...某些类型如 std::function 有时使用内存分配,有时不使用,具体取决于参数类型的大小,因此您的偏执并非完全没有道理。
C++ 通过 new/delete 分配。 New/Delete 通过 malloc/free.
分配
所以真正的问题是,你能覆盖 malloc/free 吗?答案是肯定的,看这个答案。这样您就可以跟踪所有分配,并在 运行 时捕获您的错误,这还不错。
如果你真的很硬核,你可以做得更好。您可以编辑标准“运行time C 库”以将 malloc/free 重命名为其他名称。这可以通过作为 gcc 工具链一部分的“objcopy”来实现。重命名 malloc/free 后,即 ma11oc/fr33,任何对 allocate/free 内存的调用将不再 link。
Link 你的可执行文件带有 gcc 的“-nostdlib”和“-nodefaultlibs”选项,而不是 link 你自己的库集,它是你用 objcopy 生成的。
老实说,我只见过一次成功,而且是一个你不信任 objcopy 的程序员,所以他只是使用二进制编辑器手动找到标签“malloc”“free”,然后更改它们.不过它确实有效。
编辑:
正如 Fureeish 所指出的(见评论),C++ 标准不保证 new/delete 使用 C 分配器函数。
但是,这是一个非常常见的实现,您的问题确实特别提到了 GCC。在 30 年的开发中,我从未见过一个 C++ 程序 运行 有两个堆(一个用于 C,一个用于 C++)只是因为标准允许。它根本没有优势。不过,这并不排除未来可能会有优势的可能性。
需要明确的是,我的回答假设 new USES malloc 分配内存。这并不意味着您可以假设每个新调用都会调用 malloc,因为可能涉及缓存,并且 operator new 可能会过载以在全局级别使用任何东西。请参阅此处了解 GCC/C++ 分配器方案。
https://gcc.gnu.org/onlinedocs/libstdc++/manual/memory.html
又一次编辑:
如果您想获得技术知识——这取决于您使用的 libstdc++ 版本。您可以在 new_op.cc 中找到 operator new,在(我假设是官方的)来源 repository
(我会停下来)
通常您可以查看 (suitably thorough) 文档以查看函数(例如,构造函数)是否可以 throw std::bad_alloc
。 (相反的情况通常表述为 noexcept
,因为该异常通常是操作所面临的唯一风险。)存在 std::inplace_merge
的异常,它变得 更慢 而不是在分配失败时抛出。
gcc linker 支持 -Map
选项,该选项将生成包含可执行文件中所有符号的 link 映射。如果您的应用程序中有任何内容无意中进行了动态内存分配,您会发现一个包含 *alloc
和 free
函数的部分。
如果你从一个没有分配的程序开始,你可以在每次编译后检查映射,看看你是否通过库函数调用引入了一个。
我用这个方法识别了一个
你列出的选项很全面,我想我会为其中的几个添加一些实用的颜色。
选项 1:如果您拥有正在使用的特定标准库实现的源代码,则可以通过生成静态 call graph and reading that instead. In fact the llvm opt
tool can do this for you, as demonstrated in this 问题来“简化”阅读它的过程。如果你要这样做,理论上你可以只看一个给定的方法,看看是否转到任何类型的分配函数。无需阅读源码,纯视觉
选项 4:编写脚本比您想象的要容易。先决条件:确保您使用 -ffunction-sections
构建,这允许链接器完全丢弃从未调用的函数。当您生成发布版本时,您可以简单地在 ELF 文件上使用 nm
和 grep 来查看例如 malloc
是否完全出现在二进制文件中。
例如,我有一个基于裸机 cortex-M 的嵌入式系统,我知道它没有动态内存分配,但链接到一个通用的标准库实现。在调试版本中,我可以执行以下操作:
$ nm Debug/Project.axf | grep malloc
700172bc T malloc
$
这里找到了malloc,因为死代码还没有被剥离
在发布版本中它看起来像这样:
$ nm Release/Project.axf | grep malloc
$
grep 此处将 return 如果找到匹配项则为“0”,否则为“0”以外的其他内容,因此如果您要在脚本中使用它,它将类似于:
nm Debug/Project.axf | grep malloc > /dev/null
if [ "$?" == "0" ]; then
echo "error: something called malloc"
exit 1
fi
这些方法中的任何一种都有大量的免责声明和警告。请记住,嵌入式系统尤其使用各种不同的标准库实现,并且每个实现都可以自由地做任何关于内存管理的事情。
事实上,他们甚至不必调用 malloc
和 free
,他们可以实现自己的动态分配器。当然这不太可能,但有可能,因此 grepping malloc
实际上是不够的,除非你知道标准库实现中的所有内存管理都经过 malloc
和 free
.
如果您真的想避免所有形式的动态内存分配,我知道(并且我自己也使用过)的唯一可靠方法就是完全删除堆。在我使用过的大多数裸机嵌入式系统上,堆起始地址、结束地址和大小几乎总是在链接描述文件中提供一个符号。您应该删除或重命名这些符号。如果有任何东西正在使用堆,您将收到链接器错误,这正是您想要的。
举一个非常具体的例子,newlib is a very common libc implementation for embedded systems. Its malloc implementation requires that the common sbrk()
函数存在于系统中。对于裸机系统,sbrk()
只是通过递增从链接器脚本提供的 end
符号开始的指针来实现。
如果您使用的是 newlib,并且不想弄乱链接描述文件,您仍然可以将 sbrk()
替换为一个只会出现硬故障的函数,这样您就可以立即捕获任何分配内存的尝试。在我看来,这仍然比试图盯着 运行 系统上的堆指针要好得多。
当然,您的实际系统可能不同,并且您可能使用不同的 libc 实现。这个问题实际上只能在您的系统的确切上下文中得到任何合理的满意回答,因此您可能需要自己做一些功课。它很可能与我在这里描述的非常相似。
裸机嵌入式系统的一大优点是它们提供的灵活性。不幸的是,这也意味着变量太多,除非您知道所有细节,否则几乎不可能直接回答问题,而我们在这里不了解这些细节。希望这会给你一个比盯着调试器更好的起点 window.
为确保您不使用动态内存分配,您可以重写全局new operator以便它始终抛出异常。然后 运行 针对您对要使用的库函数的所有使用进行单元测试。
您可能需要链接器的帮助以避免使用 malloc
和 free
,因为从技术上讲您无法覆盖它们。
注意:这会在测试环境中。您只是在验证您的代码不使用动态分配。完成验证后,您就不再需要覆盖,因此它不会出现在生产代码中。
你确定要避开它们吗?
当然,您不想使用专为通用系统设计的动态内存管理。那绝对不是个好主意。
但是您使用的工具链是否没有针对您的硬件提供智能工作的实现?或者有一些特殊的编译方法,允许您只使用一块已知的内存,您已经为数据区域预先调整大小和对齐。
转向容器。大多数 STL 容器允许您使用分配器专门化它们。您可以编写自己的不使用动态内存的分配器。
我经常使用微控制器和 C++,知道我不执行动态内存分配对我来说很重要。但是我想充分利用 STD 库。确定来自 STD 的 function/class 是否使用动态内存分配的最佳策略是什么?
到目前为止,我想到了这些选项:
- 阅读并理解 STD 代码。这当然是可能的,但老实说,这不是最容易阅读的代码,而且有很多。
- 阅读代码的一个变体可能是让脚本搜索内存分配并突出显示这些部分以使其更易于阅读。这仍然需要弄清楚分配内存的函数在哪里使用,等等。
- 只是测试我想使用的东西并用调试器观察内存。到目前为止,我一直在使用这种方法,但这是一种反应性方法。在设计代码时,我想事先知道我可以从 STD 中使用什么。还有就是在某些(边缘)情况下分配了内存。这些可能不会出现在这个有限的测试中。
- 最后可以做的是定期扫描生成的汇编代码以进行内存分配。我怀疑这可以编写脚本并包含在工具链中,但这同样是一种反应性方法。
如果您看到任何其他选项或有做类似事情的经验,请告诉我。
p.s。我目前主要使用 ARM Cortex-Mx 芯片,使用 GCC 进行编译。
你们在评论中有一些很好的建议,但没有实际答案,所以我会尝试回答。
从本质上讲,您是在暗示 C 和 C++ 之间存在一些实际上并不存在的差异。你怎么知道 stdlib 函数不分配内存?
允许一些 STL 函数分配内存,它们应该使用 allocators. For example, vectors take an template parameter for an alternative allocator (for example pool allocators are common). There is even a standard function for discovering if a type uses memory
但是...某些类型如 std::function 有时使用内存分配,有时不使用,具体取决于参数类型的大小,因此您的偏执并非完全没有道理。
C++ 通过 new/delete 分配。 New/Delete 通过 malloc/free.
分配所以真正的问题是,你能覆盖 malloc/free 吗?答案是肯定的,看这个答案。这样您就可以跟踪所有分配,并在 运行 时捕获您的错误,这还不错。
如果你真的很硬核,你可以做得更好。您可以编辑标准“运行time C 库”以将 malloc/free 重命名为其他名称。这可以通过作为 gcc 工具链一部分的“objcopy”来实现。重命名 malloc/free 后,即 ma11oc/fr33,任何对 allocate/free 内存的调用将不再 link。 Link 你的可执行文件带有 gcc 的“-nostdlib”和“-nodefaultlibs”选项,而不是 link 你自己的库集,它是你用 objcopy 生成的。
老实说,我只见过一次成功,而且是一个你不信任 objcopy 的程序员,所以他只是使用二进制编辑器手动找到标签“malloc”“free”,然后更改它们.不过它确实有效。
编辑:
正如 Fureeish 所指出的(见评论),C++ 标准不保证 new/delete 使用 C 分配器函数。
但是,这是一个非常常见的实现,您的问题确实特别提到了 GCC。在 30 年的开发中,我从未见过一个 C++ 程序 运行 有两个堆(一个用于 C,一个用于 C++)只是因为标准允许。它根本没有优势。不过,这并不排除未来可能会有优势的可能性。
需要明确的是,我的回答假设 new USES malloc 分配内存。这并不意味着您可以假设每个新调用都会调用 malloc,因为可能涉及缓存,并且 operator new 可能会过载以在全局级别使用任何东西。请参阅此处了解 GCC/C++ 分配器方案。
https://gcc.gnu.org/onlinedocs/libstdc++/manual/memory.html
又一次编辑:
如果您想获得技术知识——这取决于您使用的 libstdc++ 版本。您可以在 new_op.cc 中找到 operator new,在(我假设是官方的)来源 repository
(我会停下来)
通常您可以查看 (suitably thorough) 文档以查看函数(例如,构造函数)是否可以 throw std::bad_alloc
。 (相反的情况通常表述为 noexcept
,因为该异常通常是操作所面临的唯一风险。)存在 std::inplace_merge
的异常,它变得 更慢 而不是在分配失败时抛出。
gcc linker 支持 -Map
选项,该选项将生成包含可执行文件中所有符号的 link 映射。如果您的应用程序中有任何内容无意中进行了动态内存分配,您会发现一个包含 *alloc
和 free
函数的部分。
如果你从一个没有分配的程序开始,你可以在每次编译后检查映射,看看你是否通过库函数调用引入了一个。
我用这个方法识别了一个
你列出的选项很全面,我想我会为其中的几个添加一些实用的颜色。
选项 1:如果您拥有正在使用的特定标准库实现的源代码,则可以通过生成静态 call graph and reading that instead. In fact the llvm opt
tool can do this for you, as demonstrated in this 问题来“简化”阅读它的过程。如果你要这样做,理论上你可以只看一个给定的方法,看看是否转到任何类型的分配函数。无需阅读源码,纯视觉
选项 4:编写脚本比您想象的要容易。先决条件:确保您使用 -ffunction-sections
构建,这允许链接器完全丢弃从未调用的函数。当您生成发布版本时,您可以简单地在 ELF 文件上使用 nm
和 grep 来查看例如 malloc
是否完全出现在二进制文件中。
例如,我有一个基于裸机 cortex-M 的嵌入式系统,我知道它没有动态内存分配,但链接到一个通用的标准库实现。在调试版本中,我可以执行以下操作:
$ nm Debug/Project.axf | grep malloc
700172bc T malloc
$
这里找到了malloc,因为死代码还没有被剥离
在发布版本中它看起来像这样:
$ nm Release/Project.axf | grep malloc
$
grep 此处将 return 如果找到匹配项则为“0”,否则为“0”以外的其他内容,因此如果您要在脚本中使用它,它将类似于:
nm Debug/Project.axf | grep malloc > /dev/null
if [ "$?" == "0" ]; then
echo "error: something called malloc"
exit 1
fi
这些方法中的任何一种都有大量的免责声明和警告。请记住,嵌入式系统尤其使用各种不同的标准库实现,并且每个实现都可以自由地做任何关于内存管理的事情。
事实上,他们甚至不必调用 malloc
和 free
,他们可以实现自己的动态分配器。当然这不太可能,但有可能,因此 grepping malloc
实际上是不够的,除非你知道标准库实现中的所有内存管理都经过 malloc
和 free
.
如果您真的想避免所有形式的动态内存分配,我知道(并且我自己也使用过)的唯一可靠方法就是完全删除堆。在我使用过的大多数裸机嵌入式系统上,堆起始地址、结束地址和大小几乎总是在链接描述文件中提供一个符号。您应该删除或重命名这些符号。如果有任何东西正在使用堆,您将收到链接器错误,这正是您想要的。
举一个非常具体的例子,newlib is a very common libc implementation for embedded systems. Its malloc implementation requires that the common sbrk()
函数存在于系统中。对于裸机系统,sbrk()
只是通过递增从链接器脚本提供的 end
符号开始的指针来实现。
如果您使用的是 newlib,并且不想弄乱链接描述文件,您仍然可以将 sbrk()
替换为一个只会出现硬故障的函数,这样您就可以立即捕获任何分配内存的尝试。在我看来,这仍然比试图盯着 运行 系统上的堆指针要好得多。
当然,您的实际系统可能不同,并且您可能使用不同的 libc 实现。这个问题实际上只能在您的系统的确切上下文中得到任何合理的满意回答,因此您可能需要自己做一些功课。它很可能与我在这里描述的非常相似。
裸机嵌入式系统的一大优点是它们提供的灵活性。不幸的是,这也意味着变量太多,除非您知道所有细节,否则几乎不可能直接回答问题,而我们在这里不了解这些细节。希望这会给你一个比盯着调试器更好的起点 window.
为确保您不使用动态内存分配,您可以重写全局new operator以便它始终抛出异常。然后 运行 针对您对要使用的库函数的所有使用进行单元测试。
您可能需要链接器的帮助以避免使用 malloc
和 free
,因为从技术上讲您无法覆盖它们。
注意:这会在测试环境中。您只是在验证您的代码不使用动态分配。完成验证后,您就不再需要覆盖,因此它不会出现在生产代码中。
你确定要避开它们吗?
当然,您不想使用专为通用系统设计的动态内存管理。那绝对不是个好主意。
但是您使用的工具链是否没有针对您的硬件提供智能工作的实现?或者有一些特殊的编译方法,允许您只使用一块已知的内存,您已经为数据区域预先调整大小和对齐。
转向容器。大多数 STL 容器允许您使用分配器专门化它们。您可以编写自己的不使用动态内存的分配器。