Введение
Ранее уже разбиралось, как в GCC 17 и Clang можно использовать вложенные функции для колбэков без необходимости в исполняемом стеке. Но что делать, если нужно поддерживать более старые версии GCC? Можно, конечно, просто смириться с исполняемым стеком (это не так страшно, как некоторые утверждают), но есть способ обойтись и без него — с помощью небольшого хака.
GCC: вложенные функции и трамплины
Сначала стоит разобраться, как GCC вообще реализует взятие адреса вложенной функции. Ниже — упрощённый пример без использования новых макросов (пример на Godbolt).
typedef int cb_f(int y);
int baz(cb_f p, int x)
{
return p(x);
}
int foo(int k)
{
int bar(int x) { return k + x; }
return baz(bar, 2 * k);
}
На x86_64 генерируется следующий ассемблерный код.
bar.0:
movl %edi, %eax
addl (%r10), %eax
ret
foo:
subq $56, %rsp
leaq 64(%rsp), %rax
movq %rax, 32(%rsp)
movl %edi, (%rsp)
leaq 4(%rsp), %rax
movw $-17591, 4(%rsp)
movabsq $bar.0, %rcx
movq %rcx, 6(%rsp)
movw $-17847, 14(%rsp)
movq %rsp, 16(%rsp)
movl $-1864106167, 24(%rsp)
addl %edi, %edi
call *%rax
addq $56, %rsp
ret
Этот код размещает трамплин на стеке и сразу же вызывает его через инлайненную функцию baz. Трамплин — это короткая последовательность инструкций, которая загружает статический регистр фрейма в структуру на стеке, содержащую захваченные переменные родительской функции, а затем передаёт управление локальной функции. Если перевести константы -17591, -17847 и -1864106167 обратно в ассемблерные инструкции, получится следующий код x86_64.
movq $bar.0, %r11
movq $frame, %r10
jump *%r11
И статическая цепочка (static chain), и адрес кода — это непосредственные константы, используемые инструкциями пересылки внутри тела трамплина. Вместо того чтобы вызывать функцию через адрес трамплина, можно извлечь из него адрес кода и статическую цепочку, а затем использовать их со встроенной функцией __builtin_call_with_static_chain для прямого вызова локальной функции. Например, с помощью макросов peek и array_slice из библиотеки noplate это можно сделать так — для x86_64 (и модели памяти large).
unsigned char (*tramp)[24] = (void*)bar;
void *code = peek(uint64_t, &array_slice(tramp, 2, 10));
void *chain = peek(uint64_t, &array_slice(tramp, 12, 20));
Эти указатели — ровно та же информация, которую можно будет получить в ещё не выпущенном GCC 17 с помощью новых встроенных функций __builtin_call_static_chain и __builtin_call_code_adress, и её можно использовать для вызова локальной функции так же, как обсуждалось ранее.
__builtin_call_with_static_chain(((typeof(bar)*)code)(arg), chain);
Таким образом, чтение этих двух значений указателей из трамплина можно использовать как запасной механизм для старых версий GCC. Минусы в том, что трамплин всё равно создаётся, компилятор по-прежнему не может девиртуализировать косвенный вызов, а стек всё равно помечается как исполняемый. Так в чём же выгода? Поскольку трамплин фактически никогда не вызывается, стек можно снова сделать неисполняемым следующей командой, что хотя бы снимает вопросы безопасности, связанные с этой возможностью.
patchelf --clear-execstack program
Эта идея реализована в экспериментальной библиотеке noplate, где из адреса кода и статической цепочки конструируется широкий указатель.
Трамплины как дескрипторы функций
Есть ещё одна идея, которую стоит изучить подробнее: сам трамплин можно использовать как дескриптор функции. Вместо того чтобы извлекать статическую цепочку и указатель кода в момент создания трамплина, можно как обычно передавать дальше адрес самого трамплина. Но везде, где предполагается вызов трамплина, сначала проверяется, действительно ли указатель ведёт на трамплин, и уже затем извлекаются адрес кода и статическая цепочка для прямого вызова вложенной функции через __builtin_call_with_static_chain. В каком-то смысле вместо непосредственного вызова трамплина происходит его интерпретация прямо в точке вызова — с помощью крайне простого интерпретатора, способного разобрать только эту конкретную последовательность кода и настолько компактного, что его можно инлайнить (пример на Godbolt).
Литература
- GCC, Nested Functions
- GCC, Constructing Function Calls