Реализация вложенных функций
Недавно я обнаружил, что 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);
}
Эта функция вызывает функцию, которая вызывает функцию, которая возвращает счетчик из контекста и одновременно увеличивает его.
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.байт -29
Это достаточно просто запустить через дизассемблер. Предположим, что $nested.1594
является 0x01234567
а также %rsp
является 0x0123456789abcdef
, В результате разборки, предоставленной objdump
, является:
0: 41 бб 67 45 23 01 мов $0x1234567,%r11d 6: 49 и cd ab 89 67 mov $0x123456789abcdef,%r10 д: 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
Это указывает компоновщику пометить стек как исполняемый.