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


31

에서 이 답변 , 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_t은 구현 정의이므로 순진한 접근 방식은 이식성이 없습니다.
nwellnhof

@nwellnhof 캐스트가 없습니다int16_t
MM

사용할 매핑을 지정하지 않으면 제목의 질문에 대답 할 수 없습니다
MM

4
두 방법 모두 구현 정의 동작 (서명되지 않은 값을 값을 나타낼 수없는 부호있는 유형으로 변환)에 의존합니다. 예 : 첫 번째 방법에서는 0xFFFF0001u으로 표현할 수없고 int16_t두 번째 방법 0xFFFFu에서는으로 표현할 수 없습니다 int16_t.
Sander De Dycker

1
"2의 보수 표현을 가정 할 수 있기 때문에"[인용 필요]. C89와 C99는 확실히 1의 보수와 부호 크기 표현을 부정하지 않았습니다. Qv, stackoverflow.com/questions/12276957/…
에릭 타워

답변:


20

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

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

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


2
그러나 올바른 방법은 무엇입니까?
idmean

@idmean 질문에 대답하기 전에 설명이 필요합니다. 질문에 대한 의견을 요청했지만 OP가 응답하지 않았습니다.
MM

1
@ MM : 엔디안이 문제가 아니라는 질문을 편집했습니다. zwol이 해결하려는 IMHO 문제는 대상 유형으로 변환 할 때 구현 정의 동작이지만, 나는 당신에게 동의합니다. 그의 방법에 다른 문제가 있다고 생각합니다. 구현 정의 동작을 어떻게 효율적으로 해결 하시겠습니까?
chqrlie

@chqrlieforyellowblockquotes 특별히 엔디안을 언급하지 않았습니다. 두 입력 옥텟의 정확한 비트를 int16_t?
MM

@ MM : 예, 정확히 질문입니다. 나는 바이트를 썼지 만 타입이이므로 올바른 단어는 실제로 옥텟 이어야합니다 uchar8_t.
chqrlie

7

이것은 일반적으로 정확해야하며 일반적인 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의 보수를 사용합니다. 이 접근 방식이 때로는 편리한 경우 정수 데이터 유형이없는 일부 스크립팅 언어가 있습니다-부동 소수점에 대해 위에서 표시된 작업을 수정할 수 있으며 올바른 결과를 제공합니다.


있다는 C 표준은 구체적으로 위임 int16_t하고 intxx_t자신의 서명되지 않은 변종 패딩 비트없이 2의 보수 표현을 사용해야합니다. 이러한 유형을 호스팅하고에 대한 다른 표현을 사용하려면 의도적으로 왜곡 된 아키텍처가 필요 int하지만 DS9K는 이러한 방식으로 구성 할 수 있습니다.
chqrlie

@chqrlieforyellowblockquotes 좋은 지적, int혼란을 피하기 위해 사용 하기로 변경했습니다 . 실제로 플랫폼이 정의 int32_t하면 2의 보수 여야합니다.
jpa

이러한 유형은 C99에서 다음과 같이 표준화되었습니다. C99 7.18.1.1 정확한 너비 정수 유형 typedef 이름 intN_t 은 width N, 패딩 비트 없음 및 2의 보수 표현 으로 부호있는 정수 유형을 지정합니다 . 따라서 int8_t너비가 정확히 8 비트 인 부호있는 정수 유형을 나타냅니다. 다른 표현은 여전히 ​​표준에 의해 지원되지만 다른 정수 유형에 대해서는 지원됩니다.
chqrlie

업데이트 된 버전으로 (int)value유형 int에 16 비트가있는 경우 구현 정의 동작 이 있습니다. (long)value - 0x100002를 보완 해야하는 아키텍처에서는 값 0x8000 - 0x10000을 16 비트로 표현할 수 없으므로 int문제가 유지됩니다.
chqrlie

@chqrlieforyellowblockquotes 네, 방금 똑같이 나타났습니다. 대신 ~로 고정했지만 long동일하게 작동합니다.
jpa

6

다른 방법-사용 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거의 큰 엔디안 모델에 따라 교환 할 수 있습니다. 이 방법은 좋지 않지만 대안 중 하나입니다.


2
공용체 유형 punning 지정되지 않은 동작 입니까?
Maxim Egorushkin

1
@MaximEgorushkin : Wikipedia는 C 표준을 해석하는 권위있는 소스가 아닙니다.
에릭 Postpischil

2
@EricPostpischil 메시지보다는 메신저에 집중하는 것은 현명하지 않습니다.
Maxim Egorushkin

1
@ MaximEgorushkin : 네, 죄송합니다. 귀하의 의견을 잘못 읽었습니다. 가정 byte[2]int16_t같은 크기이며, 이는 하나 또는 두 가지의 다른 순서화가 아닌 일부는 임의의 비트 위치의 값을 섞는다. 따라서 적어도 컴파일 타임에 구현에 어떤 엔디안이 있는지 감지 할 수 있습니다.
피터 코 데스

1
이 표준은 유니온 멤버의 값이 멤버에 저장된 비트를 해당 유형의 값 표현으로 해석 한 결과라고 명시하고 있습니다. 형식의 표현이 구현 정의 된 구현 구현 측면이 있습니다.
MM

6

산술 연산자는 시프트비트 단위 또는 표현식에서 (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 참조하십시오 .


1
@MM 한 가지 이유 __builtin_bswap16는 ISO C의 바이트 스와핑을 효율적으로 구현할 수 없기 때문입니다.
Maxim Egorushkin

1
사실이 아니다; 컴파일러는 코드가 바이트 스와핑을 구현하고 효율적인 내장형으로 변환 함을 감지 할 수 있습니다
MM

1
로 변환 int16_t하는 방법 uint16_t은 잘 정의되어 있습니다. 음수 값은보다 큰 값으로 변환 INT_MAX되지만이 값을 다시 변환하면 uint16_t구현에서 정의 된 동작입니다. 6.3.1.3 부호있는 정수 및 부호없는 정수 1. 정수 유형의 값이 _Bool 이외의 다른 정수 유형으로 변환 된 경우 값은 새 유형으로 표시 될 수 있으며 변경되지 않습니다. ... 3. 그렇지 않으면 새 유형이 서명되고 값을 표현할 수 없습니다. 결과는 구현 정의되거나 구현 정의 신호가 발생합니다.
chqrlie

1
@MaximEgorushkin gcc는 16 비트 버전에서 그렇게 잘 보이지 않지만 clang은 ntohs/ __builtin_bswap|/ <<패턴에 대해 동일한 코드를 생성합니다 . gcc.godbolt.org/z/rJ-j87
PSkocik

3
@MM : Maxim이 " 현재 컴파일러로 는 실제로 사용할 수 없다"고 말합니다 . 물론 컴파일러는 한 번만 빨 수 없으며 연속 바이트를 정수로로드하는 것을 인식 할 수 없습니다. GCC7 또는 8은 GCC3가 수십 년 전에 하락한 후 바이트 리버스 필요 하지 않은 경우로드 / 스토어 통합을 다시 도입 했습니다. 그러나 일반적으로 컴파일러는 CPU가 효율적으로 수행 할 수 있지만 ISO C가 무시할 수있는 노출을 무시하거나 거부 한 많은 작업에 실제로 도움이 필요한 경향이 있습니다. 이식 가능한 ISO C는 효율적인 코드 비트 / 바이트 조작에 적합한 언어는 아닙니다.
Peter Cordes

4

다음은 이식 가능하고 잘 정의 된 동작에만 의존하는 다른 버전입니다 (헤더 #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버전이 덜 적합합니다 ( 조립 참조) .


귀하의 주요 관심사가 된 것 같습니다 @chqrlieforyellowblockquotes uint16_tint16_t이 버전은 그래서 여기 당신이 가고, 그 변환이없는 변환.
Maxim Egorushkin

2

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

  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

저는 언어 변호사가 아니지만 char유형 만 다른 유형의 개체 표현을 별칭으로 포함하거나 포함 할 수 있습니다. 유형 uint16_t중 하나가 아니므 로 to 의 char유형 은 잘 정의 된 동작이 아닙니다. 표준은 변환 이 잘 정의되어 있어야합니다. memcpyuint16_tint16_tchar[sizeof(T)] -> T > char[sizeof(T)]memcpy
Maxim Egorushkin

memcpyuint16_t것은 int16_t정확히 다른 하나의 과제로, 최상의 잘 정의되지 않은, 이식 할 수 없습니다 구현 정의, 당신은과 그 마술 우회하기 수 없습니다 memcpy. uint16_t2의 보수 표현을 사용 하는지 또는 패딩 비트가 존재 하는지 여부 는 중요하지 않습니다. 이는 C 표준에 의해 정의되거나 요구되는 동작이 아닙니다.
Maxim Egorushkin

너무 많은 단어와 함께, 귀하의 "솔루션"교체로 귀결 r = umemcpy(&r, &u, sizeof u)있지만 후자는, 더 좋은 이전보다하지가 무엇입니까?
Maxim Egorushkin
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.