簡體   English   中英

在結構中嵌入函數匯編代碼

[英]embed a functions assembly code in a struct

我有一個相當特殊的問題:是否可以在 C/++ 中指定函數的位置(兩者都是因為我確定這兩種語言的問題是相同的)? 為什么? 我有一個非常大的函數指針列表,我想消除它們。

(目前)這看起來像這樣(重復了一百萬次,存儲在用戶的 RAM 中):

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

因為我知道在大多數匯編語言中,函數只是“goto”指令,所以我有以下想法。 是否可以優化上述構造使其看起來像這樣?

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;

最后,這件事在內存中應該是這樣的:一個包含任意值的 int,后跟函數的匯編代碼,由 test2 引用。 我應該能夠像這樣調用這些函數: ((void(*)()) (&pointerToTheStruct + sizeof(int)))();

您可能認為我以這種方式優化應用程序很瘋狂,我無法透露有關其功能的更多詳細信息,但是如果有人對如何解決此問題有一些建議,我將不勝感激。 我不認為有一個標准的方法,所以任何通過內聯匯編器/其他瘋狂的東西來做到這一點的黑客方法也值得贊賞!

你真正需要做的唯一一件事就是讓編譯器知道你想要在結構中的函數指針的(常量)值。 然后,編譯器將(可能/希望)內聯該函數調用,無論它通過該函數指針調用它的位置:

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();
}

演示- 優化后沒有calljmp

目前尚不清楚int i是什么。 請注意,這里的代碼在技術上並不是“直接在i ”,但更不清楚您期望結構實例的外觀(是其中的代碼還是某種意義上的“靜態”?我)感覺您對編譯器實際生成的內容存在一些誤解......)。 但是考慮編譯器內聯可以幫助您的方式,您可能會找到所需的解決方案。 如果您擔心內聯后的可執行文件大小,請告訴編譯器,它會在速度和大小之間做出妥協。

這聽起來像一個糟糕的主意,原因有很多,可能不會節省內存,並且會通過用數據稀釋 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();
}

當編譯為 C 或 C++ 時,clang7.0 的編譯器輸出在 Godbolt 編譯器資源管理器上是相同的。 這是針對 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 參數。)


您必須在可執行的內存中分配對象(通常不會完成,除非它們是具有靜態存儲的const ),例如使用gcc -zexecstack編譯。 或者在 POSIX 或 Windows 上使用自定義 mmap/mprotect 或 VirtualAlloc/VirtualProtect。

或者,如果您的對象都是靜態分配的,則可以通過在每個對象之前添加一個int成員來調整編譯器輸出以將.text部分中的函數轉換為對象。 也許使用一些.section和鏈接器技巧,也許還有鏈接器腳本,您甚至可以以某種方式自動化它。

但是除非它們的長度都相同(例如填充像char code[60] ),則不會形成可以索引的數組,因此您需要某種方式來引用所有這些可變長度對象。

如果您在調用對象的函數之前修改它,則可能存在巨大的性能下降:在 x86 上,您將獲得自修改代碼管道核,用於剛寫入的內存位置附近執行代碼。

或者,如果您在調用其函數之前復制了一個對象:x86 管道刷新,或者在其他 ISA 上,您需要手動刷新緩存以使 I-cache 與 D-cache 同步(因此可以執行新寫入的字節)。 但是您不能復制這些對象,因為它們的大小沒有存儲在任何地方 您無法在機器代碼中搜索ret指令,因為0xc3字節可能出現在不是 x86 指令開頭的地方。 或者在任何 ISA 上,該函數可能有多個ret指令(尾部重復優化)。 或者以 jmp 而不是 ret (尾調用)結束。 存儲大小將開始違背節省大小的目的,在每個對象中至少消耗一個額外的字節。

在運行時將代碼寫入對象,然后轉換為函數指針,這是 ISO C 和 C++ 中的未定義行為。 在 GNU C/C++ 上,請確保在其上調用__builtin___clear_cache以同步緩存或其他任何必要的內容。 是的,即使在 x86 上也需要禁用死存儲消除優化: 請參閱此測試用例 在 x86 上,它只是編譯時的事情,沒有額外的 asm。 它實際上並沒有清除任何緩存。

如果您在運行時啟動時進行復制,則可能會在復制時分配一大塊內存並切出可變長度的塊。 如果分別對每個malloc分配,則會浪費內存管理開銷。


這個想法不會節省你的內存,除非你有和你有對象一樣多的功能

通常,您擁有的實際函數數量相當有限,許多對象都具有相同函數指針的副本。 (你有點手卷的 C++ 虛函數,但只有一個函數,你只有一個函數指針,而不是一個指向該類類型指針表的 vtable 指針。間接級別少了,顯然你'不要將對象自己的地址傳遞給函數。)

這種間接級別的幾個好處之一是,一個指針通常比函數的整個代碼小得多。 如果不是這種情況,您的函數必須是tiny

示例:有 10 個不同的函數,每個函數有 32 個字節,並且有 1000 個帶有函數指針的對象,總共有 320 字節的代碼(將在 I-cache 中保持熱狀態)和 8000 字節的函數指針。 (並且在您的對象中,每個對象又浪費了 4 個字節用於填充以對齊指針,使每個對象的總大小為 16 而不是 12 個字節。)無論如何,整個 structs + code 總共有 16320 個字節 如果您分別分配每個對象,則存在每個對象的簿記。

將機器代碼內聯到每個對象中,並且沒有填充,即 1000 * (4+32) = 36000 字節,是總大小的兩倍多。

x86-64 可能是最好的情況,其中一個指針是 8 個字節,而 x86-64 機器代碼使用(著名的復雜)可變長度指令編碼,這在某些情況下允許高代碼密度,尤其是在優化代碼時 -尺寸。 (例如代碼高爾夫。https://codegolf.stackexchange.com/questions/132981/tips-for-golfing-in-x86-x64-machine-code )。 但是除非你的函數大多是像lea eax, [rdi + rdi*2] (3 bytes=opcode + ModRM + SIB) / ret (1 byte) 這樣的小東西,它們仍然會占用超過 8 個字節。 (對於 x86-64 System V ABI 中采用 32 位整數x arg 的函數,這是return x*3; 。)

如果它們是更大函數的包裝器,則正常的call rel32指令為 5 個字節。 靜態數據的加載至少為 6 個字節(對於 RIP 相對尋址模式, opcode + modrm + rel32 ,或者專門加載 EAX 可以使用特殊的 no-modrm 編碼作為絕對地址。但在 x86-64 中,這是一個 64-位絕對,除非您也使用地址大小前綴,否則可能會導致英特爾解碼器中的 LCP 停頓。mov mov eax, [32 bit absolute address] = addr32 (0x67) + opcode + abs32 = 6 字節,所以情況更糟沒有任何好處)。

您的函數指針類型沒有任何 args(假設這是 C++,其中foo() foo(void)在聲明中表示foo(void) ,而不是像舊 C 那樣空的 arg 列表有點類似於(...) )。 因此,我們可以假設您沒有傳遞參數,因此為了做任何有用的事情,函數可能會訪問一些靜態數據或進行另一個調用。


更有意義的想法:

  • 使用像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]

與在每個對象中存儲函數指針相比​​,這是一個額外的間接級別,但它可以讓您將每個對象從 16 個字節縮小到 8 個字節,就像使用 32 位指針一樣。 或者到 5 或 6 個字節,如果您使用unsigned shortuint8_t並在 GNU C 中使用__attribute__((packed))結構。

不,不是真的。

指定函數位置的方法是使用函數指針,您已經在這樣做了。

您可以創建具有自己不同成員函數的不同類型,但是您又回到了最初的問題。

我過去曾嘗試自動生成(作為預構建步驟,使用 Python)一個帶有長switch語句的函數,該語句執行將int i映射到普通函數調用的工作。 這以分支為代價擺脫了函數指針。 我不記得在我的情況下它最終是否值得,即使我這樣做了,也不會告訴我們在你的情況下是否值得。

因為我知道在大多數匯編語言中,函數只是“goto”指令

那么,它也許比這更復雜一點...

您可能會認為我以這種方式優化應用程序是瘋了

也許。 試圖消除間接性本身並不是一件壞事,所以我認為嘗試改進這一點並沒有錯。 我只是不認為你一定可以。

但如果有人有一些指示

哈哈

我不明白這種“優化”的目標是為了節省內存嗎?

我可能誤解了這個問題,但如果你只是用一個普通函數替換你的函數指針,那么你的結構將只包含 int 作為數據和當你獲取它的地址時編譯器插入的函數指針,而不是存儲在內存中。

所以就做

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

然后sizeof(test)==sizeof(int)如果您將對齊/包裝設置為緊密,則應該成立。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM