GCC - изменить, где выполнение продолжается после возврата функции
Можно ли сделать что-то подобное в GCC?
void foo() {
if (something()) returnSomewhereElse;
else return;
}
void bar() {
foo();
return; // something failed, no point of continuing
somewhereElse:
// execution resumes here if something succeeds
// ...
}
- Можно ли этого достичь переносимым способом, используя расширения C и GCC, без использования специфичной для платформы сборки?
- состояние стека не изменится между нормальной и измененной точками возврата, поэтому возможно ли повторно использовать код, который восстанавливает стек и регистрирует состояние из обычного возврата?
- учитывая, что функция может быть или не быть встроенной, при вызове она должна изменять адрес возврата, если встроенная, она должна изменять только путь к коду, но не текущий адрес возврата функции, так как это нарушит код
- альтернативная точка возврата не обязательно должна быть меткой, но я надеюсь, что адрес расширения метки в GCC может пригодиться в этой ситуации
Просто чтобы уточнить намерение - речь идет об обработке ошибок. Этот пример является минимальным, просто чтобы проиллюстрировать вещи. Я намерен использовать это в гораздо более глубоком контексте, чтобы остановить выполнение в случае возникновения ошибки. Я также предполагаю, что состояние не меняется, в чем я могу ошибаться, потому что между двумя точками возврата не добавляются дополнительные локальные переменные, поэтому я надеялся, что код, сгенерированный компилятором для этого по возвращении foo, может быть повторно использован для этого и сэкономить на использовании longjmp
, установка и передача буферов перехода.
Пример "имеет смысл", потому что его цель - показать, чего я хочу достичь, а не почему и как это имело бы смысл в реальном коде.
Почему ваша идея проще, чем просто возвращать значение из foo() и иметь bar() либо возвращать, либо выполнять где-то Else: условно?
Это не проще, и то, что вы предлагаете, не применимо на практике, только в контексте тривиального примера, но это лучше, потому что:
1 - не требует дополнительного возврата значения
2 - не требует дополнительной проверки значения
3 - не требует дополнительного прыжка
Я, вероятно, ошибочно полагаю, что цель должна быть ясной на данный момент, а также после всех разъяснений и объяснений. Идея состоит в том, чтобы обеспечить "путь выхода кода" из глубокой цепочки вызовов без каких-либо дополнительных затрат. Повторно используя код, сгенерированный компилятором, чтобы восстановить состояние предыдущего кадра вызова и просто изменить инструкцию, с которой выполнение возобновляется после возврата из функции. При успехе пропускается "путь к escape-коду", первая ошибка, которая возникает, входит в него.
if (failure) return; // right into the escape code path
else {
doMagickHere(); // to skip the escape code path
return; // skip over the escape code path
}
//...
void bar() {
some locals;
foo();
// enter escape code path here on foo failure so
destroy(&locals); // cleanup
return; // and we are done
skipEscapeCodePath: // resume on foo success
// escape path was skipped so locals are still valid
}
Что касается заявлений Василия Старинкевича о том, что longjmp
является "эффективным" и что "даже миллиард longjmp остается разумным" - sizeof(jmp_buf)
дает мне изрядные 156 байт, что, по-видимому, является пространством, необходимым для сохранения почти всех регистров и множества других вещей, чтобы впоследствии его можно было восстановить. Это много операций, и миллиардное выполнение этого далеко далеко от моего личного понимания "эффективного" и "разумного". Я имею в виду, что миллиард буферов перехода сами по себе занимают более 145 гигабайт памяти, а также накладные расходы процессора. Не так много систем, которые могут позволить себе такие "разумные".
2 ответа
После некоторой дополнительной мысли, это не так просто, как казалось на первый взгляд. Есть одна вещь, которая мешает ему работать - код функций не учитывает контекст - нет способа узнать фрейм, в котором он был вызван, и это имеет два значения:
1 - модифицировать указатель инструкции, если он не переносимый, достаточно просто, поскольку каждая реализация определяет для него согласованное место, обычно это первое в стеке, однако изменение его значения для пропуска escape -ловушки также пропускает код, который восстанавливает состояние предыдущего кадра, поскольку этот код присутствует, а не в текущем кадре - он не может выполнить восстановление состояния, так как не имеет никакой информации о нем, для устранения этого, если дополнительная проверка и переход пропущены, является дублирование состояние восстановления кода в обоих местах, к сожалению, это можно сделать только в сборке
2 - количество инструкций, которые нужно пропустить, тоже неизвестно, и зависит от того, какой предыдущий кадр стека, в зависимости от количества локальных пользователей, нуждающихся в уничтожении, он будет меняться, это не будет единообразным значением, средство для этого будет означать, что при вызове функции в стек помещаются указатели инструкций ошибок и успешных действий, чтобы можно было восстановить один или другой в зависимости от того, произошла ошибка или нет. К сожалению, это тоже можно сделать только при сборке.
Похоже, что такая схема может быть реализована только на компиляторе уровней, требуя своего собственного соглашения о вызовах, которое помещает два местоположения возврата и вставляет код восстановления состояния в оба. И потенциальная экономия от такого подхода вряд ли заслуживает усилий по написанию компилятора.
Нет, это невозможно, и я не уверен, что точно угадаю, чего вы хотите достичь.
терминология
Возможно, вы хотите нелокальный прыжок. Внимательно прочитайте о setjmp.h, сопрограммах, стеке вызовов, обработке исключений, продолжениях и стиле передачи продолжения. Понимание того, что называется /cc в Схеме, должно быть очень полезным.
setjmp
а также longjmp
setjmp и longjmp являются стандартными функциями C99 (и они довольно быстрые, потому что сохраненное состояние на самом деле довольно мало). Будьте осторожны при их использовании (в частности, чтобы избежать утечки памяти). longjmp
(или связанный с ним siglongjmp в POSIX) - единственный способ в портативном стандарте C99 выйти из какой-либо функции и вернуться к какому-либо вызывающему.
Идея состоит в том, чтобы обеспечить "путь выхода кода" из глубокой цепочки вызовов без каких-либо дополнительных затрат.
Это именно роль longjmp
с setjmp
, Оба являются быстрыми, постоянными операциями (в частности, раскручивание стека вызовов из многих тысяч кадров вызовов с longjmp
занимает короткое и постоянное время). Накладные расходы памяти практически на один локальный jmp_buf
за точку улова, ничего страшного. jmp_buf
редко помещается за пределы стека вызовов.
Распространенным способом их эффективного использования было бы setjmp
-ed jmp_buf
в местном struct
(так в вашем кадре вызова) и передать указатель на это struct
к некоторому внутреннему static
функция (и), которая косвенно вызовет longjmp
по ошибке. следовательно setjmp
а также longjmp
может, с мудрыми соглашениями о кодировании, достаточно хорошо и эффективно имитировать сложную семантику создания и обработки исключений C++ (или исключений Ocaml, или исключений Java, которые имеют семантику, отличную от C++). Это портативные базовые блоки, достаточные для этой цели.
Практически говоря, код что-то вроде:
struct my_foo_state_st {
jmp_buf jb;
char* rs;
// some other state, e.g a ̀ FILE*` or whatever
};
/// returns a `malloc̀ -ed error message on error, and NULL on success
extern const char* my_foo (struct some_arg_st* arg);
struct my_foo_state_st
это частное государство. my_foo
это публичная функция (которую вы бы объявили в каком-то публичном заголовке). Вы сделали документ (по крайней мере, в комментарии), что он возвращает сообщение об ошибке, выделенное кучей при сбое, следовательно, вызывающий отвечает за его освобождение. В случае успеха вы задокументировали, что он возвращается NULL
, Конечно, у вас могут быть другие соглашения и другие аргументы и / или результат.
Мы объявляем и реализуем функцию ошибки, которая печатает сообщение об ошибке в состояние и выходит с longjmp
static void internal_error_printf (struct my_foo_state*sta,
int errcode,
const char *fmt, ...)
__attribute__((noreturn, format(printf(2,3))));
void internal_error_printf(struct my_foo_state*sta,
int errcode, const char *fmt, ...) {
va_arg args;
va_start(args, fmt);
vasprintf(&sta->rs, fmt, args);
va_end(args);
longjmp(sta->jb, errcode);
}
Теперь у нас есть несколько возможно сложных и рекурсивных функций, выполняющих основную часть работы. Я только рисую их, ты знаешь, что ты хочешь, чтобы они делали. Конечно, вы можете дать им некоторые дополнительные аргументы (это часто полезно, и это зависит от вас).
static void my_internal_foo1(struct my_foo_state_st*sta) {
int x, y;
// do something complex before that and compute x,y
if (SomeErrorConditionAbout(sta))
internal_error_printf(sta, 35 /*error code*/,
"errror: bad x=%d y=%d", x, y);
// otherwise do something complex after that, and mutate sta
}
static void my_internal_foo2(struct my_foo_state_st*sta) {
// do something complex
if (SomeConditionAbout(sta))
my_internal_foo1(sta);
// do something complex and/or mutate or use `sta`
}
(даже если у вас есть десятки внутренних функций, как указано выше, вы не потребляете jmp_buf
в любом из них; и вы могли бы также глубоко углубиться в них. Вам просто нужно передать указатель на struct my_foo_state_st
во всех из них, и если вы однопоточны и не заботитесь о повторном входе, вы можете сохранить этот указатель в некоторых static
переменная... или какая-то локальная для потока, даже не передавая ее в каком-либо аргументе, который я нахожу все еще предпочтительным - так как более реентерабельный и дружественный потоку)
Наконец, вот публичная функция: она устанавливает состояние и делает setjmp
// the public function
const char* my_foo (struct some_arg_st* arg) {
struct my_state_st sta;
memset(&sta, 0, sizeof(sta));
int err = setjmp(sta->jb);
if (!err) { // first call
/// put something in `sta` related to ̀ arg̀
/// start the internal processing
//// later,
my_internal_foo1(&sta);
/// and other internal functions, possibly recursive ones
/// we return NULL to tell the caller that all is ok
return NULL;
}
else { // error recovery
/// possibly release internal consumed resources
return sta->rs;
};
abort(); // this should never be reached
}
Обратите внимание, что вы можете позвонить my_foo
миллиард раз, он не будет использовать кучу памяти, когда не выйдет из строя, и стек вырастет на сотню байт (освобождается до возвращения из my_foo
). И даже если ваш личный код провалился миллиард раз, internal_error_printf
утечки памяти не происходит (потому что вы my_foo
возвращает строку ошибки, которую вызывающий должен free
) если кодирование правильно.
Следовательно, используя правильно setjmp
а также longjmp
миллиард раз не потребляет много памяти (всего несколько сотен байтов в стеке вызовов для одного локального jmp_buf
, который выскочил на my_foo
функция возврата). В самом деле, longjmp
немного дороже, чем равнина return
(но это спасение, которое return
нет), поэтому вы бы предпочли использовать его в ситуациях с ошибками.
Но используя setjmp
а также longjmp
сложен, но эффективен и переносим, и делает ваш код трудным для понимания, как описано в setjmp. Важно это серьезно прокомментировать. Используя эти setjmp
а также longjmp
умно и мудро не требуется "гигабайт" оперативной памяти, как ошибочно сказано в отредактированном вопросе (потому что вы потребляете только один jmp_buf
на стек вызовов, а не миллиарды из них). Если вы хотите более сложный поток управления, вы будете использовать местный jmp_buf
в каждой динамической "точке захвата" в стеке вызовов (и, вероятно, их будет десятки, а не миллиарды). Вам понадобятся миллионы jmp_buf
только в гипотетическом случае рекурсии нескольких миллионов фреймов вызова, каждый из которых является точкой захвата, и это нереально (рекурсия на глубину в один миллион никогда не произойдет, даже без обработки исключений).
Смотрите это для лучшего объяснения setjmp
для обработки "исключений" в C (и SFTW для других). FWIW, куриная схема имеет очень изобретательное использование longjmp
а также setjmp
(связанных с сборкой мусора и call/cc
!)
альтернативы
setcontext(3), возможно, был POSIX, но сейчас устарел.
GCC имеет несколько полезных расширений (некоторые из них понятны Clang / LLVM): операторы выражений, локальные метки, метки в качестве значений и вычисляемый переход, вложенные функции, создание вызовов функций и т. Д.
(Мне кажется, что вы неправильно понимаете некоторые концепции, в частности, точную роль стека вызовов, поэтому ваш вопрос очень неясен; я дал несколько полезных ссылок)
возвращая маленький struct
Также обратите внимание, что на некоторых ABI, в частности на x86-64 ABI в Linux, возвращается небольшое struct
(например, из двух указателей или одного указателя и одного int
или же long
или же intptr_t
число) чрезвычайно эффективно (так как оба указателя или целые числа проходят через регистры), и вы можете воспользоваться этим: решите, что ваша функция возвращает указатель на первичный результат и некоторый код ошибки, оба упакованные в один маленький struct
:
struct tworesult_st {
void* ptr;
int err;
};
struct towresult_st myfunction (int foo) {
void* res = NULL;
int errcode = 0;
/// do something
if (errcode)
return (struct tworesult_st){NULL, errcode};
else
return (struct tworesult_st){res, 0};
}
В Linux/x86-64 приведенный выше код оптимизирован (при компиляции с gcc -Wall -O
) для возврата в двух регистрах (без использования стека для возвращаемого struct
).
Использование такой функции простое и очень эффективное (без использования памяти, два члена ̀ struct` будут переданы в регистры процессора) и может быть простым:
struct tworesult_st r = myfunction(34);
if (r.err)
{ fprintf(stderr, "myfunction failed %d\n", r.err); exit(EXIT_FAILURE); }
else return r.ptr;
Конечно, вы могли бы иметь лучшую обработку ошибок (это ваше дело).
Другие подсказки
Узнайте больше о семантике, в частности, об операционной семантике.
Если переносимость не является основной проблемой, изучите соглашения о вызовах вашей системы и ее ABI и сгенерированный код ассемблера (gcc -O -Wall -fverbose-asm foo.c
тогда загляни внутрь foo.s
) и укажите соответствующий код asm
инструкции
Возможно, libffi может быть актуальным (но я до сих пор не понимаю ваших целей, только догадался о них).
Вы можете попытаться использовать выражения-метки и вычисленные gotos, но если вы не понимаете сгенерированный код ассемблера, результат может оказаться не таким, как вы ожидаете (потому что указатель стека изменяется при вызовах функций и возвращается).
Самомодифицирующийся код не одобряется (и "невозможен" в стандартном C99), и большинство реализаций C помещают двоичный код в сегмент кода, доступный только для чтения. Читайте также о функциях батута. Рассмотрим, возможно, методы JIT-компиляции, такие как libjit, asmjit, GCCJIT.
(Я твердо верю, что прагматический ответ на ваши вопросы longjmp
с подходящими правилами кодирования, или просто возвращая небольшой struct
; оба могут использоваться очень эффективно, и я не могу представить себе случая, когда они недостаточно эффективны)
Некоторые языки: схема со своим call/cc
Пролог с его функциями обратного отслеживания, возможно, более приспособлен (чем С99) к потребностям ОП.