Реализация вложенных функций

Недавно я узнал, что gcc позволяет определять вложенные функции. На мой взгляд, это крутая фича, но интересно, как ее реализовать.

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

Я знаю, что другие языки, такие как Haskell, которые имеют более запутанное соглашение о вызовах, позволяют частичному приложению поддерживать такие вещи, но я не вижу способа сделать это в C. Как это можно реализовать?

Вот небольшой пример случая, который иллюстрирует проблему:

int foo(int x,int(*f)(int,int(*)(void))) {
  int counter = 0;
  int g(void) { return counter++; }

  return f(x,g);
}

Эта функция вызывает функцию, которая вызывает функцию, которая возвращает счетчик из контекста и одновременно увеличивает его.


person fuz    schedule 18.11.2011    source источник
comment
Я не знал, что вы можете передавать указатели на вложенные функции. Это действительно хороший вопрос о том, как они работают - предположительно, вызов указателя после возврата внешней функции приводит к плохому поведению?   -  person James    schedule 18.11.2011
comment
@Autopulated Это на самом деле верно и логично, поскольку соответствующий фрейм стека больше не существует.   -  person fuz    schedule 18.11.2011
comment
Сложно назвать это крутой функцией.   -  person Victor Yarema    schedule 05.11.2019


Ответы (1)


GCC использует нечто, называемое батутом.

Информация: http://gcc.gnu.org/onlinedocs/gccint/Trampolines.html

Трамплин — это фрагмент кода, который GCC создает в стеке для использования, когда вам нужен указатель на вложенную функцию. В вашем коде батут необходим, потому что вы передаете g в качестве параметра вызову функции. Батут инициализирует некоторые регистры, чтобы вложенная функция могла ссылаться на переменные во внешней функции, а затем переходит к самой вложенной функции. Батуты очень маленькие — вы «отскакиваете» от батута в тело вложенной функции.

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

Разбор батута:

Вот пример вложенной функции в расширенном C GCC:

void func(int (*param)(int));

void outer(int x)
{
    int nested(int y)
    {
        // If x is not used somewhere in here,
        // then the function will be "lifted" into
        // a normal, non-nested function.
        return x + y;
    }
    func(nested);
}

Это очень просто, поэтому мы можем увидеть, как это работает. Вот результирующая сборка outer, за вычетом кое-чего:

subq    $40, %rsp
movl    $nested.1594, %edx
movl    %edi, (%rsp)
leaq    4(%rsp), %rdi
movw    $-17599, 4(%rsp)
movq    %rsp, 8(%rdi)
movl    %edx, 2(%rdi)
movw    $-17847, 6(%rdi)
movw    $-183, 16(%rdi)
movb    $-29, 18(%rdi)
call    func
addq    $40, %rsp
ret

Вы заметите, что большая часть того, что он делает, это запись регистров и констант в стек. Мы можем проследить и обнаружить, что в SP+4 он помещает 19-байтовый объект со следующими данными (в синтаксисе GAS):

.word -17599
.int $nested.1594
.word -17847
.quad %rsp
.word -183
.byte -29

Это достаточно легко запустить через дизассемблер. Предположим, что $nested.1594 равно 0x01234567, а %rsp равно 0x0123456789abcdef. Итоговая разборка, предоставленная objdump, такова:

   0:   41 bb 67 45 23 01       mov    $0x1234567,%r11d
   6:   49 ba ef cd ab 89 67    mov    $0x123456789abcdef,%r10
   d:   45 23 01 
  10:   49 ff e3                rex.WB jmpq   *%r11

Таким образом, батут загружает указатель стека внешней функции в %r10 и переходит к телу вложенной функции. Тело вложенной функции выглядит так:

movl    (%r10), %eax
addl    %edi, %eax
ret

Как видите, вложенная функция использует %r10 для доступа к переменным внешней функции.

Конечно, довольно глупо, что батут больше, чем сама вложенная функция. Вы могли бы легко сделать лучше. Но не очень многие люди используют эту функцию, и таким образом батут может оставаться одного размера (19 байт) независимо от того, насколько велика вложенная функция.

Заключительное примечание: в нижней части сборки есть последняя директива:

.section        .note.GNU-stack,"x",@progbits

Это указывает компоновщику пометить стек как исполняемый.

person Dietrich Epp    schedule 18.11.2011
comment
Разве нельзя было бы поместить батут в кучу, используя malloc и конечную free при возврате? - person fuz; 18.11.2011
comment
Прологам функций не должно быть слишком сложно поместить в стек ссылку доступа. как механизм для избегания батутов, не так ли? - person sarnold; 18.11.2011
comment
@sarnold Использование ссылок доступа на самом деле не решает проблему, если я правильно понял. Если вы вызываете foo из функции параметров f, какой счетчик увеличивается? Очевидно, что без дополнительной информации невозможно отследить кадр стека, соответствующий указателю на функцию... - person fuz; 18.11.2011
comment
@FUZxxl: Это приведет к утечке с longjmp - и как бы вы справились с возвратом NULL от malloc? - person Dietrich Epp; 18.11.2011
comment
Хм, тогда Паскаль, должно быть, имел некоторые дополнительные ограничения на вложенные функции, которые позволяли ссылкам доступа быть подходящим решением. Никогда не думал об этом так много. :) - person sarnold; 18.11.2011
comment
@sarnold: Паскаль может сделать это проще: просто используя другое соглашение о вызовах. - person Dietrich Epp; 18.11.2011
comment
@DietrichEpp, AFAIK, Pascal не имеет процедуры или типа указателя на процедуру и не может передавать процедуры в качестве аргументов. - person chill; 18.11.2011
comment
@chill: Некоторые диалекты Паскаля имеют указатели на функции. В любом случае вложенные функции не являются стандартными для C. - person Dietrich Epp; 18.11.2011
comment
И вот, много лет спустя... Для использования трамплинов требуется исполняемый стек, а это угроза безопасности. Чтобы избежать этой проблемы, GCC также поддерживает другую стратегию: использование дескрипторов для вложенных функций... Хорошо, что ссылка на страницу документации GCC по-прежнему указывает на ту же функцию. - person Victor Yarema; 05.11.2019
comment
Можете ли вы объяснить те же самые вещи с точки зрения символьных вычислений? т.е. чтобы объяснить, как этот ассемблер был сгенерирован с использованием некоторых концепций высокого уровня (конкретным образом, в идеале в стиле lisp). - person alinsoar; 05.02.2020
comment
@alinsoar: С точки зрения высокого уровня, если у вас есть int nested(int y);, то GCC фактически излучает что-то вроде int nested(void *link, int y); и создает батут int trampoline(int y) { return nested(0x01234567, y); }. Вот и все. Батут остается коротким, потому что он имеет оптимизированный одноуровневый вызов и использует пользовательское соглашение о вызовах для параметра ссылки. Однако ассемблер для самого батута почти наверняка создается вручную. - person Dietrich Epp; 05.02.2020
comment
@DietrichEpp этот алгоритм на самом деле такой же, как и в cgi.sice.indiana.edu/~c311/lib/exe/ - person alinsoar; 15.02.2020
comment
@alinsoar: На самом деле это другая техника. Его также называют батутом, но это другой алгоритм. - person Dietrich Epp; 16.02.2020
comment
cam Вы даете ссылку на используемый алгоритм? - person alinsoar; 16.02.2020
comment
@alinsoar: В ответе должно быть описано все. Что еще вы хотели бы знать? - person Dietrich Epp; 16.02.2020
comment
@DietrichEpp Я хочу прочитать статью, в которой подробно рассказывается о том, как GCC реализует вложенные функции, в которой подробно описано то, что вы объяснили. - person alinsoar; 16.02.2020
comment
@alinsoar: здесь особо нечего объяснять - внутренние устройства очень специфичны для GCC, а алгоритм слишком прост, чтобы требовать дополнительных деталей (на самом деле все, что делает алгоритм, - это выдает код, который записывает крошечную функцию в стек). Если вы не знакомы с внутренним устройством GCC, вам может не понравиться это чтение. Внутренние документы: gcc.gnu.org/onlinedocs/gccint/Trampolines.html - код для i386 - github.com/gcc -mirror/gcc/blob/master/gcc/config/i386/i386.c (см. ix86_trampoline_init) - person Dietrich Epp; 16.02.2020