DCL-00 변하지 않는 객체는 const로 보장해둬라
변하지 않는 객체는 const로 보장해야 한다. 객체의 불변성을 const를 사용해 보장하면
애플리케이션의 정확성과 안전성을 보장하는데 도움이 된다.
부적절한 코드
- 다음의 코드에서 객체의 값이 의도하지 않게 변경되고 있다.
- 해결 방법 - const 키워드를 사용하면 이를 해결할 수 있다.
DCL-01 내부 스코프에서 변수 이름을 재사용하지 마라
변수의 이름을 재사용할 경우, 코드 상에서 혼란을 가중시키게 된다.
때문에 다음의 경우는 피해야 한다.
- 전역 변수가 사용될 수 있는 범위 내에서 어떤 변수든 중복해서 전역 변수 이름을 사용하면 안된다.
- 어떤 블록 안에서 이미 사용되고 있는 변수와 동일한 이름으로 다른 블록에서 선언하면 안된다.
- 지역 변수가 전역 변수의 이름을 가리게 되므로 코드는 정상적으로 수행되지 않는다.
- 해결 방법 - 변수의 이름을 재사용하고 있다는 것은 변수의 이름 자체가 너무 일반적이라는 것을 의미하기 때문에
변수의 이름을 좀 더 설명적으로 정의한다.
}
DCL-02 상수 수식의 값을 테스트할 때는 정적 어썰션(static assertion)을 사용하라
어썰션은 취약성이 될 수 있는 소프트웨어의 결점을 찾아 제거하는데 사용되는 효과적인 진단 도구이다.
다만 일반적인 어썰션(assert())를 사용하는 것은 몇 가지 제약이 존재하며 다음과 같다.
- 일반적인 어썰션 도구는 프로그램이 구동 중에 동작하므로 런타임 오버해드(overhead)가 존재한다.
- 일반적인 어썰션 도구의 마지막 동작은 abort()를 호출하는 것이므로, 서버 프로그램이나 임베디드 시스템에서는 사용하기 어렵다.
- 다음 코드는 구조체의 패딩 비트를 검사하기 위해 assert 함수를 사용하고 있다.
진단은 런타임에만 일어나고 그것도 assert 함수가 포함된 코드가 실행될 때만 발견된다.
- 문제 해결 - 정적 어썰션을 구현하여 해결한다.
DCL-03 프로그램 로직 상의 고정적인 값(Hard cording value)을 나타낼 때는 의미 있는 심볼릭 상수를 사용하라
코드 상에서 리터럴(literal)을 사용할 경우, 가독성이 떨어질 수 있다. 때문에 가급적 리터럴을 직접 사용하기 보다는 심볼릭(symbolic)
상수를 통해 적절한 이름을 붙여 코드의 의도를 명확히 하는 것이 좋다. C 언어에서 심볼릭 상수를 만두는 방법은 다음과 같다.
- const로 지정된 객체
- 열거형 상수
- 객체형 매크로
- 구조체 내부의 비트 단위로 정의된 멤버의 크기
- 배열의 크기(가변 배열은 예외)
- 열거형 상수의 값
- case 상수의 값
#define identifier replacement-list
객체형 매크로는 전처리기에 의해 치환되는 구조이므로 컴파일 과정에서는 매크로의 심볼을 볼 수 없다.
그래서 대부분의 컴파일러들은 매크로 이름들을 따로 저장하고 디버거에 전달하기도 한다.
매크로는 다음 이름으로 적용될 수 있는 스코프 규칙들을 고려하지 않았기 때문에 의도하지 않는 방식으로 치환되어 기대하지 않은 결과를
만들기도 한다. 그리고 객체형 매크로는 메모리를 소비하지 않으며 따라서 포인터로 가리킬 수 도 없다.
C 프로그래머들은 심볼릭 상수로 객체형 매크로를 사용하는 편이다.
정리하면 다음과 같다.
방식 |
평가 시점 |
메모리 소비 |
디버깅 시의 심볼 |
타입 체크 |
컴파일 타임 상수 표현식 |
열거형 |
컴파일 타임 |
없음 |
있음 |
있음 |
있음 |
const 지정 |
런타임 |
있음 |
있음 |
있음 |
없음 |
매크로 |
전처리 |
없음 |
없음 |
없음 |
있음 |
1. 부적절한 코드
- 정수 리터럴에 대한 의미가 분명하지 않다.
void draw_color(int color){
switch(color){
case 0: /* ... */ break;
case 1: /* ... */ break;
case 2: /* ... */ break;
}
}
int main(){
//...
draw_color(0);
return 0;
}
- 해결 방법 - 심볼릭 상수로 변경하여 의미를 분명하게 한다.
#define RED 0
#define BLUE 0
#define GREEN 0
void draw_color(int color){
switch(color){
case RED: /* ... */ break;
case BLUE: /* ... */ break;
case GREEN: /* ... */ break;
}
}
int main(){
//...
draw_color(0);
return 0;
2. 부적절한 코드
- 버퍼의 크기가 일치하지 않다, 버퍼 오버플로가 발생할 수 있다.
#include <stdio.h>
int main(){
char buff[16];
gets(buff, 32, stdin);
printf("%s\n", buff);
return 0;
}
- 해결 방법 1 : 열거형을 사용하여 해결한다.
int main(){
char buff[BUFF_SIZE];
gets(buff, BUFF_SIZE, stdin);
printf("%s\n", buff);
return 0;
}
- 해결 방법 2: sizeof 연산자를 사용한다.
#include <stdio.h>
int main(){
char buff[16];
gets(buff, sizeof(buff), stdin);
printf("%s\n", buff);
return 0;
DCL-04 함수 선언 시 적절한 타입 정보를 포함시켜라
함수 호출 전 반드시 함수의 선언 정보가 반드시 존재해야 한다. 이는 함수 선언 정보가 없을 경우, 컴파일러는 타입 정보를 정확하게
체크할 수 없기 때문이다. 표준 라이브러리의 함수를 사용할 때 함수 선언 정보를 삽입하는 방법은 적절한 헤더 파일을 삽입하는 것이다.
함수의 선언 정보가 없는 상태로 함수를 호출할 경우, 컴파일러는 경고를 내지만 에러는 발생하지 않는다.
1. 부적절한 코드
- 함수의 선언 정보 없이 함수를 호출하면 잘못된 값이 인자로 전달될 수 있다.
- 해결 방법 - 함수의 선언 정보를 정확하게 기술한다.
2. 부적절한 코드
- 잘못된 함수 포인터를 선언해서 사용하면 의도하지 않는 결과가 나타날 수 있다.
- 해결 방법 - 함수 포인터를 정확하게 정의한다.
DCL-05 error 에러 코드를 반환하는 함수의 타입을 error_t로 정의하라
에러 코드를 반환하는 함수들은 보통 리턴 타입을 int로 사용한다. 이 때, 리턴 타입 정보만으로는 에러 코드를 반환하는지에 대한 구분이 명확하지 않다. 이 때, typedef 키워드를 사용하면 가독성을 높일 수 있다.
1. 부적절한 코드
- 함수가 에러 코드를 반환하는지 알 수 없다.
- 해결 방법 - 반환 타입을 error_t 타입으로 변경하여 함수가 에러 코드를 반환한다는 것을 주지시킨다.
DCL-06 가변 인자를 가진 함수에서는 함수 작성자와 함수 사용자 간의 약속이 지켜져야 한다.
가변 인자 함수는 여러 개의 인자를 취하는데 문제의 소지가 있다. 때문에 가변 인자 함수의 정확한 사용 방법을
인지하여 의도하지 않는 결과가 발생되지 않도록 해야 한다.
1. 부적절한 코드
- 아래의 함수에서 마지막 인자로 VA_END 값을 넘겨주지 않을 경우, 코드는 정상적으로 동작하지 않을 수 있다.
#include <stdio.h>
#include <stdarg.h>
enum { VA_END = -1 };
int average(int first, ...){
int sum =0;
int cnt = 0;
va_list args;
va_start(args, first);
int i = first;
while(i != VA_END){
sum += i;
++cnt;
i = va_arg(args, int);
}
va_end(args);
return cnt ? sum / cnt : 0?
}
int main(){
int avg = average(100, 100, 100, 100);
printf("%d\n", avg);
return 0;
}
- 해결 방법 1 - 마지막 인자로 VA_END를 넣어 해결한다.
#include <stdio.h>
#include <stdarg.h>
enum { VA_END = -1 };
int average(int first, ...){
int sum =0;
int cnt = 0;
va_list args;
va_start(args, first);
int i = first;
while(i != VA_END){
sum += i;
++cnt;
i = va_arg(args, int);
}
va_end(args);
return cnt ? sum / cnt : 0?
}
int main(){
int avg = average(100, 100, 100, 100, VA_END);
printf("%d\n", avg);
return 0;
- 해결 방법 2 - average 함수를 다시 설계한다.
#include <stdio.h>
#include <stdarg.h>
int average(int cnt, ...){
int sum =0;
int cnt = 0;
va_list args;
va_start(args, first);
int sum = first;
int i;
for(i = 0; i < cnt; i++)
sum += va_avg(args, int);
va_end(args);
return cnt ? sum / cnt : 0?
}
int main(){
int avg = average(4, 100, 100, 100, 100);
printf("%d\n", avg);
return 0;
DCL-07 불투명한 타입을 사용해 추상 데이터 타입을 구현한다.
추상 데이터 타입(ADT, Abstract Data Type)은 프로그램의 대상이 되는 사물이나 현상을 추상화하여 정의한 것이다.
추상 데이터 타입을 사용하면 그 타입의 세부적인 구현을 감출 수 있기 때문에 정보 은닉(information hiding)을 할 수 있다.
1. 부적절한 코드
- 아래의 코드에서 스택 자료구조의 내부 구조가 노출되어 외부로부터 내부 데이터를 보호 할 수 없다.
- 해결 방법 - 불완전한 타입으로 선언한 후, 포인터를 사용한다.
DCL-08 함수의 의해 바뀌지 않을 값에 대한 포인터를 함수의 매개 변수로 사용할 때는 const로 정의하라.
포인터를 매개 변수로 하는 함수는 내부적으로 대상체의 값을 변경하는 위험이 존재한다.
때문에 의도하지 않는 변경을 막으려면 const 키워드를 사용해야 한다.
1. 부적절한 코드
- 다음의 코드는 인자로 전달된 배열의 값을 임으로 변경하고 있다.
- 해결 방법 - 상수 객체를 가리키는 포인터로 변경한다.
DCL-09 현재 범위를 넘어서까지 사용되지 않을 객체는 static으로 선언해라
객체나 함수를 현재 스코프의 외부에 노출시키지 않을 것이라면 static으로 선언해야 한다.
C99 표준에서는 static에 대하여 다음과 같이 언급하고 있다.
- 한 파일 스코프 내에서 객체나 함수에 대한 식별자의 선언이 static을 포함하고 있으면 식별자는 내부에서만 결합(사용)된다.
- 한 파일 스코프에서 객체나 함수에 대한 식별자의 선언에 static과 같은 저장 클래스 지정자가 없으면 식별자는 외부에서
결합(사용) 된다.
- square 함수가 내부적(internal)으로 사용된다고 가정했을 때, 다음의 코드는 위험하다.
// Calc.h------------------------------------ double circle_area(int radius); // ------------------------------------------ // Calc.c------------------------------------- int square(int x){ return x * x; } double circle_are(int radius){ return square(radius) * 3.14; } // ------------------------------------------ |
#include <stdio.h> // in Calc.h double circle_area(int radius); int main(){ printf("%lf\n", circle_area(2)); printf("%lf\n", square(2)); return 0; } |
- 해결 방법 - square 함수를 내부 연결성(internal linkage)를 갖도록 static으로 선언한다.
// Calc.h------------------------------------ double circle_area(int radius); // ------------------------------------------ // Calc.c------------------------------------- static int square(int x){ return x * x; } double circle_are(int radius){ return square(radius) * 3.14; } // ------------------------------------------ | #include <stdio.h> // in Calc.h double circle_area(int radius); int main(){ printf("%lf\n", circle_area(2)); printf("%lf\n", square(2)); return 0; } |
DCL-10 객체를 선언할 때 적절한 지속공간을 지정하라
객체가 자신의 수명을 다한 후에도 참조된다면 정의되지 않은 행동을 유발할 수 있다.
예를 들어 수명을 다한 객체를 참조하는 포인터는 정의되지 않은 값을 갖게 된다.
수명을 다한 객체에 접근하는 것은 매우 위험하고 취약성을 만드는 결과를 초래하기도 한다.
1. 부적절한 코드
- 수명을 다한 지역 객체를 전역 변수가 참조함으로 임의의 코드를 수행할 수 있는 위험성이 존재한다.
- 해결 방법 - 전역 포인터가 유효한 객체를 가리키도록 한다.
- 다음 코드는 지역 객체의 주소를 반환하고 있다.
- 해결 방법 - 초기화할 배열을 함수의 인자로 받아 처리한다.
DCL-11 함수 인자에서 restrict로 지정된 소스 포인터와 목적 포인터가 동일한 객체를 참조하지 않게 하라
restrict 키워드는 오직 포인터에만 적용되는 키워드로 그 포인터가 데이터 객체에 접근할 수 있는 유일하고도 최초가 되는 수단임을
나타낸다. 즉 포인터가 restrict로 한정되면 그 포인터가 가리키는 데이터 블록은 그 포인터만이 접근이 가능하므로 컴파일러가 더
효율적으로 코드를 최적화 할 수 있다.
그러나 restrict 지정자를 사용할 때는 포인터들이 서로 동일한 객체를 참조하지 말아야 한다.
함수의 두 포인터가 동일한 객체를 참조하는 경우 그 결과는 미정의 동작이다.
1. 부적절한 코드
- memcpy는 restrict 지정자를 사용한 함수로 동일한 객체의 주소를 인자로 전달하면 그 결과는 알 수 없다.
- 해결 방법 1 - 메모리의 순서를 고려하지 않는다면 마지막 요소를 앞쪽에 복사한다.
- 해결 방법 2 - memmove 함수를 사용하여 처리한다. memmove 함수는 내부적으로 버퍼를 가지고 있어 안전하게 복사가 가능하다.
DCL-12 캐시되어서는 안되는 데이터에는 volatile을 사용하라
일반적으로 CPU는 성능 상의 이유로 데이터를 메모리에서 직접 읽어오지 않고 캐시를 사용하여 읽어온다.
프로그램이 아닌 하드웨어에 의해 변경되는 데이터는 캐시에 반영하지 않으므로, 메모리에서 직접 읽어와야 한다.
volatile는 데이터가 프로그램이 아닌 외부적인 요인에 의해 그 값이 변경될 수 있다고 컴파일러에 알려 캐싱을 제한할 때 사용한다.
1. 부적절한 코드
- 최적화에 의해 flag가 캐시되었다면 SIGINT를 받아도 종료되지 않는다.
- 해결방법 - 변수를 volatile로 선언한다.
DCL-13 함수 정의와 맞지 않는 타입으로 함수를 변환하지 마라
원래의 타입과 다른 타입의 함수를 참조하도록 함수 포인터를 사용하면 정의되지 않는 결과를 초래한다.
때문에 함수 포인터를 사용할 경우, 정확한 타입을 선언해서 사용해야 한다. C99에서는 다음과 같이 언급하고 있다.
- 특정 타임에 대한 함수의 포인터는 다른 타입의 함수 포인터로 변환될(컴파일러의 정책에 의해 변경가능한 상태) 수 있고, 결과는 원래의 포인터와 같아진다. 호환되지 않는 타입의 함수를 호출하는데 함수 포인터가 사용되면 알 수 없는 행동을 초래한다.
- 함수 포인터와 함수의 타입이 일치하지 않아 정의되지 않는 결과를 초래한다.
- 해결 방법 - 정확한 타입의 포인터 변수를 선언한다.
'Software' 카테고리의 다른 글
Error message 모음 (0) | 2019.02.08 |
---|---|
Secure Cordng C - Expression (0) | 2018.08.30 |
Secure Cordng C - Preprocessor (0) | 2018.07.23 |
Secure Cordng C - Coding Style (0) | 2018.07.20 |
Secure Cordng C - 포인터의 개념과 이해 #3 (0) | 2018.07.18 |