안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

컴퓨터 구조 (Architecture)/컴퓨터의 개념 및 실습

[Chapter 12] Variables and Operators - 변수와 연산자

피그브라더 2020. 2. 19. 23:00

앞선 포스팅에서 고급 언어에 대한 기본적인 개념을 알아보았다면, 이제는 본격적으로 C 언어를 파헤쳐볼 시간이다. 먼저 C 언어의 가장 기본적인 구성 요소에 해당하는 변수, 연산자의 개념을 살펴보고, LC-3를 기준으로 각 변수가 하드웨어 수준에서 어떤 원리로 할당과 접근이 이뤄지는지 알아보도록 할 것이다.

 

1. 변수

1-1. 변수 (Variables)

변수란 값이 위치하는 공간을 나타내며, 변수의 이름은 곧 그 공간에 붙여진 이름이다. 변수의 값은 언제든 바꿀 수 있는 가변적인 값이다. 반면 상수란 바꿀 수 없는 고정적인 값을 의미한다. 변수와 상수는 둘 다 값이기 때문에 자료형을 반드시 가진다.


1-2. 자료형 (Data Type)

이전 포스팅에서 설명했듯이, 자료형이란 해당 데이터가 어떤 유형의 데이터이며 그 데이터에 대해 어떤 연산이 지원되는지를 나타낸다. C 언어가 제공하는 기본 내장 자료형 중 대표적으로 세 가지만 살펴보면 다음과 같다.

 

자료형 데이터의 유형 메모리에서 차지하는 공간의 크기
int 정수 최소 16비트 (= 2바이트)
double 실수 최소 32비트 (= 4바이트)
char 문자 최소 8비트 (= 1바이트)

 

메모리에서 차지하는 공간의 크기가 고정적이지 않고 '최소'라고 표현되어 있는 이유는 무엇일까? 이는 같은 C 언어라고 하더라도 어떤 CPU와 컴파일러를 사용하느냐에 따라서 자료형의 크기가 달라질 수 있기 때문이다. 어떤 컴파일러는 int 형 데이터를 2바이트로 취급하여 번역을 하는 반면, 어떤 컴파일러는 int 형 데이터를 4바이트로 취급할 수도 있다. 통상적으로 C 언어 컴파일러는 int 형 데이터의 크기를 해당 CPU의 워드 사이즈로 간주한다. 왜 그럴까? C 언어의 약속에 따르면 int 형 데이터는 해당 CPU에서 가장 쉽고 빠르게 처리될 수 있는 크기를 가지도록 되어 있기 때문이다. 기본적으로 CPU는 워드 사이즈에 해당하는 데이터들을 대상으로 연산할 수 있는 회로들을 가지고 있기 때문에, 워드 사이즈의 데이터를 대상으로 처리할 때 속도가 가장 빠르다. 그래서 32비트 컴퓨터에서는 C 언어 컴파일러가 int 형 데이터를 4바이트로 취급하지만, 64비트 컴퓨터에서는 int 형 데이터를 8바이트로 취급하게 된다.


1-3. 스코프 (Scope)

스코프란 선언된 변수를 어디에서 접근할 수 있는지 나타내는 용어이다. 지역 변수(Local Variable)는 중괄호로 둘러싸인 블록 내부에서 선언이 되는 변수를 의미하며, 전역 변수(Global Variable)는 어떤 중괄호로도 둘러싸여 있지 않는 영역에서 선언되는 변수를 의미한다. 전역 변수는 해당 블록 내에서만 접근이 가능한 반면, 전역 변수는 어디에서든 접근이 가능하다. 컴파일러는 변수가 선언된 위치로부터 해당 변수의 스코프가 어떠한지를 파악한다.

 

 

2. 연산자

2-1. 연산자 (Operator)

연산자는 고급 언어에서 변수들의 값을 대상으로 연산을 수행하기 위해 사용하는 도구이다. 앞서 말했듯 고급 언어의 연산자는 CPU의 ISA 체계에 의존하지 않고, 내부적으로 여러 ISA 명령어를 활용하여 구현이 된다. 예를 들어 C 언어의 곱셈 연산자는 LC-3에서 ADD 명령어를 포함한 여러 개의 명령어로 구현이 될 것이다. 연산자는 다음과 같이 크게 네 가지의 측면에서 바라볼 수 있다.

 

관점 의미
기능 (Function) 무슨 기능을 수행하는 연산자인가?
우선순위 (Priority) 어떤 연산자가 우선적으로 연산되어야 하는가?
결합 방향 (Associativity) 동일한 우선순위를 가진 연산자들의 경우, 어느 방향으로 연산되어야 하는가?
Side Effect 새로운 값을 계산할 뿐 아니라 저장되어 있는 값을 바꾸기도 하는가?

2-2. Expression, Statement

2-2-1. Expression

기본적으로 하나의 변수와 하나의 상수가 Expression에 해당하며, 함수 호출 문 또한 Expression에 해당한다. 그리고 그러한 Expression들을 연산자로 결합시킨 것도 Expression에 해당한다. 따라서 변수, 상수, 연산자를 적절히 조합하여 여러 형태의 Expression을 만들어낼 수 있다. 각 Expression은 반드시 특정 자료형을 가진 하나의 값으로 계산된다. 계산되는 값의 자료형은 해당 Expression을 구성하는 요소들의 자료형에 의해 결정된다. 예를 들어 정수와 실수를 대상으로 덧셈 연산을 수행하는 경우, 그 Expression의 결괏값은 자료형이 실수가 된다. C 언어의 자료형 규칙에 따르면 산술 연산 시 데이터의 손실이 적은 방향으로 형 변환이 이루어져서 정수가 실수로 변환되기 때문이다.

 

2-2-2. Statement

하나의 완전한 일의 단위를 의미하는 것으로, CPU에 의해 순차적으로 실행이 된다. C 언어에서는 기본적으로 각 Statement를 세미콜론으로 끝내도록 되어 있다. 그리고 그러한 Statement들을 중괄호로 묶은 것도 그 자체로 하나의 Statement가 된다.


2-3. 연산자의 종류

C 언어에는 대입 연산자, 산술 연산자, 비트 연산자, 논리 연산자, 관계 연산자, 복합 연산자, 삼항 연산자 등 다양한 종류의 연산자가 존재한다. 본 포스팅은 C 언어 강좌가 아니기 때문에, 그러한 연산자들을 여기서 나열하지는 않겠다. 직접 한 번 찾아보고, 각 연산자들을 우선순위, 결합 방향, Side Effect 유무의 관점에서 바라보면 공부에 도움이 될 것이다.

 

3. 변수의 할당 및 접근

C 언어 컴파일러는 각 변수들을 어떻게 메모리에 할당하고 접근하는 것일까? 그 원리를 한 번 알아보도록 하자.


3-1. 심볼 테이블 (Symbol Table)

어셈블러처럼 컴파일러도 우선 한 번 소스 파일을 전체적으로 분석하여 각 변수들의 정보를 파악한 뒤 심볼 테이블을 구성한다. 심볼 테이블에는 각 변수의 이름, 자료형, 메모리 상에서의 위치, 스코프 등에 대한 정보가 저장된다. 그리고 그러한 정보를 바탕으로 각 변수들을 메모리에 적절히 할당하고 접근하게끔 하는 기계어를 만들어 내는 것이다.

 


3-2. 변수 할당 장소 (Variable Storage)

3-2-1. 지역 변수 (Local Variable)

지역 변수는 런타임 스택(Runtime Stack)이라는 메모리 공간에 저장된다. 정확히는 런타임 스택에 만들어지는 스택 프레임(Stack Frame, Activation Record) 내에 저장이 된다. 스택 프레임은 새로운 중괄호 블록에 진입할 때마다 런타임 스택에 새로 쌓이게 된다. LC-3 기준으로 R6은 런타임 스택의 가장 윗부분 주소를 저장하고 R5는 현재 스택 프레임의 밑바닥 주소를 저장한다. LC-3에서 런타임 스택은 주소가 감소하는 방향으로 데이터가 쌓인다. (참고로 실제 컴파일러는 모든 지역 변수를 메모리에 저장하지는 않는다. 임시적으로만 필요하다고 판단되는 경우, 성능의 최적화를 위해 레지스터에 저장하기도 한다.)

 

3-2-2. 전역 변수 (Global Variable)

전역 변수와 static 변수는 Global Data Section이라는 메모리 공간에 저장된다. LC-3 기준으로 R4 레지스터는 이 공간의 밑바닥 주소를 저장하고 있다. LC-3에서 Global Data Section은 주소가 증가하는 방향으로 데이터가 채워진다.

 

3-2-3. 심볼 테이블에서의 오프셋 (Offset)

심볼 테이블 내에는 각 변수들의 오프셋 정보가 담겨 있는데, 이는 해당 변수가 자신이 속한 구역의 기준점으로부터 얼마나 떨어져 있는가를 나타내는 정보이다. 예를 들어, 지역 변수의 오프셋은 해당 지역 변수가 속한 스택 프레임의 밑바닥으로부터 얼마나 떨어져 있는가를 나타낸다. 따라서 LC-3에서 지역 변수의 오프셋은 0보다 작거나 같다. 또한 전역 변수의 오프셋은 해당 전역 변수가 Global Data Section의 밑바닥으로부터 얼마나 떨어져 있는가를 나타낸다. 따라서 LC-3에서 전역 변수의 오프셋은 0보다 크거나 같다.

 


3-3. 변수 할당 및 접근 원리

소스 파일을 한 번 전체적으로 분석하여 얻어낸 심볼 테이블의 정보를 바탕으로, 컴파일러는 각 변수들을 할당하는 코드와 그 변수들에 접근하는 코드에 해당하는 기계어를 적절히 만들어 낸다. 예를 들어 지역 변수 A를 선언하고 A에 3이라는 값을 대입하는 코드가 있다고 해보자. 그러면 컴파일러는 심볼 테이블의 정보를 바탕으로 그 코드들을 대략 다음과 같은 방식으로 번역한다.

 

① A의 선언 : R6의 값을 심볼 테이블에 있는 A의 크기 정보만큼 감소시킨다. (지역 변수 할당)

② A의 변경 : 메모리[R5 + 심볼 테이블에 있는 A의 오프셋 정보]의 값을 변경한다. (지역 변수 접근)

 


3-4. 컴파일 예시