반응형

들어가기에 앞서...

 

Endianness 라는 개념이 있다.

보통 Big-endian과 Little-endian 두 가지가[각주:1] 있는데, 메모리에 여러 바이트 연속된 자료가 저장되는 순서를 말한다.

다른 표현으로는 바이트 오더(Byte Order)라고도 한다.

 

 

Cortex-M4에서의 Endianness

 

// Reverses the byte order in unsigned integer value.
// For example, 0x12345678 becomes 0x78563412.
__STATIC_FORCEINLINE uint32_t __REV(uint32_t value)
{
#if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 5)
  return __builtin_bswap32(value);
#else
  uint32_t result;

  __ASM volatile ("rev %0, %1" : __CMSIS_GCC_OUT_REG (result) : __CMSIS_GCC_USE_REG (value) );
  return result;
#endif
}

// Reverses the byte order within each halfword of a word.
// For example, 0x12345678 becomes 0x34127856.
__STATIC_FORCEINLINE uint32_t __REV16(uint32_t value)
{
  uint32_t result;

  __ASM volatile ("rev16 %0, %1" : __CMSIS_GCC_OUT_REG (result) : __CMSIS_GCC_USE_REG (value) );
  return result;
}

// Reverses the byte order in a 16-bit value and returns the signed 16-bit result.
// For example, 0x0080 becomes 0x8000.
__STATIC_FORCEINLINE int16_t __REVSH(int16_t value)
{
#if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8)
  return (int16_t)__builtin_bswap16(value);
#else
  int16_t result;

  __ASM volatile ("revsh %0, %1" : __CMSIS_GCC_OUT_REG (result) : __CMSIS_GCC_USE_REG (value) );
  return result;
#endif
}

 

회사에서 Cortex-M4[각주:2]로 장비를 만드는데, 이 환경에는 위와 같이 바이트 오더를 바꾸는 CPU 함수들이 있다.

이에 따라 프로토콜에서 바이트 오더가 바뀌어야 하면 적절한 함수만 호출하면 된다.

 

Big-endian과 Little-endian 외에 Middle-endian도 지원할 수 있도록 내장(intrinsic) 함수들을 만들어뒀다.

 

MFC에서의 Endianess 삽질

 

x86/x64 환경에서는 이런 CPU 명령이 있을리가 없다고 믿어 직접 만들기로 했다.

constexpr uint16_t __REVSH(uint16_t n) noexcept {
  return ((n >> 8) | (n << 8));
}

 

그런데, 좀 예쁘지(?) 않아서 좀 바꿔보기로 했다.

이를 위한 매크로도 충실하게 준비되어 있다.

constexpr uint16_t __REVSH(uint16_t n) noexcept {
  return MAKEWORD(LOBYTE(n), HIBYTE(n));
}

그리고는 정상적으로 동작하지 않았고, 망했다[...]

 

위 함수는 인자의 순서를 잘못 기술해서, 뒤집어지지 않고, 입력값을 그대로 출력해준다...

아래와 같이 작성해야 맞다.

constexpr uint16_t __REVSH(uint16_t n) noexcept {
  return MAKEWORD(HIBYTE(n), LOBYTE(n));
}

 

여기에 시간을 허비하고나서 다시 생각하니, 근본적으로 접근 방법이 틀린 것이다.

이런 함수는 내가 만들 필요가 없이 만들어져 있는 것을 활용하면 된다.

 

일단 치사하게는(?) 아래와 같이 하는 방법도 있다.

#include <WinSock2.h>

uint16_t __REVSH(uint16_t n) {
  return htons(n);
}

 

그런데, 이건 바람직한 방법이 아니다.

소켓을 사용하지 않는 경우도 있을 수가 있고, 근본적으로 htons()의 목적이 이게 아니기 때문이다.

이 함수의 목적은 Little/Endian 데이터를 무조건 Big-endian으로 바꾸는 것이라 미묘하게 다르다.

게다가, 성능도 느린 편이다.

 

이 정도 고민했을 때 뭔가 쌔한 느낌이 들었다.

아니, 설마 SIMD가 판치는 요즘 x86/x64 CPU에서 이런 기능이 없다고?

 

찾아보니 당연히 있었다[...]

80486 이전 CPU에서는 없던 명령이라는 친절한 설명도 있었고[...]

 

그리고, 역시 당연히 이를 위한 intrinsic 함수도 만들어져 있었다.

선언은 intrinc.hstdlib.h에 되어있고...

 

unsigned short _byteswap_ushort(unsigned short value);
unsigned long _byteswap_ulong(unsigned long value);
unsigned __int64 _byteswap_uint64(unsigned __int64 value);

 

결론적으로, Cortex-M4 환경과 동일한 함수를 사용하려면 아래와 같이 선언하면 된다.

 

uint16_t __REVSH(uint16_t n) {
  return _byteswap_ushort(n);
}

uint32_t __REV(uint32_t n) {
  return _byteswap_ulong(n);
}

uint32_t __REV16(uint32_t n) {
  return (((n >> 8) & 0x00ff00ff) | ((n << 8) & 0xff00ff00));
}

 

 

  1. Middle-endian도 있는데, 최근 듕귁 뫄뫄 장비에서 이걸 만나서 삽질함 [본문으로]
  2. Cortex-M4는 x86/x64와 동일하게 Little-endian을 사용함 [본문으로]
반응형

공유하기

facebook twitter kakaoTalk kakaostory naver band