오늘은 좀 뜬금없지만 엔디언에 대해서 잠시 포스팅을 하려고 합니다.

흠 먼저 엔디안이란 무엇이냐! 바로 연속된 숫자를 배열하는 방법인데요. 크게 빅 엔디안과 리틀 엔디안이 있습니다. 위키피디아 정의를 잠깐 보도록 하죠.

엔디언(Endianness)은 컴퓨터의 메모리와 같은 1차원의 공간에 여러 개의 연속된 대사을 배열하는 방법을 뜻하며, 바이트를 배열하는 방법을 특히 바이트 순서(Byte order)라 한다.

엔디언은 보통 큰 단위가 앞에 나오는 빅 엔디언(Big-endian)과 작은 단위가 앞에 나오는 리틀 엔디언(Little-endian)으로 나눌 수 있으며, 두 경우에 속하지 않거나 둘을 모두 지원하는 것을 미들 엔디언(Middle-endian)이라 부르기도 한다.


출처: 위키피디아 엔디언

그림을 보니 이해가 되죠? 솔직히 우리가 일반적으로 쓰는 컴퓨터는 대부분 리틀 엔디언이랍니다. 그런데 이런 리틀 엔디언과 빅 엔디언이 어떤 차이가 있을까요? 먼저 빅 엔디언은 사람이 숫자를 읽고 쓰는 방법과 같기 때문에 디버깅과정에서 사람이 메모리의 값을 이해하기가 더 편하답니다. 반면에 리틀 엔디언은 메모리에 저장된 하위 바이트들을 컴퓨터가 떼어 내서 쓸때 별도의 연산과정이 없기 때문에 컴퓨터가 연산에 이점이 있죠. 다음 그림을 보면서 마저 이야기를 해 봅시다.

int형 포인터와 char포인터, short포인터를 이용해서 각각 같은 주소를 가리키게 만든 다음, intPtr[0]에 -20을 저장하였다고 가정하여 봅시다. 컴퓨터는 -값을 다룰때 보통 2의 보수로 저장하기 때문에 -20은 11111111 11111111 11111111 11101100 으로 변환하여 메모리에 저장하게 됩니다. 여기서 리틀 엔디안과 빅엔디안이 저장되는 방식을 표에서 한번 보죠. 리틀 엔디안은 바이트를 거꾸로 정렬해서 메모리에 집어넣고 빅엔디안은 우리가 보는 그대로 메모리에 집어넣죠? 그래서 빅 엔디안은 우리가 디버깅하며 메모리 값을 보기에 편한겁니다. 그러나 만약에 우리가 short변수로 -20을 떼어낸 다고 생각해 봅시다. 그렇다면 빅 엔디안은 shortPtr[1]을 떼어내야 -20이 정상적으로 출력 될테고, 리틀 엔디안은 그냥 short[10]를 떼어내야겠죠? 즉 리틀 엔디안은 주소의 시작값부터 스택에 넣어 읽어들이면 되지만 빅 엔디안은 저장되어 있는 범위의 맨 뒤에서부터 역방향으로 읽어야 한다는 이야기 입니다. 각각 장점과 단점이 존재하죠. 근데 사실 오늘날의 프로세서는 여러개의 바이트를 동시에 읽어들여 동시에 계산을 수행하기 때문에 두 엔디언 사이에 사실상 차이가 없다는 군요. 바이엔디언과 미들 엔디언이 또 있는데 간단하게 요약만 하고 넘어가도록 하겠습니다.

바이 엔디언: 빅 엔디언과 리틀 엔디언 둘 중 하나를 선택 할 수 있는 것.

미들 엔디언: 빅 엔디언과 리틀 엔디언 둘 다 사용하는 것.(예를 들면 32비트 정수가 2바이트 단위로는 빅 엔디언이고 그 안에서 1바이트 단위로는 리틀 엔디언인 경우)

다음은 이를 확인해 보는 c++코드 입니다. 여러분의 컴퓨터가 리틀 엔디언이라고 가정하였습니다. 만약에 두번째 줄이 위 그림의 빅엔디언 처럼 나오면 여러분의 컴퓨터는 빅 엔디언인 거겠죠^^

#include <iostream>
#include <bitset>
using namespace std;

int main() {
	int *intPtr = new int [1];
	short *shortPtr;
	char *charPtr;
	*intPtr = -20;
	shortPtr = (short*)intPtr;
	charPtr = (char*)intPtr;
	cout << "-20을 2의 보수로 표현: ";
	for (int i = 3; i >= 0; i--) {
		bitset<8> tempBit;
		tempBit = charPtr[i];
		cout << tempBit << " ";
	}
	cout << endl << "저장되어 있는 비트\t";
	for (int i = 0; i < sizeof(int); i++) {
		bitset<8> tempBit;
		tempBit = charPtr[i];
		cout << i << ":" <<tempBit << "\t";
	}
	cout << endl;
	cout << "int로 4byte를 읽었을때: "<<*intPtr<< endl;

	cout <<"shortPtr[0]: ";
	bitset<8> bitSet0(charPtr[0]);
	cout << bitSet0 << "\t";
	bitset<8> bitSet1(charPtr[1]);
	cout <<bitSet1 << "\t";
	cout << "shortPtr[1]: ";
	bitset<8> bitSet2(charPtr[2]);
	cout << bitSet2 << "\t";
	bitset<8> bitSet3(charPtr[3]);
	cout << bitSet3 << "\t";

	cout << endl << "short로 2byte씩 읽었을때: " << endl;
	for (int i = 0; i < sizeof(int) / sizeof(short); i++) {
		cout << i << "번째: " << shortPtr[i] << "\t";
	}
	cout << endl;

	return 0;
}


이미 아시는 분들도 굉장히 많으시겠지만 혹시나 도움이 될까하여 올립니다. 
아마 visual studio 2010에서는 c코드들이 잘 컴파일이 되고 실행이 되나, visual studio 2012, 2013, 2015에서는 scanf나 fopen등이 컴파일이 되지 않아 고생하시는 분들도 있을거라 생각됩니다.

일단 그 이유부터 말씀드리자면 개발자들이 함수의 오류가 발견되면, 그 함수를 deprecate해버리고 보안버젼의 새로운 함수를 추가해 버리기 때문입니다. 
(ISO에서 c++11 표준을 정하니 ms에서 visual studio 2012부터 바꾼거죠) 
예를들어 scanf의 경우 보안상의 문제로 c++ 98에서 c++11으로 넘어가는 과정에서 deprecated되버리고 scanf_s이라는 보안버젼 함수가 새로 생겨버렸습니다. 
visual studio 2010에서는 잘 컴파일이 되는데 visual studio 2012이상 버젼에서는 정상적으로 컴파일이 안되는 이유는 visual studio 2010에서는 default로 c++98을 사용하고, visual studio 2012이상 버전부터는 c++ 11을 사용하기 때문입니다. 

즉, visual studio 2012 이상 버젼부터는 scanf를 사용하면 warning이 뜨면서 컴파일이 안됩니다. 

scanf는 최근 몇년동안 보안 문제로 말이 있었죠. 뭐 이유를 알아보면 여러가지가 있지만 콘솔 io의 경우 input size를 정해주지 않으면 command를 같이 입력하는 방식으로 공격이 가능하다는데, 제가 보안쪽을 잘 몰라서 뭐라고 말을 더 못하겠군요. 뭐 이부분에 대해선 다른 블로거 님들에게 패스를 하고, 본론으로 다시 돌아가도록 하겠습니다.


이렇게 코드를 쓰고 컴파일을 하면 보통 에러가 나며 컴파일이 안됩니다.

#include <stdio.h>
int main()
{
	int inputNum;
	scanf("%d", &inputNum);
	printf("%d \n", inputNum);

	return 0;
}

이렇게 하고 start하면 아래 그림과 같은 메세지가 뜨면서 컴파일이 되지 않죠.


에러 내용을 해석해보면, scanf라는 함수가 안전하지 않으며, scanf대신 scanf_s라는 새로운 함수를 사용하는 것을 고려해 보라고 나옵니다. 또, deprecation된 함수를 사용하려면 _CRT_SECURE_NO_WARNINGS를 사용하라고 나오네요.(deprecation을 사용하지 않으려면)


그러면 지금부터 이러한 문제를 해결하는 방법을 몇가지 말씀 드리려고 합니다.

1. _CRT_SECURE_NO_WARNINGS를 사용하여 warning을 무시한다.


#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
	int inputNum;
	scanf("%d", &inputNum);
	printf("%d \n", inputNum);

	return 0;
}

이렇게 코드의 가장 윗부분에 _CRT_SECURE_NO_WARNINGS를 디파인 해주면 정상적으로 컴파일이 되는 것을 확인할 수 있습니다.

또는 에러 코드를 보면 C4996이라고 나오죠. 이 에러코드를 직접 무시하는 코드를 헤더 윗쪽에 추가 해서 정상적으로 컴파일을 하게 만드는 방법도 있습니다. 좀 자세히 말하자면 우리가 프로젝트를 만들때 SDL 체크를 사용하면 컴파일시 추가적으로 여러가지 보안 검사를 실시 하는데, 검사중에 경고 코드 C4996(사용되지 않는 함수 사용 경고)기능을 사용하지 않도록 설정하는 것입니다.


warning에 대한 링크 : warning-msdn

#pragma warning(disable: 4996)
#include <stdio.h>
int main()
{
	int inputNum;
	scanf("%d", &inputNum);
	printf("%d \n", inputNum);

	return 0;
}

이런식으로요 ㅎㅎ.

pragma에 대해선 나중에 #if, #ifdef등과 함께 나중에 한번 포스팅 하도록 하겠습니다.

2. 대세에따라 secure 버젼의 새로운 함수를 사용한다.

fopen의 경우 주소를 리턴하는게 보안상 문제가 있다고 판명되어 주소를 리턴하지 않고 인자로 받아 설정하는 fopen_s라는 secure버젼의 함수가 새로 만들어졌습니다.

사실 어떠한 함수가 deprecated가 되었다는 것은 sw전문가들이 문제가 있다고 생각되어 deprecated를 시키고 보다 안전한 함수를 새로 만들었다는 것이기 때문에 대세를 따라 secure버젼을 사용하는 것이 좋다고 개인적으로 생각합니다.


#include <stdio.h>

int main()
{
	FILE *data;
	fopen_s(&data, "test.txt", "w");
	fprintf(data, "%s", "Hello world!");
	fclose(data);

	return 0;
}

요런식으로 씁니다. ㅎㅎ


#include <stdio.h>

int main()
{
	char s[16];
	scanf_s("%s", &s, 16);//마지막에 input data size를 적어줍니다.(이게 scanf와 비교하여 바뀐부분이죠^^)
	printf(s);

	return 0;
}

scanf_s의 경우는 이런식으로 씁니다.

뭐 이러한 정보들은 구글링하면 바로바로 나오니 그때 그때 사용법을 검색해서 쓰기 쉽습니다.

유용한 사이트

CPlusPlus Page

MSDN Page

3. Project 생성시 SDL(Security Development Lifecycle) checks 옵션을 체크 해제한다.

이걸 체크해제하고 프로젝트를 만들면 추가적인 오류검사를 수행하지 않습니다. 예를 들어 함수의 반환형을 int로 해놓고 아무것도 반환하지 않아도 컴파일이 되고 실행이 됩니다. 뭐 컴파일이야 되겠지만은 별로 권장하고 싶진 않네요.


#include <stdio.h>

int main() {
	int inputNum;
	scanf("%d", &inputNum);
	printf("%d \n", inputNum);
}

위와 같은 코드도 컴파일이 됩니다.

어쨋든 SDL chechs 해제를 한번 해보죠.

1. 프로젝트를 만들어 줍니다.


2. 다음을 클릭합니다.


3. SDL checks의 체크를 해제해 줍니다.


4. 위의 코드를 한번 실행해 봅니다.

아주 잘 실행이 되는군요 ㅎㅎ



SDL에 대한 /sdl 링크 하나 남기겠습니다.

MSDN SDL

뭐 오늘 포스팅은 여기까지입니다. 마지막 방법은 되도록 쓰지 마세요 ㅎㅎ

'Programming > C/C++' 카테고리의 다른 글

Endian(Little endian, Big endian)  (0) 2015.10.08

+ Recent posts