заменить встроенный сборочный эпилог функции tailcall на Intrinsics для x86 / x64 msvc - PullRequest
0 голосов
/ 24 августа 2018

Я взял неактивный проект и уже многое исправил в нем, но мне не удается правильно заменить замену Intrinsics на используемую встроенную сборку, которая больше не поддерживается в компиляторах msvc x86 / x64.

#define XCALL(uAddr)  \
__asm { mov esp, ebp }   \
__asm { pop ebp }        \
__asm { mov eax, uAddr } \
__asm { jmp eax }

Варианты использования:

static oCMOB * CreateNewInstance() {
    XCALL(0x00718590);
}

int Copy(class zSTRING const &, enum zTSTR_KIND const &) {
    XCALL(0x0046C2D0);
}

void TrimLeft(char) {
    XCALL(0x0046C630);
}

1 Ответ

0 голосов
/ 24 августа 2018

Этот фрагмент находится в нижней части функции (которая не может быть встроенной, и должна быть скомпилирована с ebp в качестве указателя кадра, и никаких других регистров, которые необходимо восстановить). Он выглядит довольно хрупким, иначе он полезен только в тех случаях, когда вам вообще не нужен встроенный asm.

Вместо возврата он переходит на uAddr, что эквивалентно вызову хвоста.

Не существует встроенных функций для произвольных прыжков или манипулирования стеком. Если вам это нужно, вам не повезло. Нет смысла спрашивать об этом фрагменте отдельно, только с достаточным контекстом, чтобы увидеть, как он используется. то есть важно, какой адрес возврата находится в стеке, или это нормально для его компиляции с вызовом / ret вместо jmp по этому адресу? (См. Первую версию этого ответа для простого примера использования его в качестве указателя на функцию.)


Из вашего обновления ваши сценарии использования являются лишь очень неуклюжим способом создания оболочек для указателей на абсолютные функции.

Вместо этого мы можем определить static const указатели на функции правильных типов, поэтому обертки не нужны, и компилятор может вызывать их непосредственно из любого места, где вы их используете. static const - это то, как мы сообщаем компилятору, что он может полностью встроить указатели функций, и ему не нужно хранить их где-либо как данные, если он этого не хочет, как обычно static const int xyz = 2;

struct oCMOB;
class zSTRING;
enum zTSTR_KIND { a, b, c };  // enum forward declarations are illegal

// C syntax
//static oCMOB* (*const CreateNewInstance)() = (oCMOB *(*const)())0x00718590;

// C++11
static const auto CreateNewInstance = reinterpret_cast<oCMOB *(*)()>(0x00718590);
// passing an enum by const-reference is dumb.  By value is more efficient for integer types
static const auto Copy = reinterpret_cast<int (*)(class zSTRING const &, enum zTSTR_KIND const &)>(0x0046C2D0);
static const auto TrimLeft = reinterpret_cast<void (*)(char)> (0x0046C630);

void foo() {
    oCMOB *inst = CreateNewInstance();
    (void)inst; // silence unused warning

    zSTRING *dummy = nullptr;  // work around instantiating an incomplete type
    int result = Copy(*dummy, c);
    (void) result;

    TrimLeft('a');
}

Он также прекрасно компилируется с x86-64 и 32-битным x86 MSVC, а также gcc / clang 32 и 64-битными в проводнике компилятора Godbolt . (А также архитектуры не x86). Это 32-битный вывод asm от MSVC, так что вы можете сравнить с тем, что вы получаете для своих неприятных функций-оболочек. Вы можете видеть, что это в основном полезная часть (mov eax, uAddr / jmp или call) в вызывающей стороне.

;; x86 MSVC -O3
$T1 = -4                                                ; size = 4
?foo@@YAXXZ PROC                                        ; foo
        push    ecx
        mov     eax, 7439760                          ; 00718590H
        call    eax

        lea     eax, DWORD PTR $T1[esp+4]
        mov     DWORD PTR $T1[esp+4], 2       ; the by-reference enum
        push    eax
        push    0                             ; the dummy nullptr
        mov     eax, 4637392                          ; 0046c2d0H
        call    eax

        push    97                                  ; 00000061H
        mov     eax, 4638256                          ; 0046c630H
        call    eax

        add     esp, 16                             ; 00000010H
        ret     0
?foo@@YAXXZ ENDP

При повторных вызовах одной и той же функции компилятор будет хранить указатель функции в регистре с сохранением вызова.


По какой-то причине, даже с 32-битным положением - зависимым кодом, мы не получаем прямой call rel32. Линкер может рассчитать относительное смещение от сайта вызова до абсолютного целевого значения во время соединения, поэтому у компилятора нет причин использовать косвенный регистр call.

Если мы не сказали компилятору создавать независимый от позиции код, в этом случае полезно оптимизировать адресации абсолютных адресов относительно кода для переходов / вызовов.

В 32-битном коде каждый возможный адрес назначения находится в диапазоне от каждого возможного исходного адреса, но в 64-битном это сложнее. В 32-битном режиме clang замечает эту оптимизацию ! Но даже в 32-битном режиме MSVC и gcc пропускают его.

Я немного поиграл с gcc / clang:

// don't use
oCMOB * CreateNewInstance(void) asm("0x00718590");

Вид работ, но только как полный взлом. Gcc просто использует эту строку, как если бы она была символом, поэтому она передает call 0x00718590 ассемблеру, который обрабатывает ее правильно (генерируя абсолютное перемещение, которое просто отлично связывается с исполняемым файлом без PIE). Но с -fPIE мы испускаем 0x00718590@GOTPCREL в качестве имени символа, поэтому мы облажались.

Конечно, в 64-битном режиме исполняемый файл или библиотека PIE будут вне диапазона этого абсолютного адреса, поэтому в любом случае имеет смысл только не-PIE.


Другая идея состояла в том, чтобы определить символ в asm с абсолютным адресом и предоставить прототип, который заставит gcc использовать его только напрямую, без @PLT или прохождения GOT. (Возможно, я мог бы сделать это и для хака func() asm("0x...");, используя скрытую видимость.)

Я только понял после взлома этого атрибута "hidden", что это бесполезно в позиционно-независимом коде, поэтому вы не можете использовать его в общей библиотеке или в исполняемом файле PIE.

extern "C" не является необходимым, но означает, что мне не пришлось связываться с искажением имен во встроенном асме.

#ifdef __GNUC__

extern "C" {
    // hidden visibility means that even in a PIE executable, or shared lib,
    // calls will go *directly* to that address, not via the PLT or GOT.
    oCMOB * CNI(void) __attribute__((__visibility__("hidden")));
}
//asm("CNI = 0x718590");  // set the address of a symbol, like `org 0x71... / CNI:`
asm(".set CNI, 0x718590");  // alternate syntax for the same thing


void *test() {
    CNI();    // works

    return (void*)CNI;  // gcc: RIP+0x718590 instead of the relative displacement needed to reach it?
    // clang appears to work
}
#endif

разборка скомпилированного + связанного вывода gcc для test, из Godbolt с использованием двоичного вывода, чтобы увидеть, как он собран + связан :

 # gcc -O3  (non-PIE).  Clang makes pretty much the same code, with a direct call and mov imm.
 sub    rsp,0x8
 call   718590 <CNI>
 mov    eax,0x718590
 add    rsp,0x8
 ret    

При -fPIE gcc + gas испускает lea rax,[rip+0x718590] # b18ab0 <CNI+0x400520>, т. Е. Он использует абсолютный адрес как смещение от RIP, а не вычитает. Я думаю, это потому, что gcc буквально испускает lea CNI(%rip),%rax, и мы определили CNI как символ времени сборки с этим числовым значением. К сожалению. Так что это не совсем похоже на ярлык с таким адресом, как если бы вы набрали .org 0x718590; CNI:.

Но поскольку мы можем использовать rel32 call только в исполняемых файлах, отличных от PIE, это нормально, если вы не скомпилируете с -no-pie, но забудете -fno-pie, и в этом случае вы облажались. : /

Возможно, сработал отдельный объектный файл с определением символа.

Clang, кажется, делает именно то, что мы хотим, хотя даже с -fPIE со встроенным ассемблером. Этот машинный код мог быть связан только с -fno-pie (по умолчанию для Godbolt, но не по умолчанию во многих дистрибутивах.)

 # disassembly of clang -fPIE machine-code output for test()
 push   rax
 call   718590 <CNI>
 lea    rax,[rip+0x3180b3]        # 718590 <CNI>
 pop    rcx
 ret    

Так что это на самом деле безопасно (но неоптимально, потому что lea rel32 хуже, чем mov imm32.) С -m32 -fPIE он даже не собирается.

...