【问题标题】:Is it possible to calculate function length at compile time in C++?是否可以在 C++ 编译时计算函数长度?
【发布时间】:2021-01-29 18:52:49
【问题描述】:

我有这段代码:

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() 的完整代码复制到这个数组中。

【问题讨论】:

  • 很抱歉,我的朋友,功能不能这样工作。
  • 你需要这个做什么?也许这是一个 XY 问题,我们可以针对它提出不同的解决方案。
  • 这是XY problem 的经典例子。请解释为什么?你为什么需要这个舞台的东西?解释您的代码应该提供的功能,而不是您认为提供该功能所需的功能。克服这个问题的经典方法是这样开始解释:“作为最终用户,我想要......”。
  • @David "...我打算多次修改fStart(),..." 那里具体需要修改什么,不能修改什么通过参数化(无论是 constexpr / 模板,还是在运行时)?你怎么知道应该在那里更改的操作码?
  • 只是提醒一下,从一个函数生成的代码甚至可能不会放在单个连续的内存块中。有关解释,请阅读easyperf.net/blog/2019/03/27/…

标签: c++ windows constants visual-studio-2019


【解决方案1】:

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

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

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

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

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

【讨论】:

    【解决方案2】:

    下面计算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
    

    【讨论】:

    • 我明白了。所以不能用这种方式声明BYTE数组?
    • @David 正确。您可以改用std::vector,或动态分配数组。
    【解决方案3】:

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

    在运行时生成机器码。

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

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

    在 Linux 上,您可以生成 shared libraries(可能在运行时生成 C 或 C++ 代码(如 RefPerSysGCC MELT 那样),然后使用 gcc -fPIC -shared -O 编译它)和稍后的 dlopen(3) / @987654329 @ 它。 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 的 libbacktracedladdr 来探索您的调用堆栈,您可以使用 dlclose(3) 小心地删除它们。在 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 可执行文件)

    【讨论】:

    • 感谢您的热情回复。是的,使用汇编、JIT 等很容易做到。当您使用 MSVC x86 时,组装很好。但是,x64 不允许以方便的方式插入程序集,因此有时编写 C 代码并将其复制到某个缓冲区以备不时之需会更容易。
    • 否:生成 C 代码,并在运行时将其编译为插件,然后加载该插件。这在 Linux、Windows、MacOSX 上是可行的。使用函数指针。
    • 您为什么投票结束您自己回答的问题?您是否改变了对这个问题的明确程度的看法?
    • 因为这个问题太不清楚了,我不得不对它做出几个猜测,并且因为这个问题没有提供任何minimal reproducible example,而且它没有提供任何minimal reproducible example,并且没有动机我>
    • 既然如此,你为什么要回答呢?任何一个动作都可以,即回答/投票结束。我只是很困惑你为什么选择两者兼而有之。
    猜你喜欢
    • 2017-11-10
    • 1970-01-01
    • 1970-01-01
    • 2022-01-12
    • 2011-08-23
    • 1970-01-01
    • 2021-03-21
    • 2011-01-29
    • 2021-05-26
    相关资源
    最近更新 更多