코드는 처음부터 CPU의 언어가 아니다
사람이 작성하는 코드는 보통 고급 언어로 되어 있다. C, Java, Python 같은 언어는 사람이 문제를 이해하고 표현하기 쉽게 만든 언어다. 값을 더하라, 화면에 출력하라, 파일을 열라 같은 생각을 비교적 자연스럽게 적을 수 있다.
하지만 CPU는 이런 문장을 직접 이해하지 못한다. CPU가 실제로 실행하는 것은 기계어다. 기계어는 0과 1로 된 명령이며, "메모리에서 값을 가져와라", "레지스터끼리 더하라", "특정 주소로 이동하라"처럼 CPU 회로가 바로 처리할 수 있는 형태다.
어셈블리어는 그 중간에 있다. 기계어를 사람이 조금 더 읽기 쉽게 이름으로 바꾼 언어다. 예를 들어 ADD, MOV, JMP 같은 명령은 각각 더하기, 이동, 점프를 뜻한다. 고급 언어가 사람의 생각에 가깝다면, 어셈블리어는 CPU 명령에 가깝고, 기계어는 CPU가 실제로 읽는 비트열이다.

컴파일러는 문장을 명령으로 바꾼다
컴파일러는 소스 코드를 CPU가 실행할 수 있는 형태로 바꾸는 프로그램이다. 이 과정은 단순한 번역이 아니다. 컴파일러는 먼저 코드의 구조를 분석한다. 변수 이름, 함수 호출, 조건문, 반복문이 어떤 관계를 갖는지 확인한다. 문법이 틀리면 이 단계에서 오류가 난다.
그다음 컴파일러는 코드의 의미를 더 낮은 수준의 동작으로 바꾼다. 예를 들어 a + b라는 표현은 메모리나 레지스터에서 두 값을 가져와 더한 뒤 결과를 저장하는 명령들로 변환된다. 이때 불필요한 계산을 줄이거나, 더 빠른 명령을 선택하는 최적화도 일어난다.
컴파일 결과로 보통 목적 코드가 만들어진다. 목적 코드는 기계어에 가깝지만 아직 완전한 실행 파일은 아니다. 왜냐하면 프로그램은 대개 여러 파일로 나뉘어 있고, 다른 라이브러리 함수도 사용하기 때문이다. 각 조각은 준비되었지만, 아직 하나로 연결되지는 않은 상태다.
다만 모든 언어가 같은 방식으로 바로 실행 파일을 만드는 것은 아니다. Java처럼 바이트코드로 컴파일한 뒤 가상 머신 위에서 실행되는 언어도 있고, Python처럼 실행 환경이 코드를 해석하거나 내부 형식으로 바꾸어 실행하는 언어도 있다. 방식은 달라도 핵심은 같다. 사람이 읽기 쉬운 코드는 결국 CPU가 실행할 수 있는 낮은 수준의 명령으로 이어져야 한다.
링커는 흩어진 조각을 하나로 묶는다
링커는 여러 목적 코드와 라이브러리를 모아 하나의 실행 파일로 만든다. 프로그램 안에서 어떤 함수가 다른 파일에 정의되어 있다면, 컴파일러는 일단 "이 이름의 함수가 나중에 필요하다"는 표시를 남긴다. 링커는 그 이름을 실제 코드 위치와 연결한다.
이때 중요한 개념이 심벌이다. 심벌은 함수나 전역 변수처럼 다른 코드에서 참조할 수 있는 이름이다. 링커는 심벌 목록을 맞춰 보며 "이 함수 호출은 실행 파일 안의 이 주소로 가야 한다"는 식으로 빈칸을 채운다.
라이브러리도 이 단계에서 연결된다. 예를 들어 화면 출력, 문자열 처리, 수학 계산 같은 기능은 매번 직접 만들지 않고 이미 만들어진 라이브러리를 가져다 쓴다. 정적 링크를 하면 필요한 라이브러리 코드가 실행 파일 안에 포함된다. 반대로 동적 링크를 하면 실행 파일에는 "실행할 때 이 라이브러리를 불러오라"는 정보만 들어간다.
로더는 실행 파일을 메모리에 올린다
실행 파일이 만들어졌다고 해서 곧바로 CPU가 실행하는 것은 아니다. 사용자가 프로그램을 실행하면 운영체제의 로더가 먼저 움직인다. 로더는 실행 파일을 읽어 메모리에 배치한다.
프로그램의 메모리 공간은 대략 코드 영역, 데이터 영역, 힙, 스택으로 나뉜다. 코드 영역에는 실행할 명령이 들어간다. 데이터 영역에는 전역 변수 같은 값이 놓인다. 힙은 실행 중 필요한 메모리를 동적으로 할당받는 공간이고, 스택은 함수 호출과 지역 변수를 관리하는 공간이다.
로더는 필요한 동적 라이브러리도 함께 연결한다. 실행 파일이 직접 모든 기능을 갖고 있지 않더라도, 실행 중 필요한 라이브러리를 메모리에 올리고 함수 주소를 맞춰 준다. 준비가 끝나면 로더는 프로그램의 시작 주소, 즉 진입점으로 CPU의 제어를 넘긴다. 그 순간부터 CPU는 명령어를 하나씩 가져와 실행한다.
실행 중에도 필요한 환경이 있다
프로그램은 혼자 움직이지 않는다. 실행 중에는 런타임 환경의 도움을 받는다. 런타임은 프로그램이 돌아가는 동안 필요한 기본 기능을 제공하는 층이다. 메모리 할당, 예외 처리, 입출력 처리, 스레드 관리 같은 일이 여기에 포함된다.
언어에 따라 런타임의 역할은 다르다. C 프로그램은 비교적 운영체제와 가까운 방식으로 실행되지만, Java나 JavaScript 같은 언어는 가상 머신이나 엔진 위에서 실행된다. 이 환경은 코드를 해석하거나 즉석에서 기계어로 바꾸고, 메모리 관리 같은 작업도 대신한다.
결국 사람이 쓴 코드는 여러 단계를 지나 실제 동작이 된다. 컴파일 방식의 프로그램은 소스 코드에서 목적 코드로, 다시 실행 파일로 이어지고, 실행 시 로더를 통해 메모리에 올라간다. 가상 머신이나 인터프리터를 사용하는 언어도 실행 환경의 도움을 받아 CPU가 처리할 수 있는 명령으로 바뀐다. 이 흐름을 이해하면 프로그램은 더 이상 화면 속 문장이 아니라, 컴퓨터 전체가 협력해 실행하는 정교한 구조로 보인다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.
