setjmp/longjmp в примерах

Автор: Raghu J Menon
Перевод: Андрей Киселев

Набор макроопределений setjmp/longjmp, реализованных в языке программирования C, представляют собой практически идеальную платформу для создания программ со сложным управлением. Но, прежде чем вы начнете использовать эти макросы в своей работе, вам необходимо получить достаточный объем сведений о них, иначе ваши программы станут очень трудны для понимания.

Для чего они предназначены?

Функция setjmp сохраняет состояние программы. Если быть более точным -- это содержимое регистров sp, fp и pc. Состояние программы однозначно определяется содержимым этих регистров, а так же содержимым памяти, которая включает в себя динамическую память и стек. У вас наверняка может возникнуть вопрос -- для чего нужно сохранять состояние программы? Ответ может быть более чем простым -- для того чтобы восстановить его позднее вызовом функции longjmp. Таким образом, эти функции следует рассматривать в паре друг с другом, т.е. setjmp сохраняет состояние программы, а longjmp восстанавливает его.

Синтаксис....

Синтаксис чрезвычайно прост. setjmp сохраняет состояние программы в переменной типа jmp_buf (определение находится в заголовочном файле setjmp.h). Всегда подключайте этот файл, когда собираетесь работать с этими функциями.

int setjmp (jmp_buf env);

int longjmp(jmp_buf env , int val);

Функция longjmp восстанавливает состояние программы, которое ранее было сохранено в переменной env. Назначение аргумента val я объясню позже. Прежде чем будет вызвана функция longjmp, необходимо, чтобы была вызвана функция setjmp, которая сохранит состояние программы в переменной env и вернет значение 0. Когда в программе встретится вызов функции longjmp, то будет восстановлено сохраненное ранее состояние программы и исполнение программы будет продолжено с инструкции, стоящей после вызова setjmp. Это выглядит так, как если бы функция longjmp возвращала управление через setjmp. Такой "возврат" подразумевает некоторое возвращаемое значение, аргумент val как раз и есть это возвращаемое функцией setjmp значение.

i = setjmp (env);//Сохранить состояние программы в env и вернуть 0

...........      //С этой точки будет продолжено выполнение программы
...........      //после вызова longjmp

longjmp(env,val)

Если сразу за вызовом setjmp вывести возвращаемое значение, то вы получите 2 различных значения. Первое (когда setjmp вызывается для сохранения состояния программы) -- это всегда ноль. Второе -- значение, которое передается функции longjmp через аргумент val. Таким образом код после setjmp может выполняться неоднократно. Давайте рассмотрим это на примере if-else.c

Скомпилируйте и запустите этот пример. Я полагаю вы заметили, что выполняются обе ветки условного оператора! Выглядит несколько необычно и очень похоже на fork() ("родитель" идет через ветку if, а "потомок" -- через ветку else или наоборот). Однако, в отличие от данного случая, в результате fork мы получаем два различных процесса. Вызов функции setjmp сохраняет состояние приложения в переменной env и возвращает значение 0. В результате условие выполняется и на экран выводится первое сообщение. Затем, когда вызывается функция longjmp, состояние приложения восстанавливается, что приводит к переходу в точку возврата из функции setjmp с возвращаемым значением 2.

Это возвращаемое значение определено аргументом val при вызове функции longjmp. Это приводит к тому, что условие в операторе if не выполняется и исполнение программы продолжается в ветке else. Кроме того вы наверняка заметили, что последнее сообщение не выводится программой, т.е. получается так, что функция longjmp никогда не возвращает управление. Если вы уберете из программы инструкцию exit, то вы получите бесконечный цикл, в котором будет исполняться ветка else и вызов longjmp.

Примеры практического использования.....

Как программистам, вам наверняка уже приходилось писать код, разбитый на функции или подпрограммы. (Я в свое время начинал с программы на языке C, которую написал в виде одной большой функции main, мало-помалу мне удалось разбить ее на несколько функций. Почему? Так проще отлаживать, именно поэтому.). Вложенные вызовы функций в программах могут создавать чрезвычайно сложный порядок исполнения. Всякий раз, когда в приложении возникает ошибка, вам необходимо отыскать функцию, которая ее породила. С помощью пары функций setjmp/longjmp можно значительно упростить процесс отладки таких программ. Рассмотрите пример nest.c

Эта программа не делает ничего особенного, она лишь иллюстрирует изящную обработку ошибок. В примере определено 4 функции, Каждая из этих функций, наряду с целочисленными аргументами, принимает аргумент env. В ней хранится состояние приложения, записанное setjmp, вызываемой в функции main. Наличие ошибочной ситуации определяется условным оператором в функциях. Скомпилируйте и запустите пример. Введите следующий набор значений для переменных l, m, n.

Введите целые значение для переменных l, m и n

1

4

7

Функции отработали без ошибок


Введите целые значение для переменных l, m и n

0

0

0

Ошибка в функции 1..


Введите целые значение для переменных l, m и n

1

1

2

Ошибка в функции 2..


Введите целые значение для переменных l, m и n

0

1

2

Ошибка в функции 3..


Введите целые значение для переменных l, m и n

1

2

3

Ошибка в функции 4..

На мой взгляд, этот пример довольно хорошо иллюстрирует принцип отладки с помощью пары setjmp/longjmp. Текст сообщения точно укажет вам, где произошла ошибка. А теперь рассмотрим код примера поближе. Вызов setjmp в функции main сохранит состояние программы и вернет 0. В результате условие в операторе if не будет соблюдено и таким образом эта ветка не отработает. Затем будет вызвана функция fun1 с аргументами env, l, m, n. fun1 вызовет fun2 и так далее. Если в какой либо из этих функций возникает ошибка, то вызывается longjmp, которой в качестве аргумента val передается номер функции, в которой была обнаружена ошибка. Всякий раз, когда производится вызов longjmp, исполнение программы возвращается в функцию main (в точку, следующую за вызовом setjmp). Но теперь значение переменной s будет 1, 2, 3 или 4, в зависимости от того из какой функции была вызвана longjmp. Теперь условие оператора if становится истинным и на экран выводится сообщение об ошибке, с указанием номера функции, в которой эта ошибка возникла. Если все функции отрабатывают без ошибок, то исполняется последняя строка программы. Почему я не использую обычный оператор goto? Попробуйте скомпилировать этот пример: goto.c. Сообщение об ошибке, выданное компилятором, свидетельствует о том, что goto может использоваться для выполнения только локальных (в пределах одной функции) переходов. Переходы, осуществляемые longjmp в предыдущих примерах, не являются таковыми. Встретив оператор goto, компилятор пытается отыскать локальную метку, точку перехода, в этой же функции и поэтому не может выполнить нелокальный переход.

Ошибки программистов........

С парой setjmp/longjmp связана очень трудно уловимая ошибка, она кроется не в реализации этих функций, а в том как они используются. Большинство из нас при создании своих программ совершенно не думают о стеке. При наличии ошибок, в процессе отладки мы пробуем разобраться со стеком (с помощью отладчика gdb). Всякий раз, когда вызывается функция, состояние стека изменяется. Прежде всего -- на стек помещаются аргументы, передаваемые функции, в обратном порядке. Далее, на стек помещается адрес возврата (pc) и затем регистр fp. fp и sp очищаются, чтобы создать новый фрейм стека для вызываемой функции. Вызываемая функция выделяет на стеке место для локальных переменных. Теперь, когда вы уже кое-что знаете о структуре стека, попробуйте запустить следующий пример seg.c. Он компилируется, но во время исполнения возникает ошибка. Сможете ли вы самостоятельно найти причину?

Попробуем проследить работу программы. Функция main вызывает me_first с двумя аргументами, на стек помещаются аргументы: сначала переменная env, а за ней строка "IC-Labs" (точнее -- указатель на строку. прим. перев.), затем на стек кладется содержимое регистров pc и fp. На стеке выделяется пространство для локальной переменной i. Далее следует вызов функции setjmp, которая сохраняет текущее состояние приложения. В локальную переменную записывается значение 0, которое вернула функция setjmp. После выхода из функции me_first стек возвращается в первоначальное состояние. Дальше вызывается функция i_follow с двумя аргументами -- 3 и env. Стек изменяется практически так же, как я описал выше (когда вызывалась функция me_first). Внутри функции состояние программы, сохраненное в переменной env, восстанавливается вызовом longjmp. Но на стеке остаются значения, которые были записаны при вызове i_follow, хотя теперь управление передается в функцию me_first. Она полагает, что на стеке лежит аргумент с типом (char *), который при первом вызове соответствовал строке "IC-Labs". Теперь же, после восстановления состояния программы, в этой переменной находится число 3 (значение первого аргумента функции i_follow, переданное из main). При исполнении функции printf (следующей за setjmp), возникает ошибка доступа к памяти, поскольку производится попытка вывести строку, которая находится в памяти начиная с адреса 0x3, что и вызывает ошибку несанкционированного доступа к памяти. Такого рода ошибку очень трудно обнаружить и зачастую, когда фрейм стека для функций выглядит практически одинаково, она остается незамеченной. Попробуйте заменить аргумент типа "char *" на аргумент типа int, и перезапустить программу. Будет ли теперь возникать ошибка?


Прим.перев.
Обратите внимание, что под словами "...заменить аргумент типа "char *" на аргумент типа int..." следует понимать не простую замену "char *" на "int " в объявлении функции me_first(), а целый ряд изменений:

  1. Изменить тип входного аргумента с "char*" на "int"
  2. Изменить спецификатор формата для вывода этого аргумента в вызове функции printf с %s на %d
  3. Изменить обращение к функции me_first("IC-Labs",env), заменив строку "IC-Labs" на любое целое число


Обработка сигналов........

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

В функции main объявляется обработчик сигнала, с помощью системного вызова signal. В качестве параметров этому системному вызову передаются signo(SIGALRM) -- сигнал, на который устанавливается обработчик, и собственно функция-обработчик этого сигнала. Вызов alarm посылает программе сигнал SIGALRM с интервалом в одну секунду. Функция longjmps вызывается по истечении 8 секунд.

Я практически закончил курс обучения в Правительственном колледже информационных технологий (College Trichur, Kerala, India). Шлю вам привет из маленького городка Трикур (Trichur), из солнечного штата Керала (Kerala), Индия. Если у вас имеются замечания по поводу стиля и содержания -- милости просим. Не стесняйтесь писать мне по электронной почте.



Copyright (C) 2003, Raghu J Menon. Copying license http://www.linuxgazette.com/copying.html
Published in Issue 90 of Linux Gazette, May 2003

Вернуться на главную страницу