将函数汇编代码嵌入到结构中

时间:2019-01-11 16:32:24

标签: c++ c c++11 assembly inline-assembly

我有一个非常特殊的问题:在C / ++中是否可以指定函数的位置(两者都是因为我确信这两种语言的问题都相同)?为什么?我有很多函数指针,我想消除它们。

(当前)如下所示(重复一百万次lika,存储在用户的RAM中):

riwayat_pinjam

因为我知道在大多数汇编语言中,函数只是“ goto”指令,所以我有以下想法。是否可以优化上述结构,使其看起来像这样?

struct {
    int i;
    void(* funptr)();
} test;

最后,事情应该在内存中看起来像这样:一个包含任何值的int,后跟test2引用的函数的汇编代码。我应该能够像这样调用这些函数:struct { int i; // embed the assembler of the function here // so that all the functions // instructions are located here // like this: mov rax, rbx // jmp _start ; just demo code } test2;

您可能会认为我以这种方式优化应用程序非常疯狂,而且我无法透露其功能的更多细节,但是如果有人对如何解决此问题有任何建议,我将不胜感激。 我不认为有一种标准的方法,因此通过内联汇编器/其他疯狂的方式进行此操作的任何骇人听闻的方法也将受到赞赏!

4 个答案:

答案 0 :(得分:2)

您唯一需要做的就是让编译器知道您要在结构中使用的函数指针的(常量)值。然后,编译器将(大概/希望地)内联该函数调用,只要它通过该函数指针看到它被调用的地方:

template<void(*FPtr)()>
struct function_struct {
    int i;
    static constexpr auto funptr = FPtr;
};

void testFunc()
{
    volatile int x = 0;
}

using test = function_struct<testFunc>;

int main()
{
    test::funptr();
}

Demo-优化后没有calljmp

目前尚不清楚int i的意义。请注意,该代码并不是在技术上“直接在i之后”,但是更不清楚的是您希望结构的 instances 是什么样(代码是否在其中)?还是在某种程度上是“静态的”?我觉得您的编译器实际上产生了一些误解……)。但是考虑一下编译器内联可以为您提供帮助的方式,您可能会找到所需的解决方案。如果您担心内联后的可执行文件大小,请告诉编译器,它将在速度和大小之间妥协。

答案 1 :(得分:2)

由于很多原因,这听起来像是一个可怕的主意,可能无法节省内存,并且会因使用数据稀释L1I缓存和使用代码稀释L1D缓存而损害性能。您曾经修改或复制对象:自修改代码停顿。

但是,是的,在C99 / C11中,可以在结构的末尾使用灵活的数组成员,并将其转换为函数指针。

struct int_with_code {
    int i;
    char code[];   // C99 flexible array member.  GNU extension in C++
                   // Store machine code here
                   // you can't get the compiler to do this for you.  Good Luck!
};

void foo(struct int_with_code *p) {
    // explicit C-style cast compiles as both C and C++
    void (*funcp)(void) = ( void (*)(void) ) p->code;
    funcp();
}

编译器输出from clang7.0, on the Godbolt compiler explorer与C或C ++编译时相同。这是针对x86-64 System V ABI的,其中第一个函数arg在RDI中传递。

# this is the code that *uses* such an object, not the code that goes in its code[]
# This proves that it compiles,
#  without showing any way to get compiler-generated code into code[]
foo:                                    # @foo
    add     rdi, 4         # move the pointer 4 bytes forward, to point at code[]
    jmp     rdi                     # TAILCALL

(如果您省略了C中的(void) arg类型声明,则编译器将在x86-64 SysV调用约定中首先将AL零,以防它实际上是一个可变参数,因为它没有传递FP args在寄存器中。)


您必须在可执行的内存中分配对象(通常不会这样做,除非它们const具有静态存储空间),例如用gcc -zexecstack进行编译。或在POSIX或Windows上使用自定义的mmap / mprotect或VirtualAlloc / VirtualProtect。

或者,如果所有对象都是静态分配的,则可以通过在每个.text部分之前添加一个int成员,来编译编译器输出以将.section部分中的函数转换为对象。也许有一些char code[60]和链接器技巧,甚至还有链接器脚本,您甚至可以以某种方式使它自动化。

但是,除非它们的长度都相同(例如,使用ret这样的填充),否则就不会形成可以索引的数组,因此您需要某种方式来引用所有这些可变长度对象

如果在调用对象的函数之前对其进行修改,则可能会带来巨大的性能下降:在x86上,您将获得用于执行代码的自修改代码管道核对,在刚编写的内存位置执行 near

或者,如果您在调用对象的功能之前复制了对象:x86管道刷新,或在其他ISA上,则需要手动刷新缓存以使I缓存与D缓存同步(以便可以执行新写入的字节) )。 但是您不能复制此类对象,因为它们的大小没有存储在任何地方。您无法在机器代码中搜索0xc3指令,因为ret字节可能出现在x86指令的开头以外的地方。或者在任何ISA上,该函数可能具有多个__builtin___clear_cache指令(尾部重复优化)。或者以jmp代替ret(tailcall)结束。  存储大小将开始破坏保存大小的目的,在每个对象中至少占用一个额外的字节。

在运行时将代码写入对象,然后转换为函数指针是ISO C和C ++中未定义的行为。在GNU C / C ++上,请确保在其上调用malloc以同步缓存或进行其他必要的操作。是的,即使在x86上也需要禁用死区消除优化:see this test case。在x86上,它只是编译时的东西,没有多余的asm。实际上并不会清除任何缓存。

如果在运行时启动时确实进行复制,则可能在复制时分配很大的内存块并切出可变长度的块。如果您分别lea eax, [rdi + rdi*2],那么您将浪费内存管理开销。


除非您拥有与对象一样多的功能,否则这种想法不会节省您的记忆力

通常,您的实际功能数量有限,许多对象具有相同功能指针的副本。 (您有种手动滚动的C ++虚拟函数,但是只有一个函数,您直接拥有一个函数指针,而不是指向该类类型的指针表的vtable指针。少一层的间接访问,显然您可以不要将对象自己的地址传递给函数。)

这种间接级别的诸多好处之一是,一个指针通常比函数的整个代码小得多。为了避免这种情况,您的功能必须是 tiny

示例:具有10个不同的函数(每个函数32个字节)和1000个带有函数指针的对象,您总共有320个字节的代码(将在I高速缓存中保持高温)和8000个字节的函数指针。 (在您的对象中,每个对象在填充时浪费了4个字节来对齐指针,从而使总大小为16个而不是每个对象12个字节。)总之,整个结构+代码的总大小为 16320字节 。如果您分别分配每个对象,那么每个对象都会记账。

通过将机器代码内联到每个对象中,而无需填充,即1000 *(4 + 32)= 36000字节,超过总大小的两倍。

x86-64可能是最理想的情况,其中指针是8个字节,x86-64机器代码使用(众所周知的)可变长度指令编码,从而可以实现高代码密度在某些情况下,尤其是在优化代码大小时。 (例如代码搜索。https://codegolf.stackexchange.com/questions/132981/tips-for-golfing-in-x86-x64-machine-code)。但是,除非您的函数像ret(3个字节=操作码+ ModRM + SIB)/ return x*3;(1个字节)之类的琐碎小东西,否则它们仍将占用8个字节以上。 (对于在x86-64 System V ABI中采用32位整数x arg的函数,该值为call rel32。)

如果它们是较大功能的包装,则正常的opcode + modrm + rel32指令为5个字节。对于RIP相对寻址模式,静态数据的加载量至少为6个字节(mov eax, [32 bit absolute address],或者专门加载EAX可以使用绝对地址的特殊no-modrm编码。但是在x86-64中,这是64-位绝对值,除非您也使用地址大小前缀,否则可能会导致Intel的解码器中的LCP停顿。foo() = addr32(0x67)+操作码+ abs32 = 6字节,因此这会变得更糟,无济于事)

您的函数指针类型没有任何args(假设这是C ++,其中foo(void)在声明中表示(...),而不是像旧C那样,空arg列表与{{ 1}})。因此,我们可以假设您没有传递args,因此为了做有用的事情,这些函数可能正在访问一些静态数据或进行另一个调用。


更有意义的想法:

  • 使用类似Linux x32的ILP32 ABI,其中CPU以64位模式运行,但是您的代码使用32位指针。通常,这将使每个对象只有8个字节而不是16个字节。通常,避免指针膨胀是x32或ILP32 ABI的经典用例。

    或者(糟糕)将您的代码编译为32位。但是,您将拥有过时的32位调用约定,这些约定将args传递给堆栈而不是寄存器,并且传递不到寄存器的一半,并且位置无关代码的开销也更高。 (没有EIP / RIP相对寻址。)

  • unsigned int表索引存储到函数指针表中。如果您有100个函数但有10k个对象,则该表只有100个指针。在asm中,如果所有函数都填充为相同的长度,则可以直接为代码数组(计算的goto样式)建立索引,但是在C ++中,您不能这样做。最好使用函数指针表进行间接访问。

例如

void (*const fptrs[])(void) = {
    func1, func2, func3, ...
};

struct int_with_func {
    int i;
    unsigned f;
};

void bar(struct int_with_func *p) {
    fptrs[p->f] ();
}

clang / gcc -O3输出:

 bar(int_with_func*):
    mov     eax, dword ptr [rdi + 4]            # load p->f
    jmp     qword ptr [8*rax + fptrs] # TAILCALL    # index the global table with it for a memory-indirect jmp

如果您要编译共享库,PIE可执行文件或不针对Linux,则编译器不能使用32位绝对地址来通过一条指令索引静态数组。因此,那里有一个相对于RIP的LEA,类似jmp [rcx+rax*8]

与在每个对象中存储函数指针相比​​,这是一个间接的附加级别,但是它使您可以像使用32位指针一样,将每个对象的字节数从16个缩减到8个字节。如果使用unsigned shortuint8_t并用GNU C中的__attribute__((packed))压缩结构,则为5或6字节。

答案 2 :(得分:1)

不,不是。

指定函数位置的方法是使用已经在做的函数指针。

您可以创建具有各自不同成员函数的不同类型,但随后又回到了原始问题。

我过去曾尝试过自动生成(作为使用Python的预构建步骤)一个带有长switch语句的函数,该语句完成将int i映射到普通函数的工作呼叫。这摆脱了函数指针,但以分支为代价。我不记得它最终对我来说是否值得,即使我做到了,也无法告诉我们在你的案例中是否值得。

  

因为我知道在大多数汇编语言中,函数只是“ goto”指令

好吧,这也许比这更复杂

  

您可能会认为我疯狂地优化应用程序

也许。尝试消除间接本身并不是一件坏事,因此,我认为尝试改善这一点是不对的。我只是认为您不一定可以。

  

但是如果有人有一些指针

哈哈

答案 3 :(得分:0)

我不了解这种“优化”的目的是关于节省内存吗?

我可能会误解这个问题,但是如果您只是用常规函数替换函数指针,那么您的结构将仅包含int作为数据,而当您使用函数时,编译器将插入该函数指针地址,而不是存储在内存中。

所以

struct {
    int i;
    void func();
} test;  

如果将对齐方式/装箱设置得很紧,那么sizeof(test)==sizeof(int)应该为true。

相关问题