Этот фрагмент находится в нижней части функции (которая не может быть встроенной, и должна быть скомпилирована с 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
он даже не собирается.