본문 바로가기

Software

Secure Cordng C - Preprocessor

PRE-00 함수형의 매크로보다는 인라인이나 정적 함수를 사용하라


일반 함수는 함수 호출의 오버헤드가 존재한다. 때문에 간단한 코드에 대한 함수 호출은 성능 상의
이슈가 발생할 수 있다. 그래서 전통적인 프로그래머들은 간단한 코드에 대해 일반 함수가 아닌
함수형 매크로를 사용하기도 한다. 그러나 매크로는 컴파일러에 의한 평가가 아닌 단순 치환 

구조이기 때문에 부수 효과가 발생할 수 있다.

인라인 함수란 함수 호출 코드가 함수의 기계어 코드로 치환되는 함수를 의미한다. 인라인 함수는

컴파일 타임에 처리되므로 함수 호출의 오버헤드가 없어진다. 그러므로 함수형 매크로보다는

인라인 함수를 사용하는 것이 좋다.



위험한 코드 1

  • 매크로 함수 호출 시 사용된 인자에 대한 증가, 감소, 메모리 변수 접근 등은 부수 효과를 발생시킬 수 있다.
#include <stdio.h>

#define SQR(x)    ((x) * (x))    // 16

int main(){
int i = 2;
printf("%d\n", SQR(++i));

return 0;
}



  • 해결 방법 - 인라인 함수를 사용하여 해결할 수 있다.
#include <stdio.h>

inline int sqr(int x){
return x * x;
}

int main(){
int i = 2;
printf("%d\n", sqr(++i));

return 0;
}



위험한 코드 2

  • 다음의 코드는 매크로를 사용해 전역 카운터를 증가시키는 코드이다.
    하지만 매크로가 치환되면서 지역 카운터를 증가시키고 있다.
#include <stdio.h>

size_t cnt = 0;
#define CALL_FUNC(fp) (++cnt, fp())

void print_cnt(){
printf("cnt = %d\n", cnt);
}

int main(){
size_t cnt = 0;
CALL_FUNC(print_cnt);

return 0;
}


  • 해결 방법 - 인라인 함수를 사용한다. 인라인 함수는 함수 구현부가 컴파일될 때 식별자를 전역 변수로 처리한다.
#inlcude <stdio.h>

size_t cnt = 0;

inline call_func(void(*fp)()){
++cnt;
fp();
}

void print_cnt(){
printf("cnt = %d\n", cnt);
}

int main(){
size_t cnt = 0;
call_func(print_cnt);

return 0;
}



PRE-01 매크로에서는 매개 변수에 괄호를 사용하라



부수 효과를 줄이려면 반드시 매개 변수에 괄호를 사용해야 한다.



위험한 코드

  • 아래의 함수형 매크로는 괄호를 사용하지 않아 문제가 된다.
#include <stdio.h>

#define SQR(x)    x * x

int main(){
int result = SQR(1 + 2);    // 1 + 2 * 1 + 2
printf("result = %d\n", result);

return 0;
}



  • 해결 방법 - 매크로의 모든 매개변수에 괄호를 사용하면 된다.
#include <stdio.h>

#define SQR(x)    (x) * (x)

int main(){
int result = SQR(1 + 2);    // 1 + 2 * 1 + 2
printf("result = %d\n", result);

return 0;
}



PRE-02 매크로로 치환될 영역은 반드시 괄호로 둘러싸야 한다



매크로로 치환될 영역을 괄호로 둘러싸면 근처의 표현식으로 인해 우선순위가 바뀌는 일을 방지할 수 있다.



위엄한 코드1

  • 의도하지 않은 결과과 나오게 된다.
#include <stdio.h>

#define DBL(x)    (x) + (x)

int main(){
int result = DBL(2) * DBL(2);
printf("result = %d\n", result);

return 0;
}



  • 해결 방법 - 매크로로 치환될 영역을 괄호로 둘러싼다
#include <stdio.h>

#define DBL(x)    ((x) + (x))

int main(){
int result = DBL(2) * DBL(2);
printf("result = %d\n", result);

return 0;
}



위험한 코드2

  • 치환된 코드가 수식으로 평가되어 정상적으로 동작하지 않는다.
#include <stdio.h>

#define INFO    -1
#define WARN  -2
#define ERR     -3

void log_print(int level, const char *msg){
if(level == INFO)
printf("    [INFO] %s\n", msg);
else if(level == WARN)
printf("    [WARNING] %s\n", msg);
else if(level == ERR)
printf("    [ERROR] %s\n", msg);
}

int main(){
log_print(ERR, "out of service");
return 0;
}



  • 해결 방법 - 치환될 영역은 반드시 괄호로 둘러싸거나 열거 타입의 상수로 치환한다.
#include <stdio.h>

#define INFO    (-1)
#define WARN  (-2)
#define ERR     (-3)

void log_print(int level, const char *msg){
if(level == INFO)
printf("    [INFO] %s\n", msg);
else if(level == WARN)
printf("    [WARNING] %s\n", msg);
else if(level == ERR)
printf("    [ERROR] %s\n", msg);
}

int main(){
log_print(ERR, "out of service");
return 0;
}



PRE-03 타입 인코딩 시 매크로 정의 대신 타입 정의를 사용하라



타입 정의는 스코프(scope) 규칙이 적용되는 반면 매크로 정의는 적용되지 않는다.



위험한 코드

  • 아래의 코드에서 name은 포인터로 선언되었지만 tel은 포인터로 선언되어 있지 않다.

#include <stdio.h>


#define cstring char *


int main(){

cstring name, tel;


name = "Michael";

tel = "010-0000-0000"

printf("name: %s, tel: %s\n", name, tel);


return 0;

}



  • 해결 방법 - 타입 정의를 사용한다.
#include <stdio.h>


typedef char * cstring;


int main(){

cstring name, tel;


name = "Michael";

tel = "010-0000-0000"

printf("name: %s, tel: %s\n", name, tel);


return 0;

}




PRE-04 토큰들을 연결하거나 문자열 변환을 할 때 매크로 치환을 고려하라



매크로 연산자

  • ## - 매크로가 치환되는 과정에서 두 개의 토큰을 하나로 병합(concatencation)
  • # - # 연산자 다음에 있는 토큰을 문자열화


위험한 코드

  • 아래의 코드는 줄 번호가 의도한대로 출력되지 않는다.
#define TOSTR    #x
#defien LOG(msg)    message("["__FILE__"("TOSTR(__LINE__)"(] "msg)

int main(){
#pragma LOG("hello, world");
return 0;
}



  • 해결 방법 - 매크로 인자를 치환한 다음에 문자열로 만들려면 두 단계의 매크로를 사용해야 한다.
#define _TOSTR(x)    #x
#define TOSTR    _TOSTR(x)
#defien LOG(msg)    message("["__FILE__"("TOSTR(__LINE__)"(] "msg)

int main(){
#pragma LOG("hello, world");
return 0;
}



PRE-05 헤더 파일에 항상 인클루드 가드를 둬라



소프트웨어 개발 프로젝트에서 헤더 파일의 중복으로 인한 문제가 일어나기 쉽다.

때문에 헤더파일을 설계할 때는 반드시 인클루드 가드(Include guard)를 사용해야 한다.



위험한 코드

  • 다음 코드는 헤더 파일의 중복으로 인한 문제가 발생할 수 있다.
typedef struct _Complex{
double real, image;
}Complex;

#define PI    (3.14)


  • 해결 방법 - 인클루드 가드를 적용한다.
#ifndef _MATH_H_
#define _MATH_H_

typedef struct _Complex{
double real, image;
}Complex;

#define PI    (3.14)

#endif



PRE-06 복수 구문 매크로를 do-while 루프로 감싸라


여러 실행문을 그룹으로 만들어 연속적으로 실행하는 경우, 구문상 루프로 묶어줘야만 나중에 매크로가 if처럼

한개의 실행문이나 중괄호로 묶인 실행 단위를 처리하는 부분에서 사용될 때 안전하게 작동한다.


위험한 코드

  • 다음은 복수 구문으로 정의된 함수형 매크로를 사용할 경우, 의도하지 않은 결과를 초래한다.
#include <stdio.h>

#define SWAP(x, y) \
tmp = x;         \
x = y;            \
y = tmp;

int main(){
int x, y;
int tmp;

scanf("%d %d", &x, &y);
if(x != y)
SWAP(x, y);

printf("x = %d, y = %d\n", x, y);

return 0;
}



  • 해결 방법 - do-while 루프로 감싼다.
#include <stdio.h>

#define SWAP(x, y) \
do{                \
tmp = x;          \
x = y;             \
y = tmp;          \
}while(0)

int main(){
int x, y;
int tmp;

scanf("%d %d", &x, &y);
if(x != y)
SWAP(x, y);

printf("x = %d, y = %d\n", x, y);

return 0;
}




PRE-07 절대로 불안전한 매크로를 할당, 증가, 감소, 메모리 변수 접근, 함수 호출과 함께 사용하지 마라



함수형 매크로에서 사용된 매개 변수에 대하여 증가 또는 감소 등의 연산은 부수 효과(side effect)를 발생시킬 수 있으므로

사용하면 안된다.



위험한 코드

  • 다음의 코드는 의도하지 않은 결과를 초래한다.
#incldue <stdio.h>

#define CUBE(x)    ((x) * (x) * (x))

int main(){
int n = 2;
printf("%d\n", CUBE(++n));

return 0;
}


  • 해결 방법 1. - 매개 변수에 대하여 증가 또는 감소를 수행하지 않는다. 

#incldue <stdio.h>


#define CUBE(x)    ((x) * (x) * (x))

int main(){
int n = 2;
++n;
printf("%d\n", CUBE(n));

return 0;
}



  • 해결 방법 2. - 함수의 이름에 안전하지 않음을 알린다.
#incldue <stdio.h>

#define CUBE_UNSAFE(x)    ((x) * (x) * (x))

int main(){
int n = 2;
printf("%d\n", CUBE_UNSAFE(++n));

return 0;
}



  • 해결 방법 3. - 인라인 함수로 정의한다.
#incldue <stdio.h>

inline int cube(int x) { return x * x * x; }

int main(){
int n = 2;
printf("%d\n", cube(++n));

return 0;
}


반응형