정렬되지 않은 데이터 접근

기술노트 210127

아키텍처:

Arm

컴포넌트:

compiler

업데이트:

2021-05-04 오전 8:15

소개

정렬되지 않은 데이터로 접근을 해야할 경우가 있습니다. 데이터가 네트워크나 직렬 링크(serial link)에서 수신되어 버퍼에 있을 수 있습니다. 안전하고 휴대성있게 정렬되지 않은 데이터에 접근하는 것은 매우 까다롭습니다—CPU 아키텍쳐, 컴파일러의 최적화 수준, 작업 중인 메모리 영역에 따라 결과는 달라질 수 있습니다.

본 기술 노트는 컴파일러에게 정렬되지 않은 데이터에 대해 어떻게 알리는지, 그리하여 문제를 피하는 방법을 보여줍니다.

논의

ISO/IEC 9899 C언어 표준에 따르면 :

"객체 또는 불완전 타입형 포인터는 다른 객체 또는 불완전 타입형 포인터로 변환될 수 있습니다. 만약 결과 포인터(resulting pointer)가 가리키는 것에 대한 타입과 정확히 맞지 않다면, 동작은 정의되지 않습니다(undefined behavior)".

기본 데이터 타입

uint32_t에 대한 포인터입니다:

uint32_t *data_p;

32비트 데이터 타입 uint32_t는 정렬 요구사항이 있기 때문에, uint32_t 형 포인터가 정확히 정렬되어 있음을 알 수 있습니다. (그렇지 않다면 동작이 정의되지 않을(undefined behavior) 수 있습니다.) 다음의 함수 정의를 통해,  컴파일러에게 out_p 는 정렬된 uint32_t 변수형 포인터라고 알릴 수 있습니다.

void set_data(uint32_t *out_p, uint32_t val)
{
    *out_p = val;
}

 

다음의 byte 배열이 있다고 가정해봅시다:

uint8_t network_data[] = {0,1,2,3,4,5,6,7,8,9};

컴파일러 속이기

만약 byte 배열에서 홀수 byte를 참조하고 형 변환을 통해 uint32_t 변환했다면, 동작이 정의되지 않을 수 있습니다. (undefined behavior) 그 이유는 결과 포인터(resulting pointer)가 가리키는 것에 대한 타입과 정확히 정렬되지 않았기 때문입니다.

data_p = (uint32_t*) &network_data[1];

결과 포인터 data_p 를 사용하게 되면 미지정 행동(undefined behavior)이 발생합니다.

예를들어 set_data 함수를 data_p 로 호출하면 미지정 행동(undefined behavior)이 발생합니다.

set_data(data_p, 1200);

uint8_t 형 포인터 &network_data[1]uint32_t 와 정렬된다고, 사실이 아닌 것을 컴파일러를 속이기 위해 형 변환을 통해 시도해 봅시다.

Cortex-M0에선 set_data 의 결과로 UsageFault 예외가 발생합니다.

Cortex-M3에선 같은 함수에 대해 잘 동작합니다.

Cortex-M0에선, set_data 에서 사용되는 STR 명령어는 정렬된 주소를 필요로 합니다.

Cortex-M3에선, STR 명령어는 정렬되지 않은 주소도 허용합니다.

Cortex-M0, M0+, M1의 경우, Armv6-M 아키텍처 참고 메뉴얼은 다음을 알려줍니다:

A3.2.1 Alignment behavior
The following data accesses always generate an alignment fault:
* Non word-aligned LDR and STR
* [...]

Cortex-M3, M4, M7의 경우, ARM v7-M 아키텍처 참고 메뉴얼은 다음을 알려줍니다:

A3.2.1 Alignment behavior
The following data accesses support unaligned addressing, and only 
generate alignment faults when the CCR.UNALIGN_TRP bit is set to 1:
* Non word-aligned LDR and STR
* [...]

따라서, Cortex-M0의 경우, 코드는 항상 UsageFault를 항상 발생시킵니다.

Cortex-M3의 경우, CCR.UNALIGN_TRP가 1로 셋팅 되어 있거나 그렇지 않은 경우에 따라, 코드는 UsageFault를 발생시킬 수도 있습니다. 

보시는 바와 같이, 정렬되지 않은 접근의 동작은 정의되지 않으며(undefined behavior), 프로세서 아키텍처에 따라 달라집니다.

Cortex-M3 Devices Generic User Guide에 의하면:

The Cortex-M3 processor supports unaligned access only for the following 
instructions: LDR, LDRT, LDRH, LDRHT, LDRSH, LDRSHT, STR, STRT, STRH, STRHT

Unaligned accesses are usually slower than aligned accesses. 
In addition, some memory regions might not support unaligned accesses. 
Therefore, ARM recommends that programmers ensure that accesses are aligned. 
To trap accidental generation of unaligned accesses, use the UNALIGN_TRP bit 
in the Configuration and Control Register.

정렬되지 않은 메모리 접근은 Linux Kernel documentation 에서도 설명하고 있습니다:

The effects of performing an unaligned memory access vary 
from architecture to architecture.

- Some architectures are able to perform unaligned memory accesses
  transparently, but there is usually a significant performance cost.
- Some architectures raise processor exceptions when unaligned accesses
  happen. 
- Some architectures are not capable of unaligned memory access, but will
  silently perform a different memory access

 

보시는 바와 같이, 다양한 아키텍쳐 위에서 동작하도록 이식성이 좋은 응용프로그램을 만들기 위해서는, 정렬되지 않은 접근을 피하고 미지정행동에 의존하지 않도록 해야합니다.

컴파일러로 부터 도움 요청하기

그렇다면 정렬되지 않은 접근을 피하기 위해 컴파일러에게 어떻게 도움을 요청할까? 컴파일러에게 데이터가 정렬되지 않았음을 알려야 합니다. #pragma pack, __packed를 사용하거나, IAR C/C++ 개발 가이드: "Alternatively, write your own customized functions for packing and unpacking structures"를 참고하세요.

__packed 사용하기

상기의 예제를 수정해보면, __packed 데이터 타입 속성을 사용하여 컴파일러에게 데이터가 정렬되지 않았을 수도 있다는 것을 알려야 합니다:

void set_unaligned_data(uint32_t __packed *out_p, uint32_t val)
{
    *out_p = val;
}

위의 코드로, uint32_t out_p 포인터가 정렬되지 않을 수도 있다는 것을 컴파일러에게 알리게 됩니다. 그에 따라 컴파일러가 조정될 것입니다.

정렬되지 않은 __packed 포인터를 통해 원래의 set_data 함수 호출을 시도하려고 할때, 컴파일러는 도움이 될만한 에러 메시지를 발생시킵니다.

Error[Pe167]: argument of type "uint32_t __packed *" is incompatible with parameter of type "uint32_t *"

이제 set_unaligned_data 함수는 Cortex-M0와 Cortex-M3에서 잘 동작합니다. Cortex-M0에선 더이상 STR 명령어를 사용되지 않습니다. 반면 Cortex-M3에선, 컴파일러에게 데이터가 정렬되지 않았음을 알렸음에도, STR 명령어가 사용되는 것을 볼수 있습니다. 그 이유는 컴파일러가 특정 정렬되지 않은 접근이 Cortex-M3 하드웨어에 의해 지원된다는 것을 "알고" 있기 때문입니다. 정렬되지 않은 접근에 대해 하드웨어 지원을 피하려면 --no_unaligned_access 컴파일러 옵션을 사용하세요.

Arm 기술 문서에서 나와 있듯이, "무정렬  접근의 우발적인 생성을 가두려면, UNALIGN_TRP비트를 사용하십시오.".

구조체

#pragma pack 사용하기

구조체에서, #pragma pack를 사용해 구조체를 보다 촘촘하게 배치할 수 있습니다. 이 데이터 타입 속성은 컴파일러에게 구조체가 정렬되지 않은 데이터를 잠재적으로 가지고 있다는 것을 알려줍니다. 감싸진(packed) 구조체 타입을 사용하면, 데이터가 정렬되지 않을 수도 있음을 컴파일러가 알게되고, 조정이 이루어집니다:

#pragma pack(1)
typedef struct my_packed_struct_s {
    uint8_t byte1;
    uint32_t val1;
    uint8_t byte2;
    uint32_t val2;
} my_packed_struct_t;
#pragma pack()

my_packed_struct_t *struct_p = (my_packed_struct_t*) &network_data[0];

void set_s_data(my_packed_struct_t *out_p, uint32_t v1, uint32_t v2)
{
    out_p->val1 = v1;
    out_p->val2 = v2;
}

상기의 set_s_data 함수는 my_packed_struct_t 타입을 사용했기에, 컴파일러에게 데이터가 정렬되지 않을 수도 있음을 알리게 됩니다. (my_packed_struct_t#pragma pack 명령어를 주었기에). 컴파일러는 조정을 하게 됩니다.

패킹된(packed) 구조체에 잠재적으로 정렬되지 않은 데이터를 위한 포인터를 생성하게 되면, 컴파일러는 도움이 될만한 경고 메시지를 발생시킵니다:

uint32_t *p = &out_p->val1;
Warning[Pa039]: use of address of unaligned structure member

이식성

응용프로그램이 이종의 아키텍처와 컴파일러를 넘나들어 진정한 이식성을 가지길 원한다면, 다음의 지원되는 pragma IAR-specific 리스트와 데이터 타입 속성을 고려하세요.

IAR C/C++ 개발 가이드 (January 2021) 참고:

상기의 리스트는 IAR 컴파일러들 사이에서도 __packed와 #pragma pack 변수에 대한 지원이 다르다는 것을 보여줍니다.

퍼포먼스

__packed#pragma pack을 사용할때 가장 큰 문제는 구조체안의 무정렬 요소에 대한 접근은 더 많은 코드를 사용하게 만든다는 것입니다.

IAR C/C++ 개발 가이드:

"알림: 올바르게 정렬되지 않은 객체에 접근하는 것은 더 크고 느린 코드가 필요합니다. 이러한 구조체 맴버에 여러번 접근하는 경우, 패킹(packed)되지 않은 구조체에 정확한 값을 생성하고, 그 구조체에 접근하는 것이 좋습니다."

Packing과 unpacking

IAR C/C++ 개발 가이드 에 따르면, "패킹된, 패킹되지 않은 구조체를 위한 직접 정의한 함수를 작성하라."

이것이 가장 안전하고 이식성이 좋은 길입니다. 패킹하는 것과, 패킹하지 않는 것의 단점은 구조체 데이터에 대해 두가지 관점이 필요하다는 것입니다.(packed과 unpacked)

상기의 예제를 계속 이어나가면, 패킹된, 패킹되지 않은 함수는 다음과 같이 보입니다. (my_struct_t 가 정의된 데이터와 쓰이는 일반 구조체입니다)

void unpack_data(const uint8_t *unaligned_data_p, my_struct_t *struct_p);
void pack_data(uint8_t *unaligned_data_p, const my_struct_t *struct_p);

예제 프로젝트

예제 프로젝트 2021-01-22_unaligned_8509.zip __packed, #pragma pack 에 대한 예시와 직접 정의한 packing과 unpacking에 대해 보여줍니다. 또한, Cortex-M0과 M3에서 정렬되지 않은 접근이 어떻게 UsageFault_Handler 로 끝나는지 보여줍니다.

예제 프로젝트로, C-SPY 시뮬레이터 디버거 드라이버를 사용하고, View>Memory 창으로 network_data 변수에 대해 학습하세요.

C-SPY 시뮬레이터는 정렬되지 않은 접근을 감지하는데 좋은 툴입니다. 이 유용한 디버거의 경고 메시지는 다른 아키텍쳐에서 예제 코드를 실행할 때  보여집니다:

MSP430: Warning: A word access on odd address
RL78: Word write access at odd address
RISC-V: Misaligned word data access

결론

주소가 정렬되지 않아야 한다면, #pragma pack, 혹은 __packed 사용하여 타입에 반드시 반영해야 합니다. 반드시 필요한 경우가 아니라면 이 방법은 권장하지 않습니다. 가능한 정렬된 주소를 사용하세요.

이식성과 퍼포먼스를 위해, 정렬되지 않은 메모리 접근을 피하세요:

  • 컴파일러의 도움을 받기 위해, #pragma pack 명령어 혹은 __packed 데이터 타입 속성을 사용하여 정렬되지 않은 데이터임을 알립니다.
  • 그렇지 않으면, 패킹된, 패킹되지 않은 구조체를 위해 직접 함수를 작성하세요.
  • 항상 올바른 데이터 타입을 사용하며, 형 변환을 통해 다른 데이터 타입간 포인터 변환을 피하세요.

 

 

모든 제품 이름은 해당 소유자의 상표 또는 등록 상표입니다.

 

죄송하지만, 당사 사이트에서는 Internet Explorer를 지원하지 않습니다.보다 편안한 사이트를 위해 Chrome, Edge, Firefox 등과 같은 최신 브라우저를 사용해 주시길 부탁드립니다.