라봉이의 개발 블로그

CPU와 어셈블리 언어, 기계어, 어셈블러, 컴파일러, 링커의 상관관계 [용어][c][컴퓨터구조] 본문

컴퓨터 사이언스/개인 연구

CPU와 어셈블리 언어, 기계어, 어셈블러, 컴파일러, 링커의 상관관계 [용어][c][컴퓨터구조]

Labhong 2018. 2. 27. 11:27

서론

문득 visual studio에서 c 언어로 프로그래밍 하는 도중에 궁금한 점이 생겼다.
c언어는 visual studio에서만 가능한 것인가? c언어는 무엇이지? 단순히 프로그램을 만들어주는 언어? 등등 여러 궁금한 점이 생겨서 구글링 또 구글링을 통해 정보를 얻고 얻어서 이해한 지식을 써보고자 한다.

CPU와 기계어

프로그램이란 0과 1로 된, 컴퓨터에게 어떤 동작을 실행하라는 명령어들의 집합이다. 이 프로그램이란 것을 실행시키게 되면 프로그램이라 불리는 명령어들이 메인 메모리(RAM 램)에 배치된다. 이 상태를 프로세스라고 부른다.

이 배치된 명령어들을 하나씩 순서대로, 혹은 지정된 주소에 있는 명령어들을 읽어와서 CPU에서 계산 및 처리를 하게 되고 그 명령어대로 CPU가 다른 컴퓨터 자원들의 동작, 수행을 명령한다. 그리고 이 0과 1로 이루어진 명령어 들을 기계어라고 한다.

기계어는 컴퓨터, 즉 CPU가 이해할 수 있는 언어이다.(물론 사람이 이해하기엔 너무 난해하다...) 하지만 여기서 문제점이 한 가지 존재한다.

CPU 제조사마다 기계어가 다르다!

전자제품마다 사용설명서가 다르듯이 CPU도 제품마다 명령어가 다르다. 예를 들어 Intel CPU와 AMD CPU는 같은 명령을 수행하더라도 기계어가 다를 수 있다.(0과 1의 배치가 다르다.) 또한 같은 제조사라도 버전 별로 다른 명령어를 포함할 수도 있기에 기계어는 cpu에 큰 영향을 받는다.

예를 들어,

x = 10 + 2;
y = x + 4;

이 표현을 MIPS라는 아키텍쳐의 기계어로 옮기면 다음과 같다.

001001 11101 11101 1111111111111000
001000 00001 00000 0000000000001010
001000 00001 00001 0000000000000010
101011 11101 00001 0000000000000000
001000 00010 00001 0000000000000100
101011 11101 00010 0000000000000100
001001 11101 11101 0000000000001000

출처: https://namu.wiki/w/%EA%B8%B0%EA%B3%84%EC%96%B4

어셈블리어 어셈블러

이 기계어는 0과 1로 이루어졌기 때문에 간단한 프로그램 하나를 돌리더라도 코드가 매우 길어질 뿐더러 사람이 읽기에도 매우 큰 무리가 있다.
그래서 사람이 그나마 이해할 수 있도록 어셈블리어가 생기게 되었다.

'어셈블리어'는 기계어와 일대일 대응이 되는 컴퓨터 프로그래밍의 저급 언어이다. 0과 1을 사람이 읽을 수 있는 언어로 변환되어서 가독성을 높인 것이다.
그렇지만 이 어셈블리어는 컴퓨터가 이해할 수 없는 언어이다.

컴퓨터는 오직 기계어(0과 1로 이루어진)만 이해할 수 있기 때문에 프로그램 코드인 어셈블리어를 0과 1의 집합인 기계어로 바꿔주는 역할을 하는 프로그램이 바로 이 어셈블러이다. 그러니까 프로그래머가 어셈블리어로 프로그램을 작성하면 어셈블러가 기계어로 바꾸어 컴퓨터가 이해할 수 있도록 만든다. 그렇게 프로그램이 실행할 수 있도록 한다.

아까 CPU마다 명령어가 다르다고 했었다. 따라서 당연히 CPU마다 어셈블리어의 명령어 체계가 다르다. 예를 들어 Intel 계열의 CPU에서 동작하는 게임을 만들었다면 AMD 계열의 CPU에서는 동작하지 않는다.
동작하게 만들고 싶다면 AMD 계열의 CPU에서 작동할 수 있도록 새로 프로그램을 짜야한다.

c언어와 컴파일러

CPU마다 명령어가 다르니 다른 CPU에서 프로그램을 동작하게 만드려면 다시 만들어야되는 불편함을 겪다가 생겨난 것이 c언어이다.
c언어는 이식성이 좋아서 CPU마다 프로그램을 각각 작성하지 않아도 되며 메모리에 직접접근이 가능한 저급 언어적 특징을 지니고 있고, 절차지향적 특성을 가진 언어이다.

그런데 이 c 언어는 컴파일러라는 프로그램과 함께 등장했다. '컴파일러'는 c언어, 파스칼 등으로 구현된 프로그램 코드를 어셈블리 코드 혹은 기계어 등 다른 언어로 변환해 주는 프로그램을 지칭한다.

따라서 컴파일 과정을 거치면 우리가 작성한 소스코드를 컴파일러는 이를 기계어나 어셈블리어로 변환해주고 이 변환된 코드를 어셈블러 등이 기계어로 바꾸어주게 되고 비로소 컴퓨터가 이해할 수 있는 기계어인 '목적 코드'(혹은 오브젝트 코드)가 된다.

하지만 이 자체에서 끝나는 것이 아니다. 목적 코드를 한데 엮어 커널과 연결하여 '실행 파일'(예. 윈도우 exe 파일)로 만들어 주는 '링커'가 역할을 수행해야 드디어 우리가 실행할 수 있는 프로그램이 완성된다. 위의 어셈블러와 마찬가지로 컴파일러도 Intel CPU만이 이해 가능한 어셈블리 코드로 변환 해주는 컴파일러, ARM의 어셈블리 코드로 변환해 주는 컴파일러 등 CPU마다 컴파일러 또한 다르다.

그렇다면 Visual studio는 왜 윈도우 환경에 있는 모든 컴퓨터에서 Visual studio 하나만 다운 받으면 컴파일이 되는걸까?
Visual studio는 Window 운영체제에 한하여 X86, X64, Itanium 및 최근 ARM도 지원하기 때문에 가능한 것이다.(그래서 Visual Studio 용량이 매우 크다) 한마디로 Window가 깔려있는 웬만한 컴퓨터라면 그 컴퓨터에 맞게 컴파일이 가능하다는 소리다.

운영체제와 링커

그렇다면 이제 c언어로 작성한 소스코드가 컴파일러를 통해 기계어(목적 코드)로 번역되었다면 번역된 기계어는 어떻게 프로그램으로써 실행이 될까? 하지만 이를 알기 전에 먼저 알아야 할 것이 있다.

우리나라 사람이면 보통 윈도우를 쓰기 마련인데 이 윈도우에서 작동하는 프로그램을 리눅스에서는 과연 실행이 가능할까? 불가능하다.
같은 CPU를 사용하더라도 불가능하다. 우리가 게임을 다운받을 때 보면 윈도우 버전, 맥 버전을 따로 받는 것 처럼 말이다.

마찬가지로 실행파일은 어셈블리어가 CPU에 영향을 받는 것처럼 운영체제에도 영향을 받는다. 운영체제마다 커널이 다르기 때문이다.

c언어를 윈도우에서 컴파일과 링크 과정을 거치면 *.exe 파일이 나온다.
하지만 리눅스에서 gcc로 컴파일과 링크 과정을 거칠때 따로 설정하지 않으면 a.out 파일로 나온다.(리눅스는 실행파일을 굳이 *.out 으로 설정하지 않아도 된다. 확장자의 영향을 받지 않기 때문)

여기서 하고 싶은 얘기는 운영체제에 따라 코드는 같아도 실행파일도 다르다는 점이다.
아무튼 프로그래머가 만든 기계어인 목적 코드를 윈도우 운영체제의 실행파일로 만들기 위해서는 윈도우 운영체제의 포맷에 맞춰서 구성해야한다.

이 작업은 바로 '링커'가 작업해준다. 링커는 어셈블러가 생성한 목적 파일들을 결합하여 하나의 실행 파일로 만드는 작업을 해준다. 여러 c언어 파일을 사용하는 프로그램을 만들 때 c언어를 컴파일 하면 그 모든 파일이 전부 *.obj란 오브젝트 파일로 바뀌는데(리눅스는 *.o 파일) 이 모든 오브젝트 파일와 라이브러리 파일을 한데 묶어 실행 파일인 .exe로 만들어 준다.

실행 파일이란 CPU에게 일을 시키기 위한 바이너리 형태의 명령어를 운영체제에서 요구하는 포맷에 맞춰 구성한 파일이다. 파일을 실행시키면 프로그램이 시작된다.

이 모든 걸 간단하게 요약해본다면

소스파일 -> 컴파일러 -> 어셈블리 어 -> 어셈블러 -> 오브젝트 파일(목적 파일) -> 링커 -> 실행 파일

순으로 진행되어 실행 파일을 만들게 되고 이 실행 파일을 배포하여 프로그램을 우리가 사용할 수 있게 되는 것이다.

Comments