ABOUT

성능과 운영 안정성을 함께 끌어올리는 개발자입니다.

92% Positional Error Reduction
79% p95 Latency Improvement
90%+ Long Tasks Reduction

2022.02 · 한국장학재단

우수 멘티

한국장학재단 사회 리더 대학생 멘토링 IT

2022.10 · 동작구청

우수 인재상

동작구청 우수 SW 인재

2025.05 · (주) 그랩

프로그래밍 우수상

(주) 그랩 우수 프로그램 개발

2025.05 · AWSKRUG

AWS한국사용자모임 발표

AI agent 스크립트 튜닝 관련 발표

ComputerScience

Development

Engineering

Trouble Shooting

GUESTBOOK

첫 마음부터
함께 나누는 온기

방명록 작성하러 가기

SUBSCRIBE

최신소식을
편하게 만나보세요.

기계어 (machine language)

도입

CPU가 “무슨 연산을 어떻게 수행할지”를 이해하도록 약속된 엄격한 형식입니다.

CPU고급 언어 코드를 이해하지 못하니 프로그램이 이해할 수 있는 기계어 형태로 변환되어야 합니다.

CPU가 해석하고 실행하는 명령의 형태이며, 모든 소프트웨어가 도달하는 가장 낮은 실행 언어입니다.

보통 0과 1로 인코딩된 명령어의 집합이지만 단순한 이진수 덩어리가 아닙니다.

어떤 CPU 아키텍처가 어떤 명령을 어떤 비트 패턴으로 해석할지 정의한 ISA(Instruction Set Architecture)에 따라 달라집니다.

 

정의

CPU가 직접 실행하는 비트 단위 명령어입니다.

엄밀하게 말하면 기계어 (machine language)CPU가 이해하는 명령 체계 자체를 가리키고,

기계코드 (machine code)는 그 명령이 실제 바이트열로 인코딩된 결과를 가리킵니다.

💡 TIP

어떤 명령이 어떤 레지스터와 메모리를 어떻게 바꾸는지를 중심으로 이해하는 것이 효과적입니다.

표현 방식

실제로는 2진수로 저장되지만, 16진수와 디스어셈블 형태의 결과로 읽고 분석합니다.

하드웨어는 전기적 신호의 두 상태를 이용해 정보를 표현하므로, 본질적으로 0과 1의 비트열입니다.

다만 비트열을 그대로 읽는 것은 매우 불편하기 때문에, 실무에서는 보통 같은 내용을 더 짧게 표현할 수 있는 16진수(hex)디스어셈블(disassembly) 형태로 관찰합니다.

2진수 : 10110000 00000001

16진수 : B0 01

 

같은 비트열을 사람이 보기 쉽게 다르게 적은 것일 뿐이다.

명령어 구성

단순한 숫자 나열이 아니라, 연산 종류와 대상과 주소 해석 규칙이 인코딩된 구조화된 정보다

기계어 한 줄에는 보통 “무슨 연산을 할지”, “어디에 적용할지”, “값을 어떻게 해석할지”를 함께 담고 있습니다.

아키텍처마다 길이와 배치 방식은 다르지만, 개념적으로는 다음 요소들이 핵심입니다.

구성 요소 의미 예시 개념
Opcode 어떤 연산인지 지정 ADD, LOAD, JMP
Operand 연산 대상 지정 R1, 메모리 주소, 즉시값
Addressing Mode 값을 어떻게 해석할지 지정 즉시값, 직접 주소, 간접 주소
Immerdiate / Offset 추가 데이터나 주소 보정 값 상수 5, 오프셋 16

[ opcode ] [ destination ] [ source ] [ immediate / address ]

실제 ISA에서는 이보다 훨씬 복잡한 인코딩을 사용하기도 하지만, 본질은 항상 같습니다.

연산 종류 + 연산 대상 + 해석 규칙이 비트 단위로 정의되어 있다는 점입니다.

레지스터와 메모리

명령어 자체보다 먼저 CPU가 값을 어디에 두고 어떻게 옮기는지를 봐야 한다

기계어는 추상적인 변수 이름보다 레지스터메모리 주소를 중심으로 동작합니다.

CPU는 계산을 매우 빠른 내부 저장소인 레지스터에서 수행하고, 필요한 데이터를 메모리에서 읽어와 다시 메모리에 저장합니다.
기계어를 읽는 것은 “지금 어떤 레지스터가 어떤 값을 가지고 있고, 다음 명령이 그 값을 어떻게 바꾸는가”를 추적하는 것 입니다.

✅  자주 함께 등장하는 요소

실행 과정

CPU는 기계어를 가져오고, 기계어 명령은 보통 다음과 같은 흐름으로 처리됩니다.

사이클이 초당 수억~수십억 번 반복되며, 우리가 체감하는 모든 프로그램 실행이 만들어집니다.

 

명령어 사이클 (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
핵심 포인트
고급 언어의 한 줄은 실제로 여러 개의 기계어 명령으로 분해되는 경우가 많습니다. 그래서 기계어를 보면 프로그램 실행이 생각보다 훨씬 더 세밀한 단계로 이루어진다는 점을 이해할 수 있습니다.

고급 언어에서 기계어까지

프로그램은 소스 코드에서 끝나는 것이 아니라, 컴파일·어셈블·링크 또는 JIT 과정을 거쳐 최종적으로 기계어가 되어 실행된다

네이티브 바이너리를 만드는 언어에서는 보통 소스 코드가 컴파일러를 거쳐 어셈블리어 혹은 중간 표현으로 바뀌고, 다시 오브젝트 파일과 실행 파일을 거쳐 기계어 형태로 준비됩니다.

소스 코드
   ↓
컴파일러
   ↓
어셈블리 / 중간 표현
   ↓
어셈블러
   ↓
오브젝트 파일
   ↓
링커
   ↓
실행 파일(기계어 포함)

반면 Java, Kotlin(JVM), C# 같은 환경에서는 먼저 바이트코드나 IL 같은 중간 형태가 만들어지고, 실행 시점에 VM이나 JIT 컴파일러가 최종 기계어로 변환해 CPU가 실행합니다. 즉, 경로는 다를 수 있지만 실행의 마지막 단계가 기계어라는 점은 동일합니다.

아키텍처 의존성

기계어는 CPU 아키텍처에 강하게 종속되기 때문에, 같은 프로그램이라도 x86과 ARM에서는 전혀 다른 명령어 인코딩으로 바뀐다

기계어는 보편적인 하나의 언어가 아닙니다. x86-64, ARM, RISC-V처럼 아키텍처가 다르면 지원하는 명령어, 레지스터 구조, 명령 길이, 인코딩 방식이 모두 달라집니다. 그래서 어떤 CPU용으로 만들어진 바이너리는 다른 CPU에서 그대로 실행되지 않는 경우가 일반적입니다.

이 점이 중요한 이유
  • 같은 소스 코드라도 대상 CPU에 따라 바이너리가 달라진다.
  • 디스어셈블 결과도 아키텍처마다 완전히 다르게 보인다.
  • 컴파일러 옵션에서 타깃 아키텍처를 지정하는 이유가 여기에 있다.
  • 크로스 컴파일, 에뮬레이션, 가상화가 필요한 배경도 이 구조에서 나온다.

실무와 학습에서의 중요성

기계어는 대부분의 개발자가 직접 작성하지는 않지만, 시스템의 한계와 동작 원리를 이해하려면 결국 한 번은 마주쳐야 하는 층위다

일반적인 애플리케이션 개발에서는 기계어를 직접 쓰는 일은 거의 없습니다. 그러나 성능 병목, 비정상 종료, 메모리 손상, 보안 취약점, 펌웨어 문제처럼 시스템의 가장 낮은 층위에서 원인을 찾아야 하는 순간에는 기계어 이해가 매우 강력한 무기가 됩니다.

대표적으로 중요한 분야
  • 운영체제 → 인터럽트, 시스템 콜, 문맥 교환 이해
  • 컴파일러 → 코드 생성과 최적화 결과 이해
  • 보안 → 쉘코드, ROP, 바이너리 취약점 분석
  • 임베디드 → 하드웨어 제어, 레지스터 맵 접근
  • 성능 분석 → 분기, 캐시, 명령어 수준 병렬성 이해
  • 디버깅 → 크래시 지점의 실제 실행 흐름 추적

자주 하는 오해

어셈블리어와의 관계, 아키텍처 의존성, 추상화 수준 차이를 정확히 구분해야 합니다.
  • 기계어와 어셈블리어가 완전히 같은 것이라고 생각함 → 밀접하지만 표현 층위가 다릅니다.
  • CPU가 C나 Java를 직접 이해한다고 생각함 → 결국 기계어 또는 JIT가 생성한 기계어를 실행합니다.
  • 기계어는 그냥 0과 1을 외우는 것이라고 생각함 → 핵심은 인코딩과 상태 변화 이해입니다.
  • 고급 언어 한 줄이 기계어 한 줄이라고 생각함 → 실제로는 여러 명령으로 분해되는 경우가 많습니다.
  • 모든 CPU가 같은 기계어를 쓴다고 생각함 → 아키텍처마다 완전히 다릅니다.
  • 16진수 표기는 기계어가 아니다라고 생각함 → 같은 비트열을 사람이 읽기 쉽게 쓴 표현일 뿐입니다.

 

디버깅

어떤 ISA를 보는지, 어떤 레지스터와 주소가 바뀌는지, 명령 경계가 어디인지부터 확인합니다.
1
지금 보고 있는 것이 소스 코드인지, 어셈블리인지, 실제 바이트 덤프인지 먼저 구분한다.
2
대상 CPU가 x86인지 ARM인지 같은 ISA를 먼저 확인한다.
3
각 명령 뒤에 어떤 레지스터와 플래그가 바뀌는지를 추적한다.
4
메모리 접근 명령이라면 주소가 무엇을 가리키는지와 정렬, 오프셋, 스택 위치를 함께 본다.
5
컴파일 최적화가 켜져 있다면 원래 소스 코드와 1:1 대응이 깨질 수 있다는 점을 염두에 둔다.

요약

컴퓨터가 실제로 실행하는 최종 언어이며, 이를 이해하면 코드가 하드웨어 위에서 어떻게 움직이는지 가장 본질적인 수준에서 볼 수 있다
  • ✅ 기계어는 CPU가 직접 실행하는 비트 단위 명령어다.
  • ✅ 어셈블리어는 기계어를 사람이 읽기 쉽게 표현한 기호적 언어다.
  • ✅ 기계어는 opcode, operand, addressing mode 같은 구조를 가진다.
  • ✅ CPU는 fetch-decode-execute 사이클로 기계어를 연속적으로 처리한다.
  • ✅ 같은 소스 코드라도 대상 아키텍처가 다르면 기계어 인코딩도 달라진다.
  • ✅ 기계어를 이해하면 운영체제, 컴파일러, 성능 최적화, 보안 분석의 기반이 훨씬 선명해진다.
728x90