컴퓨터는 0과 1로 연산을 수행한다는 것을 알고 있을것이다. 이를 이용해 컴퓨터에게 명령을 내리는 언어를 기계어(Machine Language)라고 정의했다. 그러나 이는 가독성이 매우 떨어졌다. 이에 사람이 이해하기 쉬운(?) 언어인 어셈블리어(Assembly Language)가 고안되었다. 아니나 다를까 이것도 어려워서 C, C++같은 언어가 등장하고 이거를 기계어로 번역해주는 컴파일러가 등장했다. 즉 컴퓨터 언어의 위계는 다음과 같다.
기계어 - 어셈블리어 - 고급언어
프로그램이란 연산장치(일반적으로 CPU)가 수행해야하는 동작을 정의한 일종의 문서다. 이전에는 물리적 방법(전선/천공카드)를 이용하여 프로그램을 저장했다. 하지만 컴퓨터 내부에 저장이 가능해지고, 컴퓨터 내부에 0과 1의 구성으로 저장된다. 이에 엔지니어들은 프로그램을 바이너리(Binary) 라고 부르곤 한다.
프로그래밍 언어는 프로그램 개발을 위해 사용되는 언어다. CPU가 수행할 명령을 프로그래밍 언어로 작성한 것을 소스 코드라고 하며 이를 기계어로 번역하는 것을 컴파일이라고 한다. 그리고 컴파일은 컴파일러라는 소프트웨어가 수행한다.
하지만 Python, Javascript 등의 언어는 컴파일이 필요하지 않으며 스크립트가 바로 CPU에 번역되어 전달된다. 이 번역/전달과정은 인터프리팅 이라고 부르며 이를 처리해주는 프로그램을 인터프리터 라고 한다.
전처리의 과정은 (1)주석 제거, (2) 매크로 치환, (3) 파일 병합 으로 이뤄진다. 아래는 컴파일 과정을 gcc의 -E옵션을 통해 살펴본 내용이다.(스크린샷에 add.c 와 add.i 사이에 >가 들어가야한다.)

컴파일은 소스코드를 다른 언어로 작성된 코드로 변환해주는 과정이다. 하지만 대부분은 어셈블리어로 변환하는 과정을 칭한다. 이 때 문법을 검사하며, 오류가 있으면 에러를 출력한다. 번역할 떄, 컴파일러는 조건을 만족할 떄, 최적화 기술을 적용하여 어셈블리어로 변환해준다. gcc에서는 -O -O0 -O1 -O2 -O3 -Os -Ofast -Og 등의 옵션을 사용하여 최적화를 적용할 수 있다.
-S옵션을 사용하면 소스코드를 어셈블리 코드로 컴파일 할 수 있다. add.i를 어셈블리어 코드로 컴파일하면 다음과 같다.
.text
.globl add
.type add, @function
add:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %edx
movl -8(%rbp), %eax
addl %edx, %eax
addl $3, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size add, .-add
.ident "GCC: (Ubuntu 13.2.0-23ubuntu4) 13.2.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
드림핵 예시와 다를 수 있는데, 이는 컴파일 환경에 따라 달라질 수 있다. 다만 수행 결과는 똑같다.
어셈블(Assemble) 이란 컴파일로 생성된 어셈블리어 코드를 ELF형식의 목적 파일(Object File)로 변환하는 과정이다. ELF는 리눅스 실행파일의 형식이며, 윈도우는 PE 형식을 갖는다.
목적 파일로 변환되면 어셈블리 코드는 기계어로 번역된다. 다음은 add.S를 목적파일로 면환하고 16진수로 출력하는 장면이다.

링크(Link) 는 앞서 말한 목적 파일들을 연결하여 실행 가능한 바이너리(즉, 프로그램)으로 만드는 과정이다. 라이브러리가 없다면 printf도 호출하지 못할 것이다. printf 함수는 gcc의 기본 라이브러리 경로의 libc에 있으며 링커는 printf를 호출하면 libc의 함수가 실행될 수 있게 연결해준다. 이후에 프로그램이 완성된다.
다음은 add.o를 링크하는 명령어다. 링커는 main함수를 찾지못해 에러를 발생시킬 수 있으므로 unresolved-symbols를 컴파일 옵션에 추가해보자.

기계어 코드는 이해하기 어렵다. 따라서 기계어를 어셈블리어로 재번역하는 과정을 수행하는데 이게 바로 디스어셈블(Disassemble) 이다.
$ objdump -d ./add -M intel로 디스어셈블 된 결과를 볼 수 있다.
하지만 여전히 디스어셈블 된 어셈블리어 코드를 이해하는 것은 어렵다. 이에 리버스 엔지니어들은 어셈블리어보다 고급 언어로 바이너리를 번역하는 디컴파일러(Decompiler) 를 개발했다.
그러나 앞서 살펴본 내용에 따르면 고급 언어로 이뤄진 바이너리가 컴파일되는 과정에서 함수가 지워지고, 축약되는 등의 과정을 거친다는 것을 알 수있다. 이를 역산하여 진짜 Original 코드를 찾는 것은 쉽지않다.(함수를 적분할 때의 적분상수 같은거?) 하지만 이로인한 왜곡은 없다.
Hex Rays, Ghidra 등의 디컴파일러들이 개발되어 있으며, IDA Freeware 라는 무료 디컴파일러도 있다.