개요
하드웨어 설계자들은 공간의 낭비를 최소화하고자 하드웨어의 제어와 상태 확인을 위한 레지스터에 비트단위로 기능을 할당합니다. 때문에 많은 시스템 제어 프로그램은 C언어의 기본 자료형 크기보다 작은 단위로 데이터를 다루어야 합니다.
이를 위해 C 언어에서는 비트단위 연산자(bitwise)와 비트 필드 자료구조를 제공합니다. 이번 포스트에서는 이들에 대해 알아보도록 하겠습니다.
참고: C언어의 포인터와 비트단위 연산의 조합은 저수준(low level) 프로그램에서 유용합니다. 이를 이용하면 대부분의 어셈블리를 대체할 수 있습니다.
비트단위 연산자(Bitwise Operator)
C 에서 제공하는 비트 연산자는 다음과 같습니다.
&
: AND|
: OR^
: XOR~
: NOT<<
: LEFT SHIFT>>
: RIGHT SHIFT
시스템 제어를 위한 환경에서 이들 비트 연산자는 다음과 같은 용도로 사용됩니다.
비트 1 설정
비트연산자 OR (|
)를 사용하면 특정 비트를 1로 설정할 수 있습니다.
number |= 1UL << n;
이 연산을 통해 number
변수의 n
번째 비트를 1로 설정합니다. 여기서 n
은 0 부터 시작합니다. 만약 여러분이 첫 번째 비트를 1로 설정하고 싶은 경우라면 n
을 0으로 설정하면 됩니다.
※ 만약 number
변수가 unsigned long
보다 큰 경우는 1ULL
을 사용하도록 합니다.
비트 0 설정
비트 연산자 AND (&
) 를 사용하면 특정 비트를 0으로 설정(clear) 할 수 있습니다
number &= ~(1UL << n);
number
변수의 n
번째 비트를 0으로 설정합니다. 이때 비트는 NOT 연산자 (~
) 를 사용하여 반전시킨 후, AND 연산을 적용하도록 합니다.
비트 반전
비트 연산자 XOR (^
) 를 사용하면 해당 비트를 토글링 할 수 있습니다. 토글링은 이진수 0을 1로 1을 0으로 변경하는 것을 말합니다.
number ^= 1UL << n;
number
변수의 n
번째 비트를 토글합니다.
비트 내용 읽기
장치의 상태를 확인하기 위해 해당 변수 또는 레지스터의 n 번째 비트의 값이 0인지 1인지 조사할 필요가 있습니다. 이때는 해당 변수를 n 만큼 오른쪽으로 시프트 한 후, AND 연산을 합니다.
bit = (number >> n) & 1U;
이 코드를 실행하면 number
변수의 n
번째 비트의 값을 변수 bit
에 넣습니다.
일반적으로는 직접 이들 코드를 사용하기보다는 아래와 같은 MACRO로 작성한 후, 이를 사용합니다.
#define BIT_SET(a,b) ((a) |= (1ULL<<(b)))
#define BIT_CLEAR(a,b) ((a) &= ~(1ULL<<(b)))
#define BIT_FLIP(a,b) ((a) ^= (1ULL<<(b)))
#define BIT_CHECK(a,b) (!!((a) & (1ULL<<(b))))
int fnTestBit(unsigned int number, unsigned int n) {
number = BIT_SET(number, n);
assert (BIT_CHECK(number, n) == 1);
number = BIT_CLEAR(number, n);
assert (BIT_CHECK(number, n) == 0);
number = BIT_FLIP(number, n);
assert (BIT_CHECK(number, n) == 1);
}
비트 필드(Bit Fields)
비트 필드는 여러 데이터를 하나의 구조체에 넣어 주는 C 언어의 자료구조의 하나입니다. 메모리 및 저장소가 고가였던 시절 메모리의 사용량을 줄일 목적으로 유용하게 사용되었으나, 현재는 이러한 목적 보다는 시스템의 제어를 위한 저수준 통신을 위해서 사용됩니다. 예를들면 여러 오브젝트를 하나의 머신워드에 넣거나 9비트짜리 정수와 같은 비표준 외부파일형식을 읽을 때 사용할 수 있습니다.
단지 변수명 뒤에 비트의 크기를 기입하면, 이런 비트필드 구조체를 만들 수 있습니다.
struct packed_struct {
unsigned int f1:1;
unsigned int f2:1;
unsigned int f3:1;
unsigned int f4:1;
unsigned int type:4;
unsigned int funny_int:9;
} pack;
여기서 정의한 구조체는 4개의 1비트짜리 플래그와 4비트짜리 type, 9비트의 funny_int로 구성됩니다.
C 컴파일러는 앞서 작성한 비트 필드 구조체를 최대한 작은 크기로 합칩니다. 여기서 주의할 것은 제시된 비트의 길이 값의 합이 해당 시스템의 정수형의 크기를 넘기면 안 된다는 것입니다. 어떤 컴파일러는 메모리 중첩을 허용하지 않기 때문에 이런 코드는 에러를 낼 가능성이 높습니다. (이에 대해서는 비트 필드의 주의점 항목을 참고하시기 바랍니다.)
이런 비트 필드 구조체 역시 일반적인 구조체의 멤버 변수에 접근하듯이 다음과 같이 사용할 수 있습니다.
pack.type = 7;
비트 필드 사용 예
일반적으로 장치를 제어하기 위한 레지스터들은 하나의 정수에 묶어서 사용합니다. 때문에 운영체제는 주변장치(디바이스)의 제어 및 상태감시를 위해 저수준 통신을 합니다.
디스크 제어 레지스터 (예시)
비트 필드를 이용하면 위와 같은 레지스터를 쉽게 정의할 수 있습니다.
struct DISK_REGISTER {
unsigned ready:1;
unsigned error_occured:1;
unsigned disk_spinning:1;
unsigned write_protect:1;
unsigned head_loaded:1;
unsigned error_code:8;
unsigned track:9;
unsigned sector:5;
unsigned command:5;
};
이제 특정 메모리의 주소에 접근하기 위해 위 구조체 포인터에 주소값(DISK_REGISTER_MEMORY)을 할당합니다.
struct DISK_REGISTER *disk_reg = (struct DISK_REGISTER *) DISK_REGISTER_MEMORY;
디스크를 제어하기 위한 코드는 직관적으로 작성할 수 있습니다.
/* Define sector and track to start read */
disk_reg->sector = new_sector;
disk_reg->track = new_track;
disk_reg->command = READ;
/* wait until operation done, ready will be true */
while ( ! disk_reg->ready ) ;
/* check for errors */
if (disk_reg->error_occured)
{ /* interrogate disk_reg->error_code for error type */
switch (disk_reg->error_code)
......
}
비트 필드에 대한 주의 사항 : 이식성
비트 필드는 어려운 연산들을 손쉽게 할 수 있도록 도와주는 편리한 방법입니다.
하지만 C언어 표준은 컴파일러가 이런 비트 필드 값을 메모리 또는 저장소에 저장하는 방법을 정의하지 않습니다. 따라서 이는 전적으로 컴파일러의 구현에 따라 결과가 달라지게 되므로 여러분은 절대 이에 대해서 사전에 가정을 해서는 안됩니다.
앞서 예를 들었던 디스크 제어의 경우도 특정 시스템에서 올바르게 동작하며 그 외의 경우에 대해 동작을 보장할 수 없습니다.
비트 필드와 관련된 표준 중 정의되지 않은 내용은 다음과 같습니다.
미정의 사항
- The alignment of the addressable storage unit allocated to hold a bit-field (6.7.2.1).
구현에 따르는 사항
- 비트 필드가 저장공간의 경계에서 어떻게 이어질 것인지. (Whether a bit-field can straddle a storage-unit boundary (6.7.2.1))
- 하나의 저장공간 안에 할당하는 비트 필드의 순서. (The order of allocation of bit-fields within a unit (6.7.2.1))
따라서 비트 필드를 사용하게 되면 동일한 코드라도 시스템에 따라 다른 실행 결과를 얻을 수 있습니다.
- 정수가 비트가 있는 것일 수도 있고, 없는 것일 수도 있습니다
- 많은 컴파일러들이 비트 필드의 크기를 시스템의 기본 정수형으로 제한하기 때문에 16 또는 32 가 될 수 있습니다
- 비트 필드가 메모리의 왼쪽에서 오른쪽으로 또는 오른쪽에서 왼쪽으로 저장될 수 있습니다.
- 비트필드가 기본자료형을 초과할 경우, 이를 바로 다음 주소 값에 저장을 할 수도 있고, 다음 워드에 저장할 수도 있습니다.
만약 현재 작성한 코드를 다른 시스템, 다른 컴파일러에서 동작시킬 계획이라면 (코드의 이식성이 가장 중요하다면), 앞서 소개하였던 비트단위 연산자를 사용해야 합니다. 하지만 이 경우 아래의 예시에 보는 것처럼 코드의 직관성이 상대적으로 떨어지게 됩니다.
unsigned int* disk_reg = (unsigned int*) DISK_REGISTER_MEMORY;
disk_reg |= (new_sector << 5);
disk_reg |= (new_track << 10);
disk_reg |= READ;
while (!((disk_reg >> 31) & 1U)) ;
if ((disk_reg >> 30) & 1U) {
switch (disk_reg >> 19) {
}
}
참고
'프로그래밍 언어 > C' 카테고리의 다른 글
[C] realpath (0) | 2021.05.28 |
---|---|
엔디언 (Endian) - 64 비트 자료형 처리 (0) | 2019.06.01 |
콘솔 다루기 (Escape Sequence) @ C (2/2) (0) | 2018.11.26 |
콘솔 다루기 (Escape Sequence) @ C (1/2) (0) | 2018.11.23 |