올바른 자료형 설계로 MISRA C, MISRA CPP 규칙 위배를 사전에 줄이는 방법을 소개한다.
- MISRA C와 MISRA C++에서는 자료형이 묵시적으로 형변환 되는 것을 금지한다.
- unsigned 자료형 변수와 signed 자료형 변수가 연산할 때(Casting 충격), 큰 자료형과 작은 자료형이 연산할 때 묵시적 형변환이 발생하여 하단 규칙들을 위배한다.
- 해당 규칙들로 인하여, 개발자가 예상하지 못한 연산 결과로 인하여 side effect가 발생할 수 있다.
연관 MISRA C 2012, MISRA C++ 2008 규칙
MISRA C 2012
- MISRA_C_2012_10_03 - 표현식의 값은 더 작은 essential 타입이나 다른 essential 타입 분류에 타입를 갖는 객체에 할당되지 않아야 함
- MISRA_C_2012_10_04 - 일반 산술 변환이 수행되는 연산자의 두 피연산자들은 필히 같은 essential 타입 분류에 속하는 타입이어야 함
- MISRA_C_2012_10_05 - 수식의 값은 적절하지 않은 essential type으로 변환되지 않아야 함
- MISRA_C_2012_10_06 - 복합 표현식의 값은 더 큰 essential 타입의 객체에 할당되지 않아야 함
- MISRA_C_2012_10_07 - 복합 수식이 기본 산술 변환을 수행하는 연산자의 피연산자로 사용된 경우, 다른 피연산자는 해당 수식의 타입보다 큰 essential 타입을 가지지 않아야 함
- MISRA_C_2012_10_08 - 복합 수식의 값은 다른 essential 타입 분류에 속하는 타입이나 더 큰 essential 타입으로 변환되지 않아야 함
MISRA C++ 2008
- MISRA_CPP_05_00_03 - cvalue 표현식(expression)의 근본 타입(underlying type)을 다른 타입으로 묵시적 변환 금지
- MISRA_CPP_05_00_04 - 근본 타입(underlying type)의 부호를 변경하는 묵시적 정수 변환 금지
- MISRA_CPP_05_00_05 - 부동소수 타입과 정수 타입 사이의 묵시적 변환 금지
- MISRA_CPP_05_00_06 - 정수형이나 부동소수형의 근본 타입(underlying type)의 크기를 줄이는 묵시적 형변환 금지
- MISRA_CPP_05_00_07 - cvalue 표현식(expression)의 근본타입(underlying type)이 변하는 부동소수형과 정수형간의 명시적 변환금지
- MISRA_CPP_05_00_08 - cvalue 표현식(expression)의 근본타입(underlying type)의 크기를 증가시키는 정수형과 부동소수형간의 명시적 변환 금지
- MISRA_CPP_05_00_09 - cvalue 표현식(expression)의 정수형 근본타입(underlying type)의 부호를 변경하는 명시적 정수 변환 금지
위험 요인
- 해당 규칙 위배가 발생하는 위험 요인은 크게 2가지가 있다.
Casting 충격
- 첫 번째는 unsigned 정수 자료형 값과 signed 정수 자료형의 값을 연산 할 때 발생하는 “Casting 충격”이다.
- “Casting 충격”은 연산에서 unsigned 정수 값과 signed 정수 값을 연산(비교 연산자 포함)할 때, signed 정수 자료형 값이 unsigned 자료형으로 묵시적 형변환이 발생한다. 음수는 매우 큰 양수로 바뀌며, 이로 인하여 의도하지 않은 연산 결과가 나올 수 있다.
예제) int32_t, uint32_t 자료형 기준으로 Casting 충격 발생 int32_t 최소값: -2147483648 int32_t 최대값: 2147483647 uint32_t 최소값: 0 uint32_t 최대값: 4294967295
- a는 음수고 b는 양수로 조건문이 거짓이 될 것 같지만, “Casting 충격”이 발생하여 a는 unsigned 정수 자료형으로 묵시적 형변환되어 2147483648U이 되어 조건문은 참이 된다.
예제)
1
2
3
4
5
6
7
int32_t a = -1;
uint32_t b = 1U;
// 묵시적 형변환 발생(Casting 충격 발생), 조건문은 참
if (a > b) {
printf("true\n");
}
큰 자료형과 작은 자료형 연산
- 두 번째는 큰 자료형과 작은 자료형이 연산할 때, 묵시적으로 형변환 할 때 발생하는 경우다.
- 큰 자료형과 작은 자료형이 연산할 때, 작은 자료형이 큰 자료형으로 묵시적 형변환이 발생한다.
작은 자료형(int 자료형)이 큰 자료형(double 자료형)으로 묵시적 형변환하는 경우, 코딩 규칙(MISRA_C_2012_10_03, MISRA_C_2012_10_06, MISRA_CPP_05_00_05)을 위배 한다.
1
자료형 확장(묵시적 형변환) -> char < short < int < long < long long < float < double < long double
- 같은 크기를 가지는 unsigned 자료형은 signed 자료형 보다 크기가 크다. 예제) int32_t < uint32_t
예제)
1
2
3
4
5
6
7
8
9
int32_t foo(int64_t a, int32_t b) {
// 묵시적 형변환 발생(a는 작은 자료형인 int32_t로 묵시적 형변환)
return a + b;
}
float_t boo(double_t a, int32_t b) {
// 묵시적 형변환 발생(b는 double_t로, a + b는 float_t로 묵시적 형변환)
return a + b;
}
수정 방향
- 함수 반환값, 함수 매개변수, 변수 연산 결과가 다른 자료형으로 묵시적 형변환 되는 경우 발생하며, 명시적 형변환을 사용하여 위배된 규칙을 수정할 수 있다.
- 그러나 함수 코드가 길어지고 로직이 복잡할수록 코드 분석에 많은 시간이 소요되므로 위배 수정이 어려워 진다.
- 따라서 규칙 위배를 수정하기 위한 가장 좋은 방법은 프로그램 설계할 때 크기와 부호를 고려하여 자료형을 사용하는 것이다.
- 전체적인 프로그램 로직을 잘 고려하여, 묵시적 형변환이 발생하지 않도록 올바른 자료형으로 변수를 선언해야 한다.
해당 규칙을 수정하면 Type_Overrun, Type_Underrun, Buffer_Overrun, Buffer_Underrun 취약점 또한 예방할 수 있다.
- 다음 예제는 규칙 위배가 발생하지 않도록 변수 자료형을 설계하는 방법이다.
- 명시적 형변환을 통해 위배를 수정 할 수 있으나, 프로그램 로직을 고려하여 올바른 자료형으로 변수를 선언하는 것을 권고한다.
- 함수 로직이 길어지는 경우 묵시적 형변환이 발생하는 모든 연산에 명시적 형변환을 수행해야하며, 이로 인해 코드 가독성이 떨어지고 에상하지 못한 변수의 값 손실이 발생 할 수 있다.
예제)
1
2
3
4
5
6
7
8
9
10
11
int64_t msgSize;
int64_t remSize;
uint64_t readSize;
// 묵시적 형변환 발생
if(readSize > remSize) {
readSize = remSize;
}
if(readSize != 0){
msgSize += readSize;
}
-> (수정 방향)
1
2
3
4
5
6
7
8
9
10
11
int64_t msgSize;
int64_t remSize;
uint64_t readSize;
// 명시적 형변환
if(static_cast(readSize) > remSize) {
readSize = remSize;
}
if(static_cast(readSize) != 0){
msgSize += readSize;
}
또는
1
2
3
4
5
6
7
8
9
10
11
int64_t msgSize;
int64_t remSize;
// 변수 자료형을 수정
int64_t readSize;
if(readSize > remSize) {
readSize = remSize;
}
if(readSize != 0){
msgSize += readSize;
}
함수 설계 및 개발 방향
- 다음은 해당 규칙의 위배가 발생하지 않도록 함수 설계 단계에서 고려해볼 사항이다.
- 수정 방향이 “자료형을 잘 설계해야 한다”는 추상적인 내용이며 함수 설계 및 개발 할 때 이를 고려하기가 결코 쉽지 않다. 다음 예제와 코드로 설명하며, 조금이라도 위배 수정에 도움이 되면 좋을 것 같다.
1. 반복문의 ‘초기식 변수’ 자료형을 잘 설계해야 한다.
- 초기식 변수를 반복문 내에서 피연산자로 사용하는 경우, unsigned 자료형과 signed 자료형이 서로 묵시적 형변환으로 위배가 발생할 수 있다.
예제)
1
2
3
4
5
6
7
8
9
10
11
12
vector AxisMgr::ThreadReferenceVector(THREADS);
uint32_t bRet = 0;
for (int32_t i = 0; i < ThreadReferenceVector.size(); i++)
{
if (ThreadReferenceVector[i] == cur_t)
{
// 위배, i 자료형은 int32_t, bRet 자료형은 uint32_t
bRet = i;
break;
}
}
-> (수정 방향)
1
2
3
4
5
6
7
8
9
10
11
12
vector AxisMgr::ThreadReferenceVector(THREADS);
int32_t bRet = 0;
for (int32_t i = 0; i < ThreadReferenceVector.size(); i++)
{
if (ThreadReferenceVector[i] == cur_t)
{
// 수정, i 자료형은 int32_t, bRet 자료형은 int32_t
bRet = i;
break;
}
}
2. 반복문 ‘초기식 변수’ 자료형은 조건식 변수 자료형과 같게해야 한다.
- 만약 ‘초기식 변수’가 반복문 내부에서 연산을 수행하는 경우, 피연산자 간의 자료형을 같게해야 한다.
예제)
1
2
3
4
5
6
7
8
9
uint32_t msgSize;
uint32_t index;
// 위배, i 자료형은 int32_t, msgSize 자료형은 uint32_t
for (int32_t i = 0; i < msgSize; i++)
{
// ...
index += i;
}
-> (수정 방향)
1
2
3
4
5
6
7
8
9
uint32_t msgSize;
uint32_t index;
// 수정, i 자료형은 int32_t, msgSize 자료형은 int32_t
for (uint32_t i = 0; i < msgSize; i++)
{
// ...
index += i;
}
3. 배열 요소를 참조할 때 사용하는 ‘인덱스, 길이, 크기’ 등은 size_t 자료형 사용을 고려해야 한다.
- 일반적으로 시스템 라이브러리에서는 ‘인덱스, 길이, 크기’를 표현하기 위해 size_t 자료형을 사용한다.
- size_t 자료형은 시스템에서 표현할 수 있는 가장 큰 unsigned 정수 자료형으로, Linux 64bit GCC 8.1에서 size_t는 typedef unsigned __int64 size_t;로 정의되어 있다.
- C++ 컨테이너에서 사용하는 size_type 자료형은 size_t 자료형과 같다.
sizeof 연산자는 size_t를 반환한다.
- C에서 size_t 자료형을 사용하는 대표적인 함수는 다음과 같다.
1 2 3 4 5
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream); int strncmp(const char* str1, const char* str2, size_t num); int memcmp(const void* ptr1, const void* ptr2, size_t num); void* memset(void* ptr, int value, size_t num); void* malloc(size_t size);
- C++에서 size_t, size_type 자료형을 사용하는 예제는 다음과 같다.
1 2 3 4
std::vector::size, size_type size() const noexcept; std::vector::size, size_type max_size() const noexcept; std::basic_string::find, string (1) size_type find (const basic_string& str, size_type pos = 0) const noexcept; std::stoll, long long stoll (const wstring& str, size_t* idx = 0, int base = 10);
예제)
1
2
3
4
5
vector AxisMgr::ThreadReferenceVector(THREADS);
// vector.size() 함수는 벡터의 크기를 반환
// 위배, i 자료형은 int32_t, ThreadReferenceVector.size() 자료형은 size_t
for (int32_t i = 0; i < ThreadReferenceVector.size(); i++)
-> (수정 방향)
1
2
3
4
5
vector AxisMgr::ThreadReferenceVector(THREADS);
// vector.size() 함수는 벡터의 크기를 반환
// 벡터의 크기를 구하는 size() 함수는 size_t 자료형을 반환
for (size_t i = 0; i < ThreadReferenceVector.size(); i++)
예제)
1
2
3
4
5
queue msgQueue;
// msgQueue.size() 함수는 큐의 크기를 반환
// 위배, msgQueue.size() 자료형은 size_t 이지만, signed int 상수를 뺄셈
uint32_t index = msgQueue.size() - 1;
-> (수정 방향)
1
2
3
4
5
queue msgQueue;
// msgQueue.size() 함수는 큐의 크기를 반환
// 수정, msgQueue.size() 자료형은 size_t 이지만, unsigned int 상수를 뺄셈
uint32_t index = _sgQueue.size() - 1U;
4. 매크로 및 매크로 함수를 사용하지 않는다.
- 매크로 및 매크로 함수는 자료형을 검사하지 않으므로, 잘못된 자료형으로 형변환 될 수 있다. 또한 매크로가 코드로 치환되는 과정에서 개발자가 예상하지 못한 연산자 우선순위 영향으로 인하여 의도하지 않은 결과가 나올 수 있다.
- 따라서 C/C++에서 매크로 및 매크로 함수를 유의하여 사용해야 한다.
- MiSRA C 2012에서는 매크로를 사용할 수 있지만, 매크로 함수 사용을 금지한다.(MISRA_C_2012_DIR_04_09)
- MISRA CPP 2008에서는 include guard를 위한 매크로 이외의 매크로 사용을 금지한다. (MISRA_CPP16_02_02)
기개발된 코드와의 호환성을 위해 매크로 관련 규칙을 정적시험에서 사전 제외하는 사례가 빈번하게 있다.
- C에서 매크로에 리터럴 접미사(예제, 1000U, 234L 등) 를 사용하면 위배를 수정할 수 있으며, 매크로 대신 전역 상수 및 enum으로 대체할 수 있다.
- C++에서 매크로를 클래스 정적 멤버 상수 또는 enum으로 대체할 수 있다.
- C/C++에서 매크로 함수는 인라인 함수로 대체하여 사용 할 수 있다.
예제)
1
2
3
4
5
6
7
8
#define BUF_SIZE 100
uint32_t length;
// 위배, i 자료형은 int32_t, BUF_SIZE는 signed 정수 자료형으로 인식
for(int32 i = 0; i < length - BUF_SIZE; i++) {
}
-> (수정 방향)
1
2
3
4
5
6
7
8
#define BUF_SIZE 100U
uint32_t length;
// 수정, i 자료형은 int32_t, BUF_SIZE는 unsigned 정수 자료형으로 인식
for(uint32 i = 0; i < length - BUF_SIZE; i++) {.
}
예제)
1
2
3
4
5
#define MESSAGE_BYTES_SIZE ((((36) + 4) / 4 ))
uint32_t n3DataWords;
// 위배, n3DataWords 자료형은 uint32_t, MESSAGE_BYTES_SIZE는 signed 정수 자료형으로 인식
int32_t sizeWord = n3DataWords + MESSAGE_BYTES_SIZE;
-> (수정 방향)
1
2
3
4
5
#define MESSAGE_BYTES_SIZE ((((36U) + 4U) / 4U ))
uint32_t n3DataWords;
// 수정, n3DataWords 자료형은 uint32_t, MESSAGE_BYTES_SIZE는 unsigned 정수 자료형으로 인식
int32_t sizeWord = n3DataWords + MESSAGE_BYTES_SIZE;
또는
1
2
3
4
5
#define MESSAGE_BYTES_SIZE ((((36) + 4) / 4 ))
int32_t n3DataWords;
// 수정, n3DataWords 자료형은 int32_t, MESSAGE_BYTES_SIZE는 signed 정수 자료형으로 인식
int32_t sizeWord = n3DataWords + MESSAGE_BYTES_SIZE;
5. 프로그램 로직을 고려하여 클래스 멤버 필드 자료형, 구조체 멤버 변수 자료형, 함수 반환 및 매개변수 자료형, 변수 자료형 등을 잘 설계해야 한다.
- 함수 호출 할 때 전달 인자 자료형 및 반환 자료형을 고려하여 변수를 선언해야 한다.
예제)
1
2
3
4
5
6
7
8
9
10
11
int32_t GetId();
uint32_t id;
int32_t label;
int32_t subLabel;
// 위배, id 자료형은 uint32_t, GetId() 반환값 자료형은 int32_t
id = GetId();
label = (id % 100000) / 10000;
subLabel = (id % 1000) / 100;
id = id * 100;
-> (수정 방향)
1
2
3
4
5
6
7
8
9
10
11
int32_t GetId();
int32_t id;
int32_t label;
int32_t subLabel;
// 수정, id 자료형은 int32_t, GetId() 반환값 자료형은 int32_t
id = GetId();
label = (id % 100000) / 10000;
subLabel = (id % 1000) / 100;
id = id * 100;
예제)
1
2
3
4
5
6
void SetOffset(uint32_t byteOffset, uint32_ bitOffset);
int32_t GetBitOffset();
uint32_t wordBitOffset;
// 위배, wordBitOffset 자료형은 uint32_t, GetBitOffset() 함수 반환값 자료형은 int32_t
inBuffer.SetOffset(wordBitOffset + GetBitOffset());
-> (수정 방향)
1
2
3
4
5
6
void SetOffset(uint32_t byteOffset, uint32_ bitOffset);
uint32_t GetBitOffset();
uint32_t wordBitOffset;
// 수정, wordBitOffset 자료형은 uint32_t, GetBitOffset() 함수 반환값 자료형은 uint32_t
inBuffer.SetOffset(wordBitOffset + GetBitOffset());
Comments powered by Disqus.