[c] 2 바이트를 부호있는 16 비트 정수로 변환하는 올바른 방법은 무엇입니까?

에서 이 답변 , zwol는 이 주장을했다 :

2 바이트의 데이터를 외부 소스에서 16 비트 부호있는 정수로 변환하는 올바른 방법은 다음과 같은 도우미 기능을 사용하는 것입니다.

#include <stdint.h>

int16_t be16_to_cpu_signed(const uint8_t data[static 2]) {
    uint32_t val = (((uint32_t)data[0]) << 8) |
                   (((uint32_t)data[1]) << 0);
    return ((int32_t) val) - 0x10000u;
}

int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
    uint32_t val = (((uint32_t)data[0]) << 0) |
                   (((uint32_t)data[1]) << 8);
    return ((int32_t) val) - 0x10000u;
}

위의 함수 중 적절한 것은 배열에 리틀 엔디안이 포함되어 있는지 또는 빅 엔디안이 포함되어 있는지에 따라 다릅니다. 엔디안은 여기서 문제가되지 않지만 zwol이로 변환 0x10000uuint32_t값 에서 빼는 이유가 궁금 합니다 int32_t.

왜 이것이 올바른 방법 입니까?

반환 유형으로 변환 할 때 구현 정의 동작을 어떻게 피합니까?

2의 보수 표현을 가정 할 수 있으므로이 간단한 캐스트는 어떻게 실패합니까? return (uint16_t)val;

이 순진한 솔루션의 문제점 :

int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
    return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
}



답변

int16 비트 인 경우 버전의 표현식 값이 다음과 같은 경우 버전은 구현 정의 동작에 의존합니다.return 문이 범위를 벗어났습니다 int16_t.

그러나 첫 번째 버전에도 비슷한 문제가 있습니다. 예를 들어 int32_t에 대한 typedef int이고 입력 바이트가 모두 0xFF이면 return 문의 뺄셈 결과는 다음과 같습니다.UINT_MAX 로 변환 될 때 구현 정의 동작을 유발합니다 int16_t.

당신이 연결하는 대답에는 몇 가지 중요한 문제가 있습니다.


답변

이것은 일반적으로 정확해야하며 일반적인 2의 보수 대신 부호 비트 또는 1의 보수 표현 을 사용하는 플랫폼에서도 작동 합니다. 입력 바이트는 2의 보수로 가정합니다.

int le16_to_cpu_signed(const uint8_t data[static 2]) {
    unsigned value = data[0] | ((unsigned)data[1] << 8);
    if (value & 0x8000)
        return -(int)(~value) - 1;
    else
        return value;
}

지점 때문에 다른 옵션보다 비쌉니다.

이것이 달성하는 것은 표현이 플랫폼의 int표현과 어떻게 관련 되는지에 대한 가정을 피한다는 것입니다 unsigned. 캐스트 int대상은 대상 유형에 맞는 모든 숫자의 산술 값을 유지해야합니다. 반전은 16 비트 숫자의 최상위 비트가 0이되도록하기 때문에 값이 적합합니다. 그런 다음 1의 단항 -과 뺄셈은 2의 보수 부정에 대한 일반적인 규칙을 적용합니다. 플랫폼에 따라 대상 INT16_MINint유형에 맞지 않으면 여전히 오버플로가 발생할 수 있으며이 경우 long사용해야합니다.

질문의 원본 버전과의 차이점은 반환 시간에 발생합니다. 원본은 항상 빼고 0x100002의 보수로 부호가있는 오버플로가 int16_t범위를 줄 바꿈하는 반면 이 버전에는 부호없는 랩 오버 if( undefined ) 를 피하는 명시 적 기능 이 있습니다 .

실제로 오늘날 사용되는 거의 모든 플랫폼은 2의 보수 표현을 사용합니다. 사실, 플랫폼 표준 규격이있는 경우 stdint.h정의는 것을 int32_t, 그것은 있어야 그것에 대한 2의 보수를 사용합니다. 이 접근 방식이 때로는 편리한 경우 정수 데이터 유형이없는 일부 스크립팅 언어가 있습니다-부동 소수점에 대해 위에서 표시된 작업을 수정할 수 있으며 올바른 결과를 제공합니다.


답변

다른 방법-사용 union:

union B2I16
{
   int16_t i;
   byte    b[2];
};

프로그램에서 :

...
B2I16 conv;

conv.b[0] = first_byte;
conv.b[1] = second_byte;
int16_t result = conv.i;

first_byte그리고 second_byte거의 큰 엔디안 모델에 따라 교환 할 수 있습니다. 이 방법은 좋지 않지만 대안 중 하나입니다.


답변

산술 연산자는 시프트비트 단위 또는 표현식에서 (uint16_t)data[0] | ((uint16_t)data[1] << 8)보다 작은 유형에서는 작동하지 않으므로 int해당 uint16_t값이 int(또는 unsignedif sizeof(uint16_t) == sizeof(int))로 승격됩니다 . 그럼에도 불구하고 2 바이트 만 값을 포함하기 때문에 정답을 얻을 수 있습니다.

빅 엔디안에서 리틀 엔디안으로의 변환을위한 또 다른 올바른 버전 (리틀 엔디안 CPU 가정)은 다음과 같습니다.

#include <string.h>
#include <stdint.h>

int16_t be16_to_cpu_signed(const uint8_t data[2]) {
    int16_t r;
    memcpy(&r, data, sizeof r);
    return __builtin_bswap16(r);
}

memcpy표현을 복사하는 데 사용되며 int16_t이는 표준을 준수하는 방법입니다. 이 버전은 또한 1 개의 명령어로 컴파일됩니다 . 어셈블리를movbe 참조하십시오 .


답변

다음은 이식 가능하고 잘 정의 된 동작에만 의존하는 다른 버전입니다 (헤더 #include <endian.h>는 표준이 아니고 코드는 다음과 같습니다).

#include <endian.h>
#include <stdint.h>
#include <string.h>

static inline void swap(uint8_t* a, uint8_t* b) {
    uint8_t t = *a;
    *a = *b;
    *b = t;
}
static inline void reverse(uint8_t* data, int data_len) {
    for(int i = 0, j = data_len / 2; i < j; ++i)
        swap(data + i, data + data_len - 1 - i);
}

int16_t be16_to_cpu_signed(const uint8_t data[2]) {
    int16_t r;
#if __BYTE_ORDER == __LITTLE_ENDIAN
    uint8_t data2[sizeof r];
    memcpy(data2, data, sizeof data2);
    reverse(data2, sizeof data2);
    memcpy(&r, data2, sizeof r);
#else
    memcpy(&r, data, sizeof r);
#endif
    return r;
}

little-endian 버전 은을 movbe사용 하여 단일 명령어로 컴파일 하고 clang, gcc버전이 덜 적합합니다 ( 조립 참조) .


답변

모든 기고자에게 답변을 주셔서 감사합니다. 다음은 집단 작업이 요약 한 내용입니다.

  1. C 표준 7.20.1.1에 따라 정확한 너비 정수 유형 : types uint8_t,int16_tuint16_t 표현의 실제 비트가되도록 의해 지정된 순서대로 배열 명백하게, 패딩 비트없이 2 바이트들을 2의 보수 표현을 사용한다 함수 이름
  2. 부호없는 16 비트 값을 계산 (unsigned)data[0] | ((unsigned)data[1] << 8)(little endian 버전의 경우) 단일 명령어로 컴파일되고 부호없는 16 비트 값이 생성됩니다.
  3. C 표준 6.3.1.3에 따라 부호있는 정수 및 부호없는 정수 : type 값 uint16_t을 부호있는 유형으로 변환int16_t 이 대상 형식의 범위에없는 경우 구현 정의 동작이 있습니다. 표현이 정확하게 정의 된 유형에 대해서는 특별한 규정이 없습니다.
  4. 이 구현 정의 동작을 피하기 위해 부호없는 값이 더 큰지 테스트 INT_MAX하고을 빼서 해당하는 부호있는 값을 계산할 수 0x10000있습니다. zwol 이 제안한 모든 값에 대해이 작업을 수행하면 범위를 벗어난 값이 생성 될 수 있습니다int16_t 동일한 구현 정의 동작으로 있습니다.
  5. 에 대한 테스트 0x8000비트를 하면 컴파일러가 비효율적 인 코드를 생성합니다.
  6. 구현 정의 동작없이보다 효율적인 변환은 유형 제거를 사용 합니다. 은 노조를 통한 정리를 사용하지만이 접근법의 정의에 관한 논쟁은 여전히 ​​C 표준의위원회 수준에서도 열려 있습니다.
  7. 유형 제거 는 이식 가능하고 정의 된 동작을 사용하여 수행 할 수 있습니다.memcpy .

포인트 2와 7을 결합하면 다음은 gccclang을 사용하여 단일 명령어로 효율적으로 컴파일되는 이식 가능하고 완전히 정의 된 솔루션입니다 .

#include <stdint.h>
#include <string.h>

int16_t be16_to_cpu_signed(const uint8_t data[2]) {
    int16_t r;
    uint16_t u = (unsigned)data[1] | ((unsigned)data[0] << 8);
    memcpy(&r, &u, sizeof r);
    return r;
}

int16_t le16_to_cpu_signed(const uint8_t data[2]) {
    int16_t r;
    uint16_t u = (unsigned)data[0] | ((unsigned)data[1] << 8);
    memcpy(&r, &u, sizeof r);
    return r;
}

64 비트 어셈블리 :

be16_to_cpu_signed(unsigned char const*):
        movbe   ax, WORD PTR [rdi]
        ret
le16_to_cpu_signed(unsigned char const*):
        movzx   eax, WORD PTR [rdi]
        ret


답변