도입
CPU는 고급 언어 코드를 이해하지 못하니 프로그램이 이해할 수 있는 기계어 형태로 변환되어야 합니다.
CPU가 해석하고 실행하는 명령의 형태이며, 모든 소프트웨어가 도달하는 가장 낮은 실행 언어입니다.
보통 0과 1로 인코딩된 명령어의 집합이지만 단순한 이진수 덩어리가 아닙니다.
어떤 CPU 아키텍처가 어떤 명령을 어떤 비트 패턴으로 해석할지 정의한 ISA(Instruction Set Architecture)에 따라 달라집니다.
정의
엄밀하게 말하면 기계어 (machine language)는 CPU가 이해하는 명령 체계 자체를 가리키고,
기계코드 (machine code)는 그 명령이 실제 바이트열로 인코딩된 결과를 가리킵니다.
💡 TIP
어떤 명령이 어떤 레지스터와 메모리를 어떻게 바꾸는지를 중심으로 이해하는 것이 효과적입니다.
표현 방식
하드웨어는 전기적 신호의 두 상태를 이용해 정보를 표현하므로, 본질적으로 0과 1의 비트열입니다.
다만 비트열을 그대로 읽는 것은 매우 불편하기 때문에, 실무에서는 보통 같은 내용을 더 짧게 표현할 수 있는 16진수(hex)나 디스어셈블(disassembly) 형태로 관찰합니다.
2진수 : 10110000 00000001
16진수 : B0 01
같은 비트열을 사람이 보기 쉽게 다르게 적은 것일 뿐이다.
명령어 구성
기계어 한 줄에는 보통 “무슨 연산을 할지”, “어디에 적용할지”, “값을 어떻게 해석할지”를 함께 담고 있습니다.
아키텍처마다 길이와 배치 방식은 다르지만, 개념적으로는 다음 요소들이 핵심입니다.
[ opcode ] [ destination ] [ source ] [ immediate / address ]
실제 ISA에서는 이보다 훨씬 복잡한 인코딩을 사용하기도 하지만, 본질은 항상 같습니다.
연산 종류 + 연산 대상 + 해석 규칙이 비트 단위로 정의되어 있다는 점입니다.
레지스터와 메모리
기계어는 추상적인 변수 이름보다 레지스터와 메모리 주소를 중심으로 동작합니다.
CPU는 계산을 매우 빠른 내부 저장소인 레지스터에서 수행하고, 필요한 데이터를 메모리에서 읽어와 다시 메모리에 저장합니다.
기계어를 읽는 것은 “지금 어떤 레지스터가 어떤 값을 가지고 있고, 다음 명령이 그 값을 어떻게 바꾸는가”를 추적하는 것 입니다.
✅ 자주 함께 등장하는 요소
- ✔️ 레지스터 (Register)
- ✔️ Program Counter (PC)
- ✔️ Stack Pointer (SP)
- ✔️ Flags / Stautus Register
- ✔️ Memory Address
실행 과정
사이클이 초당 수억~수십억 번 반복되며, 우리가 체감하는 모든 프로그램 실행이 만들어집니다.
명령어 사이클 (fetch-decode-execute cycle)
정의 CPU가 명령어를 처리하는 과정에는 정형화된 흐름이 있습니다. CPU는 프로그램을 구성하는 각 명령어를 일정한 주기로 반복 실행합니다. 이 반복 흐름을 명령어 사이클(Instruction Cycle)이라고
develop-enchantment.tistory.com
예시
아래는 x = a + b 같은 단순 연산을 서로 다른 계층에서 본 예시입니다.
기계어 부분은 이해를 돕기 위한 가상 ISA 예시이며, 실제 아키텍처의 인코딩과는 다를 수 있습니다.
[고급 언어]
x = a + b;
[어셈블리어 개념 예시]
LOAD R1, [a]
LOAD R2, [b]
ADD R1, R2
STORE [x], R1
[기계어 개념 예시]
0001 0001 1010
0001 0010 1011
0010 0001 0010
0011 0001 1100
고급 언어에서 기계어까지
네이티브 바이너리를 만드는 언어에서는 보통 소스 코드가 컴파일러를 거쳐 어셈블리어 혹은 중간 표현으로 바뀌고, 다시 오브젝트 파일과 실행 파일을 거쳐 기계어 형태로 준비됩니다.
소스 코드
↓
컴파일러
↓
어셈블리 / 중간 표현
↓
어셈블러
↓
오브젝트 파일
↓
링커
↓
실행 파일(기계어 포함)
반면 Java, Kotlin(JVM), C# 같은 환경에서는 먼저 바이트코드나 IL 같은 중간 형태가 만들어지고, 실행 시점에 VM이나 JIT 컴파일러가 최종 기계어로 변환해 CPU가 실행합니다. 즉, 경로는 다를 수 있지만 실행의 마지막 단계가 기계어라는 점은 동일합니다.
아키텍처 의존성
기계어는 보편적인 하나의 언어가 아닙니다. x86-64, ARM, RISC-V처럼 아키텍처가 다르면 지원하는 명령어, 레지스터 구조, 명령 길이, 인코딩 방식이 모두 달라집니다. 그래서 어떤 CPU용으로 만들어진 바이너리는 다른 CPU에서 그대로 실행되지 않는 경우가 일반적입니다.
- 같은 소스 코드라도 대상 CPU에 따라 바이너리가 달라진다.
- 디스어셈블 결과도 아키텍처마다 완전히 다르게 보인다.
- 컴파일러 옵션에서 타깃 아키텍처를 지정하는 이유가 여기에 있다.
- 크로스 컴파일, 에뮬레이션, 가상화가 필요한 배경도 이 구조에서 나온다.
실무와 학습에서의 중요성
일반적인 애플리케이션 개발에서는 기계어를 직접 쓰는 일은 거의 없습니다. 그러나 성능 병목, 비정상 종료, 메모리 손상, 보안 취약점, 펌웨어 문제처럼 시스템의 가장 낮은 층위에서 원인을 찾아야 하는 순간에는 기계어 이해가 매우 강력한 무기가 됩니다.
- 운영체제 → 인터럽트, 시스템 콜, 문맥 교환 이해
- 컴파일러 → 코드 생성과 최적화 결과 이해
- 보안 → 쉘코드, ROP, 바이너리 취약점 분석
- 임베디드 → 하드웨어 제어, 레지스터 맵 접근
- 성능 분석 → 분기, 캐시, 명령어 수준 병렬성 이해
- 디버깅 → 크래시 지점의 실제 실행 흐름 추적
자주 하는 오해
- 기계어와 어셈블리어가 완전히 같은 것이라고 생각함 → 밀접하지만 표현 층위가 다릅니다.
- CPU가 C나 Java를 직접 이해한다고 생각함 → 결국 기계어 또는 JIT가 생성한 기계어를 실행합니다.
- 기계어는 그냥 0과 1을 외우는 것이라고 생각함 → 핵심은 인코딩과 상태 변화 이해입니다.
- 고급 언어 한 줄이 기계어 한 줄이라고 생각함 → 실제로는 여러 명령으로 분해되는 경우가 많습니다.
- 모든 CPU가 같은 기계어를 쓴다고 생각함 → 아키텍처마다 완전히 다릅니다.
- 16진수 표기는 기계어가 아니다라고 생각함 → 같은 비트열을 사람이 읽기 쉽게 쓴 표현일 뿐입니다.
디버깅
요약
- ✅ 기계어는 CPU가 직접 실행하는 비트 단위 명령어다.
- ✅ 어셈블리어는 기계어를 사람이 읽기 쉽게 표현한 기호적 언어다.
- ✅ 기계어는 opcode, operand, addressing mode 같은 구조를 가진다.
- ✅ CPU는 fetch-decode-execute 사이클로 기계어를 연속적으로 처리한다.
- ✅ 같은 소스 코드라도 대상 아키텍처가 다르면 기계어 인코딩도 달라진다.
- ✅ 기계어를 이해하면 운영체제, 컴파일러, 성능 최적화, 보안 분석의 기반이 훨씬 선명해진다.