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이로 변환 0x10000u
된 uint32_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);
}
답변
int
16 비트 인 경우 버전의 표현식 값이 다음과 같은 경우 버전은 구현 정의 동작에 의존합니다.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_MIN
의 int
유형에 맞지 않으면 여전히 오버플로가 발생할 수 있으며이 경우 long
사용해야합니다.
질문의 원본 버전과의 차이점은 반환 시간에 발생합니다. 원본은 항상 빼고 0x10000
2의 보수로 부호가있는 오버플로가 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
(또는 unsigned
if 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
버전이 덜 적합합니다 ( 조립 참조) .
답변
모든 기고자에게 답변을 주셔서 감사합니다. 다음은 집단 작업이 요약 한 내용입니다.
- C 표준 7.20.1.1에 따라 정확한 너비 정수 유형 : types
uint8_t
,int16_t
및uint16_t
표현의 실제 비트가되도록 의해 지정된 순서대로 배열 명백하게, 패딩 비트없이 2 바이트들을 2의 보수 표현을 사용한다 함수 이름 - 부호없는 16 비트 값을 계산
(unsigned)data[0] | ((unsigned)data[1] << 8)
(little endian 버전의 경우) 단일 명령어로 컴파일되고 부호없는 16 비트 값이 생성됩니다. - C 표준 6.3.1.3에 따라 부호있는 정수 및 부호없는 정수 : type 값
uint16_t
을 부호있는 유형으로 변환int16_t
이 대상 형식의 범위에없는 경우 구현 정의 동작이 있습니다. 표현이 정확하게 정의 된 유형에 대해서는 특별한 규정이 없습니다. - 이 구현 정의 동작을 피하기 위해 부호없는 값이 더 큰지 테스트
INT_MAX
하고을 빼서 해당하는 부호있는 값을 계산할 수0x10000
있습니다. zwol 이 제안한 모든 값에 대해이 작업을 수행하면 범위를 벗어난 값이 생성 될 수 있습니다int16_t
동일한 구현 정의 동작으로 있습니다. - 에 대한 테스트
0x8000
비트를 하면 컴파일러가 비효율적 인 코드를 생성합니다. - 구현 정의 동작없이보다 효율적인 변환은 유형 제거를 사용 합니다. 은 노조를 통한 정리를 사용하지만이 접근법의 정의에 관한 논쟁은 여전히 C 표준의위원회 수준에서도 열려 있습니다.
- 유형 제거 는 이식 가능하고 정의 된 동작을 사용하여 수행 할 수 있습니다.
memcpy
.
포인트 2와 7을 결합하면 다음은 gcc 와 clang을 사용하여 단일 명령어로 효율적으로 컴파일되는 이식 가능하고 완전히 정의 된 솔루션입니다 .
#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;
}
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