IT이야기

비트 엔디안이 비트 필드에서 문제가 되는 이유

cyworld 2021. 3. 25. 21:37
반응형

비트 엔디안이 비트 필드에서 문제가되는 이유는 무엇입니까?


비트 필드를 사용하는 모든 이식 가능한 코드는 리틀 엔디안 플랫폼과 빅 엔디안 플랫폼을 구분하는 것 같습니다. 이러한 코드의 예는 Linux 커널의 struct iphdr 선언을 참조하십시오 . 비트 엔디안이 왜 문제인지 이해하지 못합니다.

내가 이해하는 한, 비트 필드는 비트 레벨 조작을 용이하게하는 데 사용되는 순전히 컴파일러 구조입니다.

예를 들어 다음 비트 필드를 고려하십시오.


struct ParsedInt {
    unsigned int f1:1;
    unsigned int f2:3;
    unsigned int f3:4;
};
uint8_t i;
struct ParsedInt *d = &i;
여기에서 쓰기 d->f2는 간단하고 읽기 쉬운 표현입니다 (i>>1) & (1<<4 - 1).

그러나 비트 연산은 잘 정의되어 있으며 아키텍처에 관계없이 작동합니다. 그렇다면 비트 필드가 이식되지 않는 이유는 무엇입니까?


C 표준에 따르면 컴파일러는 원하는 임의의 방식으로 비트 필드를 거의 자유롭게 저장할 수 있습니다. 비트가 할당 된 위치에 대한 어떠한 가정도 할 수 없습니다 . 다음은 C 표준에 지정되지 않은 몇 가지 비트 필드 관련 사항입니다.

지정되지 않은 동작

  • 비트 필드 (6.7.2.1)를 보유하기 위해 할당 된 주소 지정 가능 저장 장치의 정렬.

구현 정의 동작

  • 비트 필드가 저장 단위 경계에 걸쳐있을 수 있는지 여부 (6.7.2.1).
  • 유닛 내 ​​비트 필드 할당 순서 (6.7.2.1).

Big / little endian은 물론 구현에 따라 정의됩니다. 이는 구조체가 다음과 같은 방법으로 할당 될 수 있음을 의미합니다 (16 비트 정수 가정).

PADDING : 8
f1 : 1
f2 : 3
f3 : 4

or

PADDING : 8
f3 : 4
f2 : 3
f1 : 1

or

f1 : 1
f2 : 3
f3 : 4
PADDING : 8

or

f3 : 4
f2 : 3
f1 : 1
PADDING : 8

어느 것이 적용됩니까? 추측하거나 컴파일러에 대한 심층적 인 백엔드 문서를 읽으십시오. 여기에 빅 엔디안 또는 리틀 엔디안으로 32 비트 정수의 복잡성을 추가합니다. 그런 다음 컴파일러가 구조체로 처리되기 때문에 비트 필드 내에서 임의의 수의 패딩 바이트 를 추가 할 수 있다는 사실을 추가합니다 (구조체의 맨 처음에는 패딩을 추가 할 수 없지만 다른 모든 곳에서는 추가 할 수 없음).

그리고 비트 필드 유형 = 구현 정의 동작으로 일반 "int"를 사용하거나 (부호없는) int = 구현 정의 동작 이외의 다른 유형을 사용하는 경우 어떤 일이 발생하는지 언급하지 않았습니다.

따라서 C 표준은 비트 필드를 구현하는 방법에 대해 매우 모호하기 때문에 질문에 답하기 위해 이식 가능한 비트 필드 코드와 같은 것은 없습니다. 비트 필드가 신뢰할 수있는 유일한 것은 부울 값의 덩어리가되는 것입니다. 여기서 프로그래머는 메모리에서 비트의 위치를 ​​신경 쓰지 않습니다.

유일한 이식 가능한 솔루션은 비트 필드 대신 비트 연산자를 사용하는 것입니다. 생성 된 기계어 코드는 정확히 동일하지만 결정적입니다. 비트 연산자는 모든 시스템의 모든 C 컴파일러에서 100 % 이식 가능합니다.


내가 이해하는 한, 비트 필드는 순전히 컴파일러 구조입니다.

그리고 그것은 문제의 일부입니다. 비트 필드의 사용이 컴파일러가 '소유 한'것으로 제한 되었다면 컴파일러가 비트를 패킹하거나 정렬하는 방법은 누구에게도 거의 문제가되지 않습니다.

그러나 비트 필드는 하드웨어 레지스터, 통신을위한 '와이어'프로토콜 또는 파일 형식 레이아웃과 같은 컴파일러 도메인 외부의 구조를 모델링하는 데 훨씬 더 자주 사용됩니다. 이러한 것에는 비트를 배치하는 방법에 대한 엄격한 요구 사항이 있으며이를 모델링하기 위해 비트 필드를 사용하는 것은 구현 정의에 의존해야 함을 의미하며, 더 나쁜 경우에는 컴파일러가 비트 필드를 배치하는 방법의 지정되지 않은 동작에 의존해야합니다. .

요컨대, 비트 필드는 가장 일반적으로 사용되는 것처럼 보이는 상황에 유용 할만큼 충분히 지정되지 않았습니다.


ISO / IEC 9899 : 6.7.2.1/10

구현은 비트 필드를 보유하기에 충분히 큰 어 드레서 블 저장 장치를 할당 할 수 있습니다. 충분한 공간이 남아 있으면 구조에서 다른 비트 필드 바로 뒤에 오는 비트 필드가 동일한 단위의 인접한 비트로 패킹됩니다. 공간이 충분하지 않은 경우 적합하지 않은 비트 필드가 다음 유닛에 삽입되거나 인접 유닛과 겹치는 지 여부가 구현에 따라 정의됩니다. 단위 내에서 비트 필드의 할당 순서 (상위에서 낮은 순서로 또는 낮은 순서에서 높은 순서로)는 구현에 따라 정의됩니다. 주소 지정이 가능한 저장 장치의 정렬은 지정되지 않았습니다.

시스템 엔디안 또는 비트에 관계없이 이식 가능한 코드를 작성하려고 할 때 비트 필드 순서 또는 정렬에 대한 가정을하는 대신 비트 시프트 연산을 사용하는 것이 더 안전합니다.

EXP11-C 도 참조하십시오 . 한 유형을 예상하는 연산자를 호환되지 않는 유형의 데이터에 적용하지 마십시오 .


비트 필드 액세스는 기본 유형에 대한 작업 측면에서 구현됩니다. 예에서 unsigned int. 따라서 다음과 같은 것이 있다면 :

struct x {
    unsigned int a : 4;
    unsigned int b : 8;
    unsigned int c : 4;
};

field b에 액세스하면 컴파일러는 전체에 액세스 한 unsigned int다음 적절한 비트 범위를 이동하고 마스킹합니다. (글쎄, 그것은하지 않습니다 해야 하지만, 우리는 않는 척 수 있습니다.)

빅 엔디안에서 레이아웃은 다음과 같습니다 (가장 중요한 비트 우선).

AAAABBBB BBBBCCCC

리틀 엔디안에서 레이아웃은 다음과 같습니다.

BBBBAAAA CCCCBBBB

리틀 엔디안에서 빅 엔디안 레이아웃에 액세스하거나 그 반대로 액세스하려면 몇 가지 추가 작업을 수행해야합니다. 이식성의 이러한 증가는 성능 저하를 가져오고 구조체 레이아웃은 이미 이식 가능하지 않기 때문에 언어 구현자는 더 빠른 버전을 사용했습니다.

이것은 많은 가정을합니다. 또한 sizeof(struct x) == 4대부분의 플랫폼에서 유의하십시오 .


비트 필드는 머신의 엔디안 상태에 따라 다른 순서로 저장됩니다. 이는 경우에 따라 중요하지 않을 수 있지만 다른 경우에는 중요 할 수 있습니다. 예를 들어 ParsedInt 구조체가 네트워크를 통해 전송 된 패킷의 플래그를 나타내고, 리틀 엔디안 머신 및 빅 엔디안 머신이 전송 된 바이트와 다른 순서로 해당 플래그를 읽었다 고 가정 해 보겠습니다.


가장 중요한 점을 반영하려면 : 단일 컴파일러 / HW 플랫폼에서 소프트웨어 전용 구성으로 이것을 사용하는 경우 엔디안은 문제가되지 않습니다. 여러 플랫폼 또는 하드웨어 비트 레이아웃에 맞게 필요에 걸쳐 코드 나 데이터를 사용하는 경우, 그것은 IS 문제. 그리고 많은 전문 소프트웨어는 크로스 플랫폼이므로주의해야합니다.

다음은 가장 간단한 예입니다. 이진 형식으로 숫자를 디스크에 저장하는 코드가 있습니다. 이 데이터를 바이트 단위로 명시 적으로 디스크에 쓰고 읽지 않으면 반대 엔디안 시스템에서 읽을 경우 동일한 값이 아닙니다.

구체적인 예 :

int16_t s = 4096; // a signed 16-bit number...

내 프로그램이 내가 읽고 자하는 디스크의 데이터와 함께 제공된다고 가정 해 봅시다.이 경우 4096으로로드하고 싶다고 가정 해 보겠습니다.

fread((void*)&s, 2, fp); // reading it from disk as binary...

여기서는 명시 적 바이트가 아닌 16 비트 값으로 읽었습니다. 즉, 내 시스템이 디스크에 저장된 엔디안과 일치하면 4096을 얻고 그렇지 않으면 16을 얻습니다 !!!!!

따라서 엔디안의 가장 일반적인 용도는 이진수를 대량로드 한 다음 일치하지 않으면 bswap을 수행하는 것입니다. 과거에는 인텔이 이상한 사람이었고 바이트를 교환하는 고속 명령을 제공했기 때문에 빅 엔디안으로 데이터를 디스크에 저장했습니다. 요즘 인텔은 너무 일반적이어서 종종 리틀 엔디안을 기본값으로 만들고 빅 엔디안 시스템에서 스왑합니다.

느리지 만 엔디안 중립적 인 접근 방식은 모든 I / O를 바이트 단위로 수행하는 것입니다.

uint_8 ubyte;
int_8 sbyte;
int16_t s; // read s in endian neutral way

// Let's choose little endian as our chosen byte order:

fread((void*)&ubyte, 1, fp); // Only read 1 byte at a time
fread((void*)&sbyte, 1, fp); // Only read 1 byte at a time

// Reconstruct s

s = ubyte | (sByte << 8);

이것은 엔디안 스왑을 수행하기 위해 작성한 코드와 동일하지만 더 이상 엔디안을 확인할 필요가 없습니다. 그리고 매크로를 사용하여이를 덜 고통스럽게 만들 수 있습니다.

프로그램에서 사용하는 저장 데이터의 예를 사용했습니다. 언급 된 다른 주요 응용 프로그램은 하드웨어 레지스터를 작성하는 것입니다. 여기서 해당 레지스터는 절대 순서가 있습니다. 이것이 등장하는 매우 일반적인 장소 중 하나는 그래픽입니다. 엔디안이 잘못되면 빨간색과 파란색 채널이 반전됩니다! 다시 말하지만, 문제는 이식성 중 하나입니다. 주어진 하드웨어 플랫폼과 그래픽 카드에 간단히 적용 할 수 있지만 동일한 코드가 다른 컴퓨터에서 작동하도록하려면 테스트해야합니다.

다음은 고전적인 테스트입니다.

typedef union { uint_16 s; uint_8 b[2]; } EndianTest_t;

EndianTest_t test = 4096;

if (test.b[0] == 12) printf("Big Endian Detected!\n");

비트 필드 문제도 존재하지만 엔디안 문제와 직교합니다.


지적하자면 우리는 비트 엔디안이나 비트 필드의 엔디안이 아닌 바이트 엔디안 문제에 대해 논의 해 왔으며 이는 다른 문제로 넘어갑니다.

크로스 플랫폼 코드를 작성하는 경우 구조체를 바이너리 객체로 작성하지 마십시오. 위에서 설명한 엔디안 바이트 문제 외에도 컴파일러간에 모든 종류의 패킹 및 포맷 문제가있을 수 있습니다. 언어는 컴파일러가 실제 메모리에 구조체 또는 비트 필드를 배치하는 방법에 대한 제한을 제공하지 않으므로 디스크에 저장할 때 구조체의 각 데이터 멤버를 한 번에 하나씩, 가급적이면 바이트 중립적 인 방식으로 작성해야합니다.

이 패킹은 비트 필드의 "비트 엔디안"에 영향을줍니다. 다른 컴파일러가 비트 필드를 다른 방향으로 저장할 수 있고 비트 엔디안이 추출 방법에 영향을 미치기 때문입니다.

따라서 문제의 두 수준을 모두 염두에 두십시오. 바이트 엔디안은 단일 스칼라 값 (예 : 부동 소수점)을 읽는 컴퓨터의 능력에 영향을 미치고 컴파일러 (및 빌드 인수)는 집계 구조에서 읽는 프로그램의 능력에 영향을 미칩니다.

내가 과거에 한 일은 중립적 인 방식으로 파일을 저장하고로드하고 데이터가 메모리에 배치되는 방식에 대한 메타 데이터를 저장하는 것입니다. 이를 통해 호환되는 경우 "빠르고 쉬운"바이너리로드 경로를 사용할 수 있습니다.

참조 URL : https://stackoverflow.com/questions/6043483/why-bit-endianness-is-an-issue-in-bitfields

반응형