CPU가 어떻게 코드를 처리하는가

리눅스 커널 API 문서를 읽다가 길을 잃었다. 단순히 API 사용법을 찾으려다 보니 “커널이 뭔지”부터 시작해서 드라이버, GPIO, 기계어, ISA, 레지스터까지 줄줄이 연결되어버렸다. 처음에는 간단한 궁금증이었는데 결국 CPU가 코드를 처리하는 원리 전체를 파고들었다.

커널은 중재자다

커널의 역할을 한 문장으로 요약하면 “사용자의 명령을 하드웨어가 이해할 수 있는 언어로 번역해주는 중재자”다.

식당으로 비유하면 이렇다. 손님(사용자)이 메뉴를 보고 주문하면, 웨이터(커널)가 그 주문을 주방(하드웨어)에 구체적인 조리 지시로 전달한다. 손님은 음식이 어떻게 만들어지는지 몰라도 된다. 커널 덕분에 우리는 “파일을 열어라”라고 말할 수 있고, 실제로 디스크의 몇 번 섹터에서 데이터를 읽어오는지 알 필요가 없다.

커널은 C 언어로 작성된다. 커널 모듈을 개발하면 커널 기능을 확장하거나 드라이버를 추가할 수 있다. 다만 커널 코드는 특권 모드에서 실행되기 때문에 잘못 작성하면 시스템 전체가 죽는다. printf 대신 printk, malloc 대신 kmalloc을 써야 하는 것도 사용자 공간 코드와 다른 점이다.

드라이버와 GPIO

드라이버는 특정 장치를 제어하는 명세서다. TV 리모컨처럼, 커널이 하드웨어를 제어하려면 그 장치에 맞는 드라이버가 있어야 한다.

GPIO(General Purpose Input/Output)는 외부 신호를 감지하거나 외부로 신호를 보내는 범용 입출력 인터페이스다. 임베디드 환경에서 LED를 제어하거나 센서 값을 읽을 때 쓴다. 사용자 명령이 커널을 거쳐 드라이버를 통해 GPIO 핀에 전달되고, 그게 LED를 켜거나 끄는 물리적 신호가 된다.

C → 어셈블리 → 기계어

명령이 하드웨어까지 전달되는 경로는 이렇다:

사용자 입력
  → C 언어 코드 (사람이 읽을 수 있는 수준)
  → 어셈블리 언어 (CPU 명령 단위로 분해된 표현)
  → 기계어 (0과 1의 이진수, CPU가 직접 실행)
  → ALU가 계산, 메모리/디스크에 결과 저장

ALU(산술 논리 장치)는 CPU 내부에서 덧셈·뺄셈·논리 연산을 담당한다. 엄청 단순한 명령들만 처리하는데, 그 단순한 명령들이 초당 수십억 번 실행되면서 복잡한 프로그램이 돌아간다.

ISA: CPU의 기계어 사전

기계어는 CPU마다 다르다. 0010100101이 어떤 명령인지는 CPU 설계 시 ISA(Instruction Set Architecture)에서 정의된다.

x86에서는 대략 이런 식이다:

  • 0001 0000: MOV (값 이동)
  • 0010 1000: ADD (더하기)
  • 0101 0001: SUB (빼기)

CPU 제조사마다 ISA가 다르다. x86은 인텔/AMD가 데스크탑·노트북에 쓰고, ARM은 애플 실리콘·삼성이 모바일·임베디드에 쓴다. ARM 기반 맥북이 x86 윈도우 프로그램을 바로 실행하지 못하는 게 이 이유다.

Fetch → Decode → Execute → Write Back

CPU가 명령을 처리하는 순서는 4단계다:

  1. Fetch: 메모리에서 명령어를 가져온다
  2. Decode: 내부 디코더가 기계어를 해석한다
  3. Execute: ALU를 사용해 명령을 실행한다
  4. Write Back: 결과를 레지스터나 메모리에 저장한다

y = x + a 같은 단순한 연산도 어셈블리로 내려가면 세 줄이 된다:

MOV RAX, x    ; 레지스터에 x 값 로드
ADD RAX, a    ; a를 더함
MOV y, RAX    ; 결과를 메모리의 y에 저장

for문 1000번이면? 초기화·조건검사·누적·증가·점프 등 6~8개 명령이 1000번 반복되니까 최소 6,000개 이상의 기계어 명령이 실행된다.

메모리 계층 구조

레지스터에 데이터가 없으면 CPU는 더 빠른 캐시부터 순서대로 찾는다:

단계접근 속도
레지스터~1 사이클
L1 캐시~3-4 사이클
L2 캐시~10 사이클
L3 캐시~40 사이클
RAM100+ 사이클

레지스터는 x86 기준 16~32개, ARM은 31개 정도다. 작업이 끝나면 레지스터 값은 덮어써질 수 있다. 함수 호출 시에는 스택에 백업해두는 방식으로 처리한다.

왜 이걸 알아야 할까

솔직히 이런 로우레벨 지식이 당장 Spring Boot 코드 짜는 데 직접적인 도움은 안 된다. 그런데 JVM의 JIT 컴파일이 왜 빠른지, 메모리 캐시 최적화가 왜 중요한지, CPU-bound와 I/O-bound의 차이가 뭔지를 이해하는 배경 지식이 된다. 추상화 계층 아래에서 실제로 무슨 일이 일어나는지 한 번쯤 뚫고 내려가보면, 위쪽 코드를 보는 시각이 달라진다.

출처