Info

해당 글에서는 다음과 같은 것들을 다룬다.

  1. switch, case내부에서 왜 변수 선언이 안되는지
  2. switch, case의 디스어셈블 결과확인 및 분석
  3. braces 내부에서 선언된 변수의 어셈블리 코드 분석

왜 switch, case내부에서 왜 변수 선언이 안될까?

switch, case 문법은 하나의 braces의 묶음으로 이루어진다.

case label들은 goto문법과 같이 서로의 영역을 건너뛰어 동작하지만, 컴파일러 입장에서는 braces단위로 영역을 구분하기 때문에 비록 다른 label에서 선언되고 초기화된다해도(혹은 건너뛴다고 해도) 여기에 접근할 수 있다고 여기게 된다.

C++ standard 표준은 초기화를 건너뛴 변수에 접근할 수 있는 상황을 허용하지 않으므로 에러가 발생하게 된다.

int type = 2;
switch (type) {
  case 1:
    int x = 100; // 'x' 선언 및 초기화
    // ... x 사용 ...
    break;
  case 2: // 'type'이 2면 여기로 바로 점프
    // 여기서 'x'는 스코프 내에 있지만, case 1을 건너뛰었으므로
    // 초기화되지 않은 상태로 접근될 위험이 있다.
    // std::cout << x << std::endl; // 컴파일 에러!
    break;
}

switch, case의 디스어셈블 결과확인 및 분석

그렇다면 switch, case 내부에서 선언된 변수는 어셈블리로 어떻게 처리될까?

아래 코드를 살펴보자.

__attribute__((noinline)) // Prevent inlining
int test(int choice) {
	int ret;
    switch (choice) {
        case 1: {
            volatile int temp_val_case1 = 100;
            ret = temp_val_case1 + 5;
            break;
        }
        case 2: {
            volatile int temp_val_case2 = 200;
            ret = temp_val_case2 + 10;
            break;
        }
        default: {
            volatile int temp_val_default = -1;
            ret = temp_val_default;
            break;
        }
    }
    return ret;
}
          test:
341808b0:   cmp     r0, #1
220       int test(int choice) {
341808b2:   sub     sp, #16
222           switch (choice) {
341808b4:   beq.n   0x341808d2 <test+34>
341808b6:   cmp     r0, #2
341808b8:   bne.n   0x341808c6 <test+22>
229                   volatile int temp_val_case2 = 200;
341808ba:   movs    r3, #200        @ 0xc8
341808bc:   str     r3, [sp, #8]
230                   ret = temp_val_case2 + 10;
341808be:   ldr     r0, [sp, #8]
341808c0:   adds    r0, #10
240       }
341808c2:   add     sp, #16
341808c4:   bx      lr
234                   volatile int temp_val_default = -1;
341808c6:   mov.w   r3, #4294967295
341808ca:   str     r3, [sp, #12]
235                   ret = temp_val_default;
341808cc:   ldr     r0, [sp, #12]
240       }
341808ce:   add     sp, #16
341808d0:   bx      lr
224                   volatile int temp_val_case1 = 100;
341808d2:   movs    r3, #100        @ 0x64
341808d4:   str     r3, [sp, #4]
225                   ret = temp_val_case1 + 5;
341808d6:   ldr     r0, [sp, #4]
341808d8:   adds    r0, #5
240       }
341808da:   add     sp, #16
341808dc:   bx      lr
341808de:   nop 

먼저 변수의 저장에 대해서 확인해보자.

어셈블리 코드를 살펴보면 다음과 같은데

sub     sp, #16

어떤 braces에 위치하던 관계 없이 함수의 시작부에서 모두 할당하는 것을 볼 수 있다.

braces 내부에서 선언된 변수의 어셈블리 코드 분석

평소 braces 내부에 변수를 선언하면 braces를 벗어날 경우, 스택 공간이 다시 해제되어 공간적 효율을 가질 수 있을 것으로 생각했었다.

이것이 잘못된 것임을 추가로 검증하고자한다.

다음 코드를 살펴보자.

__attribute__((noinline)) // Prevent inlining
int test(int choice) {
	int ret;
	{
		volatile int a = 10;
		ret = a;
	}
	{
		volatile int a = 20;
		ret = a;
	}
	{
		volatile int a = 30;
		ret = a;
	}
	return ret;
}
          test:
341808b0:   movs    r1, #10
227       		volatile int a = 20;
341808b2:   movs    r2, #20
231       		volatile int a = 30;
341808b4:   movs    r3, #30
220       int test(int choice) {
341808b6:   sub     sp, #16
223       		volatile int a = 10;
341808b8:   str     r1, [sp, #4]
224       		ret = a;
341808ba:   ldr     r1, [sp, #4]
227       		volatile int a = 20;
341808bc:   str     r2, [sp, #8]
228       		ret = a;
341808be:   ldr     r2, [sp, #8]
341808c0:   str     r3, [sp, #12]
232       		ret = a;
341808c2:   ldr     r0, [sp, #12]
235       }
341808c4:   add     sp, #16
341808c6:   bx      lr

switch case 때와 마찬가지로 braces와 관계 없이 할당된 데이터 만큼의 스택을 할당하는 것을 확인할 수 있다.

341808b6:   sub     sp, #16

즉, braces내부에 위치하더라도 함수 시작시 모든 변수가 사전에 할당된다.

테스트 환경

OS: Window11 IDE & ToolChain : STM32Cube Board: STM32N6-DK