source

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

factcode 2023. 1. 19. 20:59
반응형

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

비트필드를 사용하는 모든 휴대용 코드는 리틀엔디언 플랫폼과 빅엔디언 플랫폼을 구별하는 것으로 보입니다.이러한 코드의 예는 Linux 커널의 structure iphdr 선언을 참조하십시오.나는 왜 비트 엔디안이 문제가 되는지 전혀 이해하지 못한다.

제가 알기로는 비트필드는 순전히 컴파일러 구조이며 비트레벨 조작을 용이하게 하기 위해 사용됩니다.

들어 비트필드에 대해 해 보겠습니다.


struct ParsedInt {
    unsigned int f1:1;
    unsigned int f2:3;
    unsigned int f3:4;
};
uint8_t i;
struct ParsedInt *d = &i;
Here, writing d->f2 is simply a compact and readable way of saying (i>>1) & (1<<4 - 1).

단, 비트 동작은 명확하게 정의되어 아키텍처에 관계없이 동작합니다.그럼 왜 비트필드는 이식할 수 없는 거죠?

C 표준에 따르면 컴파일러는 원하는 임의의 방법으로 비트 필드를 저장할 수 있습니다.비트가 할당되어 있는 장소를 상정할 수 없습니다.다음은 C 표준에서 지정되지 않은 비트필드 관련 몇 가지입니다.

지정되지 않은 동작

  • 비트 필드(6.7.2.1)를 유지하기 위해 할당된 주소 지정 가능한 스토리지 유닛의 정렬입니다.

구현 정의 동작

  • 비트 필드가 스토리지 유닛 경계를 가로지를 수 있는지 여부(6.7.2.1).
  • 유닛 내 비트필드 할당 순서(6.7.2.1).

Big/Little endian도 물론 구현 정의되어 있습니다.즉, 다음과 같은 방법으로 구조를 할당할 수 있습니다(16비트 ints를 전제로 합니다).

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비트 정수의 복잡함을 더합니다(큰 endian 또는 작은 endian).그런 다음 컴파일러가 비트 필드 내 임의의 수의 패딩 바이트를 추가할 수 있다는 사실을 추가합니다.이는 구조체로 취급되기 때문입니다(구조체의 맨 앞부분에는 패딩을 추가할 수 없지만 다른 모든 곳에 패딩을 추가할 수 없습니다).

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

이 질문에 답하자면 C 표준은 비트필드의 구현 방법이 매우 모호하기 때문에 휴대용 비트필드 코드와 같은 것은 없습니다.비트필드를 신뢰할 수 있는 유일한 것은 프로그래머가 메모리 내 비트의 위치에 신경 쓰지 않는 부울값의 청크가 되는 것입니다.

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

제가 알기로는 비트필드는 순전히 컴파일러 구조입니다.

그리고 그게 문제의 일부입니다.만약 비트 필드의 사용이 컴파일러가 소유하는 것으로 제한된다면, 컴파일러가 어떻게 비트를 포장하거나 명령하는지는 그 누구에게도 큰 문제가 되지 않을 것이다.

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

즉, 비트필드는 가장 일반적으로 사용되는 상황에서 유용하게 사용할 수 있을 정도로 충분히 지정되지 않았습니다.

ISO/IEC 9899: 6.7.2.1/10

구현은 비트엘드를 보유하기에 충분히 큰 주소 지정 가능한 스토리지 유닛을 할당할 수 있다.충분한 공간이 남아 있는 경우 구조에서 다른 비트 δeld 바로 뒤에 이어지는 비트 δeld는 동일한 단위의 인접 비트로 채워져야 한다.불충분한 공간이 남아 있는 경우, 맞지 않는 비트 요소가 다음 유닛에 배치되는지 또는 인접 유닛과 겹치는지 여부가 구현 정의된다.단위 내 비트 렐드의 할당 순서(고차에서 저차 또는 저차에서 고차)는 구현 정의된다. 주소 지정 가능한 스토리지 유닛의 정렬은 명확하지 않습니다.

시스템 엔디안성 또는 비트성에 관계없이 휴대용 코드를 쓸 때 비트 필드 순서나 정렬에 대한 가정을 하지 말고 비트 시프트 연산을 사용하는 것이 안전합니다.

EXP11-C」도 참조해 주세요. 호환되지 않는 유형의 데이터에 한 가지 유형을 기대하는 연산자를 적용하지 마십시오.

비트 필드 액세스는 기본 유형에 대한 운영 측면에서 구현됩니다.예에서는 " " 입니다.unsigned int다음과 같이 합니다.

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

하는 경우bunsigned int다음으로 적절한 비트 범위를 이동 및 마스크합니다.(그럴 필요는 없지만 그렇다고 가정할 수는 있습니다.)

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

AAAABBBB BBBBCCCC

little endian의 레이아웃은 다음과 같습니다.

BBBBAAAA CCCCBBBB

little endian 또는 little endian에서 큰 endian 레이아웃에 액세스하려면 추가 작업을 수행해야 합니다.이러한 휴대성의 향상은 퍼포먼스에 악영향을 미칩니다.구조 레이아웃은 이미 포터블이 아니기 때문에 언어 구현자는 더 빠른 버전을 선택했습니다.

이치노, 「 」, 「 」라고 하는 도 주의해 주세요.sizeof(struct x) == 4많은 플랫폼이 있습니다.

비트 필드는 머신의 endian-ness에 따라 다른 순서로 저장됩니다.이것은 경우에 따라서는 문제가 되지 않을 수도 있지만, 다른 경우에는 문제가 될 수도 있습니다.예를 들어, 파싱이네트워크를 통해 전송된 패킷의 플래그를 나타내는 int structure, 작은 endian 머신 및 big endian 머신은 전송된 바이트와 다른 순서로 플래그를 읽습니다.이것은 명백한 문제입니다.

가장 중요한 점을 반영하려면:단일 컴파일러/HW 플랫폼에서 소프트웨어만의 구성으로 사용하는 경우 엔디안성은 문제가 되지 않습니다.여러 플랫폼에서 코드 또는 데이터를 사용하거나 하드웨어 비트 레이아웃을 일치시켜야 하는 경우 문제가 됩니다.많은 프로페셔널 소프트웨어는 크로스 플랫폼이기 때문에 주의가 필요합니다.

다음으로 가장 간단한 예를 제시하겠습니다.디스크에 바이너리 형식의 숫자를 저장하는 코드가 있습니다.이 데이터를 직접 디스크에 바이트 단위로 쓰고 읽지 않으면 반대쪽 엔디안 시스템에서 읽으면 같은 값이 되지 않습니다.

구체적인 예:

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

프로그램에 읽을 데이터가 디스크에 포함되어 있다고 칩시다.이 경우 4096으로 로드하고 싶다고 합시다.

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

여기서는 명시적 바이트가 아닌 16비트 값으로 읽습니다.즉, 디스크에 보존되어 있는 엔디안니스와 시스템이 일치하면 4096이 되고, 일치하지 않으면 16이 됩니다.!!

따라서 endianness의 가장 일반적인 용도는 이진수를 대량 로드하고 일치하지 않으면 bswap을 실행하는 것입니다.과거에는 인텔이 홀수였기 때문에 바이트 교환을 위한 고속 명령을 제공했기 때문에 데이터를 디스크에 저장했습니다.오늘날 인텔은 너무 일반적이기 때문에 Little Endian을 디폴트로 하여 빅 엔디안 시스템에서 스와프하는 경우가 많습니다.

느리지만 중립적인 접근법은 모든 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

반응형