是否可以在 C++ 编译时计算函数长度?

问题描述

我有这段代码

constexpr static VOID fStart()
{
    auto a = 3;
    a++;
}

__declspec(naked) 
constexpr static VOID fEnd() {};

static constexpr auto getFSize()
{
    return (SIZE_T)((PBYTE)fEnd - (PBYTE)fStart);
}

static constexpr auto fSize = getFSize();
static BYTE func[fSize];

是否可以在不使用任何标准库的情况下在编译期间将“func[fSize]”数组大小声明为“fStart()”的大小?以后需要将 fStart() 的完整代码复制到这个数组中。

解决方法

标准 C++ 中没有获取函数长度的方法。

您需要使用特定于编译器的方法。

一种方法是让链接器创建一个段,并将您的函数放在该段中。然后使用段的长度。

您可以使用一些汇编语言结构来做到这一点;取决于汇编程序和汇编代码。

注意:在嵌入式系统中,移动功能代码是有原因的,例如移动到片上存储器或交换到外部存储器,或者对代码执行校验和。

,

下面计算fStart函数的“字节大小”。但是,这种方式不能作为 constexpr 获得大小,因为强制转换失去了编译时的常量性(参见示例 Why is reinterpret_cast not constexpr?),并且两个不相关的函数指针的差异不能在没有某种铸造。

#pragma runtime_checks("",off)
__declspec(code_seg("myFunc$a")) static void fStart()
{   auto a = 3; a++; }
__declspec(code_seg("myFunc$z")) static void fEnd(void)
{   }
#pragma runtime_checks("",restore)

constexpr auto pfnStart = fStart;                               // ok
constexpr auto pfnEnd = fEnd;                                   // ok
// constexpr auto nStart = (INT_PTR)pfnStart;                   // error C2131

const auto fnSize = (INT_PTR)pfnEnd - (INT_PTR)pfnStart;        // ok
// constexpr auto fnSize = (INT_PTR)pfnEnd - (INT_PTR)pfnStart; // error C2131
,

在某些处理器和一些已知的编译器和 ABI 约定上,您可以做相反的事情:

在运行时生成机器码。

对于 Linux 上的 x86/64,我知道 GNU lightningasmjitlibgccjit 这样做。

elf(5) 格式知道函数的大小。

在 Linux 上,您可以生成 shared libraries(可能在运行时生成 C 或 C++ 代码(如 RefPerSysGCC MELT 那样),然后使用 gcc -fPIC -shared -O 进行编译)然后是 dlopen(3) / dlsym(3) 它。 dladdr(3) 非常很有用。您将使用函数指针。

还阅读一本关于 linkers and loaders 的书。

但是你通常不能在不做一些 relocation 的情况下移动机器代码,除非机器代码是 position-independent code(通常 PIC 的运行速度比普通代码慢)。

相关主题是 garbage collection 的代码(甚至 agents)。您需要阅读 garbage collection handbook 并从 SBCL 之类的实现中汲取灵感。

还请记住,一个好的优化 C++ 编译器允许展开循环、内联扩展函数调用、删除死代码、进行函数克隆等......所以可能会发生机器代码函数甚至不连续的情况:两个 C 函数foo()bar() 可以共享数十种常见的机器指令。

阅读 Dragon book,并研究 GCC 的源代码(并考虑使用您的 GCC plugin 对其进行扩展)。另请查看由 gcc -O2 -Wall -fverbose-asm -S 生成的汇编代码。 GCC 的一些实验性变体可能能够生成在您的 GPGPU 上运行的 OpenCL 代码(然后,函数结束的概念本身就没有意义)

使用通过 C 和 C++ 生成的插件,如果您使用 Ian Taylor 的 dlclose(3)dladdr 来探索您的调用堆栈,您可以使用 libbacktrace 小心地删除它们。在 99% 的情况下,这是不值得的,因为在实践中,Linux 进程(在 2021 年的当前 x86-64 笔记本电脑上)可能会执行一百万次 dlopen(3),就像我的 manydl.c 程序演示(它生成“随机”C 代码,将其编译为唯一的 /tmp/generated123.sodlopen,并重复多次)。

(在台式机和服务器计算机上)覆盖机器码的唯一原因是持久的服务器进程每秒生成机器码。如果这是您的场景,您应该提到它(使用 Java 类加载器生成 JVM 字节码可能更有意义)。

当然,在 16 位微控制器上,情况非常不同。

是否可以在 C++ 中在编译时计算函数长度?

不,因为在运行时某些函数不再存在。

编译器以某种方式删除了它们。或者克隆它们。或者内联它们。

对于 C++ 来说,standard containers 实际上很重要:发生了很多模板扩展,包括在某些时候必须由优化编译器删除的无用代码。

(想想 - 在 2021 年使用最近的 GCC 10.2 或 11 编译。使用无处不在,并与 gcc -O3 -flto -fwhole-program 链接:一个函数 foo23 可能被定义但从未被调用,并且那么它不在 ELF 可执行文件中)