포스트

임베디드 스터디 - 포인터 및 어레이 연산

임베디드 스터디 - 포인터 및 어레이 연산

이번 글 참고자료:
EmbeddedInterviewlab

포인터

  • & : 특정 변수의 데이터를 저장하는 주소
  • * : 특정 변수의 주소에 해당하는 데이터
  • *(&x) = x
1
2
3
4
 /* volatile tells the compiler: never optimise away reads/writes */
volatile uint32_t *const GPIOA_ODR = (volatile uint32_t *)0x40020014;

*GPIOA_ODR |= (1U << 5);   /* set bit 5 — turn on an LED */

포인터 연산

  • 포인터의 연산은 포인터의 원본 변수 데이터 타입을 따른다.
1
2
3
4
5
6
7
uint8_t  *bp = (uint8_t  *)0x2000;   /* byte pointer    */
uint16_t *hp = (uint16_t *)0x2000;   /* halfword pointer */
uint32_t *wp = (uint32_t *)0x2000;   /* word pointer     */

bp++;  /* 0x2001  — advances 1 byte  */
hp++;  /* 0x2002  — advances 2 bytes */
wp++;  /* 0x2004  — advances 4 bytes */
  • 포인터 연산과 어레이 기반 데이터 선언은 다음과 같이 동질성을 띈다.
1
2
3
4
5
6
7
int32_t samples[4] = {10, 20, 30, 40};
int32_t *p = samples;          /* points to samples[0] */

/* All three lines read samples[2] == 30 */
int32_t a = samples[2];
int32_t b = *(p + 2);         /* pointer + offset      */
int32_t c = p[2];             /* equivalent syntax     */
  • 포인터 연산의 임베디드 SW 개발에서 다음 상황에 유심히 고려해서 사용해야한다.
    • 버퍼 조작(Buffer traversal) : UART 수신 버퍼를 바이트 단위로 조작하는 상황
    • 레지스터 뱅크(Register Banks) : 인접한 레지스터 사이에 일정한 오프셋이 있는데, 포인터가 빈 오프셋 구간을 접근하는 상황
    • 데이터 캐스팅(Casting between width) : uint32_t 레지스터를 4개의 uint8_t 데이터로 읽기 위해서 Byte 포인터로 변환하여 읽어야한다. 그렇지 않으면 포인터 연산 과정에서 3byte가 스킵된다.

어레이 vs 포인터

비고Array (int arr[8])Pointer (int *p)
sizeof전체 크기 (32)포인터 크기 (4 or 8)
Assignment할당 불가 : arr = other; 잘못된 문법할당 가능 p = other;
Decay함수에 전달될 때 int* 형태로 전달됨형태 변환 없음
Storage각 인자를 메모리에 할당함주소만 갖고 있음 (각 인자는 다른 메모리 주소에 저장)
&arrint (*)[8] (pointer to array)int ** (pointer to pointer)
Initialization데이터가 메모리에 복사됨; "hello" 가 어레이에 저장됨string 데이터가 있는 주소로 포인터가 지정됨 (read-only)
  • 어레이는 자신의 전체 크기를 정보로 갖고 있지만, 포인터는 주소값만 갖고 있다.
1
2
3
4
5
6
7
8
9
10
11
void print_size(int data[]) {
    /* data has decayed — sizeof gives pointer size, NOT array size */
    printf("%zu\n", sizeof(data));   /* prints 4 or 8 */
}

int main(void) {
    int buf[16];
    printf("%zu\n", sizeof(buf));    /* prints 64 (16 * 4) */
    print_size(buf);                 /* prints 4 or 8!     */
    return 0;
}

이중포인터 (Pointer-to-Pointer)

  • 포인터 주소를 저장하는 포인터
  • 임베디드에서 주로 2가지 용도로 사용된다.
    • 호출자의 포인터를 이동시킬 때
    • 포인터 어레이 (Arrays of Pointers)에서 각 요소의 포인터를 참조할 때
1
2
3
4
5
6
7
    /* parse_field advances *cursor past the parsed bytes */
bool parse_field(const uint8_t **cursor, const uint8_t *end, uint16_t *out) {
    if (*cursor + 2 > end) return false;
    *out = (uint16_t)((*cursor)[0] << 8 | (*cursor)[1]);
    *cursor += 2;   /* caller's pointer moves forward */
    return true;
}

Void 포인터

  • 타입의 지정이 없는 포인터 변수
    • void*에 어떠한 포인터 변수도 할당할 수 있지만, 해당 주소에 있는 변수를 불러오기 위해 데이터 타입 캐스팅이 필수적이다.
  • 임베디드 시스템에서 다음 용도로 주로 사용된다
    • Callback context pointers : ISR 콜백이나 RTOS 태스크에 데이터를 전달하기 위해 사용
    • memcpy/memset : 메모리에 변수 할당이나 전달용으로 사용된다
    • Generic data structure : 링 버퍼(Ring Buffer) 라이브러리에 데이터를 스태킹하기 위해 사용한다.
  • void* 변수는 포인터 연산이 금지되어있다. 포인터 연산을 위해, 우선 캐스팅을 해야한다.
1
2
3
4
5
6
7
8
9
10
/* Generic swap — works for any type by operating on raw bytes */
void swap(void *a, void *b, size_t size) {
    uint8_t temp[size];              /* VLA — acceptable for small sizes */
    memcpy(temp, a, size);
    memcpy(a, b, size);
    memcpy(b, temp, size);
}

int x = 1, y = 2;
swap(&x, &y, sizeof(int));          /* x == 2, y == 1 */

Null 포인터

  • NULL 포인터 참조는 정의되지 않은 동작으로 정의한다.
    • Cortex-M은 Hard Fault 트리거로 사용되기도 한다.
    • 프로세서에 따라 NULL 포인터를 처리하는 방식이 다르기도 한다.
  • 기능 동작의 신뢰성 유지를 위해 검증용으로 사용되기도 한다.
    • MISRA C에서는 모든 포인터는 변수 참조 전에 NULL 포인터 검사를 수행해야한다
      또한, NULL 포인터를 사용하는 함수는 무조건 설계 문서에 반영해야한다.
1
2
3
4
5
6
7
/* Guard every pointer before use */
void uart_send(const uint8_t *buf, size_t len) {
    if (buf == NULL || len == 0) return;
    for (size_t i = 0; i < len; i++) {
        UART_TX_REG = buf[i];
    }
}

허상 포인터 (Dangling Pointer)

  • 이미 메모리의 변수 할당이 해제된 주소를 포인팅하는 포인터
    • 오류가 발생해도 디버깅하기 까다롭다.
  • Dangling Pointer 방지법
    • 지역 변수의 주소를 포인터로 return하지 않는다.
    • 동적 할당 변수 해제(free()) 후 해당 포인터는 NULL로 처리한다.
    • static이나 호출자 입력 변수를 사용한다.

함수 포인터

  • 함수가 선언된 메모리 주소를 포인팅하는 포인터 변수.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* typedef for readability */
typedef void (*irq_handler_t)(void);

/* Dispatch table — one handler per IRQ line */
static irq_handler_t handlers[16] = {NULL};

void register_handler(uint8_t irq, irq_handler_t fn) {
    if (irq < 16) handlers[irq] = fn;
}

void dispatch(uint8_t irq) {
    if (irq < 16 && handlers[irq] != NULL) {
        handlers[irq]();      /* call through pointer */
    }
}

Const 포인터

  • const 포인터는 사용 방법에 따라 메모리 할당 방식이 다르다.
    • const 위치에 따른 해석 방법은 다른 변수들과 마찬가지로 Right to left 방법을 따른다.
선언해석포인터 위치데이터 위치
const int *pPointer to const intRAM (스택/전역).rodata (플래시)
int * const pconst pointer to int.rodata (플래시)RAM
const int * const pconst pointer to const int.rodata (플래시).rodata (플래시)
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.