Art of Pr0gr4m

동일한 포인터 역참조 시 값이 다른 케이스 본문

IT/C Language

동일한 포인터 역참조 시 값이 다른 케이스

pr0gr4m 2023. 10. 6. 19:24

다음 코드의 결과를 예상해보자.

    int x = 1, y = 2;    // clang을 사용한다면 x와 y 선언 순서 변경
    int *px = &x + 1;
    int *py = &y;
    if (!memcmp(px, py, sizeof(*px))) {    // px와 py는 같은 주소를 참조함
        *px = 3;
        printf("x : %d, y : %d, *px : %d, *py : %d\n", x, y, *px, *py);
    }

오브젝트 x와 y가 메모리상에 연속되어있는 시스템에서는 당연히 다음과 같은 결과를 보일 것이다.

#1
x : 1, y : 3, *px : 3, *py : 3

하지만 이는 Undefined Behavior 코드로, 다음과 같은 결과를 만들어낼 수 있다.

#2
x : 1, y : 2, *px : 3, *py : 2

어째서 이런 결과가 가능할까?

  1. 표준 본문에서 역참조 연산에 대한 UB 언급은 다음과 같다 : If an invalid value has been assigned to the pointer, the behavior of the unary * operator is undefined.
  2. 배열의 원소는 항상 연속적인 것이 보장된다. 따라서 int arr[2] = { 0, 1 }; 와 같은 코드가 있을 때 &arr[0] + 1 == &arr[1] 이 '보장' 된다.
  3. int x = 1, y = 2; 라는 코드는 메모리 상에서 통상적으로 &x + 1 == &y 의 레이아웃을 가지게 된다. 하지만 언어 차원에서 이를 '보장'하지 않는다. 예를 들어, x는 스택 메모리에 y는 레지스터에 할당된 경우를 생각해보자.
  4. 해당 내용에 의해 컴파일러는 px가 참조하는 대상이 y라는 것을 '보장'할 필요가 없어진다. 따라서, 컴파일러는 px == &y 라는 가정하에 생기는 최적화 방어를 할 필요가 없어진다.
  5. 결국 현재 위 코드에서 포인터 px가 특정한 오브젝트를 참조하고 있다는 보장이 없기에, 역참조한 값을 lvalue로 사용했을 때 발생하는 side effect를 항상 발생할 필요가 없어진다.
  6. 따라서 if (!memcmp(px, py, sizeof(*px))) 에서 볼 수 있듯이, 두 포인터는 동일한 곳을 참조하는 포인터이지만, py는 y 오브젝트를 참조하고 있음이 보장되며, px는 y 오브젝트를 참조하고 있음이 보장되지 않는다.
  7. 그러므로 *px = 3; 이라는 코드는 실제로 px를 역참조하여 3을 대입하는 side effect를 발생시키지 않고, 상수 전파로 최적화가 가능해진다.
  8. printf("x : %d, y : %d, *px : %d, *py : %d\n", x, y, *px, *py); 코드에서 *px 는 상수 전파로 인해 3을 바로 사용할 수 있게 된다.

위와 같은 흐름으로 동일한 주소를 참조하고 있는 포인터가 역참조 시 서로 다른 값이 나오게 된다.
실제로 위 코드를 gcc나 clang 컴파일러를 이용해서 -O0 으로 최적화 없이 컴파일하면 1번과 같은 결과가 나오지만, -O2 으로 최적화하면 2번과 같은 결과가 나오게 된다. (clang을 사용하는 경우 int y = 2, x = 1;와 같이 선언해야 한다.)

결론적으로 past to end of object 포인터가 또 다른 object를 참조한다고 보장되지 않는 경우 invalid 포인터로 취급하여 UB 동작을 발생시킨다. (past to end of object 참조는 UB가 아니지만, 해당 참조 포인터를 역참조하면 UB이다.)

그리고 이는 패딩 등의 이유로 구조체 내의 멤버 또한 마찬가지다.

struct T {
    int x, y;
};

struct T t;
// &t.x + 1이 t.y를 참조한다고 보장하지 않음

따라서, 제너럴한 자료 구조의 구현 시 past to end of object 포인터를 사용할때 유의가 필요하다.
(제너럴한 자료 구조는 https://pr0gr4m.tistory.com/entry/Linux-Kernel-5-Linked-List 참고)

'IT > C Language' 카테고리의 다른 글

C Initializer 주저리  (0) 2018.02.18
ISO/IEC 9899 (draft)  (2) 2017.07.10