본문 바로가기
Base/CS

컴퓨터가 코드를 실행하기까지 (feat. 메모리, 프로세서, 컴파일러)

by joooing 2021. 6. 6.
반응형

컴퓨터

컴퓨터는 한마디로 명령에 따라 데이터를 조작하는 기계이다. 조작을 위해서는 '프로세서'와 '메모리'의 역할이 중요하다. 메모리는 무슨 명령을 수행해야하는지, 명령을 수행하려면 어떤 데이터가 필요한 지를 적어두는 공간이고, 프로세서는 메모리에서 명령과 데이터를 꺼내와서 실제로 명령을 수행하는 부품이다.

 

메모리(RAM) = 수행할 명령들, 명령 수행을 위한 데이터들을 보관
프로세서(CPU) = 메모리에서 명령과 데이터를 꺼내와서 실제로 명령을 수행

 

 

메모리

메모리는 왜 필요할까? 예를들어 누군가 우리한테 500에서 600까지의 합을 구하라고 했다고 생각해보자. 500 + 501 를 먼저 계산하고 1001이라는 숫자를 어딘가 적어둘 것이다. 다음에는 1001에 다음 수인 502을 더해 적어두고... 이런 과정을 반복할 것이다. (물론 암산이 가능한 사람도있겠지만 보통은 이럴것이다,,) 이와 비슷하게 컴퓨터도 지금까지의 연산결과와 현재 처리하는 숫자를 어딘가 기록해두어야 여러개의 데이터들을 가지고 처리해야하는 연산을 수행할 수 있다. 이런 기록을 담당하는 것이 메모리의 역할이다.

 

1. 메모리 셀

메모리는 '셀'로 구성되는데, 이 셀에 데이터들을 조금씩 나누어 저장한다. 셀들을 구별하기 위해 주소번호도 붙여둔다. 메모리가 할 수 있는 일은 크게 두 가지이다. 데이터를 쓰거나 읽거나 하는 것인데, 이렇게 하려면 셀을 조작해야 한다. 좀 더 구체적으로 말하면 데이터를 읽고/쓰는 작업을 하려면 셀의 주소번호를 알아내서, 해당 셀과 정보를 주고(쓰기) 받아야(읽기) 하는 것이다.

 

2. 메모리에 데이터 쓰고, 읽기

전선으로 이어진 메모리(RAM)와 프로세서(CPU)의 모습

 

셀의 주소번호는 전선을 통해 이진수로 전달된다. 예를들어, 총 8개의 전선이 있으면 각 전선마다 이진수 숫자를 하나씩 전송하는 것이다. 전압이 세면 1, 전압이 낮으면 0과 같은 식으로 말이다. 8개 전선으로 들어온 데이터를 모으면 11001111같은 이진수 숫자가 나오게 된다.

 

그렇다면 이번 명령이 쓰라는 명령인지, 읽으라는 명령인지는 어떻게 알까? 이를 위한 특별한 전선도 있다. 전선 한개로 1과 0을 표현해 각각 쓰기모드, 읽기모드를 나타내는 것이다. 쓰기모드라면 전선들을 통해 외부에서 들어온 데이터를 읽어들여 지정된 주소의 셀에 기록하고, 반대로 읽기모드라면 해당 셀에 저장되어있는 데이터를 전선을 통해 밖으로 내보내는 것이다.

 

이렇게 한 데이터를 전송하기 위해 함께 쓰이는 전선들을 묶어 버스(bus)라고 부른다. 위에서 주소를 표현하는데 함께 쓰였던 8가닥의 전선이 하나의 주소버스가 되는 것이다. 데이터를 표현할 때 쓰이는 다른 8가닥은 데이터버스라고 부른다. 주소버스는 데이터를 밖으로 보낼때만 쓰이기 때문에 단방향이지만, 데이터버스는 데이터를 읽고 쓸 때 모두 사용하기 때문에 양방향으로 동작한다.

 

프로세서(CPU)

프로세서는 '컴퓨터 운영을 위해 기본적인 명령어들을 처리하고 반응하기 위한 논리회로'를 말한다. 컴퓨터가 하는 모든 일을 총괄하는게 CPU라면, CPU를 보조하며 연산, 제어의 핵심부분을 담당하는 것을 프로세서라고 생각하면 된다. 하지만 프로세스라는 용어는 점차적으로 CPU라는 용어를 대체해왔다고 하기 때문에 이 글에서는 CPU로 표현하려 한다.

 

CPU는 메모리에서 명령, 데이터를 가져와 실제로 명령, 계산을 수행한다고 앞서 말했다. 이런 과정을 계속 반복하면서 때로는 출력값이나 중간 계산을 메모리에 저장하기도 한다.

 

 

1. 레지스터(register)

메모리는 셀로 구성되었다고 했는데, CPU에도 이런 자체 메모리 셀들이 있다. 바로 레지스터이다. CPU가 계산을 할 때는 레지스터에 저장된 수를 가지고 계산을 수행한다. 레지스터는 RAM과 서로 데이터를 교환하기도 한다.

 

예를들어 1 + 2라는 연산을 수행한다고 하고, 메모리 20번셀에는 1이, 30번셀에는 2가 담겼다고 해보자. 이 연산을 명령으로 표현하면 이렇게 표현할 수 있다.

 

  1. 메모리 20번 셀의 데이터를 레지스터 3번으로 복사해라.
  2. 메모리 30번 셀의 데이터는 레지스터 5번으로 복사해라.
  3. 레지스터 3번에 담긴 수를 레지스터 5번 수에 더해라.

 

2. 명령어 집합(instruction set)

명령어 집합CPU가 할 수 있는 모든 연산을 모아 적어둔 리스트라고 할 수 있다. 이 리스트에 있는 연산들에도 각각 번호가 할당되어있다.

우리가 작성한 코드를 실행하는건 결국 명령어 집합의 연산들 중 적절한 것들을 골라 차례로 실행시키는 것이다. 그렇다면 코드라는건 결국 CPU 연산 번호들을 실행순서대로 나열해둔 것이라고 할 수 있다.

 

대충 이런모양인데, 우리가 잘 아는 AND, OR같은 연산들도 보인다. (출처 : https://slidesplayer.org/slide/11328151/)

 

3. CPU 아키텍처

보통 앱들을 보면, 모바일 전용 앱과 PC전용 앱이 따로 있는 경우가 많다. 이 이유는 핸드폰과 컴퓨터의 CPU 아키텍처가 서로 다르기 때문이다. 용어가 낯설지만, 이 말은 결국 방금 설명한 명령어 집합이 서로 다르다는 것이다. 명령어 집합이 다르면, 명령들에 부여된 번호도 달라지게 될 것이다. 코드는 명령어 번호들을 실행순으로 늘어놓은 것이라고 했는데 이 번호들이 다르다면, 당연히 정상적으로 동작하지 않게 될 것이다. 결국 CPU 아키텍처가 다르면 같은 앱이라도 한쪽에선 잘 실행되지만 다른쪽에선 작동하지 않을 수 있게 된다.

 

32bit 아키텍처, 64bit 아키텍처 에 대해 들어본 적이 있을 수도 있다. 여기서 앞의 비트수는 명령어 하나에 몇자리의 이진수를 처리할 수 있느냐를 의미한다. 32bit를 예로 들면, 한 명령에 32자리의 이진수를 처리할 수 있다는 것이고 총 2^32가지의 표현이 가능하다는 것이다. 이는 메모리에 있는 2^32개의 데이터에 각각 주소를 부여해둘 수 있다는 것이고, 주소가 부여된 데이터는 빠르게 찾아 처리할 수 있기 때문에 비트수가 커질수록, 처리속도는 빨라질 것이다.

 

하지만 메모리에 담긴 데이터가 이 비트로 표현 가능한 개수보다 많다면, 추가적인 처리가 필요하다. 따라서 32bit 아키텍처에서는 2^32bit, 환산하면 약4GB 정도의 메모리에까지만 주소를 부여할 수 있다. 따라서 무조건 64bit가 좋으니 써야한다기 보다, RAM 용량에 맞게 비트 수를 선택하는 것이 중요하다. RAM이 4GB 이하라면 32든 64든상관없지만 4GB를 넘어가면 64bit가 적합하다.

 

4. 프로그램 카운터(PC, Program Counter)

다시 말하지만 CPU는 메모리에서 명령어를 계속 가져오고 수행하는 작업을 반복하는 역할을 한다. 메모리에는 다양한 명령들이 실행되기를 기다리고 있는데, 이 중 어떤 것부터 처리해야하는지를 알려주는 역할을 하는 것이 바로 프로그램 카운터이다. 프로그램 카운터도 CPU의 레지스터 중 하나인데, 다음에 실행할 명령을 메모리에서 찾아 그 주소를 알려준다. CPU는 아래와 같은 과정을 무한히 반복하며 명령들을 차례로 실행시킨다.

 

1. 프로그램 카운터에는 다음에 실행할 명령어의 주소가 들어있다.
2. 해당 주소에서 명령어를 가져온다.
3. 프로그램 카운터를 증가시켜 그 다음 실행할 명령어 주소를 대입한다.
4. 가져온 명령을 실행한다.

 

여기서 카운터를 특정 값만큰 증가시켜, 다음 실행 주소로 이동시키는 것을 '점프'라고 한다. 여기서 조건을 추가해 처리할 수도 있다. 예를들어 '레지스터 10번값이 100이면, 프로그램 카운터를 5로 지정해라' 이런식이다. 우리가 작성하는 코드로 표현하면, if(x = 0) { five() } 이런 식이 될 것이다.

 

컴퓨터는 덧셈, 비교, 이동 같은 정말 단순한 연산들만 수행한다. 아무리 복잡한 프로그램을 만드는 코드라도, 결국 단순하게 만들고 나면 이런 연산들만으로 표현할 수 있는 것이다. 놀랍기도 하고 새삼 프로그래밍 언어를 쓸 수 있다는 것에 감사하게 되었다..

 

 

컴파일 (compile)

지금까지 우리가 작성하는 코드를 단순한 연산들로 변환된다는 것을 알게 되었다. 하지만 컴퓨터는 분명 0, 1만 알아듣는다고 알고있는데 이런 단순한 연산들을 컴퓨터는 어떻게 구분하고 알아들어서 처리하는 것일까? 바로 여기서 컴파일러가 중요한 역할을 한다.

 

1. 컴파일러 (compiler)

우리는 프로그래밍 언어를 사용해 CPU 명령들을 좀 더 우리가 이해하기 쉬운 방식으로 바꿔 작성한다. 이 이후에는 컴파일러라는 프로그램을 써서, 우리의 코드를 CPU가 이해할 수 있는 기계어 명령어로 바꾼다.

 

컴파일러는 이렇게 번역만 해줄 뿐만 아니라 코드를 최적화시켜주기도 한다. 우리가 작성한 코드를 더 효율적으로 실행할 수 있게 바꿔주는 것이다. 컴파이러는 다양한 최적화 규칙들을 알고 있기 때문에 그에 맞춰 비효율적인 부분을 수정한 후 이진 코드로 번역한다. 예를들어 우리가 재귀를 써서 작성한 코드를 반복문으로 재작성해주는 식이다. 덕분에 우리는 최적화보다는 코드의 가독성을 높이는 쪽에 더 집중할 수 있게 되었다.

 

2. 인터프리터 (interpreter)

출처 : https://velog.io/@damiano1027

 

프로그래밍 언어 중에는 스크립트 언어라고 부르는 언어들이 있다. 대표적으로 Javascript, Python, Ruby같은 것들이 있는데, 이런 언어로 작성한 코드들은 컴파일을 하지 않고도 실행할 수 있다. 컴파일을 통해 실행되는 코드들은 해석된 후 CPU가 직접 실행하지만, 스크립트 언어로 작성한 코드들은 인터프리터를 통해서 실행된다. 물론 인터프리터는 컴퓨터에 미리 설치해 두어야 한다.

 

출처 : https://kimmy100b.github.io/tech/2020/03/25/compiler-interpreter/

컴파일러는 모든 코드를 미리 기계어로 번역해둔 다음에 실행한다. 하지만 인터프리터코드를 한줄한줄 실시간으로 번역하고 실행한다. 따라서 코드 한 줄을 실행할 때, 번역 후 실행하는 인터프리터보다는 이미 번역을 마치고 실행만 하면 되는 컴파일러의 속도가 더 빠르다. 하지만 코드가 굉장히 긴 경우, 컴파일러는 해석하는데 오랜 시간을 쏟느라 실행까지의 대기시간이 길어지지만 인터프리터는 기다릴 필요 없이 일단 바로 실행할 수 있기 때문에 시간을 절약할 수 있다는 장점이 있다.

 

3. 운영체제 (OS, Operating System)

출처 : https://www.fun-coding.org/oshistory.html

Mac OS, Window, 리눅스 등 운영체제에 대해 들어본 적이 있을 것이다. 프로그램을 Mac버전, Window버전 중 선택해 다운로드 받은 경험도 있을 수 있다. 이번에는 이렇게 같은 프로그램을 굳이 운영체제별로 나누어 만드는 이유에 대해 알아보려한다.

 

정말 간단히 계산기 프로그램을 다운받았다고 생각해보자. 계산기는 내부적으로는 더하기 빼기 같은 계산을 할 줄 알아야한다. 뿐만 아니라 외부적으로는 사용자가 어떤 숫자와 기호를 눌렀는지 등 입력을 인식할 줄도 알아야한다. 계산후에 답을 보여주는 출력도 할줄 알아야한다.

이번에는 이 프로그램이 실행되는 컴퓨터 입장을 살펴보자. 사용자마다 가지고 있는 컴퓨터의 종류도 다를 수 있고, 사용하는 마우스나 키보드가 다 다를수도 있다. 이런 하드웨어 장치들은 각자 제어하는 방법도 다를 수 있다. 프로그램은 사용자가 키보드로 입력한 정보를 입력받아와야 하는데, 이렇게 모든 하드웨어들의 각자 다른 제어법들을 모두 고려해 프로그램을 만들려면 고려할게 굉장히 많아질 것이다.

 

운영체제는 이런 고려를 하지 않아도 되게 도와준다. 프로그램은 그냥 운영체제에 system call이라는 요청만 보내면, 운영체제가 알아서 모든 하드웨어들과 대응할 수 있게 알아서 처리해준다. 컴퓨터의 하드웨어와 프로그램 사이의 통역사같은 역할을 한다고 생각하면 된다. 하지만 운영체제마다, 프로그램이 보내야하는 system call의 종류가 다르다. 따라서 프로그램을 만들때는 하드웨어까지 고려하진 않더라도, 운영체제의 종류는 고려해 만들어야하는 것이다.

 

마무리

명령에 따라 데이터를 조작하는 기계인 컴퓨터가 어떻게 명령을 알아듣고 처리하는지 알아보았다. 우리가 프로그래밍 언어를 사용해 코드를 작성하면, 컴파일러는 컴퓨터가 알아들을 수 있는 기계어로 번역을 하고 CPU는 이 명령들을 실행한다. 스크립트 언어를 사용한 경우라면 이 과정 없이 바로 인터프리터를 통해 한줄씩 번역해 실행하기도 한다.

 

CPU가 명령을 처리하는 과정을 좀 더 자세히 살펴보면, 명령들을 순서대로 실행하기 위해 메모리를 사용한다. 메모리에 명령들과 명령 실행에 필요한 데이터들을 담아두고, CPU는 메모리에서 차례대로 명령과 데이터를 가져와 연산을 수행하는 것이다. CPU는 내부에 자체적으로 레지스터라는 저장공간을 가지고 있는데, 여기 담긴 값들을 가지고 연산을 한다. 레지스터가 메모리와 소통을 하며 현재 처리하는 명령이 무엇이고, 필요한 데이터가 무엇인지에 대한 정보를 가져오는 것이다.

 

우리가 어떤 복잡한 코드를 써도 이렇게 여러 단계를 거치면 CPU가 실행할 수 있는 단순한 명령들로 바뀔 수 있다. 다음 글에서는 이 중에서도 메모리는 어떻게 구성되고, 어떻게 수많은 정보들을 좀 더 효율적으로 저장하는지에 대해서도 좀 더 알아보고자 한다.

반응형

댓글