1 of 147

Язык программирования Си

Владимир Валерьевич Соловьёв

Huawei, НГУ

vladimir.conwor@gmail.com

t.me/conwor

vk.com/conwor

Обработка ошибок

2 of 147

Ошибки разработчика

Неизбежны, но должны быть устранены во время тестирования, потому что являются подлинно некорректными событиями в программе

2

3 of 147

Ошибки разработчика

Неизбежны, но должны быть устранены во время тестирования, потому что являются подлинно некорректными событиями в программе

Проверяются ассёртами, отключаемыми при публикации программы для пользователей

3

4 of 147

Ошибки окружения

Ввод некорректных данных, нехватка памяти, отсутствие файлов, необходимых программе, отключение устройств, обрыв связи, …

4

5 of 147

Ошибки окружения

Ввод некорректных данных, нехватка памяти, отсутствие файлов, необходимых программе, отключение устройств, обрыв связи, … - не являются невозможными событиями!

5

6 of 147

Ошибки окружения

Ввод некорректных данных, нехватка памяти, отсутствие файлов, необходимых программе, отключение устройств, обрыв связи, … - не являются невозможными событиями!

Они всегда возможны, их нельзя устранить разработкой программы, их нужно проверять и обрабатывать - то есть, включать в логику работы программы

6

7 of 147

Ошибки окружения

Ввод некорректных данных, нехватка памяти, отсутствие файлов, необходимых программе, отключение устройств, обрыв связи, … - не являются невозможными событиями!

Они всегда возможны, их нельзя устранить разработкой программы, их нужно проверять и обрабатывать - то есть, включать в логику работы программы

Не могут проверятся ассёртами

7

8 of 147

Терминология

Обнаружение ошибки - проверка некоторого предиката, свидетельствующего о проблеме (например, сравнение результата malloc с NULL)

8

9 of 147

Терминология

Обнаружение ошибки - проверка некоторого предиката, свидетельствующего о проблеме (например, сравнение результата malloc с NULL)

Обработка ошибки - действия, которые предпринимает программа после обнаружения ошибки, для возврата в нормальное состояние

9

10 of 147

Терминология

Обнаружение ошибки - проверка некоторого предиката, свидетельствующего о проблеме (например, сравнение результата malloc с NULL)

Обработка ошибки - действия, которые предпринимает программа после обнаружения ошибки, для возврата в нормальное состояние

Ошибка - будем называть этим термином также информацию, которая определяет, что именно произошло, и которая должна быть доставлена от места обнаружения до места обработки

10

11 of 147

Прерывание программы

Самый простой вид обработки ошибки - применяется тогда, когда невозможно вернуть программу в нормальное состояние

11

12 of 147

Прерывание программы

Самый простой вид обработки ошибки - применяется тогда, когда невозможно вернуть программу в нормальное состояние

Как правило осуществляется непосредственно в месте обнаружения

12

13 of 147

Прерывание программы

Самый простой вид обработки ошибки - применяется тогда, когда невозможно вернуть программу в нормальное состояние

Как правило осуществляется непосредственно в месте обнаружения

Даже в таком случае иногда бывает необходимо сделать какие-то нетривиальные действия - закрыть файлы, отправить сообщение по сети, совершить диагностику, …

13

14 of 147

Проблема прерывания программы

14

main

Стек вызовов

15 of 147

Проблема прерывания программы

15

main

start_net_protocol

Стек вызовов

Здесь открыли общение по сети, которое нужно закрыть перед выходом

16 of 147

Проблема прерывания программы

16

main

start_net_protocol

foo

with_log

Стек вызовов

Здесь открыли общение по сети, которое нужно закрыть перед выходом

Здесь открыли файл, который нужно закрыть перед выходом

17 of 147

Проблема прерывания программы

17

main

start_net_protocol

foo

with_log

bar

baz

malloc

Стек вызовов

Здесь открыли общение по сети, которое нужно закрыть перед выходом

Памяти не хватило

Здесь открыли файл, который нужно закрыть перед выходом

18 of 147

Проблема прерывания программы

18

main

start_net_protocol

foo

with_log

bar

baz

Стек вызовов

Здесь открыли общение по сети, которое нужно закрыть перед выходом

Обнаружили ошибку

Здесь открыли файл, который нужно закрыть перед выходом

19 of 147

Проблема прерывания программы

19

main

start_net_protocol

foo

with_log

bar

baz

Стек вызовов

Здесь открыли общение по сети, которое нужно закрыть перед выходом

Обнаружили ошибку

Здесь открыли файл, который нужно закрыть перед выходом

20 of 147

Завершающие действия

Чтобы отпустить ресурс (закрыть файл, попрощаться в сетевом протоколе и т.п.) нужно вернуться по стеку вызовов от места обнаружения ошибки к месту, где началась работа с этим ресурсом

20

21 of 147

Завершающие действия

Чтобы отпустить ресурс (закрыть файл, попрощаться в сетевом протоколе и т.п.) нужно вернуться по стеку вызовов от места обнаружения ошибки к месту, где началась работа с этим ресурсом

Иногда можно решить эту проблему регистрацией atexit коллбэков - действий, которые произойдут при завершении программы

Мы будем это делать в одной из лабораторных работ

21

22 of 147

Завершающие действия

Чтобы отпустить ресурс (закрыть файл, попрощаться в сетевом протоколе и т.п.) нужно вернуться по стеку вызовов от места обнаружения ошибки к месту, где началась работа с этим ресурсом

Иногда можно решить эту проблему регистрацией atexit коллбэков - действий, которые произойдут при завершении программы

Мы будем это делать в одной из лабораторных работ

Это не всегда удобно, и совершенно точно не подходит для обработки ошибок без прерывания программы

22

23 of 147

Обработка без прерывания

Очень часто ошибки обрабатываются не прерыванием программы, а некоторым влиянием на окружение, ставшее источником ошибки, и переповтором действий

23

24 of 147

Обработка без прерывания

Очень часто ошибки обрабатываются не прерыванием программы, а некоторым влиянием на окружение, ставшее источником ошибки, и переповтором действий

Любое GUI приложение, открывающее файлы, не упадёт, если вы введёте несуществующее имя файла, а сообщит вам об этом и предложит повторить попытку

24

25 of 147

Обработка без прерывания

Очень часто ошибки обрабатываются не прерыванием программы, а некоторым влиянием на окружение, ставшее источником ошибки, и переповтором действий

Любое GUI приложение, открывающее файлы, не упадёт, если вы введёте несуществующее имя файла, а сообщит вам об этом и предложит повторить попытку

Многие программы хранят в памяти кэши - данные, которые можно всегда перепрочитать с диска или по сети, или перевычислить. В случае нехватки памяти, программа может сбросить кэши и переповторить выделение памяти

25

26 of 147

Обработка без прерывания

Очень часто ошибки обрабатываются не прерыванием программы, а некоторым влиянием на окружение, ставшее источником ошибки, и переповтором действий

Любое GUI приложение, открывающее файлы, не упадёт, если вы введёте несуществующее имя файла, а сообщит вам об этом и предложит повторить попытку

Многие программы хранят в памяти кэши - данные, которые можно всегда перепрочитать с диска или по сети, или перевычислить. В случае нехватки памяти, программа может сбросить кэши и переповторить выделение памяти

И так далее - примеров намного больше, чем обработок с прерываниями

26

27 of 147

Обработка без прерывания

Очень часто ошибки обрабатываются не прерыванием программы, а некоторым влиянием на окружение, ставшее источником ошибки, и переповтором действий

Любое GUI приложение, открывающее файлы, не упадёт, если вы введёте несуществующее имя файла, а сообщит вам об этом и предложит повторить попытку

Многие программы хранят в памяти кэши - данные, которые можно всегда перепрочитать с диска или по сети, или перевычислить. В случае нехватки памяти, программа может сбросить кэши и переповторить выделение памяти

И так далее - примеров намного больше, чем обработок с прерываниями

В таком случае возникает задача перейти от места обнаружения ошибки к месту, имеющему право обработать ошибку, и перенести туда информацию об ошибке

27

28 of 147

Право обработать ошибку

Рассмотрим функцию int str_to_int(char* str), конвертирующую строки в числа; в ходе её работы могут произойти как минимум два вида ошибок:

  1. В строке присутствуют символы, не являющиеся цифрами
  2. При составлении числа возникает переполнение

28

29 of 147

Право обработать ошибку

Рассмотрим функцию int str_to_int(char* str), конвертирующую строки в числа; в ходе её работы могут произойти как минимум два вида ошибок:

  • В строке присутствуют символы, не являющиеся цифрами
  • При составлении числа возникает переполнение

Имеет ли право функция str_to_int сама обработать ошибку?

29

30 of 147

Право обработать ошибку

Рассмотрим функцию int str_to_int(char* str), конвертирующую строки в числа; в ходе её работы могут произойти как минимум два вида ошибок:

  • В строке присутствуют символы, не являющиеся цифрами
  • При составлении числа возникает переполнение

Имеет ли право функция str_to_int сама обработать ошибку? Нет!

30

31 of 147

Право обработать ошибку

Рассмотрим функцию int str_to_int(char* str), конвертирующую строки в числа; в ходе её работы могут произойти как минимум два вида ошибок:

  • В строке присутствуют символы, не являющиеся цифрами
  • При составлении числа возникает переполнение

Имеет ли право функция str_to_int сама обработать ошибку? Нет!

Например, вы вызвали её не для того, чтобы получить результат конверсии, а только для того, чтобы узнать, является ли строка числом

31

32 of 147

Утилитарные/библиотечные функции

Неформальная категория функций, которые делают какую-то полезную работу, не зная, в каком контексте вызваны - это позволяет их переиспользовать во множестве контекстов

32

33 of 147

Утилитарные/библиотечные функции

Неформальная категория функций, которые делают какую-то полезную работу, не зная, в каком контексте вызваны - это позволяет их переиспользовать во множестве контекстов

В идеале такие функции должны быть чистыми (без побочных эффектов), либо со строго специфицированными побочными эффектами (как например, утилитарные функции IO)

33

34 of 147

Утилитарные/библиотечные функции

Неформальная категория функций, которые делают какую-то полезную работу, не зная, в каком контексте вызваны - это позволяет их переиспользовать во множестве контекстов

В идеале такие функции должны быть чистыми (без побочных эффектов), либо со строго специфицированными побочными эффектами (как например, утилитарные функции IO)

Общаться с вашим пользователем такие функции точно не имеют никакого права, а все возможные обнаруженные ими ошибки должны стать некоторой формой возвращаемого значения

Мы уже встречались с подобным подходом - функция scanf возвращает EOF, если не сумела ничего прочитать (конец файла)

34

35 of 147

Как это сделать?

Если во множестве значений возвращаемого типа есть некорректные значения, как NULL для функции malloc, можно использовать их, как признак ошибки

35

36 of 147

Как это сделать?

Если во множестве значений возвращаемого типа есть некорректные значения, как NULL для функции malloc, можно использовать их, как признак ошибки

36

int str_to_int(char* str) {

...

if (!isdigit(c)) {

}

if (overflow) {

}

...

}

37 of 147

Как это сделать?

Если во множестве значений возвращаемого типа есть некорректные значения, как NULL для функции malloc, можно использовать их, как признак ошибки

37

int str_to_int(char* str) {

...

if (!isdigit(c)) {

// return error: "non-digit symbol"

}

if (overflow) {

// return error: "overflow"

}

...

}

38 of 147

Как это сделать?

Если во множестве значений возвращаемого типа есть некорректные значения, как 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 of 147

Коды ошибок

Мы уже изучали, как вернуть из функции несколько значений (дополнительные параметры-указатели); в задаче обработки ошибок можно применить ту же технику

39

40 of 147

Коды ошибок

Мы уже изучали, как вернуть из функции несколько значений (дополнительные параметры-указатели); в задаче обработки ошибок можно применить ту же технику

Через дополнительные параметры-указатели можно вернуть из функции любую информацию об ошибке

40

41 of 147

Коды ошибок

Мы уже изучали, как вернуть из функции несколько значений (дополнительные параметры-указатели); в задаче обработки ошибок можно применить ту же технику

Через дополнительные параметры-указатели можно вернуть из функции любую информацию об ошибке

Как правило ограничиваются одним целым числом, которое называют кодом ошибки

41

42 of 147

Коды ошибок

42

int str_to_int(char* str) {

...

if (!isdigit(c)) {

// return error: "non-digit symbol"

}

if (overflow) {

// return error: "overflow"

}

...

}

43 of 147

Коды ошибок

43

int str_to_int(char* str, int* err_code) {

...

if (!isdigit(c)) {

// return error: "non-digit symbol"

}

if (overflow) {

// return error: "overflow"

}

...

}

44 of 147

Коды ошибок

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 of 147

Коды ошибок

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 of 147

Коды ошибок

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 of 147

Нагрузка на прототип функции

Коды ошибок засоряют прототип функции, что усложняет вызов и ухудшает читаемость кода

47

48 of 147

Нагрузка на прототип функции

Коды ошибок засоряют прототип функции, что усложняет вызов и ухудшает читаемость кода

Попытка передать ещё какую-то информацию об ошибке, кроме кода, сделает ситуацию ещё хуже

48

49 of 147

Нагрузка на прототип функции

Коды ошибок засоряют прототип функции, что усложняет вызов и ухудшает читаемость кода

Попытка передать ещё какую-то информацию об ошибке, кроме кода, сделает ситуацию ещё хуже

Особенно плохо это для тех вызовов, для которых вы точно знаете, что ошибки быть не может - утилитарная функция одна, а вызываете вы её из разных контекстов!

49

50 of 147

Нагрузка на прототип функции

50

int err_code = 0;

int x = str_to_int(str, &err_code);

assert(err_code == 0);

51 of 147

Нагрузка на прототип функции

51

int err_code = 0;

int x = str_to_int(str, &err_code);

assert(err_code == 0);

Вы уверены, что str состоит только из цифр, и переполнения нет

52 of 147

Нагрузка на прототип функции

52

int err_code = 0;

int x = str_to_int(str, &err_code);

assert(err_code == 0);

Вы уверены, что str состоит только из цифр, и переполнения нет

Вы подкрепляете эту уверенность assert’ом

53 of 147

Нагрузка на прототип функции

53

int err_code = 0;

int x = str_to_int(str, &err_code);

assert(err_code == 0);

Вы уверены, что str состоит только из цифр, и переполнения нет

Вы подкрепляете эту уверенность assert’ом

Но это всё равно мусорный код!

54 of 147

Нагрузка на прототип функции

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 of 147

Нагрузка на прототип функции

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 of 147

Прокси-функции

Прокси-функции (функции-обёртки) - функции, реализующие свою задачу не самостоятельно, а вызывающие основную функцию, совершая при этом какие-то вторичные действия, связанные с контекстом вызова

56

57 of 147

Прокси-функции

Прокси-функции (функции-обёртки) - функции, реализующие свою задачу не самостоятельно, а вызывающие основную функцию, совершая при этом какие-то вторичные действия, связанные с контекстом вызова

Самый распространённый пример - прокси-функции, определяющие популярные значения аргументов по умолчанию (default arguments)

57

58 of 147

Прокси-функции

58

int str_to_int(char* str, int* err_code);

59 of 147

Прокси-функции

59

int str_to_int(char* str, int* err_code);

int str_to_int_radix(char* str, unsigned char radix, int* err_code);

Функция более общего вида

60 of 147

Прокси-функции

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 of 147

Прокси-функции

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 of 147

Прокси-функции

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);

Функция более общего вида

Прокси-функция!

Определяющая аргумент по умолчанию

63 of 147

Прокси-функции

Прокси-функции (функции-обёртки) - функции, реализующие свою задачу не самостоятельно, а вызывающие основную функцию, совершая при этом какие-то вторичные действия, связанные с контекстом вызова

Самый распространённый пример - прокси-функции, определяющие популярные значения аргументов по умолчанию (default arguments)

63

64 of 147

Прокси-функции

Прокси-функции (функции-обёртки) - функции, реализующие свою задачу не самостоятельно, а вызывающие основную функцию, совершая при этом какие-то вторичные действия, связанные с контекстом вызова

Самый распространённый пример - прокси-функции, определяющие популярные значения аргументов по умолчанию (default arguments)

Прокси-функции можно использовать для преобразования ошибки в ассёрт

64

65 of 147

Прокси-функции

65

int err_code = 0;

int x = str_to_int(str, &err_code);

assert(err_code == 0);

Вы уверены, что str состоит только из цифр, и переполнения нет

66 of 147

Прокси-функции

66

int x = trusted_str_to_int(str);

Вы уверены, что str состоит только из цифр, и переполнения нет

67 of 147

Прокси-функции

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 of 147

Нагрузка на прототип

Уменьшить нагрузку на прототип можно ещё больше, передавая и принимая код ошибки через глобальные переменные

68

69 of 147

Нагрузка на прототип

Уменьшить нагрузку на прототип можно ещё больше, передавая и принимая код ошибки через глобальные переменные

Существует специальные глобальные переменные для этой цели, которые использует стандартная библиотека, и которые рекомендуется использовать самим при написании кода

69

70 of 147

errno

Квази-переменная (некоторая lvalue-сущность) типа int, объявленная в файле errno.h

70

71 of 147

errno

Квази-переменная (некоторая lvalue-сущность) типа int, объявленная в файле errno.h

Используется функциями стандартной библиотеки Си и POSIX-функциями в качестве места для записи кода ошибки

Нужно занулить errno перед вызовом опасной функции и проверить его после вызова

71

72 of 147

errno

Квази-переменная (некоторая lvalue-сущность) типа int, объявленная в файле errno.h

Используется функциями стандартной библиотеки Си и POSIX-функциями в качестве места для записи кода ошибки

Нужно занулить errno перед вызовом опасной функции и проверить его после вызова

Стандарт POSIX дополнительно определяет, что errno - потокобезопасная, то есть, на каждый поток исполнения присутствует своя копия errno

72

73 of 147

Проблема промежуточных функций

73

Пусть функция foo, имеющая право обрабатывать ошибки, вызывает две библиотечные функции bar и baz

foo()

bar()

baz()

74 of 147

Проблема промежуточных функций

74

foo()

bar()

baz()

ququ()

bebe()

pepe()

ouch()

Пусть функция foo, имеющая право обрабатывать ошибки, вызывает две библиотечные функции bar и baz

Которые также вызывают по две функции, которые могут обнаружить ошибки

75 of 147

Проблема промежуточных функций

75

foo()

bar()

baz()

ququ()

bebe()

pepe()

ouch()

Пусть функция foo, имеющая право обрабатывать ошибки, вызывает две библиотечные функции bar и baz

Которые также вызывают по две функции, которые могут обнаружить ошибки

Если в этой функции будет обнаружена ошибка

76 of 147

Проблема промежуточных функций

76

foo()

bar()

baz()

ququ()

bebe()

pepe()

ouch()

Пусть функция foo, имеющая право обрабатывать ошибки, вызывает две библиотечные функции bar и baz

Которые также вызывают по две функции, которые могут обнаружить ошибки

Если в этой функции будет обнаружена ошибка

Продолжать исполнение вот этой будет нельзя!

77 of 147

Проблема промежуточных функций

Все функции в стеке вызовов между точкой обнаружения ошибки и точкой обработки становятся вынуждены принимать участие в передаче

77

78 of 147

Проблема промежуточных функций

Все функции в стеке вызовов между точкой обнаружения ошибки и точкой обработки становятся вынуждены принимать участие в передаче

Тривиальное участие (пронос ошибки, перевыброс) - проверка кода ошибки и выход из функции

78

79 of 147

Проблема промежуточных функций

Все функции в стеке вызовов между точкой обнаружения ошибки и точкой обработки становятся вынуждены принимать участие в передаче

Тривиальное участие (пронос ошибки, перевыброс) - проверка кода ошибки и выход из функции

Логика программы сильно загрязняется постоянными однотипными проверками (подавляющее большинство функций не имеют права обрабатывать ошибки)

79

80 of 147

Суть проблемы

80

// опасный код начинается

...

foo()

...

// опасный код закончился

if (was_error) {

// обработка ошибки

}

81 of 147

Суть проблемы

81

// опасный код начинается

...

foo() // -> bar() -> baz() -> ...

...

// опасный код закончился

if (was_error) {

// обработка ошибки

}

82 of 147

Суть проблемы

82

// опасный код начинается

...

foo() // -> bar() -> baz() -> ...

...

// опасный код закончился

if (was_error) {

// обработка ошибки

}

83 of 147

Суть проблемы

83

// опасный код начинается

...

foo() // -> bar() -> baz() -> ...

...

// опасный код закончился

if (was_error) {

// обработка ошибки

}

84 of 147

Суть проблемы

84

// опасный код начинается

...

foo() // -> bar() -> baz() -> ...

...

// опасный код закончился

if (was_error) {

// обработка ошибки

}

goto!

85 of 147

Суть проблемы

85

// опасный код начинается

...

foo() // -> bar() -> baz() -> ...

...

// опасный код закончился

if (was_error) {

// обработка ошибки

}

goto!

Ну, почти

86 of 147

Суть проблемы

86

// опасный код начинается

...

foo() // -> bar() -> baz() -> ...

...

// опасный код закончился

if (was_error) {

// обработка ошибки

}

goto!

Ну, почти

x = 42

M + 4

Фрейм main

Фрейм bar

Фрейм foo

main

Фрейм baz

x = 42

Фрейм

87 of 147

Суть проблемы

goto может перейти только внутри функции, а нам нужен прыжок из глубины стека обратно через несколько фреймов (их нужно удалить со стека)

87

88 of 147

Суть проблемы

88

// опасный код начинается

...

foo() // -> bar() -> baz() -> ...

...

// опасный код закончился

if (was_error) {

// обработка ошибки

}

x = 42

M + 4

Фрейм main

Фрейм bar

Фрейм foo

main

Фрейм baz

x = 42

Фрейм

89 of 147

Суть проблемы

89

// опасный код начинается

...

foo() // -> bar() -> baz() -> ...

...

// опасный код закончился

if (was_error) {

// обработка ошибки

}

x = 42

Фрейм main

Фрейм bar

Фрейм foo

main

Фрейм baz

x = 42

Фрейм

90 of 147

setjmp.h

Библиотека для реализации нелокальных переходов - перемещений между функциями

90

91 of 147

setjmp.h

Библиотека для реализации нелокальных переходов - перемещений между функциями

Состоит из трёх элементов:

  1. Тип jmp_buf, являющийся некоторым массивом, умеющем хранить calling environment (контекст точки вызова)

91

92 of 147

setjmp.h

Библиотека для реализации нелокальных переходов - перемещений между функциями

Состоит из трёх элементов:

  • Тип jmp_buf, являющийся некоторым массивом, умеющем хранить calling environment (контекст точки вызова)

  • Функция setjmp, запоминающая точку вызова, в которую можно будет потом вернуться

92

93 of 147

setjmp.h

Библиотека для реализации нелокальных переходов - перемещений между функциями

Состоит из трёх элементов:

  • Тип jmp_buf, являющийся некоторым массивом, умеющем хранить calling environment (контекст точки вызова)

  • Функция setjmp, запоминающая точку вызова, в которую можно будет потом вернуться

  • Функция longjmp, совершающая прыжок в эту точку откуда угодно

93

94 of 147

Как это выглядит

94

jmp_buf env;

if (!setjmp(env)) {

// опасный код

...

foo(env) // -> bar() -> baz() -> ...

...

} else {

// обработка ошибки

}

95 of 147

Как это выглядит

95

jmp_buf env;

if (!setjmp(env)) {

// опасный код

...

foo(env) // -> bar() -> baz() -> ...

...

} else {

// обработка ошибки

}

Переменная, в которой может быть сохранён контекст исполнения

96 of 147

Как это выглядит

96

jmp_buf env;

if (!setjmp(env)) {

// опасный код

...

foo(env) // -> bar() -> baz() -> ...

...

} else {

// обработка ошибки

}

Переменная, в которой может быть сохранён контекст исполнения

Сохранение контекста и одновременное обозначение точки, в которую надо будет, если что, вернуться

97 of 147

int setjmp(jmp_buf env)

Ключевой элемент нелокальных переходов - портал для перемещения в опасный код и из места обнаружения ошибки в код обработки

97

98 of 147

int setjmp(jmp_buf env)

Ключевой элемент нелокальных переходов - портал для перемещения в опасный код и из места обнаружения ошибки в код обработки

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

98

99 of 147

int setjmp(jmp_buf env)

Ключевой элемент нелокальных переходов - портал для перемещения в опасный код и из места обнаружения ошибки в код обработки

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

Возвращает 0, если была вызвана (sic!), и тогда мы переходим в опасный код

99

100 of 147

int setjmp(jmp_buf env)

Ключевой элемент нелокальных переходов - портал для перемещения в опасный код и из места обнаружения ошибки в код обработки

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

Возвращает 0, если была вызвана (sic!), и тогда мы переходим в опасный код

Возвращает не 0, если не была вызвана (sic!), и тогда мы переходим в код обработки ошибки

100

101 of 147

int setjmp(jmp_buf env)

101

102 of 147

Как это выглядит

102

jmp_buf env;

if (!setjmp(env)) {

// опасный код

...

foo(env) // -> bar() -> baz() -> ...

...

} else {

// обработка ошибки

}

Переменная, в которой может быть сохранён контекст исполнения

Сохранение контекста и одновременное обозначение точки, в которую надо будет, если что, вернуться

env - это ключ от портала, и мы должны передавать его опасному коду, чтобы тот мог вернуться

103 of 147

Как это выглядит

103

jmp_buf env;

if (!setjmp(env)) {

// опасный код

...

foo(env) // -> bar() -> baz() -> ...

...

} else {

// обработка ошибки

}

Переменная, в которой может быть сохранён контекст исполнения

Сохранение контекста и одновременное обозначение точки, в которую надо будет, если что, вернуться

env - это ключ от портала, и мы должны передавать его опасному коду, чтобы тот мог вернуться

104 of 147

Как это выглядит

104

jmp_buf env;

if (!setjmp(env)) {

// опасный код

...

foo(env) // -> bar() -> baz() -> ...

...

} else {

// обработка ошибки

}

Переменная, в которой может быть сохранён контекст исполнения

Сохранение контекста и одновременное обозначение точки, в которую надо будет, если что, вернуться

env - это ключ от портала, и мы должны передавать его опасному коду, чтобы тот мог вернуться

longjmp(env, 37);

105 of 147

void longjmp(jmp_buf env, int val)

Принимает jmp_buf, в точку заполнения которого надо вернуться, и val, который нужно как бы вернуть из функции setjmp, которая заполняла jmp_buf

105

106 of 147

void longjmp(jmp_buf env, int val)

Принимает jmp_buf, в точку заполнения которого надо вернуться, и val, который нужно как бы вернуть из функции setjmp, которая заполняла jmp_buf

val выполняет роль кода ошибки; если вызвать longjmp со значением 0, она заменит его на 1

106

107 of 147

Как это выглядит

107

jmp_buf env;

if (!setjmp(env)) {

// опасный код

...

foo(env) // -> bar() -> baz() -> ...

...

} else {

// обработка ошибки

}

longjmp(env, 37);

108 of 147

Как это выглядит

108

jmp_buf env;

if (!setjmp(env)) {

// опасный код

...

foo(env) // -> bar() -> baz() -> ...

...

} else {

// обработка ошибки

}

longjmp(env, 37);

109 of 147

Как это выглядит

109

jmp_buf env;

if (!setjmp(env)) {

// опасный код

...

foo(env) // -> bar() -> baz() -> ...

...

} else {

// обработка ошибки

}

longjmp(env, 37);

110 of 147

Как это выглядит

110

jmp_buf env;

if (!setjmp(env)) {

// опасный код

...

foo(env) // -> bar() -> baz() -> ...

...

} else {

// обработка ошибки

}

longjmp(env, 37);

111 of 147

Значения объектов

При прыжке из longjmp в точку вызова setjmp значения практически всех объектов в программе не изменяются

111

112 of 147

Значения объектов

При прыжке из longjmp в точку вызова setjmp значения практически всех объектов в программе не изменяются

Единственное исключение: значения локальных переменных функции, в которой была позвана setjmp, и которые были изменены между вызовами setjmp и longjmp, становятся неопределёнными

112

113 of 147

Что это означает

Хотя стандарт языка не определяет этого, нелокальный переход реализован достаточно однозначно:

113

114 of 147

Что это означает

Хотя стандарт языка не определяет этого, нелокальный переход реализован достаточно однозначно:

  1. jmp_buf - это массив, в который записываются значения всех регистров, включая SP и PC после вызова функции setjmp

114

115 of 147

Что это означает

Хотя стандарт языка не определяет этого, нелокальный переход реализован достаточно однозначно:

  • jmp_buf - это массив, в который записываются значения всех регистров, включая SP и PC после вызова функции setjmp

  • setjmp - это даже не функция, а просто конструкция, по которой компилятор генерирует код, заполняющий jmp_buf и записывающий в один из регистров R число 0

115

116 of 147

Что это означает

Хотя стандарт языка не определяет этого, нелокальный переход реализован достаточно однозначно:

  • jmp_buf - это массив, в который записываются значения всех регистров, включая SP и PC после вызова функции setjmp

  • setjmp - это даже не функция, а просто конструкция, по которой компилятор генерирует код, заполняющий jmp_buf и записывающий в один из регистров R число 0

  • longjmp записывает в регистры то, что было сохранено в jmp_buf, кроме регистра R, в который записывает val, и последними двумя движениями устанавливает SP и PC из jmp_buf - прыжок в прошлое совершился

116

117 of 147

Значения объектов

Из реализации понятно, что никакого отката в прошлое не происходит - все объекты, в которые было что-то записано на пути от setjmp до longjmp, сохраняют свои значения

117

118 of 147

Значения объектов

Из реализации понятно, что никакого отката в прошлое не происходит - все объекты, в которые было что-то записано на пути от setjmp до longjmp, сохраняют свои значения

Но локальные переменные функции, в которой вызывался setjmp, могли находиться на регистрах!

118

119 of 147

Размещение переменных

119

int x;

int y;

setjmp(env);

use(x);

use(y);

120 of 147

Размещение переменных

120

int x;

int y;

setjmp(env);

use(x);

use(y);

Предположим, что переменная x лежит на стеке

121 of 147

Размещение переменных

121

int x;

int y;

setjmp(env);

use(x);

use(y);

Предположим, что переменная x лежит на стеке

А переменная y на регистре R

122 of 147

Размещение переменных

122

int x;

int y;

setjmp(env);

use(x);

use(y);

Предположим, что переменная x лежит на стеке

А переменная y на регистре R

Это чтение памяти из стека

123 of 147

Размещение переменных

123

int x;

int y;

setjmp(env);

use(x);

use(y);

Предположим, что переменная x лежит на стеке

А переменная y на регистре R

Это чтение памяти из стека

А это чтение регистра R

124 of 147

Размещение переменных

124

int x;

int y;

setjmp(env);

use(x);

use(y);

Предположим, что переменная x лежит на стеке

А переменная y на регистре R

Это чтение памяти из стека

А это чтение регистра R

Регистр R запишется в env и будет восстановлен при вызове longjmp

125 of 147

Размещение переменных

125

int x;

int y;

setjmp(env);

use(x);

use(y);

Предположим, что переменная x лежит на стеке

А переменная y на регистре R

Это чтение памяти из стека

А это чтение регистра R

Регистр R запишется в env и будет восстановлен при вызове longjmp

Стековые слоты никто никуда не записывал, их могли изменить между setjmp и longjmp

126 of 147

Размещение переменных

126

int x;

int y;

setjmp(env);

use(x);

use(y);

Предположим, что переменная x лежит на стеке

А переменная y на регистре R

Это чтение памяти из стека

Прочитается то значение, которое было при вызове longjmp

А это чтение регистра R

Регистр R запишется в env и будет восстановлен при вызове longjmp

Стековые слоты никто никуда не записывал, их могли изменить между setjmp и longjmp

127 of 147

Размещение переменных

127

int x;

int y;

setjmp(env);

use(x);

use(y);

Предположим, что переменная x лежит на стеке

А переменная y на регистре R

Это чтение памяти из стека

Прочитается то значение, которое было при вызове longjmp

А это чтение регистра R

Прочитается то значение, которое было до вызова setjmp

Регистр R запишется в env и будет восстановлен при вызове longjmp

Стековые слоты никто никуда не записывал, их могли изменить между setjmp и longjmp

128 of 147

Размещение переменных

Зависит от уровня оптимизаций компилятора, генерации отладочной информации и т.д.

128

129 of 147

Размещение переменных

Зависит от уровня оптимизаций компилятора, генерации отладочной информации и т.д.

В целом компилятор старается использовать регистры, на стек переменные складываются в двух случаях: если не хватает места на регистрах, либо если вы взяли у переменной адрес

129

130 of 147

Размещение переменных

Зависит от уровня оптимизаций компилятора, генерации отладочной информации и т.д.

В целом компилятор старается использовать регистры, на стек переменные складываются в двух случаях: если не хватает места на регистрах, либо если вы взяли у переменной адрес

Адрес берётся как раз для того, чтобы передать его куда-то, чтобы там по адресу что-то записали

Есть ещё причины (например, массивы просто невозможно не адресовать)

130

131 of 147

TL;DR

Если вы не брали адрес своей переменной и не меняли её после вызова setjmp, то после вызова longjmp в ней будет то же самое значение, что и было до вызова setjmp

Это гарантируется стандартом и действительно так на практике

131

132 of 147

TL;DR

Если вы не брали адрес своей переменной и не меняли её после вызова setjmp, то после вызова longjmp в ней будет то же самое значение, что и было до вызова setjmp

Это гарантируется стандартом и действительно так на практике

Если вы брали адрес переменной и куда-то его передавали, и там по нему что-то записали, то согласно стандарту в ней находится неопределённое значение, но на практике будет то, что записали

Очень зыбкая почва!

132

133 of 147

TL;DR

Если вы не брали адрес своей переменной и не меняли её после вызова setjmp, то после вызова longjmp в ней будет то же самое значение, что и было до вызова setjmp

Это гарантируется стандартом и действительно так на практике

Если вы брали адрес переменной и куда-то его передавали, и там по нему что-то записали, то согласно стандарту в ней находится неопределённое значение, но на практике будет то, что записали

Очень зыбкая почва!

Если вы не брали адрес, но изменяли значение, то оно будет неопределённым

Это соответствует стандарту и действительно так на практике

133

134 of 147

TL;DR от TL;DR

Поменьше локальных переменных рядом с setjmp, и не берите от них адреса

134

135 of 147

Проблема промежуточных функций

Пусть в функции есть завершающие действия (закрытие файлов, сетевой протокол, …) или, что встречается намного чаще, удаление динамической памяти

135

136 of 147

Проблема промежуточных функций

Пусть в функции есть завершающие действия (закрытие файлов, сетевой протокол, …) или, что встречается намного чаще, удаление динамической памяти

В таком случае нельзя просто так пролетать мимо неё на setjmp-longjmp механизме, приходится вставлять перевыбрасывающие обработчики ошибок

136

137 of 147

Проблема промежуточных функций

Пусть в функции есть завершающие действия (закрытие файлов, сетевой протокол, …) или, что встречается намного чаще, удаление динамической памяти

В таком случае нельзя просто так пролетать мимо неё на setjmp-longjmp механизме, приходится вставлять перевыбрасывающие обработчики ошибок

Проблема слабее, чем была с кодами ошибок, но остаётся

137

138 of 147

Проблема промежуточных функций

138

void foo(jmp_buf env) {

int* arr = nc_malloc(42 * sizeof(int));

...

bar(env);

...

free(arr);

}

139 of 147

Проблема промежуточных функций

139

void foo(jmp_buf env) {

int* arr = nc_malloc(42 * sizeof(int));

...

bar(env);

...

free(arr);

}

Нам передали внешний контекст

140 of 147

Проблема промежуточных функций

140

void foo(jmp_buf env) {

int* arr = nc_malloc(42 * sizeof(int));

...

bar(env);

...

free(arr);

}

Нам передали внешний контекст

Мы передаём его дальше по стеку

141 of 147

Проблема промежуточных функций

141

void foo(jmp_buf env) {

int* arr = nc_malloc(42 * sizeof(int));

...

bar(env);

...

free(arr);

}

Нам передали внешний контекст

Мы передаём его дальше по стеку

Если функция bar (или кто-то, кому она передаст этот контекст дальше) воспользуется им, фрейм foo пропадёт со стека, код не будет завершён

142 of 147

Проблема промежуточных функций

142

void foo(jmp_buf env) {

int* arr = nc_malloc(42 * sizeof(int));

...

bar(env);

...

free(arr);

}

Нам передали внешний контекст

Мы передаём его дальше по стеку

Если функция bar (или кто-то, кому она передаст этот контекст дальше) воспользуется им, фрейм foo пропадёт со стека, код не будет завершён

Потенциальная утечка памяти

143 of 147

Проблема промежуточных функций

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 of 147

Проблема промежуточных функций

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);

}

}

145 of 147

setjmp & longjmp

Из-за общей запутанности, проблемы значений объектов и нерешённой проблемы промежуточных функций нелокальные переходы очень редко используются для обработки ошибок

145

146 of 147

setjmp & longjmp

Из-за общей запутанности, проблемы значений объектов и нерешённой проблемы промежуточных функций нелокальные переходы очень редко используются для обработки ошибок

В других языках программирования есть механизм исключений, который является доработанной идеей нелокального перехода; проблему промежуточных функций при этом решают с помощью разных инструментов (RAII, try-with-resources, …)

146

147 of 147

setjmp & longjmp

Из-за общей запутанности, проблемы значений объектов и нерешённой проблемы промежуточных функций нелокальные переходы очень редко используются для обработки ошибок

В других языках программирования есть механизм исключений, который является доработанной идеей нелокального перехода; проблему промежуточных функций при этом решают с помощью разных инструментов (RAII, try-with-resources, …)

Нелокальный переход также можно использовать для реализации корутин (кооперативной многозадачности) - это будет одна из последних ваших лабораторных работ

147