<aside> 🥕

컴파일러에게 “이 변수나 코드를 최적화하지 말고 항상 메모리에서 직접 읽어라”라고 지시하는 역할

</aside>

과연 시스템 콜을 호출하는 쪽에서는 어떤 흐름을 갖는 걸까?라는 의문점에서 시작해 코드를 살펴보던 중 마주한 volatile 키워드에 대해 정리하고자 합니다. CSAPP에서 분명 마주했었는데, 말을 못하겠어서 정리해보고자 합니다.

10-11주 _ 6팀-12.jpg

등장 위치

lib/user/syscall.c

/* 어셈블리 명령어(syscall)를 사용해 커널에 시스템 콜을 직접 요청. 시스템 콜
 * 번호와 최대 6개의 인자를 레지스터에 넣어 커널로 전달. 커널에서 해당 시스템
 * 콜을 처리한 뒤 결과값 반환 */
__attribute__((always_inline)) static __inline int64_t syscall(
    uint64_t num_, uint64_t a1_, uint64_t a2_, uint64_t a3_, uint64_t a4_,
    uint64_t a5_, uint64_t a6_) {
  int64_t ret;
  register uint64_t *num asm("rax") = (uint64_t *)num_;
  register uint64_t *a1 asm("rdi") = (uint64_t *)a1_;
  register uint64_t *a2 asm("rsi") = (uint64_t *)a2_;
  register uint64_t *a3 asm("rdx") = (uint64_t *)a3_;
  register uint64_t *a4 asm("r10") = (uint64_t *)a4_;
  register uint64_t *a5 asm("r8") = (uint64_t *)a5_;
  register uint64_t *a6 asm("r9") = (uint64_t *)a6_;

  __asm __volatile(
      "mov %1, %%rax\\n"
      "mov %2, %%rdi\\n"
      "mov %3, %%rsi\\n"
      "mov %4, %%rdx\\n"
      "mov %5, %%r10\\n"
      "mov %6, %%r8\\n"
      "mov %7, %%r9\\n"
      "syscall\\n"
      : "=a"(ret)
      : "g"(num), "g"(a1), "g"(a2), "g"(a3), "g"(a4), "g"(a5), "g"(a6)
      : "cc", "memory");
  return ret;
}

valatile의 핵심 역할

⭐️ 컴파일러 최적화 방지

컴파일러는 성능 향상을 위해 코드를 자동으로 최적화합니다 :

// volatile이 없는 경우
int x = 10;
int y = x;
int z = x;  // 컴파일러가 "x를 또 읽을 필요 없이 y 값을 복사하자"고 최적화할 수 있음

// volatile이 있는 경우
volatile int x = 10;
int y = x;  // 메모리에서 x 읽음
int z = x;  // 다시 메모리에서 x 읽음 (캐시된 값 사용 안 함)

인라인 어셈블리에서 _volatile의 중요성

_volatile이 없는 경우 vs 있는 경우

_volatile이 없는 경우

// volatile 없는 경우
write(1, "Hello", 5);
write(1, "World", 5);

// 컴파일러가 이렇게 최적화할 수 있음:
// "같은 write 시스템콜을 두 번 호출하네? 한 번만 호출하고 결과를 재사용하자!"
// → 잘못된 최적화!

_volatile이 있는 경우

// volatile 있는 경우
write(1, "Hello", 5);  // 반드시 실행
write(1, "World", 5);  // 반드시 실행 (최적화로 제거되지 않음)

volatile이 필요한 상황들

1️⃣ 하드웨어 레지스터 접근

volatile int *hardware_register = (int *)0x12345678;
*hardware_register = 1;  // 하드웨어에 명령 전송
*hardware_register = 2;  // 또 다른 명령 전송
// volatile 없으면 컴파일러가 첫 번째 쓰기를 제거할 수 있음