Язык программирования Си
Обработка ошибок
Ошибки разработчика
Неизбежны, но должны быть устранены во время тестирования, потому что являются подлинно некорректными событиями в программе
2
Ошибки разработчика
Неизбежны, но должны быть устранены во время тестирования, потому что являются подлинно некорректными событиями в программе
Проверяются ассёртами, отключаемыми при публикации программы для пользователей
3
Ошибки окружения
Ввод некорректных данных, нехватка памяти, отсутствие файлов, необходимых программе, отключение устройств, обрыв связи, …
4
Ошибки окружения
Ввод некорректных данных, нехватка памяти, отсутствие файлов, необходимых программе, отключение устройств, обрыв связи, … - не являются невозможными событиями!
5
Ошибки окружения
Ввод некорректных данных, нехватка памяти, отсутствие файлов, необходимых программе, отключение устройств, обрыв связи, … - не являются невозможными событиями!
Они всегда возможны, их нельзя устранить разработкой программы, их нужно проверять и обрабатывать - то есть, включать в логику работы программы
6
Ошибки окружения
Ввод некорректных данных, нехватка памяти, отсутствие файлов, необходимых программе, отключение устройств, обрыв связи, … - не являются невозможными событиями!
Они всегда возможны, их нельзя устранить разработкой программы, их нужно проверять и обрабатывать - то есть, включать в логику работы программы
Не могут проверятся ассёртами
7
Терминология
Обнаружение ошибки - проверка некоторого предиката, свидетельствующего о проблеме (например, сравнение результата malloc с NULL)
8
Терминология
Обнаружение ошибки - проверка некоторого предиката, свидетельствующего о проблеме (например, сравнение результата malloc с NULL)
Обработка ошибки - действия, которые предпринимает программа после обнаружения ошибки, для возврата в нормальное состояние
9
Терминология
Обнаружение ошибки - проверка некоторого предиката, свидетельствующего о проблеме (например, сравнение результата malloc с NULL)
Обработка ошибки - действия, которые предпринимает программа после обнаружения ошибки, для возврата в нормальное состояние
Ошибка - будем называть этим термином также информацию, которая определяет, что именно произошло, и которая должна быть доставлена от места обнаружения до места обработки
10
Прерывание программы
Самый простой вид обработки ошибки - применяется тогда, когда невозможно вернуть программу в нормальное состояние
11
Прерывание программы
Самый простой вид обработки ошибки - применяется тогда, когда невозможно вернуть программу в нормальное состояние
Как правило осуществляется непосредственно в месте обнаружения
12
Прерывание программы
Самый простой вид обработки ошибки - применяется тогда, когда невозможно вернуть программу в нормальное состояние
Как правило осуществляется непосредственно в месте обнаружения
Даже в таком случае иногда бывает необходимо сделать какие-то нетривиальные действия - закрыть файлы, отправить сообщение по сети, совершить диагностику, …
13
Проблема прерывания программы
14
main
Стек вызовов
Проблема прерывания программы
15
main
start_net_protocol
Стек вызовов
Здесь открыли общение по сети, которое нужно закрыть перед выходом
Проблема прерывания программы
16
main
start_net_protocol
foo
with_log
Стек вызовов
Здесь открыли общение по сети, которое нужно закрыть перед выходом
Здесь открыли файл, который нужно закрыть перед выходом
Проблема прерывания программы
17
main
start_net_protocol
foo
with_log
bar
baz
malloc
Стек вызовов
Здесь открыли общение по сети, которое нужно закрыть перед выходом
Памяти не хватило
Здесь открыли файл, который нужно закрыть перед выходом
Проблема прерывания программы
18
main
start_net_protocol
foo
with_log
bar
baz
Стек вызовов
Здесь открыли общение по сети, которое нужно закрыть перед выходом
Обнаружили ошибку
Здесь открыли файл, который нужно закрыть перед выходом
Проблема прерывания программы
19
main
start_net_protocol
foo
with_log
bar
baz
Стек вызовов
Здесь открыли общение по сети, которое нужно закрыть перед выходом
Обнаружили ошибку
Здесь открыли файл, который нужно закрыть перед выходом
Завершающие действия
Чтобы отпустить ресурс (закрыть файл, попрощаться в сетевом протоколе и т.п.) нужно вернуться по стеку вызовов от места обнаружения ошибки к месту, где началась работа с этим ресурсом
20
Завершающие действия
Чтобы отпустить ресурс (закрыть файл, попрощаться в сетевом протоколе и т.п.) нужно вернуться по стеку вызовов от места обнаружения ошибки к месту, где началась работа с этим ресурсом
Иногда можно решить эту проблему регистрацией atexit коллбэков - действий, которые произойдут при завершении программы
Мы будем это делать в одной из лабораторных работ
21
Завершающие действия
Чтобы отпустить ресурс (закрыть файл, попрощаться в сетевом протоколе и т.п.) нужно вернуться по стеку вызовов от места обнаружения ошибки к месту, где началась работа с этим ресурсом
Иногда можно решить эту проблему регистрацией atexit коллбэков - действий, которые произойдут при завершении программы
Мы будем это делать в одной из лабораторных работ
Это не всегда удобно, и совершенно точно не подходит для обработки ошибок без прерывания программы
22
Обработка без прерывания
Очень часто ошибки обрабатываются не прерыванием программы, а некоторым влиянием на окружение, ставшее источником ошибки, и переповтором действий
23
Обработка без прерывания
Очень часто ошибки обрабатываются не прерыванием программы, а некоторым влиянием на окружение, ставшее источником ошибки, и переповтором действий
Любое GUI приложение, открывающее файлы, не упадёт, если вы введёте несуществующее имя файла, а сообщит вам об этом и предложит повторить попытку
24
Обработка без прерывания
Очень часто ошибки обрабатываются не прерыванием программы, а некоторым влиянием на окружение, ставшее источником ошибки, и переповтором действий
Любое GUI приложение, открывающее файлы, не упадёт, если вы введёте несуществующее имя файла, а сообщит вам об этом и предложит повторить попытку
Многие программы хранят в памяти кэши - данные, которые можно всегда перепрочитать с диска или по сети, или перевычислить. В случае нехватки памяти, программа может сбросить кэши и переповторить выделение памяти
25
Обработка без прерывания
Очень часто ошибки обрабатываются не прерыванием программы, а некоторым влиянием на окружение, ставшее источником ошибки, и переповтором действий
Любое GUI приложение, открывающее файлы, не упадёт, если вы введёте несуществующее имя файла, а сообщит вам об этом и предложит повторить попытку
Многие программы хранят в памяти кэши - данные, которые можно всегда перепрочитать с диска или по сети, или перевычислить. В случае нехватки памяти, программа может сбросить кэши и переповторить выделение памяти
И так далее - примеров намного больше, чем обработок с прерываниями
26
Обработка без прерывания
Очень часто ошибки обрабатываются не прерыванием программы, а некоторым влиянием на окружение, ставшее источником ошибки, и переповтором действий
Любое GUI приложение, открывающее файлы, не упадёт, если вы введёте несуществующее имя файла, а сообщит вам об этом и предложит повторить попытку
Многие программы хранят в памяти кэши - данные, которые можно всегда перепрочитать с диска или по сети, или перевычислить. В случае нехватки памяти, программа может сбросить кэши и переповторить выделение памяти
И так далее - примеров намного больше, чем обработок с прерываниями
В таком случае возникает задача перейти от места обнаружения ошибки к месту, имеющему право обработать ошибку, и перенести туда информацию об ошибке
27
Право обработать ошибку
Рассмотрим функцию int str_to_int(char* str), конвертирующую строки в числа; в ходе её работы могут произойти как минимум два вида ошибок:
28
Право обработать ошибку
Рассмотрим функцию int str_to_int(char* str), конвертирующую строки в числа; в ходе её работы могут произойти как минимум два вида ошибок:
Имеет ли право функция str_to_int сама обработать ошибку?
29
Право обработать ошибку
Рассмотрим функцию int str_to_int(char* str), конвертирующую строки в числа; в ходе её работы могут произойти как минимум два вида ошибок:
Имеет ли право функция str_to_int сама обработать ошибку? Нет!
30
Право обработать ошибку
Рассмотрим функцию int str_to_int(char* str), конвертирующую строки в числа; в ходе её работы могут произойти как минимум два вида ошибок:
Имеет ли право функция str_to_int сама обработать ошибку? Нет!
Например, вы вызвали её не для того, чтобы получить результат конверсии, а только для того, чтобы узнать, является ли строка числом
31
Утилитарные/библиотечные функции
Неформальная категория функций, которые делают какую-то полезную работу, не зная, в каком контексте вызваны - это позволяет их переиспользовать во множестве контекстов
32
Утилитарные/библиотечные функции
Неформальная категория функций, которые делают какую-то полезную работу, не зная, в каком контексте вызваны - это позволяет их переиспользовать во множестве контекстов
В идеале такие функции должны быть чистыми (без побочных эффектов), либо со строго специфицированными побочными эффектами (как например, утилитарные функции IO)
33
Утилитарные/библиотечные функции
Неформальная категория функций, которые делают какую-то полезную работу, не зная, в каком контексте вызваны - это позволяет их переиспользовать во множестве контекстов
В идеале такие функции должны быть чистыми (без побочных эффектов), либо со строго специфицированными побочными эффектами (как например, утилитарные функции IO)
Общаться с вашим пользователем такие функции точно не имеют никакого права, а все возможные обнаруженные ими ошибки должны стать некоторой формой возвращаемого значения
Мы уже встречались с подобным подходом - функция scanf возвращает EOF, если не сумела ничего прочитать (конец файла)
34
Как это сделать?
Если во множестве значений возвращаемого типа есть некорректные значения, как NULL для функции malloc, можно использовать их, как признак ошибки
35
Как это сделать?
Если во множестве значений возвращаемого типа есть некорректные значения, как NULL для функции malloc, можно использовать их, как признак ошибки
36
int str_to_int(char* str) {
...
if (!isdigit(c)) {
}
if (overflow) {
}
...
}
Как это сделать?
Если во множестве значений возвращаемого типа есть некорректные значения, как NULL для функции malloc, можно использовать их, как признак ошибки
37
int str_to_int(char* str) {
...
if (!isdigit(c)) {
// return error: "non-digit symbol"
}
if (overflow) {
// return error: "overflow"
}
...
}
Как это сделать?
Если во множестве значений возвращаемого типа есть некорректные значения, как NULL для функции malloc, можно использовать их, как признак ошибки
38
int str_to_int(char* str) {
...
if (!isdigit(c)) {
// return error: "non-digit symbol"
}
if (overflow) {
// return error: "overflow"
}
...
}
В типе int нет никаких значений, которые были бы некорректными для функции str_to_int!
Коды ошибок
Мы уже изучали, как вернуть из функции несколько значений (дополнительные параметры-указатели); в задаче обработки ошибок можно применить ту же технику
39
Коды ошибок
Мы уже изучали, как вернуть из функции несколько значений (дополнительные параметры-указатели); в задаче обработки ошибок можно применить ту же технику
Через дополнительные параметры-указатели можно вернуть из функции любую информацию об ошибке
40
Коды ошибок
Мы уже изучали, как вернуть из функции несколько значений (дополнительные параметры-указатели); в задаче обработки ошибок можно применить ту же технику
Через дополнительные параметры-указатели можно вернуть из функции любую информацию об ошибке
Как правило ограничиваются одним целым числом, которое называют кодом ошибки
41
Коды ошибок
42
int str_to_int(char* str) {
...
if (!isdigit(c)) {
// return error: "non-digit symbol"
}
if (overflow) {
// return error: "overflow"
}
...
}
Коды ошибок
43
int str_to_int(char* str, int* err_code) {
...
if (!isdigit(c)) {
// return error: "non-digit symbol"
}
if (overflow) {
// return error: "overflow"
}
...
}
Коды ошибок
44
int str_to_int(char* str, int* err_code) {
...
if (!isdigit(c)) {
*err_code = 1; // non-digit symbol
// return
}
if (overflow) {
*err_code = 2; // overflow
// return
}
...
}
Коды ошибок
45
int str_to_int(char* str, int* err_code) {
...
if (!isdigit(c)) {
*err_code = 1; // non-digit symbol
return 0;
}
if (overflow) {
*err_code = 2; // overflow
return 0;
}
...
}
Коды ошибок
46
int str_to_int(char* str, int* err_code) {
...
if (!isdigit(c)) {
*err_code = 1; // non-digit symbol
return 0;
}
if (overflow) {
*err_code = 2; // overflow
return 0;
}
...
}
int err_code = 0;
int x = str_to_int(str, &err_code);
if (err_code != 0) {
... // обработка ошибки
}
Нагрузка на прототип функции
Коды ошибок засоряют прототип функции, что усложняет вызов и ухудшает читаемость кода
47
Нагрузка на прототип функции
Коды ошибок засоряют прототип функции, что усложняет вызов и ухудшает читаемость кода
Попытка передать ещё какую-то информацию об ошибке, кроме кода, сделает ситуацию ещё хуже
48
Нагрузка на прототип функции
Коды ошибок засоряют прототип функции, что усложняет вызов и ухудшает читаемость кода
Попытка передать ещё какую-то информацию об ошибке, кроме кода, сделает ситуацию ещё хуже
Особенно плохо это для тех вызовов, для которых вы точно знаете, что ошибки быть не может - утилитарная функция одна, а вызываете вы её из разных контекстов!
49
Нагрузка на прототип функции
50
int err_code = 0;
int x = str_to_int(str, &err_code);
assert(err_code == 0);
Нагрузка на прототип функции
51
int err_code = 0;
int x = str_to_int(str, &err_code);
assert(err_code == 0);
Вы уверены, что str состоит только из цифр, и переполнения нет
Нагрузка на прототип функции
52
int err_code = 0;
int x = str_to_int(str, &err_code);
assert(err_code == 0);
Вы уверены, что str состоит только из цифр, и переполнения нет
Вы подкрепляете эту уверенность assert’ом
Нагрузка на прототип функции
53
int err_code = 0;
int x = str_to_int(str, &err_code);
assert(err_code == 0);
Вы уверены, что str состоит только из цифр, и переполнения нет
Вы подкрепляете эту уверенность assert’ом
Но это всё равно мусорный код!
Нагрузка на прототип функции
54
int err_code = 0;
int x = str_to_int(str, &err_code);
assert(err_code == 0);
Вы уверены, что str состоит только из цифр, и переполнения нет
Вы подкрепляете эту уверенность assert’ом
Но это всё равно мусорный код!
int x = str_to_int(str, NULL);
Нагрузка на прототип функции
55
int err_code = 0;
int x = str_to_int(str, &err_code);
assert(err_code == 0);
Вы уверены, что str состоит только из цифр, и переполнения нет
Вы подкрепляете эту уверенность assert’ом
Но это всё равно мусорный код!
int x = str_to_int(str, NULL);
Очень сомнительное решение - заменили assert на UB
Прокси-функции
Прокси-функции (функции-обёртки) - функции, реализующие свою задачу не самостоятельно, а вызывающие основную функцию, совершая при этом какие-то вторичные действия, связанные с контекстом вызова
56
Прокси-функции
Прокси-функции (функции-обёртки) - функции, реализующие свою задачу не самостоятельно, а вызывающие основную функцию, совершая при этом какие-то вторичные действия, связанные с контекстом вызова
Самый распространённый пример - прокси-функции, определяющие популярные значения аргументов по умолчанию (default arguments)
57
Прокси-функции
58
int str_to_int(char* str, int* err_code);
Прокси-функции
59
int str_to_int(char* str, int* err_code);
int str_to_int_radix(char* str, unsigned char radix, int* err_code);
Функция более общего вида
Прокси-функции
60
int str_to_int(char* str, int* err_code);
int str_to_int_radix(char* str, unsigned char radix, int* err_code);
Частная функция (radix == 10), но чаще востребованная (вероятно)
Функция более общего вида
Прокси-функции
61
int str_to_int(char* str, int* err_code) {
return str_to_int_radix(str, 10, err_code);
}
int str_to_int_radix(char* str, unsigned char radix, int* err_code);
Функция более общего вида
Прокси-функция!
Прокси-функции
62
int str_to_int(char* str, int* err_code) {
return str_to_int_radix(str, 10, err_code);
}
int str_to_int_radix(char* str, unsigned char radix, int* err_code);
Функция более общего вида
Прокси-функция!
Определяющая аргумент по умолчанию
Прокси-функции
Прокси-функции (функции-обёртки) - функции, реализующие свою задачу не самостоятельно, а вызывающие основную функцию, совершая при этом какие-то вторичные действия, связанные с контекстом вызова
Самый распространённый пример - прокси-функции, определяющие популярные значения аргументов по умолчанию (default arguments)
63
Прокси-функции
Прокси-функции (функции-обёртки) - функции, реализующие свою задачу не самостоятельно, а вызывающие основную функцию, совершая при этом какие-то вторичные действия, связанные с контекстом вызова
Самый распространённый пример - прокси-функции, определяющие популярные значения аргументов по умолчанию (default arguments)
Прокси-функции можно использовать для преобразования ошибки в ассёрт
64
Прокси-функции
65
int err_code = 0;
int x = str_to_int(str, &err_code);
assert(err_code == 0);
Вы уверены, что str состоит только из цифр, и переполнения нет
Прокси-функции
66
int x = trusted_str_to_int(str);
Вы уверены, что str состоит только из цифр, и переполнения нет
Прокси-функции
67
Проверяем, без всякого UB
int x = trusted_str_to_int(str);
int trusted_str_to_int(char* str) {
int err_code = 0;
int result = str_to_int(str, &err_code);
assert(err_code == 0);
return result;
}
Вы уверены, что str состоит только из цифр, и переполнения нет
Нагрузка на прототип
Уменьшить нагрузку на прототип можно ещё больше, передавая и принимая код ошибки через глобальные переменные
68
Нагрузка на прототип
Уменьшить нагрузку на прототип можно ещё больше, передавая и принимая код ошибки через глобальные переменные
Существует специальные глобальные переменные для этой цели, которые использует стандартная библиотека, и которые рекомендуется использовать самим при написании кода
69
errno
Квази-переменная (некоторая lvalue-сущность) типа int, объявленная в файле errno.h
70
errno
Квази-переменная (некоторая lvalue-сущность) типа int, объявленная в файле errno.h
Используется функциями стандартной библиотеки Си и POSIX-функциями в качестве места для записи кода ошибки
Нужно занулить errno перед вызовом опасной функции и проверить его после вызова
71
errno
Квази-переменная (некоторая lvalue-сущность) типа int, объявленная в файле errno.h
Используется функциями стандартной библиотеки Си и POSIX-функциями в качестве места для записи кода ошибки
Нужно занулить errno перед вызовом опасной функции и проверить его после вызова
Стандарт POSIX дополнительно определяет, что errno - потокобезопасная, то есть, на каждый поток исполнения присутствует своя копия errno
72
Проблема промежуточных функций
73
Пусть функция foo, имеющая право обрабатывать ошибки, вызывает две библиотечные функции bar и baz
foo()
bar()
baz()
Проблема промежуточных функций
74
foo()
bar()
baz()
ququ()
bebe()
pepe()
ouch()
Пусть функция foo, имеющая право обрабатывать ошибки, вызывает две библиотечные функции bar и baz
Которые также вызывают по две функции, которые могут обнаружить ошибки
Проблема промежуточных функций
75
foo()
bar()
baz()
ququ()
bebe()
pepe()
ouch()
Пусть функция foo, имеющая право обрабатывать ошибки, вызывает две библиотечные функции bar и baz
Которые также вызывают по две функции, которые могут обнаружить ошибки
Если в этой функции будет обнаружена ошибка
Проблема промежуточных функций
76
foo()
bar()
baz()
ququ()
bebe()
pepe()
ouch()
Пусть функция foo, имеющая право обрабатывать ошибки, вызывает две библиотечные функции bar и baz
Которые также вызывают по две функции, которые могут обнаружить ошибки
Если в этой функции будет обнаружена ошибка
Продолжать исполнение вот этой будет нельзя!
Проблема промежуточных функций
Все функции в стеке вызовов между точкой обнаружения ошибки и точкой обработки становятся вынуждены принимать участие в передаче
77
Проблема промежуточных функций
Все функции в стеке вызовов между точкой обнаружения ошибки и точкой обработки становятся вынуждены принимать участие в передаче
Тривиальное участие (пронос ошибки, перевыброс) - проверка кода ошибки и выход из функции
78
Проблема промежуточных функций
Все функции в стеке вызовов между точкой обнаружения ошибки и точкой обработки становятся вынуждены принимать участие в передаче
Тривиальное участие (пронос ошибки, перевыброс) - проверка кода ошибки и выход из функции
Логика программы сильно загрязняется постоянными однотипными проверками (подавляющее большинство функций не имеют права обрабатывать ошибки)
79
Суть проблемы
80
// опасный код начинается
...
foo()
...
// опасный код закончился
if (was_error) {
// обработка ошибки
}
Суть проблемы
81
// опасный код начинается
...
foo() // -> bar() -> baz() -> ...
...
// опасный код закончился
if (was_error) {
// обработка ошибки
}
Суть проблемы
82
// опасный код начинается
...
foo() // -> bar() -> baz() -> ...
...
// опасный код закончился
if (was_error) {
// обработка ошибки
}
Суть проблемы
83
// опасный код начинается
...
foo() // -> bar() -> baz() -> ...
...
// опасный код закончился
if (was_error) {
// обработка ошибки
}
Суть проблемы
84
// опасный код начинается
...
foo() // -> bar() -> baz() -> ...
...
// опасный код закончился
if (was_error) {
// обработка ошибки
}
goto!
Суть проблемы
85
// опасный код начинается
...
foo() // -> bar() -> baz() -> ...
...
// опасный код закончился
if (was_error) {
// обработка ошибки
}
goto!
Ну, почти
Суть проблемы
86
// опасный код начинается
...
foo() // -> bar() -> baz() -> ...
...
// опасный код закончился
if (was_error) {
// обработка ошибки
}
goto!
Ну, почти
x = 42
M + 4
Фрейм main
Фрейм bar
Фрейм foo
main
Фрейм baz
x = 42
Фрейм
Суть проблемы
goto может перейти только внутри функции, а нам нужен прыжок из глубины стека обратно через несколько фреймов (их нужно удалить со стека)
87
Суть проблемы
88
// опасный код начинается
...
foo() // -> bar() -> baz() -> ...
...
// опасный код закончился
if (was_error) {
// обработка ошибки
}
x = 42
M + 4
Фрейм main
Фрейм bar
Фрейм foo
main
Фрейм baz
x = 42
Фрейм
Суть проблемы
89
// опасный код начинается
...
foo() // -> bar() -> baz() -> ...
...
// опасный код закончился
if (was_error) {
// обработка ошибки
}
x = 42
Фрейм main
Фрейм bar
Фрейм foo
main
Фрейм baz
x = 42
Фрейм
setjmp.h
Библиотека для реализации нелокальных переходов - перемещений между функциями
90
setjmp.h
Библиотека для реализации нелокальных переходов - перемещений между функциями
Состоит из трёх элементов:
91
setjmp.h
Библиотека для реализации нелокальных переходов - перемещений между функциями
Состоит из трёх элементов:
92
setjmp.h
Библиотека для реализации нелокальных переходов - перемещений между функциями
Состоит из трёх элементов:
93
Как это выглядит
94
jmp_buf env;
if (!setjmp(env)) {
// опасный код
...
foo(env) // -> bar() -> baz() -> ...
...
} else {
// обработка ошибки
}
Как это выглядит
95
jmp_buf env;
if (!setjmp(env)) {
// опасный код
...
foo(env) // -> bar() -> baz() -> ...
...
} else {
// обработка ошибки
}
Переменная, в которой может быть сохранён контекст исполнения
Как это выглядит
96
jmp_buf env;
if (!setjmp(env)) {
// опасный код
...
foo(env) // -> bar() -> baz() -> ...
...
} else {
// обработка ошибки
}
Переменная, в которой может быть сохранён контекст исполнения
Сохранение контекста и одновременное обозначение точки, в которую надо будет, если что, вернуться
int setjmp(jmp_buf env)
Ключевой элемент нелокальных переходов - портал для перемещения в опасный код и из места обнаружения ошибки в код обработки
97
int setjmp(jmp_buf env)
Ключевой элемент нелокальных переходов - портал для перемещения в опасный код и из места обнаружения ошибки в код обработки
Принимает jmp_buf и заполняет его контекстом точки вызова себя
98
int setjmp(jmp_buf env)
Ключевой элемент нелокальных переходов - портал для перемещения в опасный код и из места обнаружения ошибки в код обработки
Принимает jmp_buf и заполняет его контекстом точки вызова себя
Возвращает 0, если была вызвана (sic!), и тогда мы переходим в опасный код
99
int setjmp(jmp_buf env)
Ключевой элемент нелокальных переходов - портал для перемещения в опасный код и из места обнаружения ошибки в код обработки
Принимает jmp_buf и заполняет его контекстом точки вызова себя
Возвращает 0, если была вызвана (sic!), и тогда мы переходим в опасный код
Возвращает не 0, если не была вызвана (sic!), и тогда мы переходим в код обработки ошибки
100
int setjmp(jmp_buf env)
101
Как это выглядит
102
jmp_buf env;
if (!setjmp(env)) {
// опасный код
...
foo(env) // -> bar() -> baz() -> ...
...
} else {
// обработка ошибки
}
Переменная, в которой может быть сохранён контекст исполнения
Сохранение контекста и одновременное обозначение точки, в которую надо будет, если что, вернуться
env - это ключ от портала, и мы должны передавать его опасному коду, чтобы тот мог вернуться
Как это выглядит
103
jmp_buf env;
if (!setjmp(env)) {
// опасный код
...
foo(env) // -> bar() -> baz() -> ...
...
} else {
// обработка ошибки
}
Переменная, в которой может быть сохранён контекст исполнения
Сохранение контекста и одновременное обозначение точки, в которую надо будет, если что, вернуться
env - это ключ от портала, и мы должны передавать его опасному коду, чтобы тот мог вернуться
Как это выглядит
104
jmp_buf env;
if (!setjmp(env)) {
// опасный код
...
foo(env) // -> bar() -> baz() -> ...
...
} else {
// обработка ошибки
}
Переменная, в которой может быть сохранён контекст исполнения
Сохранение контекста и одновременное обозначение точки, в которую надо будет, если что, вернуться
env - это ключ от портала, и мы должны передавать его опасному коду, чтобы тот мог вернуться
longjmp(env, 37);
void longjmp(jmp_buf env, int val)
Принимает jmp_buf, в точку заполнения которого надо вернуться, и val, который нужно как бы вернуть из функции setjmp, которая заполняла jmp_buf
105
void longjmp(jmp_buf env, int val)
Принимает jmp_buf, в точку заполнения которого надо вернуться, и val, который нужно как бы вернуть из функции setjmp, которая заполняла jmp_buf
val выполняет роль кода ошибки; если вызвать longjmp со значением 0, она заменит его на 1
106
Как это выглядит
107
jmp_buf env;
if (!setjmp(env)) {
// опасный код
...
foo(env) // -> bar() -> baz() -> ...
...
} else {
// обработка ошибки
}
longjmp(env, 37);
Как это выглядит
108
jmp_buf env;
if (!setjmp(env)) {
// опасный код
...
foo(env) // -> bar() -> baz() -> ...
...
} else {
// обработка ошибки
}
longjmp(env, 37);
Как это выглядит
109
jmp_buf env;
if (!setjmp(env)) {
// опасный код
...
foo(env) // -> bar() -> baz() -> ...
...
} else {
// обработка ошибки
}
longjmp(env, 37);
Как это выглядит
110
jmp_buf env;
if (!setjmp(env)) {
// опасный код
...
foo(env) // -> bar() -> baz() -> ...
...
} else {
// обработка ошибки
}
longjmp(env, 37);
Значения объектов
При прыжке из longjmp в точку вызова setjmp значения практически всех объектов в программе не изменяются
111
Значения объектов
При прыжке из longjmp в точку вызова setjmp значения практически всех объектов в программе не изменяются
Единственное исключение: значения локальных переменных функции, в которой была позвана setjmp, и которые были изменены между вызовами setjmp и longjmp, становятся неопределёнными
112
Что это означает
Хотя стандарт языка не определяет этого, нелокальный переход реализован достаточно однозначно:
113
Что это означает
Хотя стандарт языка не определяет этого, нелокальный переход реализован достаточно однозначно:
114
Что это означает
Хотя стандарт языка не определяет этого, нелокальный переход реализован достаточно однозначно:
115
Что это означает
Хотя стандарт языка не определяет этого, нелокальный переход реализован достаточно однозначно:
116
Значения объектов
Из реализации понятно, что никакого отката в прошлое не происходит - все объекты, в которые было что-то записано на пути от setjmp до longjmp, сохраняют свои значения
117
Значения объектов
Из реализации понятно, что никакого отката в прошлое не происходит - все объекты, в которые было что-то записано на пути от setjmp до longjmp, сохраняют свои значения
Но локальные переменные функции, в которой вызывался setjmp, могли находиться на регистрах!
118
Размещение переменных
119
int x;
int y;
setjmp(env);
use(x);
use(y);
Размещение переменных
120
int x;
int y;
setjmp(env);
use(x);
use(y);
Предположим, что переменная x лежит на стеке
Размещение переменных
121
int x;
int y;
setjmp(env);
use(x);
use(y);
Предположим, что переменная x лежит на стеке
А переменная y на регистре R
Размещение переменных
122
int x;
int y;
setjmp(env);
use(x);
use(y);
Предположим, что переменная x лежит на стеке
А переменная y на регистре R
Это чтение памяти из стека
Размещение переменных
123
int x;
int y;
setjmp(env);
use(x);
use(y);
Предположим, что переменная x лежит на стеке
А переменная y на регистре R
Это чтение памяти из стека
А это чтение регистра R
Размещение переменных
124
int x;
int y;
setjmp(env);
use(x);
use(y);
Предположим, что переменная x лежит на стеке
А переменная y на регистре R
Это чтение памяти из стека
А это чтение регистра R
Регистр R запишется в env и будет восстановлен при вызове longjmp
Размещение переменных
125
int x;
int y;
setjmp(env);
use(x);
use(y);
Предположим, что переменная x лежит на стеке
А переменная y на регистре R
Это чтение памяти из стека
А это чтение регистра R
Регистр R запишется в env и будет восстановлен при вызове longjmp
Стековые слоты никто никуда не записывал, их могли изменить между setjmp и longjmp
Размещение переменных
126
int x;
int y;
setjmp(env);
use(x);
use(y);
Предположим, что переменная x лежит на стеке
А переменная y на регистре R
Это чтение памяти из стека
Прочитается то значение, которое было при вызове longjmp
А это чтение регистра R
Регистр R запишется в env и будет восстановлен при вызове longjmp
Стековые слоты никто никуда не записывал, их могли изменить между setjmp и longjmp
Размещение переменных
127
int x;
int y;
setjmp(env);
use(x);
use(y);
Предположим, что переменная x лежит на стеке
А переменная y на регистре R
Это чтение памяти из стека
Прочитается то значение, которое было при вызове longjmp
А это чтение регистра R
Прочитается то значение, которое было до вызова setjmp
Регистр R запишется в env и будет восстановлен при вызове longjmp
Стековые слоты никто никуда не записывал, их могли изменить между setjmp и longjmp
Размещение переменных
Зависит от уровня оптимизаций компилятора, генерации отладочной информации и т.д.
128
Размещение переменных
Зависит от уровня оптимизаций компилятора, генерации отладочной информации и т.д.
В целом компилятор старается использовать регистры, на стек переменные складываются в двух случаях: если не хватает места на регистрах, либо если вы взяли у переменной адрес
129
Размещение переменных
Зависит от уровня оптимизаций компилятора, генерации отладочной информации и т.д.
В целом компилятор старается использовать регистры, на стек переменные складываются в двух случаях: если не хватает места на регистрах, либо если вы взяли у переменной адрес
Адрес берётся как раз для того, чтобы передать его куда-то, чтобы там по адресу что-то записали
Есть ещё причины (например, массивы просто невозможно не адресовать)
130
TL;DR
Если вы не брали адрес своей переменной и не меняли её после вызова setjmp, то после вызова longjmp в ней будет то же самое значение, что и было до вызова setjmp
Это гарантируется стандартом и действительно так на практике
131
TL;DR
Если вы не брали адрес своей переменной и не меняли её после вызова setjmp, то после вызова longjmp в ней будет то же самое значение, что и было до вызова setjmp
Это гарантируется стандартом и действительно так на практике
Если вы брали адрес переменной и куда-то его передавали, и там по нему что-то записали, то согласно стандарту в ней находится неопределённое значение, но на практике будет то, что записали
Очень зыбкая почва!
132
TL;DR
Если вы не брали адрес своей переменной и не меняли её после вызова setjmp, то после вызова longjmp в ней будет то же самое значение, что и было до вызова setjmp
Это гарантируется стандартом и действительно так на практике
Если вы брали адрес переменной и куда-то его передавали, и там по нему что-то записали, то согласно стандарту в ней находится неопределённое значение, но на практике будет то, что записали
Очень зыбкая почва!
Если вы не брали адрес, но изменяли значение, то оно будет неопределённым
Это соответствует стандарту и действительно так на практике
133
TL;DR от TL;DR
Поменьше локальных переменных рядом с setjmp, и не берите от них адреса
134
Проблема промежуточных функций
Пусть в функции есть завершающие действия (закрытие файлов, сетевой протокол, …) или, что встречается намного чаще, удаление динамической памяти
135
Проблема промежуточных функций
Пусть в функции есть завершающие действия (закрытие файлов, сетевой протокол, …) или, что встречается намного чаще, удаление динамической памяти
В таком случае нельзя просто так пролетать мимо неё на setjmp-longjmp механизме, приходится вставлять перевыбрасывающие обработчики ошибок
136
Проблема промежуточных функций
Пусть в функции есть завершающие действия (закрытие файлов, сетевой протокол, …) или, что встречается намного чаще, удаление динамической памяти
В таком случае нельзя просто так пролетать мимо неё на setjmp-longjmp механизме, приходится вставлять перевыбрасывающие обработчики ошибок
Проблема слабее, чем была с кодами ошибок, но остаётся
137
Проблема промежуточных функций
138
void foo(jmp_buf env) {
int* arr = nc_malloc(42 * sizeof(int));
...
bar(env);
...
free(arr);
}
Проблема промежуточных функций
139
void foo(jmp_buf env) {
int* arr = nc_malloc(42 * sizeof(int));
...
bar(env);
...
free(arr);
}
Нам передали внешний контекст
Проблема промежуточных функций
140
void foo(jmp_buf env) {
int* arr = nc_malloc(42 * sizeof(int));
...
bar(env);
...
free(arr);
}
Нам передали внешний контекст
Мы передаём его дальше по стеку
Проблема промежуточных функций
141
void foo(jmp_buf env) {
int* arr = nc_malloc(42 * sizeof(int));
...
bar(env);
...
free(arr);
}
Нам передали внешний контекст
Мы передаём его дальше по стеку
Если функция bar (или кто-то, кому она передаст этот контекст дальше) воспользуется им, фрейм foo пропадёт со стека, код не будет завершён
Проблема промежуточных функций
142
void foo(jmp_buf env) {
int* arr = nc_malloc(42 * sizeof(int));
...
bar(env);
...
free(arr);
}
Нам передали внешний контекст
Мы передаём его дальше по стеку
Если функция bar (или кто-то, кому она передаст этот контекст дальше) воспользуется им, фрейм foo пропадёт со стека, код не будет завершён
Потенциальная утечка памяти
Проблема промежуточных функций
143
void foo(jmp_buf env) {
int* arr = nc_malloc(42 * sizeof(int));
jmp_buf rethrow_env;
int err_code;
if (err_code = setjmp(rethrow_env)) {
...
bar(rethrow_env);
...
} else {
free(arr);
longjmp(env, err_code);
}
}
Проблема промежуточных функций
144
void foo(jmp_buf env) {
int* arr = nc_malloc(42 * sizeof(int));
jmp_buf rethrow_env;
int err_code;
if (err_code = setjmp(rethrow_env)) {
...
bar(rethrow_env);
...
} else {
free(arr);
longjmp(env, err_code);
}
}
setjmp & longjmp
Из-за общей запутанности, проблемы значений объектов и нерешённой проблемы промежуточных функций нелокальные переходы очень редко используются для обработки ошибок
145
setjmp & longjmp
Из-за общей запутанности, проблемы значений объектов и нерешённой проблемы промежуточных функций нелокальные переходы очень редко используются для обработки ошибок
В других языках программирования есть механизм исключений, который является доработанной идеей нелокального перехода; проблему промежуточных функций при этом решают с помощью разных инструментов (RAII, try-with-resources, …)
146
setjmp & longjmp
Из-за общей запутанности, проблемы значений объектов и нерешённой проблемы промежуточных функций нелокальные переходы очень редко используются для обработки ошибок
В других языках программирования есть механизм исключений, который является доработанной идеей нелокального перехода; проблему промежуточных функций при этом решают с помощью разных инструментов (RAII, try-with-resources, …)
Нелокальный переход также можно использовать для реализации корутин (кооперативной многозадачности) - это будет одна из последних ваших лабораторных работ
147