问题描述
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 lightning、asmjit、libgccjit 这样做。
elf(5) 格式知道函数的大小。
在 Linux 上,您可以生成 shared libraries(可能在运行时生成 C 或 C++ 代码(如 RefPerSys 和 GCC 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.so
和 dlopen
,并重复多次)。
(在台式机和服务器计算机上)覆盖机器码的唯一原因是持久的服务器进程每秒生成机器码。如果这是您的场景,您应该提到它(使用 Java 类加载器生成 JVM 字节码可能更有意义)。
当然,在 16 位微控制器上,情况非常不同。
是否可以在 C++ 中在编译时计算函数长度?
不,因为在运行时某些函数不再存在。
编译器以某种方式删除了它们。或者克隆它们。或者内联它们。
对于 C++ 来说,standard containers 实际上很重要:发生了很多模板扩展,包括在某些时候必须由优化编译器删除的无用代码。
(想想 - 在 2021 年使用最近的 GCC 10.2 或 11 编译。使用无处不在,并与 gcc -O3 -flto -fwhole-program
链接:一个函数 foo23
可能被定义但从未被调用,并且那么它不在 ELF 可执行文件中)