Unaligned Memory Access

Unaligned Memory Access

메모리 주소가 접근 크기로 정렬되어 있지 않은 경우 대안을 알아봅니다.

Zephyr 둘러보던 중 다음 코드를 만났습니다:

#define UNALIGNED_GET(p)						\
__extension__ ({							\
	struct  __attribute__((__packed__)) {				\
		__typeof__(*(p)) __v;					\
	} *__p = (__typeof__(__p)) (p);					\
	__p->__v;							\
})

위 코드를 조목조목 따져보는 것으로, 메모리 주소가 접근 크기로 정렬되어 있지 않은 경우 어떻게 처리해야 하는지 알아보겠습니다.

C 표준

우선, 정렬되지 않은 메모리 접근이 왜 문제가 될까요?

메모리 주소가 접근 크기로 정렬되어 있지 않다는 건 메모리주소 / 접근크기 결과 나머지가 0이 아니라는 걸 뜻합니다.

예컨대 uint32_t 의 경우 0x0 번지와 0x4 번지로의 접근은 정렬되어 있고, 0x1, 0x2, 0x3 번지로의 접근은 정렬되어 있지 않습니다.

C17 표준 66쪽의 6.2.8.1 과:

Complete object types have alignment requirements which place restrictions on the addresses at which objects of that type may be allocated. An alignment is an implementation-defined integer value representing the number of bytes between successive addresses at which a given object can be allocated. An object type imposes an alignment requirement on every object of that type: stricter alignment can be requested using the _Alignas keyword.

74쪽의 6.3.2.3.7 에서처럼:

A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned68) for the referenced type, the behavior is undefined. Otherwise, when converted back again, the result shall compare equal to the original pointer. When a pointer to an object is converted to a pointer to a character type, the result points to the lowest addressed byte of the object. Successive increments of the result, up to the size of the object, yield pointers to the remaining bytes of the object.

C 표준에서는 complete object type 에 대한 정렬 요구사항을 명시하고 있습니다.

CPU 아키텍처

Unaligned memory access 를 지원하지 않는 프로세스에서 정렬되지 않은 메모리 주소로 multi-byte 접근을 할 경우 fault 가 발생합니다.

가령, uint32_t val = *((uint32_t *)0x1) 은 ARM 아키텍처의 경우 ldr 로 변환되어 정렬되지 않은 메모리 접근을 발생시킵니다.

이를 우회하기 위해서는 컴파일러에서 해당 구문을 바이트 단위 명령어로 변환하도록 유도해야 하는데, 이때 PACK_STRUCT 또는 memcpy 를 사용합니다.

UNALIGNED_GET 는 위 두가지 방식보다 코딩을 용이하게 해주는 매크로인데, 다소 복잡해보이는 이유는

  1. pack 속성을 주입하기 위해 구조체를 사용
  2. double evaluation 을 피하기 위해 새로운 변수에 파라미터를 할당해야하기 때문입니다.

컴파일러 전처리기에서 발생하는 double evaluation 에 대해서는 링크를 참조해주세요.

마지막으로 다음 세가지 방식의 메모리 접근이 실제로 어떻게 이루어지는지 어셈블리 코드를 첨부합니다:

# *(uint32_t *)ptr;
ldr     r0, [r0]

# memcpy(&dst, ptr, 4);
add     r0, sp, r2
bl      memcpy

# typedef struct __attribute__((__packed__)) { uint32_t b; } unaligned32_t;
# ((unaligned32_t *)ptr)->b;
ldrb    r3, [r0]
ldrb    r1, [r0, #1]
ldrb    r2, [r0, #2]
orr     r3, r3, r1, lsl #8
ldrb    r0, [r0, #3]
orr     r3, r3, r2, lsl #16
orr     r0, r3, r0, lsl #24

References