Chapter 24. Error Handling

지환·2022년 3월 20일
0

학생이 만든 program에선 예상치 못한 input이 들어왔을때 실패하고 종료돼버릴 수도 있지만~
상업용 프로그램이라면 "bulletproof"여야한다.
예상하고 적절히 대응해서 error를 복구해야한다.

error check하는 2가지 방법
1. by using assert macro
2. by testing errno variable

error detection과 handling은 C의 강점이 아니다.
run-time error를 하나의 방식이 아니라 다양한 방식으로 나타낸다.
실제로 이런게 일어나더라도, 프로그램 그냥 실행되는 경우가 있어서 간과하기 쉽다.
C++, Java 같은 언어는 "exception handling"을 통해 더 잘 탐지하고 반응할 수 있다.

24.1 The <assert.h> Header

Diagnostics

assert

void assert(scalar expression);

발생 가능한 문제를 미리 detect할 수 있게 하는 macro 함수이다.

인자로 "assertion"이 와야한다.
아무 scalar type이나 오면 된다. ex) int, float, pointer..

assert가 실행될때 argument를 test해서 nonzero라면 넘어가고,
zero라면 stderr에 message를 작성하고 abort함수를 호출해 프로그램을 종료한다.

ex)

assert(0 <= i && i < 10);
a[i] = 0;

이렇게 해두면 assert 함수가 i가 제대로 된 범위에 있는지 확인할 수 있게 한다.
만약 i가 저 범위를 넘어서면
Assertion failed: 0 <= i && i < 10, file demo.c,, line 109
이런식으로 stderr에 message를 띄운다.
(C99부터는 함수이름도 추가해서 띄운다. 형식은 compiler마다 다를 수 있지만 제공해야되는 정보는 standard에 명시돼있다.)

단점
assert를 추가하면 running time이 좀 길어진다.
하나 추가한다고 많이 길어지는건 아니지만 예상치못한 결과를 가져올 수 있어서,
많은 프로그래머들은 test할때만 assert를 사용하고 그 이후엔 비활성화시킨다.

#define NDEBUG
#include <assert.h>

이렇게 <assert.h>를 include하기 전에 NDEBUG macro를 정의해두면 assert는 비활성화 된다.

그래서 assert에 side effect를 가진 인자는 안넣는게 좋다.(함수 호출 등..)
나중에 NDEBUG로 비활성화 될 수도 있으니까..

(C++에선 assert가 디버깅 툴이라고 하네)

24.2 The <errno.h> Header

Errors

몇몇 standard library 함수는 errno에 error code(양수)를 저장하여 failure을 나타낸다.
errno를 사용하는 대부분 함수는 <math.h>의 함수들이지만 다른 library에도 좀 있긴있다.

errno 사용예시)

errno = 0;
y = sqrt(x);
if (errno != 0) {
	fprintf(stderr, "sqrt error; program terminated.\n");
    exit(EXIT_FAILURE);
}

이렇게 errno를 test해서 error가 발생했다면 그에 맞게 처리할 수 있다.

errno0을 저장해주는거 주의하자. 이전에 이미 다른 값이 저장돼있을 수 있다.
library functions never clear errno, 따라서 사용 전에 직접 clear 해줘야한다.

EILSEQ macro
<errno.h>EILSEQ macro는 encoding error가 발생했을 때 errno에 저장된다.
주로 <wchar.h>의 함수에서 주로 그런다.
나머지 두개는 알다싶이 EDOM 이랑 ERANGE

perror & strerror

void perror(const char *s);    //<stdio.h>
char *strerror(int errnum);    //<string.h>

<errno.h>에 있진 않지만 errno variable과 연관돼있는 함수들이다.

perror

perror 함수는 errno의 값을 기반으로 에러메시지를 stderr에 출력해준다.
순서는 (1)its argument, (2),, (3)a space, (4)message determined by the value of errno, (5)\n이다.

위 예시를 좀 수정해서 보자면,

errno = 0;
y = sqrt(x);
if (errno != 0) {
	perror("sqrt error");
    exit(EXIT_FAILURE);
}

// result : sqrt error: Numerical argument out of domain

자세한 문구는 implementation마다 다를 수 있지만, 저렇게 간다.

strerror

strerror 함수는 error code를 넘겨주면 해당 error를 설명하는 string의 pointer를 반환한다.

대부분 argument로는 errno의 값들이 들어가지만, 그냥 integer 이어도 반환한다.
특정 errno에 대해, perror에서 출력하는 메시지와 strerror에서 반환하는 string은 같다.


24.3 The <signal.h> Header

Signal Handling

예외적인 상황인 signal을 handling할 수 있는 기능을 제공한다.

signals은 아래 두 카테고리로 분류된다.
1) run-time error (ex. division by zero)
2) events caused outside the program

많은 OS에서 user가 개입하고, running program을 kill 할 수 있게 하는데, 이런 것들이 C에선 signal로 간주된다.
signal은 특정 point에서 발생하는 것이 아니라 프로그램 실행중에 언제든 발생해, 동시에 발생하지 않는다.
예기치 못할때 발생하기 때문에, 특별한 방식으로 처리

이 section에서는 signal에 대해 표준에 나온 것만 cover한다. 하지만 실제로 UNIX에선 꽤 중요한 역할을 한다.

signal 이해 참고 : https://jhnyang.tistory.com/143

Signal Macros

NameMeaning
SIGABRTAbnormal termination (possibly caused by a all of abort
SIGFPEError during an arithmetic operation (possibly division by zero)
SIGILLInvalid instruction
SIGINTInterrupt
SIGSEGVInvalid storage access
SIGTERMTermination request
다 양수이고, SIG로 시작하는 macro를 implementation에서 따로 더 지원해도 된다.
반대로 OS에 따라 이게 다 의미가 있지는 않은 경우도 있다.

The signal Function

signal

void (*signal(int sig, void (*func)(int)))(int);
chapter 18 참고해서 분석해보면,, signal은 sig와 func을 argument로 받는 함수이고,
이 함수는 void를 반환하고 int를 argument로 가지는 함수 포인터를 반환한다..

특정 signal이 발생했을 때 사용할 signal-handling function을 install한다.

보기와는 다르게 사용법은 간단하다.
첫번째 인자로 특정 signal의 code를 입력하고,
두번째 인자로 첫번째 인자에 해당하는 signal이 발생했을때 handling할 함수의 포인터를 입력한다.
ex) signal(SIGINT, handler); : SIGINT 발생시, handler 함수 호출되도록 install

반환값은 해당 signal에 이전에 설정됐던 handling 함수의 포인터이다. 이를 저장해뒀다가 나중에 쓸 수 있다.(대부분은 버림)
ex) orig_handler = signal(SIGINT, handler); : SIGINT의 이전 handling 함수 포인터를 orig_handler에 저장

SIG_ERRsignal함수에 오류가 생겼는지 판단하게 해주는 macro이다.(signal handler처럼 생겼지만,,)
signal 함수가 실패하면 SIG_ERR을 반환하고, errno에 positive number을 저장한다.

if (signal(SIGINT, handler) failed"); {
	perror("signal(SIGINT, handler) failed"); //errno에 값 저장하니까 perror가능
    ...
}

handler
handling 함수void return typeint parameter를 가져야 한다.
signal이 발생하면 handler에게 signal의 code가 전달된다. 이걸 유용하게 사용할 수 있다. 다른 signal에 같은 handling 함수를 install 할 수도 있다.

signal handler로 signal을 무시하거나, 다르게 따로 처리하거나, 아님 그냥 프로그램을 종료시키는 등의 작업을 할 수 있다.
(handling 함수는 우리가 작성할 수 있음)

handler가 반환되고 나서는 signal이 발생한 부분부터 다시 시작한다.
예외가 있는데,
(1)signal이 SIGABRT였으면 handler반환 뒤에 바로 종료되고,
(2)signal이 SIGFPE였으면 반환뒤는 undefined(즉, 이 상황을 만들지 마라)
(3)SIGILL, SIGSEGV, SIGFPE가 호출하고 반환된 경우 undefined (p.634랑 C99 문서보니 그렇다고 함)

abort나 raise에 의해 호출된게 아니라면,
handler는 library 함수를 호출하거나 static storage duration의 변수를 사용해선 안된다.

library 함수 호출할 수 있는 예외가 있는데(Q&A),
signal 함수는 호출할 수 있다.(signal handler가 스스로 reinstall 할 수 있게함)
abort, _Exit 함수도 호출 가능 (C99부터)

static storage duration 변수 호출 안되는 것도 예외가 있는데(Q&A),
sig_atomic_t 변수는 volatile이라면 static이어도 호출된다.
(자세한 이유는 p.638 참고)

Predefined Signal Handlers

Handler는 우리가 작성할 수도 있지만 미리 정의된걸 사용할 수도 있다.

SIG_DFL : 기본 방식으로 signal을 처리한다. implementation defined 이지만, 대부분 포로그램을 종료한다.
SIG_IGN : Ignore signal

프로그램이 시작될때 각각의 signal은 implementation에 따라 위 둘 중 하나로 initialize된다.

"SIG_" 로 시작하고 대문자가 따라오는 형으로 더 정의될 수도 있다.

사용자 지정 handling 함수 내에서 같은 signal이 발생하면??
infinity recursion을 막기 위해 C89에서는
programmer가 설정한 signal-handling function이 호출되면
해당 signal의 handler를 SIG_DFL로 reset하거나, handler가 진행중일때는 "해당" signal이 발생하는걸 막는다.

C99에서는 해당 signal뿐만 아니라 다른 signal도 disable될지를 implementation이 고를 수 있다.

제한(C99) : abortraise signal이 발생해서 handler가 호출되면 해당 handler는 raise를 사용할 수 없다.

signal handler 호출 이후 다시 설치해야되는지는 implementation defined이다.
즉, signal handler 호출 되고 나면 SIG_DFL로 reset시키는 경우에는,
reinstall 해야한다.

handler가 errno 건드릴거같으면 저장해뒀다가 복구하기..
https://stackoverflow.com/questions/48378213/how-to-deal-with-errno-and-signal-handler-in-linux

The raise Funtion

raise

int raise(int sig);

signal이 주로 run-time error나 external events로 발생하긴 하지만,
직접 일으킬 수도 있다.

Paramter
code for signal
ex) raise(SIGABRT);

Return
성공시 : 0
실패시 : nonzero


24.2 The <setjmp.h> Header

Nonlocal Jumps

보통 함수가 return하면 호출된 부분으로 돌아간다. goto statement를 사용하더라도, 같은 함수 내에서만 jump할 수 있다.
하지만 <setjmp.h> 헤더를 사용하면 한 함수에서 다른 함수로 returning 없이 jump할 수 있다.

setjmp macro를 통해 프로그램내에서 위치를 mark하고,
longjmp function을 통해 해당 위치로 jump할 수 있다.
주로 error handling에서 쓰인다.

setjmp

int setjmp(jmp_buf env);

Paramterjmp_buf variable을 넣어준다. 그럼 현재 environment를 해당 variable에 저장한다.
Return값은 0이다.

Q. 이 함수 argument에 어떻게 값을 저장하는거지? passed by value아닌가?
A. 표준에서는 `jmp_buf`가 array type이라고 말한다.
따라서 passed by pointer라서 가능한 것이다.

C standard에서 소개하는 setjmp 사용법
(다른 방식으로 사용하면 UB이다.)

  1. expression statement의 expression으로 사용(void로 cast해도 됨)

  2. if, switch, while, do, for statement의 part of controlling expression으로 사용
    "전체" controlling expression은 아래 4개 중 하나의 형태를 가져야 한다.
    (1)setjmp(...)
    (2)!setjmp(...)
    (3)constexpr op setjmp(...)
    (4)setjmp(...) op constexpr
    (constexpr은 integer constant expression이고, op는 relational 혹은 equality operator이다.)

longlmp

void longjmp(jmp_buf env, int val);

이동하고자 하는 위치의 setjmp에 parameter로 쓰인 같은 jmp_buf 변수를 첫번째 인자로 pass한다.

longjmpsetjmp call로 가서 반환하게 된다.(jump)
이때의 setjmp 반환값은 longjmp두번째 인자val이다.
(setjmp 기본 0 반환하니까, val0이면 1 반환)

즉.. setjmp가 처음에는 0을 반환하고, 다음에 longjmp로 다시 돌아오게 되면,
그때는 setjmp가 longjmp의 두번째 인자 값을 반환한다.(이때는 0 반환하는 경우 없음)

Restriction (어길시 UB)
1. longjmp의 첫번째 argument는 쓰이기전에 setjmp를 사용해 initialize 돼있었어야 한다.
2. longjmp를 호출할때 setjmp를 호출하는 함수는 return돼있으면 안된다.


Q&A

errno 사용전에 0 저장해주고 쓰라고 했는데, UNIX에선 그렇게 하는걸 못봤다, 왜지?
UNIX program은 주로 OS의 함수를 호추하는데, 이런 system call은 special return value(ex. -1, null pointer)를 갖는다.(errno에 값을 저장하기도 함)
이 return 값만으로도 충분히 error 사실을 인지할 수 있어서 굳이 사용할 필요가 없는 것이다.
C standard library의 함수도 이런식으로 작동하는 경우가 있는데, errno를 error를 signal 하기위한 용도가 아니라 어떤 종류의 error인지 알기위해 사용한다.

<errno.h>에 다른 macro들도 EDOM, ERANGE 말고 다른 macro들도 많은데, legal한가?
Yes, C standard에서 다른 error condition도 나타내도록 허용한다. E로 시작하고, 숫자나 대문자가 따라오면 된다. UNIX도 엄청 많이 지원한다.

SIGFPE, SIGSEGV ??
Floating Point Exception
SEGmentation Violation

p.634에 tsignal.c 프로그램 signal handler안에서 printf 호출하는데 이거 illegal아닌가?
다시 잘 보면 abortraise에 의해 발생하면 library 함수 써도 괜찮다고 했다.
얘는 raise에 의해 호출됐으니 괜찮다.

longjmp쓰고나면 변수는 어떤 값을 가지지?
대부분은 longjmp 호출할때의 값을 유지한다.
하지만 setjmp를 포함하는 함수의 automatic variable은 indetermitate이다.
(volatile이거나 setjmp 뒤로 수정된 적 없는 변수값은 determinate)

0개의 댓글