컴파일러에게 “이 변수나 코드를 최적화하지 말고 항상 메모리에서 직접 읽어라”라고 지시하는 역할
과연 시스템 콜을 호출하는 쪽에서는 어떤 흐름을 갖는 걸까?라는 의문점에서 시작해 코드를 살펴보던 중 마주한 volatile 키워드에 대해 정리하고자 합니다. CSAPP에서 분명 마주했었는데, 말을 못하겠어서 정리해보고자 합니다.
등장 위치
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;
}
C
복사
어셈블리 코드에서 등장
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 읽음 (캐시된 값 사용 안 함)
C
복사
인라인 어셈블리에서 _volatile의 중요성
_volatile이 없는 경우 vs 있는 경우
_volatile이 없는 경우
// volatile 없는 경우
write(1, "Hello", 5);
write(1, "World", 5);
// 컴파일러가 이렇게 최적화할 수 있음:
// "같은 write 시스템콜을 두 번 호출하네? 한 번만 호출하고 결과를 재사용하자!"
// → 잘못된 최적화!
C
복사
_volatile이 있는 경우
// volatile 있는 경우
write(1, "Hello", 5); // 반드시 실행
write(1, "World", 5); // 반드시 실행 (최적화로 제거되지 않음)
C
복사
volatile이 필요한 상황들
하드웨어 레지스터 접근
volatile int *hardware_register = (int *)0x12345678;
*hardware_register = 1; // 하드웨어에 명령 전송
*hardware_register = 2; // 또 다른 명령 전송
// volatile 없으면 컴파일러가 첫 번째 쓰기를 제거할 수 있음
C
복사
멀티스레딩
volatile bool flag = false;
// 스레드 1
while (!flag) {
// 대기...
// volatile 없으면 컴파일러가 flag를 레지스터에 캐시해서
// 다른 스레드가 변경해도 감지 못할 수 있음
}
// ------------------------------------------------
// 스레드 2
flag = true; // 스레드 1에게 신호
C
복사
시스템콜
PintOS의 경우 :
__asm __volatile(
"syscall\n" // 🚨 이 명령어는 절대 최적화되면 안 됨!
...
);
C
복사
이유는 다음과 같습니다 :
1.
부작용(side effect) : syscall 명령어는 커널 상태를 변경함
2.
순서 중요 : 시스템 콜 순서가 바뀌면 프로그램 동작이 완전히 달라짐
3.
메모리 상태 변경 : 시스템콜이 메모리 내용을 변경할 수도 있음


