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) к потребностям ОП.

Другие вопросы по тегам