학생이 만든 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"을 통해 더 잘 탐지하고 반응할 수 있다.
<assert.h>
HeaderDiagnostics
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가 디버깅 툴이라고 하네)
<errno.h>
HeaderErrors
몇몇 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가 발생했다면 그에 맞게 처리할 수 있다.
errno
에 0
을 저장해주는거 주의하자. 이전에 이미 다른 값이 저장돼있을 수 있다.
library functions never clear errno
, 따라서 사용 전에 직접 clear 해줘야한다.
EILSEQ macro
<errno.h>
의 EILSEQ
macro는 encoding error가 발생했을 때 errno
에 저장된다.
주로 <wchar.h>
의 함수에서 주로 그런다.
나머지 두개는 알다싶이 EDOM
이랑 ERANGE
void perror(const char *s); //<stdio.h>
char *strerror(int errnum); //<string.h>
<errno.h>
에 있진 않지만 errno
variable과 연관돼있는 함수들이다.
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
함수는 error code를 넘겨주면 해당 error를 설명하는 string의 pointer를 반환한다.
대부분 argument로는 errno
의 값들이 들어가지만, 그냥 integer 이어도 반환한다.
특정 errno
에 대해, perror
에서 출력하는 메시지와 strerror
에서 반환하는 string은 같다.
<signal.h>
HeaderSignal 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
Name | Meaning |
---|---|
SIGABRT | Abnormal termination (possibly caused by a all of abort |
SIGFPE | Error during an arithmetic operation (possibly division by zero) |
SIGILL | Invalid instruction |
SIGINT | Interrupt |
SIGSEGV | Invalid storage access |
SIGTERM | Termination request |
다 양수이고, SIG로 시작하는 macro를 implementation에서 따로 더 지원해도 된다.
반대로 OS에 따라 이게 다 의미가 있지는 않은 경우도 있다.
signal
Functionvoid (*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_ERR
은 signal
함수에 오류가 생겼는지 판단하게 해주는 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 type과 int
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 참고)
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) : abort
나 raise
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
raise
Funtionint raise(int sig);
signal이 주로 run-time error나 external events로 발생하긴 하지만,
직접 일으킬 수도 있다.
Paramter
code for signal
ex) raise(SIGABRT);
Return
성공시 : 0
실패시 : nonzero
<setjmp.h>
HeaderNonlocal Jumps
보통 함수가 return하면 호출된 부분으로 돌아간다. goto
statement를 사용하더라도, 같은 함수 내에서만 jump할 수 있다.
하지만 <setjmp.h>
헤더를 사용하면 한 함수에서 다른 함수로 returning 없이 jump할 수 있다.
setjmp
macro를 통해 프로그램내에서 위치를 mark하고,
longjmp
function을 통해 해당 위치로 jump할 수 있다.
주로 error handling에서 쓰인다.
int setjmp(jmp_buf env);
Paramter로 jmp_buf
variable을 넣어준다. 그럼 현재 environment를 해당 variable에 저장한다.
Return값은 0
이다.
Q. 이 함수 argument에 어떻게 값을 저장하는거지? passed by value아닌가?
A. 표준에서는 `jmp_buf`가 array type이라고 말한다.
따라서 passed by pointer라서 가능한 것이다.
C standard에서 소개하는 setjmp
사용법
(다른 방식으로 사용하면 UB이다.)
expression statement의 expression으로 사용(void
로 cast해도 됨)
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이다.)
void longjmp(jmp_buf env, int val);
이동하고자 하는 위치의 setjmp
에 parameter로 쓰인 같은 jmp_buf
변수를 첫번째 인자로 pass한다.
longjmp
는 setjmp
call로 가서 반환하게 된다.(jump)
이때의 setjmp
반환값은 longjmp
의 두번째 인자인 val
이다.
(setjmp
기본 0
반환하니까, val
이 0
이면 1
반환)
즉.. setjmp가 처음에는 0을 반환하고, 다음에 longjmp로 다시 돌아오게 되면,
그때는 setjmp가 longjmp의 두번째 인자 값을 반환한다.(이때는 0 반환하는 경우 없음)
Restriction (어길시 UB)
1. longjmp
의 첫번째 argument는 쓰이기전에 setjmp
를 사용해 initialize 돼있었어야 한다.
2. longjmp
를 호출할때 setjmp
를 호출하는 함수는 return돼있으면 안된다.
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아닌가?
다시 잘 보면 abort
나 raise
에 의해 발생하면 library 함수 써도 괜찮다고 했다.
얘는 raise
에 의해 호출됐으니 괜찮다.
longjmp
쓰고나면 변수는 어떤 값을 가지지?
대부분은 longjmp
호출할때의 값을 유지한다.
하지만 setjmp
를 포함하는 함수의 automatic variable은 indetermitate이다.
(volatile
이거나 setjmp
뒤로 수정된 적 없는 변수값은 determinate)