본문 바로가기

Software/C/C++

[C++] 비트 필드의 인식과 코딩의 자유로움에 대하여

 

비트 필드의 인식과 코딩의 자유로움에 대하여


                                                                        작성일 : 2001년 4월 28일

                                                                        재작성일 : 2001년 9월 3일

                                                                        작성자 : 고임

http://www.dcclick.net ( 고임의 동아리.. )

http://www.devpia.com ( 고임이 자주 가는 개발자 홈페이지 )


이 글은 제 동아리 홈페이지의 질문 답란에서 답란으로 적었던 저의 글을 토대로 틀린 부분에 대한 수정과 재 편집하여 작성되었습니다. 만일 문서를 편집하여 사용하고 싶으신 분은 걍 맘대로 하시고.. 원작자에 대한 예의만 지켜주시면 감사 드리겠습니다.

단! 이 문서의 대한 내용을 상업적으로는 절대 불가 합니다. 만일 그렇게 사용하고 싶으시면.. 꽁자로 베풀던지요. 푸헤헤.. 상업적으로 쓰일 때가 있을진 모르겠지만.. 쩝. --;;



들어가며

이 글을 처음 시작하게 된 건 누군가 나의 동아리 사이트에 비트 필드에 관한 질문을 올려 놓기 시작하면서부터였다. C의 문법책에 보면 단 몇 줄로 설명을 써놓은 어떻게 보면 소외된 문법 중에 하나이다. C언어에서 중요했던 건 포인터니 또는 for문이니 하는 것들이었을 뿐.. 작은 몸짓으로 거대한 힘을 발휘하게 하는 이 비트필드에 관한 이야기는 정작 찾아볼 수 없었던 것이다. 비트필드는 실제적으로 많이 쓰이지는 않는 문법 중에 하나이다.  그만큼 쓰는 곳이 한정되어 있으며, 없더라도 그만일 수 있는 것이 바로 이 비트 필드에 관한 이야기가 다루어지지 않는 이유 중에 하나이다. 그에 따라 비트 필드에 대해서 설명해 놓은 책도 거의 없고, 비트 필드는 지겹게 사용했던 필자조차도 이번 문서 작성을 하면서 잘못 알고 있던 부분을 발견하고야 말았다. --;;따라서 답변자체도 엉망으로 해버렸던.. 슬픈 일을 맞이하게 만들었다. 이에 비트필드를 사용하고자 하는 사람들이 좀더 쉽게 비트필드의 개념을 익히고, 실무에 적용을 빠르게 할 수 있게 하기 위해 이 문서를 작성을 하기로 한다.

 이 작은 문법을 하나 앎으로써 얻게 되는 커다란 코딩상의 잇점이나 프로그래밍 구조의 획기적인 변화는 실로 놀라게 되지 않을 수가 없게 될 것이다. 물론 내 의견에 동의하지 않는 프로그래머가 있을 진 몰라도 내가 습득한 것 중에 가장 기분 좋게 배우고.. 가장 멋지게 생각하는 문법은 바로 포인터와 union, 비트필드이다.

 필자는 C++에서는 참조를 가장 좋아한다.

 참조와 포인터, union, 비트필드와의 공통점은 코드를 굉장히 나만의 색깔로 만들 수 있으며 깔끔하게 만들어준다는 것이다. 물론 초보자들이 보면 어리둥절하며 어려운 코드가 될 수 있겠지만 차차 실력이 쌓여가면서.. 정말 멋진 것이라고 생각할 것이다.

만일 누군가 나에게 “난 나의 코드가 그 자체로써 하나의 짜임새있는 건축물로 만들고 싶어!”라고 한다면 주저없이.. “포인터와 union과 비트필드를 공부하라”라고 이야기 할 것이다.

자 그러면 본론으로 들어가자.


사용 예제의 환경 :

OS: windows 2000,

컴파일러 : visual C++ 6.0


∙비트 필드란?

비트 필드란 건..  비트별로 데이타를 다룰 수 있도록 해주는 구조체의 문법에서 특별 용법에 하나입니다.

이 부분은 컴파일러 제작이나.. 혹은 하드웨어를 제어할때 아주 편리한데.. 왜냐하면 디바이스들의 레지스터 값들은 대부분 각 비트들이 서로 다른 의미의 플래그 값을 가지고 있습니다.

따라서 한 바이트에서 각 비트 단위처리를 쉽게 해줄 수 있습니다.

문법의 관련 사항은 아주 간단합니다. 마치 스터럭쳐와 비슷하게 생겼습니다.

흔한 하나의 예를 들어보면 다음과 같습니다.


struct bitMyData{

  char a : 4; // [데이터 형] [항목의 이름] : [비트의 수]

  char b : 4;

};


struct strMyData{

 char a;

 char b;

};

void main(void)

{

struct bitMyData bit;

struct strMyData str;


  bit.a = 0x34;

  bit.b = 0x55;

  str.a = 0x34;

  str.b = 0x55;

}

 우선 비트 필드의 이름부터 집고 넘어가도록 합시다. 비트 필드는 말 그대로 bit의 field를 같이 붙여논 이름인데. field의 의미는 영역의 의미를 가지고 있습니다. 그렇다면 비트의 영역이란 이야기가 되겠지요.

따라서 비트 필드는 위에 이름의 의미대로 어떤 메모리 공간을 비트의 영역을 나누어주는 문법입니다.

 여기서  비트 필드의 전체의 크기를 알려주는 건. 각 비트 필드 항목의 데이타형입니다. 각 항목의 데이터형이 char이므로 따라서 1바이트의 크기입니다.(물론 이렇게 단순하게 결정되지는 않습니다. 자세한 이야기는 뒤에서 하기로 하겠습니다. )  ":" 옆의 숫자는 단지. 그 항목의 이름으로 액세스할 수 있는 비트의 범위만을 알려주게 됩니다. 이때 컴파일러는 이 정보를 가지고 비트필드 전체의 크기를 할당하거나 비트 필드의 데이터형을 결정해 주지 않습니다.

 비트 영역의 이름은 각 항목의 이름이 되며, 각각의 비트의 영역의 크기는 콜론 뒤의 숫자로 결정되어지게 됩니다. 물론 선언이 먼저 되는 순서대로 LSB부터 비트의 영역이 확보가 됩니다. 이때 기본이 되는 크기.. 즉 어떤 메모리 공간의 크기는 각 항목에 지정되어있는 데이터형에 의존하게 됩니다.

bitMyData의 경우 각각의 항목이 char이므로 전체적 크기는 1바이트가 되게 됩니다. 이때 항목의 각각의 데이터형은 같게 해주어야 합니다.


":" 이것이 의미하는건 이것이 “비트필드를 사용한다“ 라는 의미이며, 그 해당 항목에 대한 비트 범위를 명시하기 위해 써줄 때 필요한 것뿐입니다.


자 struct와 비교해보도록 할까요?

이 두 개는 외관상 거의 비교가 힘들지만.. 사용법은 전혀 틀립니다.

우선 str의 크기는 2바이트의 크기를 갖지만.. bit의 크기는 1바이트를 갖습니다. 만일 #pragma pack(1)을 하지 않은 상황이라면.. 비주얼 C++6.0에서는 둘 다 4바이트의 크기를 잡을 수도 있습니다. 지금 이야기는 바이트 정렬 문제를 빼고 이야기하는 것입니다.


bit : 항목 a와 b를 가짐.

str : 항목 a와 b를 가짐.


항목

전체 변수의 크기

bit

a, b

1바이트

str

a, b

2바이트


bit.a = 0x34;

bit.b = 0x55;

str.a = 0x34;

str.b = 0x55;


만일 위와 같이 대입했을 때 실제로 할당된 값을 보면 다음과 같다.


항목

할당값

bit

a

0x04


b

0x05


항목

할당값

str

a

0x34


b

0x55


실행 결과 값을 보면 struct와 비트필드와 서로 틀리다는 것이 한눈에 들어오게 된다. 자. 그렇다면 이런 결과는 어떻게 나온 것일까? 비트 필드는 앞에서 설명한대로 유효 비트 영역을 가지고 있는데.. 현제 struct bitMyData 의 구조는 항목 a와 b가 각각 4비트의 영역을 가지고 있으며, 한 바이트를 각각 4비트씩 따로 영역을 가지고 있게 된다. 따라서 대입하는 값의 하위 4 비트만을 취해서 해당 비트 영역에 값을 넣게 된다.

그림으로 그려보면 다음과 같습니다.




0x5

0x4


bit

MSB

0101

0100

LSB



b 영역

a 영역



자 우선 Visual C++을 켜서 실제로 이렇게 되는지 이 결과만을 확인해 보도록 합시다.


아직 의문이 많이 남아있겠지만.. 걍 꾹 참고 다음으로 넘어가도록 해봅시다. 슬슬 가려운데는 긁어드리도록 하겠습니다.


자 그러면 스트럭쳐와 비트 필드가 어떻게 의미가 틀린지 한줄 한줄 따라가 보도록 하겠습니다.

비트 필드의 예

01: struct data{

02: unsigned int part1:1;

03: unsigned int part2:5;

04: unsigned int part3:6;

05: };


1. 번째 줄 : 사용자 데이터 형의 이름을 data라고 정한다.

2. 번째 줄 : 전체 unsigned int 형 중에서 1번째 비트를 part1의 영역으로 확보한다.

3. 번째 줄 : 전체 unsigned int 형 중에서 2번째 비트에서부터 5개의 비트를 part2의 영역으로 확보한다.

4. 번째 줄 : 전체 unsigned int 형 중에서 part1과 part2의 영역을 제외한 나머지 영역에서 6개의

            비트를 part3의 영역으로 확보한다.

MSB <---------------------------> LSB

0000 0000 0000 0000 0000 {0000 00}{00 000}{0}


위와 같은 전체 32비트에서.. 가장 첫 비트는 part1의 영역입니다. 위에 묶어 놓은 것이 아마 보일 것입니다.

LSB에서 부터 차례대로.  part1, part2, part3의 영역으로 나뉩니다.

비트 필드의 항목들의 형이 unsigned int형이므로 메모리에는 전체 바이트 즉 4바이트의 공간이 만들어지게 됩니다. 물론 이것의 자세한 과정은 다음으로 미루기로 합니다.

여기에서 : 옆에 있는 숫자를 다 합쳐보면 12입니다. 그리고 저 비트필드의 전체의 크기는 각 항목이. unsigned int 이므로 unsigned int만큼의 크기를 할당받고 그 데이타 형 크기 만큼에서 각 비트 필드(비트 영역)으로 나누게 됩니다. 여기에선 전체 비트 필드의 크기는 4바이트(32bit)가 됩니다. 그 영역의 범위는 바로 ":" 다음에 있는 숫자만큼으로 나누게 됩니다.

보면 ":"다음에 있는 모든 숫자를 합쳐보면 12가 된다는 이야기는 전체 32비트에서 LSB부터 시작하여 12비트까지만 영역이 설정되어있고 나머지 부분은 그냥 쓰레기 값이 들어가게 된다는 소리입니다. 첫 비트는 part1이란 영역으로 액세스가 가능하고 두 번째 비트부터 여섯 번째 비트는 part2이란 영역으로 액세스가 가능하며 나머지 일곱 번째부터 열두번째 비트 영역은 part3로 값을 설정하거나 가져올 수 있습니다.


만일 struct의 경우는 번역은.. 이렇게 됩니다.

01: struct data{

02: unsigned int part1;

03: unsigned int part2;

04: unsigned int part3;

05: };

1. 번째 줄 : 사용자 데이타형의 이름을 data라고 정한다.

2. 번째 줄 : unsigned int 형의 항목을 part1이란 이름올 설정한다.

3. 번째 줄 : unsigned int 형의 항목을 part2이란 이름올 설정한다.

4. 번째 줄 : unsigned int 형의 항목을 part3이란 이름올 설정한다.


이것으로 보았을 때. 스트럭쳐의 경우에는 세 개의 변수 값이 들어갈 공간이 하나의 범주 이름인 struct data란 이름으로써 들어가게 됩니다. 제가 설명을 하면서 비트필드는 확보한다란 이야기를 해놓고 스트럭쳐에선 설정한다. 란 말로 바꾼 의미는 실제로 그 이름 자체가 변수로써 유효한가 아닌가를 의미하도록 하기 위해서였습니다.

 자 여기에선 “확보”라는 단어에 대한 의미가 많은 논쟁거리가 있을 수 있습니다. 물론 제 나름대로의 의미를 파악하여 단어를 선택한 것이므로 절대적이기 보다 어떤 개인의 관점이라고 보아주시면 감사드리겠습니다.


자! 그러면 이 비트 필드의 내부로 들어가서 비트필드의 애매한 부분을 살펴보면서 비트필드를 어떻게 올바로 어떻게 활용을 할 것인지에 대한 이야기를 펼쳐보이도록 하겠습니다.


∙애매한 느낌 그것은 비트필드

우선 이 비트 필드를 사용하기 위해서는 정확히 몇 바이트를 비트 단위로 제어하고 싶은가를 우선 결정해야 합니다.


예를 들어..

struct stData{

unsigned long a:1; // LSB

unsigned long b:15;

unsigned long c:16; // MSB

};

와 같이 했을때 struct stData data; 는 4바이트의 크기를 가집니다. 그리고 각 비트는 a, b, c부분으로 나뉘게 되고 a는 data의 첫 번째 비트 b는 data의 두 번째 비트에서 16번째 비트까지 그리고 c는 data의 나머지 부분의 비트들을 의미하게 됩니다.


그러면 각각의 a, b, c는 몇 바이트의 크기를 가질까요.. 아쉽게도 각각의 a, b, c는 크기를 갖지 않습니다. 정확히 말하면 비트 단위 영역의 크기를 가질 뿐입니다. 만일 sizeof(data.a)를 하게 된다면 항목 a에 설정된 비트수가 반환되는 것이 아닌 항목 a가 설정이 되어있는 데이터 형의 바이트 수가 리턴되게 됩니다. 즉 unsigend long형의 바이트수인 4바이트를 리턴하게 됩니다.

a, b, c는 정수형 실수형의 의미를 갖지 않습니다. 단지 전체의 바이트의 크기의 형을 써준 뒤 얼마만큼의 비트를 차지할 것인지를 알려주는 것입니다.

struct stData{

unsigned long a:1; // LSB

unsigned long b:15;

unsigned long c:16; // MSB

};

여기에서 unsigned long a:1;을 해석해보면 unsigned int라고 하는 것은 전체 바이트 즉 4바이트 중에 a라는 부분은 1비트를 차지한다 정도의 의미를 가지고 있습니다.


struct stData data;


data.a = 0xffffffff;  로 대입을 해도.


a의 가장 첫 번재 비트만이 의미가 있습니다.

data.a = 0x00000001;과 의미가 같다는 말이지요..


이건 data.a = 1; 과 같은 의미입니다. 이진수로 표시해보면

0000 0000 0000 0000 0000 0000 0000 0001

과 같다는 이야기이죠.


다음의 데이터 b도 마찬가지입니다.


data.b = 0xffffffff; 로 넣어도..


data의 값에는 b의 영역에만 2진수의 1의 값이 들어갑니다.

만일 어떤 long형의 변수의 값을 data.b에 대입한다고 했을 때 이것은 다음과 같은 mask효과를 주게 됩니다.

long tmData = 100;

data.b = tmData & 0x00007fff;

data.b = 0x00007fff;


자 따라서 tmData의 1로 설정이 된 부분만이 data.b에 대입이 된다는 이야기입니다.

0000 0000 0000 0000 0000 0111 1111 1111 ( 1로 세팅된 값은 현제의 비트필드에서 tmData의 유효 비트 )


이렇게 15개의 비트가 의미를 지닌다고 이야기를 할 수 있습니다. 마지막 영역인 c의 경우도

data.c = 0xffffffff;  이것은 data.c = 0x0000ffff; 와 같은 결과를 냅니다.


만일 여기서도 어떤 long형의 변수의 값을 data.c에 대입한다고 했을 때도 다음과 같은 mask 효과를 가집니다.

long tmData = 100;

data.c = tmData & 0x00007fff;

data.c = 0x00007fff;


즉 위에서 처럼 data.c에 할당된 비트의 영역인 16개의 비트만이 의미를 갖기 때문입니다.

0000 0000 0000 0000 1111 1111 1111 1111 ( 1로 세팅된 값은 현제의 비트필드에서 tmData의 유효 비트 )


따라서 위에서 살펴본 봐와 같이.. 비트 필드에서 a, b, c처럼 각각의 요소는 존재의 의미가 없습니다.

따라서 크기가 존재하지 않습니다. 단지 비트별로 처리한다 정도만의 의미를 갖게 되는거구요..

만일 data의 모든 값을 0xffffffff; 로 바꾸고 싶으면 각각의 요소에

data.a = 0xffffffff;

data.b = 0xffffffff;

data.c = 0xffffffff;

라고 넣어주어도 상관없고.


각 비트 영역만 넣어주고 싶다면..


// a는 첫 번째 비트에만 의미가있음

// 0000 0000 0000 0000 0000 0000 0000 0001

data.a = 0x00000001;


//b는 첫 번째부터 15번째의 비트까지만 의미가 있음

// 0000 0000 0000 0000 0111 1111 1111 1111

data.b = 0x00007fff;


//b는 첫 번째부터 16번째의 비트까지만 의미가 있음

// 0000 0000 0000 0000 1111 1111 1111 1111

data.c = 0x0000ffff;


이렇게 대입을 하면 data는 전체의 값이 0xffffffff가 됩니다.


지금까지 이야기를 한 것을 정리하면 비트 필드의 각 요소 뒤에 :뒤의 수의 값은 그 비트 필드 항목의 요소와 같은 데이터형의 변수의 대입할 때 그 변수의 첫 번째부터 얼마까지를 유효하게 볼 것인가를 정해줍니다. 즉 마스크 연산과 같은 효과를 나타냅니다.

또한 비트 필드의 항목들은 선언된 순서부터 LSB를 MSB까지 순서대로 채워져 나갑니다.


실제로 비트 필드를 비트필드만을 가지고 사용하진 않습니다. 왜냐하면 비트필드만을 사용하면 굉장히 불편하기 때문입니다.

프로그래밍을 하다보면 비트필드의 각각의 영역보다 전체의 필드의 값을 더 많이 사용하게 되는데..

따라서 비트 필드 전체의 값을 가져와야 하는데 비트 필드만을 가지고는 방법이 없습니다.

자 다음의 예제를 보시기 바랍니다.


첫 번째 예제

자 이제 첫 번째 비트필드의 예제입니다.

struct stData{

unsigned short a: 5;

unsigned short b: 5;

unsigned short c: 5;

unsigned short d: 1;

};

위의 예시는 조합형 한글을 분리해내는데 아주 편리한 비트 필드입니다. 우선 전체 크기는 unsigned short이므로.. 2 바이트의 크기를 갖습니다.

위와 같이 비트 필드를 적용한 이유는 조합형은 마지막 비트가 한글인지 영문인지 알아내는데 사용이 되고.. 초성 중성 종성 이렇게 각각의 부분이 5비트씩 끊어지기 때문입니다.


자. 이제 다음과 같이 비트 필드 변수를 정의했다고 칩시다.

struct stData data;


이 data의 각 비트 항목의 접근은 가능하지만 도대체 data에 관한 접근이 불가능합니다. 물론 같은 데이터형으로는 가능합니다 하지만 제가 위에서 설명을 드리지 않았지만.. 저 스트럭쳐 크기가 2바이트 즉, unsigned short의 크기라고 해서.. 직접 unsigned short의 값을 대입 할 수 없습니다.


struct stData data;


data = 0xff00;


라고 대입할 수 없습니다. 그건 당연한 이야기입니다. 자 기본적인 이야기이지만 굉장히 중요한 기본 원리입니다. 제가 항상 강조하지만 언제나 기본은 중요합니다.

= 연산자는 대입 연산자로써, 이항 연산자이고 L-Value와 R-Value를 가지고 있습니다.

여기서 L-Value의 형은 struct stData의 형이며

R-Value는 const unsigned short형이죠..

따라서 형이 틀리므로 대입이 불가능합니다.

 = 연산자는 항상 L-Value와 R-Value의 형이 같아야 합니다.


따라서 위의 비트 필드를 적절하고 유용하게 쓰려면 union과 typedef을 이용하여 다음과 같이 바꾸어야 합니다.


typedef struct tagstData{

unsigned short a: 5;

unsigned short b: 5;

unsigned short c: 5;

unsigned short d: 1;

}stData, * pstData;


typedef union tagunData{

unsigned short value;

stData field;

}unData, *punData;


이렇게 바꾸면 value항목으로 전체 값을 액세스하거나 대입할 수 있고.. field 항목으로 각각에 해당되는 비트의 값을 액세스하거나 대입할 수 있게 됩니다.

(여기서 typedef과 union의 용법을 아실려면 고임의 “내 맘대로 되는 게 있다! 바로 사용자 데이터형” 이란 문서를 참조하시거나  C언어 관련 문법책을 참조하시기 바랍니다.)

자 이제 stData의 전체 값을 unsigned short형으로 받을 수 있게 되었습니다. 즉 다음과 같은 코딩이 가능해 졌다는 것입니다.

unsigned short input;

unData test;


  input = MyGets();


  test.value = input;


  if(test.field.d == 1)

  { 

    printf("한글“);

  }

  else

  {

    printf("한글 아님“);

  }


와 같은 코드를 만들어 낼 수 있게 되었습니다.  물론 비트 필드의 항목의 이름을 그것의 용도에 맞게 쓴다면 더욱 금상첨화이겠지요..


두 번째 예제로 가겠습니다. 이제부터의 예제는 혹! 이렇게도 쓰일 수도 있는가? 이렇게 쓰면 어케되는가? 에 대한 총괄적인 질문에 대한 답변이 되겠습니다.


typedef struct tagstData{

unsigned short a: 1;

unsigned short b: 3;

unsigned short c: 5;

}stData;


typedef union tagunData{

  unsigned short value;

  stData field;

}unData, *punData;


이런 경우는 struct stData의 총 크기는 16비트입니다. 즉 2바이트가 되지만 비트는 총 9비트 만이 지정이 되어있습니다. 물론 #pragma pack(1)이라고 코딩했다는 걸 가정합니다.

자 그러면 나머지 7비트는 무엇으로 채워지며, 그 위치는 어케되는가? 라는 질문을 던질 수가 있습니다.

이것에 대답에 앞서 위에서 비트 필드를 먼저 사용하려면 내가 사용하려는 비트필드의 총크기와 각 요소의 총크기를 미리 설계를 해놓고 나서 사용하는게 좋다고 이야기 했습니다. 데이터 구조를 설계를 하다보면 위와 같은 일이 종종 벌어지게 됩니다.  위와 같은 예제의 코딩은 아주 바람직 하지 않은 코드임을 밝히고 들어가도록 하겠습니다.

사용자 삽입 이미지

 

자 위와 같이 스트럭쳐를 설정하고 나서


tmData.value = 0; 이란 코드에서 tmData전체의 값을 0으로 세팅합니다. 그리고 나서 각 필드의 항목을 0xffff로 설정을 하고 나면..

각 항목의 값들은 자신에게 설정되어있는 유효 비트만큼 1로 세팅이 된 값을 가지고 있으며,

value의 값은 총 9비트가 1로 세팅이 된 것 만큼의 결과 값을 가집니다. 또한 LSB에서부터 9비트까지는 사용이 되고 나머지 비트는 전혀 사용이 되질 않습니다.

만일 처음에 0으로 초기화 하지 않았다면.. 나머지 비트는 쓰레기 값이 채워집니다.


프로그래밍 할땐 이런 것이 거대한 버그의 작은 시작점이 될 수 있으니 각별히 신경써야 됩니다.


세 번째 예제

typedef struct tagstData{

unsigned short a: 1;

unsigned short b: 3;

unsigned char c: 5;

}stData;

자.. 이 경우는 각 항목의 데이터 형이 틀릴 경우는 과연 어케 되는가? 에 대한 예제입니다.

이런 경우는 컴파일이 되긴 합니다. 이 경우는 참 설명하기 난감한데, 그 이유는 저 데이터 형 만큼의 크기가 커다란 덩어리 생기게 되고.. 즉 항목 a와 b는 unsigned short의 영역을 나누고 항목 c는 unsigned char의 영역에서 그 비트 영역을 갖게 됩니다.

만일 바이트 정렬이 1로 맞추어져 있다면.. #pragma pack(1)이라면  stData의 크기는 3바이트의 크기를 가지며 위와 상응하는 union의 구조는 다음과 같게 됩니다.

typedef struct tagstValue{

  unsigned short v1;

  unsigned char v2;

}stValue;


typedef union tagunData{

  stValue val;

  stData field;

}unData;


조금은 복잡해 보이지만.. 자 우선 그림을 보고 빠른 이해를 돕기로 해보겠습니다.

사용자 삽입 이미지

 

우선 전체적인 소스는 다음과 같습니다. 우선 첫줄의 #pragma pack(1)과 중간쯤에 #pragma pack()에 대해서 설명해 보면.. 첫줄의 #pragma pack(1)은 바이트정렬을 1바이트에 맞추겠다는 의미이며 #pragma pack()은 원래의 상태대로 되돌리겠다는 의미입니다. 즉 내가 만든 형태의 스트럭쳐 형태만 1 바이트 정렬하고 나머지는 현제의 컴파일 옵션에 따르겠다는 의미입니다.

자! 우리가 중요한 건 다음입니다.

사용자 삽입 이미지
 각 변수의 어드레스 값과 크기
사용자 삽입 이미지
 어드레스의 실제 값

위의 두 개의 그림을 보고 다음과 같은 그림을 만들어 낼 수 있습니다.

사용자 삽입 이미지

 

이것을 보면 비트 필드에서 같은 데이터형이었던 영역 a와 b와 다른 데이터형이었던 c항목은 서로 다른 기본 위치 값을 가집니다.

비트 필드에서 서로 다른 데이터형일 때 서로 다른 기본 위치 값을 갖게 하게 됩니다.

즉 tmData.field.a와 tmData.field.b는 tmData.val.v1의 전체 값에서 비트 영역을 각각 확보하게 되고,

 tmData.field.c는 tmData.val.v2의 값에서 해당 비트 영역을 확보하게 됩니다.

따라서 이 경우 비트 필드는 총 32비트의 크기의 메모리를  16비트, 8비트의 두 개의 영역으로 나누어서 사용이 되어집니다.

 

네 번째 예제

typedef struct tagstData{

  unsigned char a:5;

  unsigned char b:3;

  unsigned char c:5;

  unsigned char d:7;

}stData;


이 경우는 어떨까요? 이 경우는 unsigned char형으로 전부 항목들이 설정되어 있으며, 비트 필드로 설정이 되어 있는 총 비트수가 20비트가 됩니다. 아까 첫 번째의 예제를 돌이켜 생각해 본다면.. unsigned char 가 8비트이므로 총 비트필드의 개수와 맞지 않게 되어 머리를 어리둥절하게 만들 것입니다.

실제로 컴파일을 해보면 컴파일이 되며, 저 위의 비트 필드 형태로 그 메모리에 확보가 됨을 알게 됩니다. 그렇다면? 총 몇 바이트가 확보가 될까요?

답은 총 3바이트입니다.

첫 번째 바이트는 항목 a, b 그리고 두 번째 바이트는 c 마지막 세 번째 바이트는 d로 비트 영역이 나누어지게 됩니다.  컴파일러는 만일 비트 필드의 항목들의 데이타 형이 모두 같다면 첫 번째 항목부터 시작하여 각각의 비트 수를 누적시키면서, 그 형의 최대 비트수를 넘는지 확인 한 다음 만일 넘게 된다면 넘지 않는 항목들을 그 데이터 형의 메모리를 하나 할당한 다음 그 항목들에 비트 필드를 구성하게 됩니다. 그리고 다음 항목부터는 새롭게 그 데이터형의 메모리를 다시 할당해서 구성하게 됩니다.


만일 예제가 이렇게 되었다면

typedef struct tagstData{

  unsigned char a:1;

  unsigned char b:3;

  unsigned char c:4;

  unsigned char d:7;

}stData;


이것은 총 2바이트에 걸쳐서 비트 필드를 구성하게 됩니다.


typedef struct tagstData{

  unsigned char a:6;

  unsigned char b:7;

  unsigned char c:5;

  unsigned char d:7;

}stData;


만일 이렇다면 답은? 네 당연히 4바이트가 됩니다. 각각의 항목들의 비트필드의 범위가 커서 항목들의 조합이 이 비트필드의 데이터형인 unsigned char(8비트)의 범위를 넘어서게 되기 때문에 각각의 항목에 하나의 unsigned char의 크기만이 할당되게 됩니다.


자 하나만 더해볼까요?

typedef struct tagstData{

  unsigned char a:2;

  unsigned char b:7;

  unsigned char c:5;

  unsigned char d:4;

}stData;


이 경우는 어떨까요? 지금까지 제 글을 주의깊게 읽어보신 분들은 바로 답을 내실 수 있을 겁니다. 그렇습니다. 4바이트의 공간을 확보해주게 됩니다. a와 b의 비트수를 더하니 총 9비트이므로 unsigned char의 데이터형을 넘어버려, a라는 항목을 따로 메모리에 확보하게 됩니다. 마찬가지로 나머지 항목들도 선언이 된 순서대로 비트를 더해서 계산을 하기 때문에 항목들은 서로 다른 메모리에 각자의 비트 영역을 확보하게 됩니다.


물론 이것은 모든 컴파일러마다 동일하다고 이야기 할 순 없습니다. 이것에 대해 각각의 컴파일러는 어떻게 구현을 해놓았는지는 확실치가 않고, Visual C++에서만 테스트 한것이므로 다른 컴파일러에서는 다른 결과를 낼 수도 있음을 미리 알려드립니다.


다섯 번째 예제


이런 경우는 아예 컴파일이 되지 않습니다.

왜냐? 위에 주석에 써있는 것처럼 “:”의 오른편에 쓰여지는 비트 영역의 수의 한계는 바로 앞에 선언되어있는 데이터형의 비트 수이기 때문에 위와 같이 8비트의 크기를 가지는 unsigned char의 항목의 비트 영역을 14로 설정하게 되면 컴파일 에러를 내게 됩니다.

따라서 이렇게 코딩하는 것은 문법적 에러를 야기 시킵니다.
 

나가며..

대충의 비트필드에 대한 이야기가 끝이 났습니다. 휴~~ 정말 힘들군요.. 지금 다시 보니.. 깔끔하지도 않은 문장에다가. 대충 만든 예제에 성의가 넘 없어서. 올리기가 싫은데..

화려한 업데이트를 기약하며.. 걍 올리기로 했습니다.

부록으로 미진한 내용 때문에.. 예제라도 많아야 된다. 라는 생각에.. 몇 가지의 예제를 적어 놓았습니다
 

하드웨어에 관련된 예제

예를 들어.. 인텔의 51계열 CPU에서 PSW 레지스터의 각 비트는 다음과 같은 의미를 지닙니다.

PSW(program stauts word)


MSB







LSB

비트 번호

7

6

5

4

3

2

1

0

각 플래그 이름

CY

AC

F0

RS1

RS0

OV

Non

P


각 플래그의 의미

CY : 캐리플래그

AC : 보조 캐리 플래그

F0 : 사용자용 비트

RS1 : 레지스터 뱅크 선택 비트

RS0 : 레지스터 뱅크 선택 비트

OV : 오버플로우

Non : 사용하지 않음

P : 패리티 비트


이 PSW는 8bit의 값을 유지하고 있고. 총 한 바이트입니다. 이 값들은 마이크로 콘트롤러의 지금 현재의 상태를 모니터링 하거나 세팅 해주는 역할을 하는 주요한 레지스터입니다.


여기서 각 비트를 제어를 하고 싶다고 칩시다. 만일 RS1을 0으로 하고 RS0를 1로 하고 싶다고 합시다. 그러면 다음과 같이 데이타를 넣어야 합니다.

unsigned char PSW;


PSW = PSW | 0x08;


이렇게 되면 현재 RS1의 비트의 위치파악이나 의미파악도 힘들게 됩니다. 만일 #define의 문법이나 매크로를 사용할 수도 있겠지만.. 디버깅이 힘들어진다는 이유 때문에 그렇게 권장하고 싶지는 않습니다.


이럴 때 비트필드를 이용하게 되면

typedef struct tagRegister{

unsigned char P:1; // LSB

unsigned char Non:1;

unsigned char OV:1;

unsigned char RS0:1;

unsigned char RS1:1;

unsigned char F0:1;

unsigned char AC:1;

unsigned char CY:1; // MSB

}Register;


typedef union tag51Reg {

unsigned char w_value;

Register Reg;

}51Reg;


이런 비트 필드를 만들어 놓고 코딩을 하면

51Reg PSW;


PSW.Reg.RS0 = 1;

PSW.Reg.RS1 = 0;


과 같이 할 수 있습니다. 물론 우리가 작성해야할 초기 코드는 많아졌지만. 코드가 훨씬 더 가독성이 늘어났죠.. 그리고 코드를 작성하기가 쉬워집니다.

이렇게 각 비트를 제어하기 편하도록 해주는 것이 비트 필드입니다.


10진수를 2진법으로 바꾸는 예제

아래의 예는 실제 숫자 값을 2진법으로 바꾸는 것인데..

#include<iostream.h>

#include <stdio.h>


typedef struct wordbits {

unsigned b0:1;

unsigned b1:1;

unsigned b2:1;

unsigned b3:1;

unsigned b4:1;

unsigned b5:1;

unsigned b6:1;

unsigned b7:1;

unsigned b8:1;

unsigned b9:1;

unsigned b10:1;

unsigned b11:1;

unsigned b12:1;

unsigned b13:1;

unsigned b14:1;

unsigned b15:1;

} Wordbits;


typedef union aword {

unsigned w_value;

Wordbits w_bits;

} Aword;


main()

{

Aword aw;

unsigned ans;

char charAns[17] = "";


cin >> ans;

aw.w_value = ans;


sprintf(charAns, "%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d",aw.w_bits.b15 , aw.w_bits.b14 ,

aw.w_bits.b13 , aw.w_bits.b12 ,

aw.w_bits.b11 , aw.w_bits.b10 ,

aw.w_bits.b9 , aw.w_bits.b8 ,

aw.w_bits.b7 , aw.w_bits.b6 ,

aw.w_bits.b5 , aw.w_bits.b4 ,

aw.w_bits.b3 , aw.w_bits.b2 ,

aw.w_bits.b1 , aw.w_bits.b0);


cout << charAns;

return 0;

}

unsigned int 형 같은 경우는 실제로 메모리에 저장되는 binary값과 10진수로 저장이 되는 값과 동일 하기 때문에 비트 필드를 이용해서 보여주어도 상관이 없습니다.

만일 float 형의 데이타를 비트 필드를 이용해서 보여주려면 약간의 문제가 발생이 되겠죠..

하지만 정수나 문자형의 값의 경우에는 별 문제가 없습니다.

물론 다른 방법으로 2진수를 바꾸는 방법이 있고, 더 효율적입니다. 이 예제는 단지.. 비트 필드를 이렇게도 이용하는 구나라는 것을 느끼게 해주기 위해서 만들었습니다.



∙SimSoccer에서 사용된 비트필드


조금 아래에 가보면 비트필드를 어케사용했는지에 대한 내용이 나옵니다.

심히 부끄러운 코드이긴 하지만.. --;; 이런식으로 이용을 했구나. 라고 생각해주시면 감사드리겠습니다.

SimSoccer는 아직도 진행중인 비상업용 공동 프로젝트입니다.  지금 현제 몇 가지의 일 때문에 업데이트가 늦어지고는 있지만.. 쩝..

문철 성님이.. 으흑.. 완빵 압박을 주고 있지만.. 그래도.. 제 맘대로 늦어질 것 같습니다.


/////////////////////////////////////////////////////////////////

//

// Project Name : XParagon - SimSoccer

// File : CSimPacket.h

// Author : koim

// Description  : Packet Design

// Date : 5.31 ,2001

// Group : Double Click & Vision

//

/////////////////////////////////////////////////////////////////

#if !defined(AFX_SIMPACKET_H__FBD7FF4D_A5DA_419B_AA97_75D99A86BA7A__INCLUDED_)

#define AFX_SIMPACKET_H__FBD7FF4D_A5DA_419B_AA97_75D99A86BA7A__INCLUDED_


#if _MSC_VER > 1000

#pragma once

#endif // _MSC_VER > 1000


#include "AxisDef.h"

#include "SimAllMsg.h"


#pragma pack(1)


/////////////////////////////////////////////////////////////////

//

//

typedef struct tagChunkHeader{

  BYTE id[1]; // 패킷의 아이디이다.

} ChunkHdr, *pChunkHdr;

//

/////////////////////////////////////////////////////////////////

// 아직 명령 패킷은 디자인 하지 않음

//typedef struct tagCommand

//{

//}CommMod, *pCommMod;

/////////////////////////////////////////////////////////////////


/////////////////////////////////////////////////////////////////

// 1. 명령 모드

// Ack에 관련된.. 스트럭쳐이다.

typedef struct tagXpragonAck{

  ChunkHdr hdr;

  BYTE Ack;

}XpragonAck, *pXpragonAck;



// 에러 아이디이며 특정 메시지 문자열의 번호를 가지고 있다.

//

typedef struct tagXparagonMsg{

  ChunkHdr hdr;

  enum simMsgID msgId;

}XparagonMsg, * pXparagonMsg;


/////////////////////////////////////////////////////////////////

/////////////////////////////////////////////////////////////////

// 2. 경기 모드

//

// 객체 위치의 x 10bit (최대 값 : 1024)

// 객체 위치의 y 10bit (최대 값 : 1024)

// 객체의 절대 각도 Angle 8bit (최대 값 : 256)

// 객체 속도 4bit (최대값 : 16)

// 객체 정보의 총 바이트 수 32bit (4byte)

//

// 정보가 필요한 총 객체 수 로봇 6대, 공 1개

// 경기에 필요한 총 객체들에 필요한 바이트 수 4X7 = 28byte

//

// 절대 시간 정보 24bit(최대값 : 16777216: 1초에 60번 시간이 지난다고 하면 최대4660분)

// 절대 시간 정보의 총 바이트 수 24bit (3byte)

//

//경기자들에게 보내는 총 바이트수

//경기에 필요한 총 객체 정보 + 절대 시간 정보 = 31byte

/////////////////////////////////////////////////////////////////


typedef DWORD typeObjPara;

typedef BYTE typeMinePara;

typedef BYTE typeAbsTime[3];


//각 필드의 최대값..

#define MAX_SIMOBJ_X  1023

#define MAX_SIMOBJ_Y  1023

#define MAX_SIMOBJ_ANGLE  255

#define MAX_SIMOBJ_VEL  15


typedef struct tagSimObjBit{

  typeObjPara x:10; // 객체 위치의 x

  typeObjPara y:10; // 객체 위치의 x

  typeObjPara angle:8; // 객체의 절대 각도

  typeObjPara vel:4; // 객체의 속도

}*pSimObjBit, SimObjBit;


typedef union utagSimObj { // 객체 하나의 정보를 담는 데이타.

  SimObjBit section;

  typeObjPara lump;

}*puSimObj, uSimObj;


// 로봇 축구에서 최대 객체수.. 팀 하나당 로봇 세 대. 공 하나 합은 7

#define MAX_SIM_OBJ 7


typedef struct tagDStoP{ //서버가 플레이어에게 주는 데이타.

  uSimObj playObj[MAX_SIM_OBJ];

  typeAbsTime AbsTime;

}*pDStoP, DStoP;


typedef union utagServToPlayer{

  DStoP section; // 데이타를 세팅할땐 이 부분으로 한다.

  BYTE pack[31]; // 통신으로 보낼때 이 부분으로 보낸다.

}*puServToPlayer, uServToPlayer;


/////////////////////////////////////////////////////////////////

//  두번째로 경기자가 서버에게 정보를 날리는 부분이다.

//

// 각도 플래그

//  방향에 대한 플래그 1bit (+방향인가? -방향인가?)

//  각 변화율에 대한 플래그 3bit ( 8각도 )

//

// 속도 플래그

//  가속도 방향 대한 플래그 1bit (+방향인가? -방향인가?)

//  속도에 변화율에 대한 플래그 3bit ( 최대 속도 변화율 8 )

//

// 객체의 총 각도와 속도에 대한 변화 플래그   1byte

// 자신들의 로봇만 업데이트하면 되므로 로봇 3개의 위치와 속도와 각도를 넣어주면 된다.

//

// 경기에 필요한 총 객체들의 정보 데이터 3byte

//

// 어떤 시간에 보낸 데이터에 결과인지를 알기 위한 미리 입력 받았던 절대 시간 정보

// 절대 시간 정보 24bit(최대값 : 16777216: 1초에 60번 시간이 지난다고 하면 최대4660분)

// 절대 시간 정보의 총 바이트 수 24bit (3byte)

//

// 경기자들이 서버로 보내는 데이터의 총 크기 6byte

//

/////////////////////////////////////////////////////////////////

enum BEARING {enMINUS = 0, enPLUS}; // 각도와 속도 플래그에 대한 enum 타입


#define MAX_ANGLE_VARING 7 // 최대 각 변화율

#define MAX_VEL_VARING   7 // 최대 속도 변화율


typedef struct tagMineObjBit{

  typeMinePara bangle:1; //각도 방향에 대한 플래그 1bit

  typeMinePara angle:3; // 각 변화율에 대한 플래그 3bit ( 8각도 )

  typeMinePara bvel:1; // 가속도 방향 대한 플래그 1bit (+방향인가? -방향인가?)

  typeMinePara vel:3; //  속도에 변화율에 대한 플래그 3bit ( 최대 속도 변화율 8 )

}*pMineObjBit, MineObjBit;


typedef union utagMineObj { // 객체 하나의 정보를 담는 데이타.

  MineObjBit section;

  typeMinePara lump;

}*puMineObj, uMineObj;


//자신의 정보는 자신이 제어할 수 있는 로봇 세 대의 객체를 같이 포함한다.

#define MAX_MINE_OBJ 3


typedef struct tagDPtoS{ // 플레이어가 서버에게 주는 데이타.

  uMineObj playObj[MAX_MINE_OBJ];

  typeAbsTime AbsTime;

}*pDPtoS, DPtoS;


typedef union utagPlayerToServ{

  DPtoS section; // 데이타를 세팅할땐 이 부분으로 한다.

  BYTE pack[6]; // 통신으로 보낼때 이 부분으로 보낸다.

}*puPlayerToServ, uPlayerToServ;


/////////////////////////////////////////////////////////////////

// 3. 관중 모드

//

// 관중 모드의 클라이언트들에게 앞의 경기 모드의 데이타를 똑같이 보내준다.

/////////////////////////////////////////////////////////////////




/////////////////////////////////////////////////////////////////

// 경기 모드에서 절대 시간을 DWORD형으로 가져오거나. 세팅하거나.

// 1씩 증가시키기 위한 인라인 함수이다.

/////////////////////////////////////////////////////////////////


#define GetAbsTimeN(name) name##.section.AbsTime


template<class T> void SetAbsTime(T &Vbyte, DWORD &Vlong)

{

  GetAbsTimeN(Vbyte)[2] = (BYTE) (0xff & ((Vlong) >> 16)); \

  GetAbsTimeN(Vbyte)[1] = (BYTE) (0xff & ((Vlong) >> 8)); \

  GetAbsTimeN(Vbyte)[0] = (BYTE) (0xff & (Vlong)); \

}


template<class T> void GetAbsTime(T &Vbyte, DWORD &Vlong)

{

  (Vlong) = (DWORD) GetAbsTimeN(Vbyte)[2];

  (Vlong) = (Vlong) << 8;

  (Vlong) = (DWORD)GetAbsTimeN(Vbyte)[1] + (Vlong);

  (Vlong) = (Vlong) << 8;

  (Vlong) = (DWORD) GetAbsTimeN(Vbyte)[0] + (Vlong);

}


template<class T> void IncAbsTime(T &Vbyte)

{

DWORD __tm = 0;


  GetAbsTime(Vbyte, __tm);

  __tm++;

  SetAbsTime(Vbyte, __tm);

}


template<class T> void DecAbsTime(T &Vbyte)

{

DWORD __tm = 0;


  GetAbsTime(Vbyte, __tm);

  __tm--;

  SetAbsTime(Vbyte, __tm);

}



/////////////////////////////////////////////////////////////////

// 최종 통신 데이타 구조

// 서버에서 플레이어들에게 보내는 통신용 패킷

//

typedef struct tagSToPCommData{

  ChunkHdr hdr; // 이것이 바로 청크의 아이디이다.

  uServToPlayer cmmData; // 서버에서 경기자에게 보내는 정보

} SToPCommData, *pSToPCommData;

//

// 플레이어가 서버에게 보내는 통신용 패킷

//

typedef struct tagPToSCommData{

  ChunkHdr hdr; // 이것이 바로 청크의 아이디이다.

  uPlayerToServ cmmData; // 경기자가  서버에게 보내는 정보

}PToSCommData, * pPToSCommData;

//

// 하나의 패킷 데이타로 모든 종류의 데이타를 받는다.

//

typedef union tagXparagonComm{

  PToSCommData Xptos; // 경기자에서 서버로 6바이트 크기.

  SToPCommData Xstop; // 서버에서 클라이언트로 32바이트 크기.

}Xparagon, *pXparagon;

//

/////////////////////////////////////////////////////////////////


#pragma pack()


class CSimPacket 

{

public:

        CSimPacket();

        virtual ~CSimPacket();


};


#endif // !defined(AFX_SIMPACKET_H__FBD7FF4D_A5DA_419B_AA97_75D99A86BA7A__INCLUDED_)