C언어 전역변수 / 지역변수 저장(메모리) 위치

C 언어의 전역변수는 메모리(memory)의 정적위치에 저장되고 지역변수는 CPU 레지스터(register)나 스택(stack)에 저장된다. 어셈블리 코드로 각각 어떻게 동작하는지 살펴보자.

* 확인 방법 : C 코드를 컴파일하여 생성된 RISC-V 어셈블리(assembly) 코드로 확인

전역변수

전역변수에 1을 대입하는 코드를 살펴보자.

<C 코드>
int global_int;

void global_variable(void)
{
    global_int = 1;
}

<symbol table>
location                  size     name
10000000 g     O .sbss 00000004 global_int

global_int 전역변수가 컴파일 시에 정해진 정적 위치인 0x10000000 번지에 위치한다.

<어셈블리 코드>
00000000 <global_variable>:
   0:	100007b7          	lui	a5,0x10000
   4:	00100713          	li	a4,1
   8:	00e7a023          	sw	a4,0(a5) # 10000000 <global_int>
   c:	00008067          	ret
global_int 전역변수의 메모리 위치인 0x10000000 번지에 1을 store 한다.

지역변수

지역변수가 register에 할당되는 경우

<C 코드>
volatile int volatile_int;

int analyze_local_variable1(void)
{
    int local_int1 = volatile_int;
    int local_int2 = volatile_int;
    
    local_int1 += 1000;
    local_int2 += 2000;
    
    return local_int1 + local_int2;        
}
volatile_int 변수를 volatile로 선언하여 컴파일러는 값이 언제든 바뀔 수 있다고 인지하기 때문에 해당 변수 접근 관련 최적화를 하지 않는다. 위의 코드에서 컴파일러 최적화가 되면 local_int1 / local_int2 변수가 register에 각각 할당되지 않기 때문에 volatile을 사용했다.

<symbol table>
location                  size     name
10000000 g     O .sbss 00000004 volatile_int

volatile_int 전역변수가 0x10000000 번지에 위치한다.

<어셈블리 코드>
00000000 <analyze_local_variable1>:
   0:	100007b7          	lui	a5,0x10000

# local_int1이 a0 register에 할당됨
int local_int1 = volatile_int;
   4:	0007a503          	lw	a0,0(a5) # 10000000 <volatile_int>

# local_int2가 a5 register에 할당됨
int local_int2 = volatile_int;
   8:	0007a783          	lw	a5,0(a5)

local_int1 += 1000;
   c:	3e850513          	addi	a0,a0,1000

local_int2 += 2000;
  10:	7d078793          	addi	a5,a5,2000

# 함수 return 값을 a0 register에 넘기므로 a0에 local_int1과 local_int2의 합을 대입함
return local_int1 + local_int2;
  14:	00f50533          	add	a0,a0,a5
  18:	00008067          	ret
위와 같이 지역변수가 a0 / a5 register에 할당된 것처럼 컴파일러는 가능하면 지역변수를 CPU register 할당하려고 한다. 하지만 사용 가능한 CPU register가 없는 등의 이유가 있으면 stack에 할당한다.

지역변수가 stack에 할당되는 경우

<C 코드>
#define VAR_SIZE 4

int global_int[VAR_SIZE];

void analyze_local_variable2(void)
{
    int local_int[VAR_SIZE];

    local_int[3] = 4;

    for(int i=0; i<VAR_SIZE; i++){
        global_int[i] = local_int[i];
    }
}

<symbol table>
location                      size     name
10000000 g     O .test_data 00000010 global_int

global_int 배열이 0x10000000 번지에 위치한다.

<어셈블리 코드>
00000000 <analyze_local_variable2>:

# stack에 배열 크기인 16byte를 할당
int local_int[VAR_SIZE];
   0:	ff010113          	addi	sp,sp,-16

# 컴파일러 최적화에 인해 반복문이 unrolling 됨
global_int[0] = local_int[0];
   4:	00012703          	lw	a4,0(sp)
   8:	100007b7          	lui	a5,0x10000
   c:	00078793          	mv	a5,a5 # 불필요 코드, 컴파일러 최적화가 부족한 것으로 보임
  10:	00e7a023          	sw	a4,0(a5) # 10000000 <global_int>

global_int[1] = local_int[1];
  14:	00412703          	lw	a4,4(sp)
  18:	00e7a223          	sw	a4,4(a5)

global_int[2] = local_int[2];
  1c:	00812703          	lw	a4,8(sp)
  20:	00e7a423          	sw	a4,8(a5)

# local_int[3]이 a4 register에 할당됨
# stack(sp+12)에 할당된 공간이 있지만 최적화로 인해 register를 사용함
local_int[3] = 4;
  24:	00400713          	li	a4,4

global_int[3] = local_int[2];
  28:	00e7a623          	sw	a4,12(a5)

# 배열에 할당한 stack을 해제
  2c:	01010113          	addi	sp,sp,16

  30:	00008067          	ret
위의 코드는 지역변수인 local_int 배열이 stack에 저장되었다. 지역변수가 register가 아닌 stack에 할당되게 만드는 방법 중 하나는 사용 가능한 register를 다 사용하는 건데, 그러면 코드가 너무 길어진다. 그래서 초기화되지 않은 지역변수 배열을 사용해보니 stack에 할당돼서 예제에 사용했다.

local_int 배열이 stack에 할당되기 전 / 후의 stack 상태
위의 그림은 local_int 배열을 stack에 할당하기 전에 stack pointer가 0x10008000 번지를 가
리킨다고 가정했으며, stack에 local_int 배열을 할당하기 전과 할당한 후의 메모리 상태를 나타낸다.

정리

전역변수는 컴파일 시에 정해진 메모리의 정적 위치에 할당이 되고 지역변수는 register나 stack에 할당된다. 컴파일러는 지역변수는 가능하면 register에 할당하려고 하나 사용 가능한 register가 없는 등의 이유가 있으면 stack에 할당한다.

* Feedback은 언제나 환영합니다.

Comments