1. Home
  2. CS/System
  3. [시스템 소프트웨어] 어셈블리어 개요 / 어셈블리어(Assembly)란?

[시스템 소프트웨어] 어셈블리어 개요 / 어셈블리어(Assembly)란?

* 본 글은 학부생의 입장에서 공부 내용을 정리하기 위해 작성되었습니다. 틀린 내용이 있으면 피드백 부탁드리며, 무분별한 비방 작성시 차단 될 수 있음을 알려드립니다.

 

이 글을 읽기 전에 알고 있으면 좋은 내용

- 디지털 논리(디지털 논리 회로)

- 컴퓨팅 구조

- C/C++ 및 프로그래밍 언어에 대한 지식

안녕하세요 파일입니다. 프로그래밍을 한 번이라도 해보신 분들은 "어셈블리어" 에 대해 한 번 쯤은 들어보셨을 겁니다. 오늘은 이 어셈블리어가 뭔지 간략하게 알아봅시다. 우선 본 글은 인텔 32비트 컴퓨터를 기준으로 설명합니다. 보통 32비트 프로그램은 x86, 64비트 프로그램은  x64(또는 x86-64) 라는 이름으로 부릅니다. 즉, x86이 어쩌고 하면 32비트 컴퓨터에 대한 이야기를 하는것이다 라고 이해하면 됩니다.

 

본 글은 기본적으로 시스템 소프트웨어와 C언어의 관점에서 전공자의 이해를 돕고자 작성합니다.

 

또 어셈블리어를 이해하기 위해 앞에서 설명하는 내용이 많은데 아는 내용이면 넘어가셔도 좋지만, 모르는 내용이라면 스킵하지 말고 앞부분을 잘 정독하셔야 이해하실 수 있을겁니다.

 

CISC vs RISC

인텔 CPU는 많은 명령어를 사용하며, 이렇게 복잡하고 많은 명령어를 사용한다는 의미에서 CISC(Complex Instruction Set Computer) 라고 부릅니다. 그런데 우리가 스마트폰에서 사용하는 ARM 프로세서(CPU) 의 경우 CPU의 명령어가 복잡하지 않고, 갯수도 적습니다. 그래서 RISC(Reduced Instruction Set Computer) 라고 부르게 됩니다.

CISC, RISC 각각 영어 의미 그대로 이해하시면 되고 보통 전공자 기준으로 컴퓨팅 구조 시간에 자세히 배울 내용들이라 자세한 내용은 생략하도록 하겠습니다.

 

컴퓨터 구조 (간략)

 

우선 간략하게 CPU 와 Memory(==Dram, 우리가 말하는 초록색 시금치, 즉 램입니다) 가 상호작용 하는 모습을 먼저 간단히 알아봅시다. 위 사진에서 먼저 주목할 부분은 저기 CPU와 Memory 사이에 연결된 빨간색과 파란색 네모 박스로 쳐진 부분 입니다.

 

일단은 저 화살표는 CPU 기준에서, 오른쪽 초록색 부분의 화살표는 CPU가 Memory로 Write(쓰기) 할 수 있다는 뜻이고, 왼쪽 파란색 네모 박스 부분의 화살표는 CPU가 Memory 에서 Read(읽기) 할 수 있다는 뜻 입니다.

 

 

1-A

우선 CPU는 Memory에게 특정 주소(Address) 를 제공해서 그 주소에 있는 데이터(Data)를 읽거나 쓸 수 있습니다. CPU는 램에 일방적으로 주소를 제공해서, 데이터를 읽거나 쓰기가 가능합니다. 그렇기에 화살표 방향이 주소는 CPU->Memory 쪽으로 단방향으로 전달되는 그림이고, 데이터는 주소를 제공해서 CPU가 양방향으로 읽기/쓰기가 가능하기 때문에 양방향 화살표로 표현됩니다.

 

쉽게 말해서 CPU는 RAM에다가 주소를 줘서 그곳의 데이터를 읽어올 수도 있고 쓸 수도 있습니다.

CPU : 램아~ 내가 "0x0000" 번지 주소 데이터를 읽어오고 싶은데 데이터좀 읽어올 수 있게 해주라!

 

2-A

이제 Instructions 라는 부분은 명령어라는 뜻 입니다. 우리가 컴퓨터에서 *.exe 실행 파일을 실행하면 OS가 그 프로그램을 메모리(Dram) 상으로 로딩할 것이고, CPU는 메모리 상에 올라온 프로그램의 명령어(Instruction) 를 실행하게 될 것 입니다. 단 화살표를 보다 싶이 CPU는 Memory에서 명령어를 읽을 수만 있지, Memory에 명령어를 저장하는것은 불가합니다.

 

 

이제 CPU안에 뭐가 들어가 있는지 CPU안의 내용물들을 한번 봐봅시다.

 

1. Registers

레지스터는 CPU가 사용할 데이터를 임시적으로 저장하는 내부 메모리 공간 입니다. 예를 들어 CPU가 계산을 할 때 1+2*3을 계산한다고 치면 보통 2*3을 먼저 계산하고, 그 결과값(중간값)인 6을 임시적으로 저장한다음에 최종적으로 1 + 6을 수행해야 하는데 이 6을 임시적으로 저장할 때 레지스터를 활용합니다.

레지스터는 용량이 아주 작고 필요로 하는 몇 가지만 존재 합니다. (아마 디지털 논리회로 과목을 수강하셨다면 Flip-Flop 을 이용해서 직접 CPU 레지스터를 회로로 구현해보는 걸 배우셨을겁니다.)

 

C언어를 기준으로 설명드리면 레지스터는 CPU가 직접 내부에서 사용하는 변수(저장공간)다! 라고 이해할 수 있겠습니다.

 

2. EIP

레지스터엔 실제로 이름이 있는데 보통 E + 알파벳 2글자로 구성됩니다. ex) EAX, EBX, ECX 등등 (E로 시작하면 레지스터 이름이라고 보시면 됩니다.) EIP(Extended Instruction Pointer) 는 레지스터 중 하나로 아주 중요한 녀석인데, CPU가 다음번에 해야 할 일이 무엇인지 기억하는 녀석입니다. 다음 실행할 명령어의 주소를 저장하고 있게 됩니다.

현재 명령어의 실행이 완료된다면, CPU는 EIP 레지스터에 무슨 값이 저장되어 있는지 보고, 그 주소로 따라가서 다음 명령어를 실행하게 됩니다.

 

3. Condition Code Register

최근 수행한 산수연산(사칙연산)에 대한 정보를 저장하는 특별한 레지스터 입니다. 조건 분기(Conditional Branch) 에 사용되는데 나중에 JMP 명령어 같은걸 배우게 되면 그때 아시게 될 겁니다.

 

그리고 메모리(Dram) 에는 우리의 소스 코드, 전역 변수, OS 데이터 등이 저장되게 됩니다.

 

C 코드가 실행 파일(Binary) 로 바뀌는 과정

//main.c
#include <stdio.h>

int main()
{
    printf("Hello World!\n");
    return 0;
}

다음은 C언어로 방금 제가 작성해 온 "Hello World"를 모니터에 출력하는 코드입니다.

 

컴퓨터는 기본적으로 이런 문자로 이루어진 C코드를 이해하지 못하고 0,1 의 이진수(기계어)만 이해할 수 있기 때문에, 실행을 위해선 저 C코드를 컴퓨터가 이해하고 실행할 수 있도록 기계어로 번역해줘야 합니다.

 

C언어 아주 초반부에 배우겠지만 이런건 컴파일러라는 프로그램이 알아서 해주죠? 코드를 기계어로 바꿔주는 통역사입니다. (물론 엄밀히 따지면 특정 프로그래밍 언어를 다른 프로그래밍 언어로 번역하는 프로그램이 컴파일러지만, 여기선 고수준 언어를 저수준 언어[기계어]로 번역하는 컴파일러를 통칭하도록 합니다.)

 

gcc main.c -o main.exe

gcc 컴파일러를 이용해 방금 작성한 코드를 컴파일 해줍니다.

 

같은 경로에 main.exe라는 파일이 생성되었고, 실행해보니 정상적으로 Hello World! 라는 내용이 출력되는걸 볼 수 있습니다. gcc 컴파일러가 우리 C코드를 기계가 이해할 수 있도록 번역해줬기 때문에 이렇게 잘 나오게 된 것이죠.

 

보통은 C언어로 코드 잘 적고, 나머지는 이미 똑똑한 사람들이 만들어둔 컴파일러 실행해서 컴파일 하면 된다~ 라고 배우죠. 실제로 틀린 말은 아닙니다 ㅋㅋㅋ 사실 처음엔 C언어 자체도 어려운데 컴파일 과정은 더더욱 이해가 안갈겁니다. 물론 이 글 읽으러 오신 분들은 C언어 정도는 기본적으로 대부분은 하실줄 아실거라 믿고 컴파일 과정을 조금 살펴볼겁니다.

 

그래야 어셈블리어를 이해할 수 있거든요.

gcc가 어떻게 C코드를 기계어로 바꾸는지 알아봅시다!

 

아까전엔 예시가 main.c 파일 하나였지만 여기선 p1.c , p2.c 를 컴파일해서 p라는 실행 파일 하나를 만든다고 가정해봅시다. 실행한 명령어는 gcc p1.c p2.c -o p 입니다.

 

1. 우선 GCC 컴파일러는 우리가 문자로 짠 코드를 읽어 보겠죠. 우리가 짠 코드는 text가 됩니다.

2. 우리의 눈에 보이지 않지만, gcc는 자동적으로 -s 옵션을 붙여서 우리가 짠 코드를 중간에 어셈블리어로 변환합니다. (어셈블리 코드)

3. 방금전에 변환한 어셈블리 코드를 바이너리(.o)로 바꿉니다.

4. 중간에 링커가 개입하여, 외부 라이브러리 함수와 우리의 실행 코드(.o) 파일을 합쳐줍니다. 즉 링커는 외부 라이브러리를 우리의 실행 파일로 가지고 들어옵니다. 예를 들어서 p1.c 에서 <stdio.h> 를 include 하고, printf() 함수를 호출했다고 가정하면 이 printf 역시 stdio.h 에 있는 외부의 표준 C라이브러리 함수이기 때문에 링커가 이 함수 내용을 가지고 들어와야 합니다.

5. 최종적으로 실행가능한 프로그램, 즉 실행파일이 만들어집니다. (p 라는 실행파일)

 

=> 여기서 주목할 것은 2번째 동작인데 gcc는 내부적으로 우리의 C언어 코드를 1차적으로 어셈블리어로 바꾸고 있다는 겁니다. 그리고 그 어셈블리 코드는 또 어셈블러란 녀석이 최종적으로 기계어로 바꾸고 있습니다. 그러면 이제 드디어 어셈블리어가 뭔지 알아봅시다!

 

어셈블리어(Assembly Language)

 

 

드디어 등장한 메인 토픽입니다. 어셈리어란 우리가 컴퓨터에서 프로그래밍 할 때 사용할 수 있는 가장 저수준(Low-Level) 언어 중 하나로 기계와 가까운 수준의 프로그래밍을 하는 것 입니다. 저수준 언어는 2종류 밖에 없는데 기계어와 어셈블리어 입니다. 어셈블리어로 프로그래밍 한다는 것은 한마디로 CPU에서 제공하는 명령어를 기반으로 하여 프로그램을 짜는 것을 의미하게 됩니다.

 

기본적으로 어셈블리어로 프로그래밍 하는걸 '어셈블리 프로그래밍' 한다고 표현하며, 기본적으로 어셈블리어로 만든 프로그램을 실행 파일로 만들고 싶다면 (실행 가능한 기계어 코드로 만들고 싶다면) '어셈블러' 라는 일종의 컴파일러가 또 필요하게 됩니다. 이를 이용해 어셈블러를 기계어로 번역해 빌드하면 실행 파일이 생성되게 됩니다.

 

아직은 잘 와닿지 않을 터이니 예제를 몇가지 봐보겠습니다. 우선 아까도 보셨지만 C언어를 기계어로 컴파일 하기 위해선 중간에 gcc 컴파일러가 자동적으로 -s 옵션을 붙여서 C언어 코드를 어셈블리 코드로 바꾸게 됩니다. 

 

*Machine Code = 기계어

물론 컴파일 할 때 아무 옵션 없이 줘서 그렇지 우리가 명시적으로 c코드를 -S 옵션을 줘서 컴파일 하면 C 코드가 어떻게 어셈블리 코드로 변환됐는지 확인해볼 수 있습니다.

 

예를 들어서 저 sum 으로 합을 구하는 코드를 gcc -s code.c 로 컴파일 해주면 GCC가 C코드를 어떻게 어셈블리 코드로 바꾸는지 확인해볼 수 있습니다. 왼쪽의 C언어로 작성된 sum 코드는 실제로 오른쪽에 보이는 어셈블리 코드로 변환되게 됩니다. 이때 code.c에 대한 어셈블리 코드 파일인 code.s 파일이 생성되게 됩니다.

 

여기서 어셈블리어 한 줄 한 줄을 CPU의 명령어, 즉 instruction 이라고 합니다.

예를 들어서 저기 movl 이라는 명령어는 메모리나 레지스터의 값을 옮기는 (복사해 덮어쓰기 하는) 명령어 입니다. 물론 어셈블리어를 아직 모르면 외계어에 불과할꺼니 저 한줄 한줄을 명령어라고 한다구나~ 정도만 이해하면 됩니다.

 

우선 컴퓨터가 이해할 수 없는 C언어 코드를 기계어와 C언어의 중간 단계 언어인 어셈블리어로 바꾸는데 까진 성공했습니다만, 여전히 CPU는 실제로 이런 문자 기반의 명령어를 이해할 수 없습니다. CPU는 실제로 각 명령어에 대한 '숫자 코드' 만 이해할 수 있습니다. CPU에 실제로 명령을 내리려면 0001010101010111.. 처럼 이진수의 나열로 입력을 해야지 레지스터의 값을 저장하거나, 메모리의 값을 읽거나 할 수 있습니다. 

 

즉 컴퓨터가 이해할 수 있도록, 어셈블리어를 컴퓨터가 유일하게 이해할 수 있는 기계어(0,1)로 변환해줘야 합니다.

이 역할을 바로 아까도 언급한 어셈블러가 하게 됩니다.

 

어셈블리어는 기계어와 기본적으로 1:1로 대응되어 있습니다. 기계어 한 줄이, 어셈블리 명령어 한줄로 대응됩니다. 이러한 특징 때문에 어셈블리어를 기계어로 변환하는 어셈블러를 만드는게, 컴파일러를 만드는것보다 상대적으로 쉽습니다. 이런 특징때문에 기계어를 어셈블리어로 변환하는 것 또한 자유로운 편입니다. (어셈블러 <-> 기계어 양방향 전환이 상대적으로 자유롭다는 뜻입니다.)

 

또 역으로 생각하면 기계어, 이진수의 나열로 CPU를 조작하는게 어려우니깐 만들어진게 어셈블리어라고 이해할 수 있습니다. CPU를 조작하려고 0010011010와 같이 전기적 신호(0V나 5V와 같은 전압으로) 를 줘서 조작하는게 아니라 , 인간이 이해할 수 있는 문자로 CPU를 조작하고자 어셈블리어와 어셈블러를 만든것이죠.

 

어셈블리어 특징

간략하게 나마 어셈블리어가 데이터를 어떻게 다루는지도 알아보겠습니다. C언어에서 '자료형(Data Type)' 이라는 

부분을 배우는데 이것과 비슷한 느낌이라고 보시면 됩니다.

 

Integer Data

우선 어셈블리어에서 정수형(int) 값은 1,2, 또는 4바이트로 표현될 수 있으며, 특정한 데이터 값(1, 2, 3) 이 될 수도 있고 주소가 될 수도 있습니다.

 

Floating Point Data

또한 실수형(소수) 데이터도 존재합니다. 실수형 데이터는 4,  8, 또는 10바이트로 표현됩니다.

 

Primitive Operations

어셈블리어에서는 기본적으로 사칙 연산이 가능하며, 메모리(DRAM), 레지스터 간 데이터 이동이 가능합니다.

*여기서 이동이란건 어떤 위치에 있는 데이터를 복사해서 다른 곳으로 붙여넣기 하는 느낌이라고 보시면 됩니다.

 

 

즉 어셈블리어를 사용하면 명령어를 통해 CPU안의 레지스터- 레지스터간 데이터를 이동시킬 수도 있고 레지스터 - 메모리 간 데이터를 이동시킬 수도 있습니다. 단 딱 한가지 안되는게 있는데 메모리 - 메모리 간 이동입니다. 그러니깐 DRAM 에서 A라는 위치에 저장된 데이터를 B라는 위치에 바로 덮어씌우는게 안된다는 겁니다.

 

이것은 인텔에서 메모리 - 메모리간 데이터를 이동시키는 명령어를 만들지 않아서 안됩니다. 만약에 이런걸 하고 싶으면 우선 DRAM에서 데이터를 CPU 레지스터로 저장하고, 다시 CPU 레지스터에서 메모리 위치로 데이터를 써서 데이터를 옮겨야 합니다.

 

 

Transfer Control

어셈블리어엔 jmp 와 cmp 명령어 같은것이 있는데 이것들을 이용하면 흔히 C언어에서 보던 'if 문' 의 동작을 비슷하게 수행할 수 있습니다. Conditional Branch 라고 하는데 나중에 다루도록 하겠습니다.

 

기계어 코드

아까전 SUM을 어셈블리어 코드로 변환한 code.s 파일을 다시 어셈블러를 통해 기계어로 변환하면 다음과 같은 모습이 됩니다. 어셈블리어에 해당하는 내용이 CPU가 이해할 수 있는 고유 숫자 코드(기계어)로 변환되었습니다. 총 13바이트로 변환되었고, 아까 작성한 각각의 명령어는 여기서 1, 2 또는 3바이트 숫자로 변환되게 됩니다.

 

우선 아까전의 명령어(instruction) 에 대해 변환된 0x55, 0x89 와 같이 고유 번호로 변환된 이 숫자들을 '바이너리 인코딩' 이라고 부릅니다. 또 저기 sum 함수에 대해서 왼쪽에 0x401040 라는 주소가 보이는데 만약에 sum이 실행되면 메모리의 0x401040 위치에 해당 명령어를 올리게 되어 실행합니다.

 

* 목적 코드 (Object Code) : 기계어로 번역된 파일

 

 

기계어 코드 (Cont'd)

기본적으로 바이너리 코드는 메모장 같은 텍스트 에디터로 읽을 수 없습니다. 아마 *.exe 같은 실행 파일을 메모장이나 텍스트 에디터로 열어보면 문자가 전부 깨져서 표시되는걸 경험해본적이 있으실 겁니다. 이진 파일을 열고 싶으면, HEX 에디터 같은걸 써서 여시거나, 올리디버거 같은걸로 열어서 기계어를 어셈블리어로 복구해야 합니다.

 

어셈블리어 변환 예시 

만약에 int t = x + y; 와 같은 C언어 코드를 어셈블리어로 바꾼다면 addl 8(%ebp), %eax 라는 코드로 변환해낼 수 있습니다. 이건 어셈블리어 개요 글이지 어셈블리어 강의 글이 아니므로 이건 그냥 설명없이 넘어가도록 하겠습니다. 아까도 말했듯이 C언어가 어셈블리어로 바뀌고, 또 맨 아래에 03 45 08 과 같이 3바이트의 기계어로 바뀌었다는 거 정도만 확인하고 넘어갑니다.

 

어셈블리어가 활용될 수 있는 곳

사실 우리가 어셈블리어로 실제 코딩을 해볼일은 거~의 없습니다. 기본적으로 C언어로 코딩한다음 컴파일러가 최적화 과정을 거쳐서 생성한 어셈블리 코드가 우리가 직접 X86 어셈블리어를 열심히 공부해서 작성한 어셈블리 코드보다 성능이 더 좋을 가능성이 큽니다. (요새 CPU는 싱글코어가 아닌 멀티코어 CPU기 때문에 이런것도 고려하면서 작성해야 하니깐요.)

 

또, 옛시대의 단순한 CPU는 어셈블리어를 사용해서 칩의 퍼포먼스를 그럭저럭 끌어낼 수 있었으나 현대 32비트 컴퓨터부터 시작하여 여러 워크스테이션 용 CPU가 등장함에 따라 파이프 라인 기법, 슈퍼스칼라 구조, 캐시 등 온갖 속도 향상 기법들이 도입되여 사람이 그 성능을 끌어내는게 불가능에 가까워졌습니다.

 

즉, 컴파일러보다 최적화를 잘 할 자신이 없으면 어셈블리어로 직접 코딩하는것보다 C나 C++로 코딩하는게 생산성도 훨씬 좋고, 코드 가독성도 좋고 성능도 좋다는 뜻입니다 -.-.

 

또 앞에서 설명드리진 않았지만 기본적으로 CPU 계열사 별로 기계어 명령이 다 다르기 때문에 기계어에 1:1 로 대응될 수 있는 어셈블리어 명령어 역시 통일된 규격이 없습니다. (기계어가 다르니 당연히 이와 1:1 매칭되는 어셈블리어도 다름.)  문법이 아키텍쳐 별로 다 다르고, 어셈블러의 종류에 따라서도 문법 / 매크로 등이 다 다릅니다.

그래서 어셈블리어 마스터가 어려운 겁니다. 

 

그러나 C언어의 경우 대부분 똑똑한 사람들이 이미 CPU 별로 컴파일러를 다 만들어놔서, C언어로 코드를 작성한 다음에 맥이라면 맥의 C언어 컴파일러, 리눅스라면 리눅스의 C언어 컴파일러 등을 설치해서 컴파일하여 각 CPU에 맞게 실행할 수 있는 기계어를 생성하면 끝이죠.

 

??? : 아니 어셈블리어가 활용되는 곳이라고 제목 달아놓고 왜 C언어 장점 소개하고있어요?

 

ㅎㅎ 진정하시구요. 앞부분은 떡밥이였고 이제 어셈블리어가 활용될 수 있는 곳에 대해 소개해드립니다.

 

1. 컴파일러가 최적화 할 수 없는 부분을 사람이 부분적으로 최적화 (인라인 어셈블리)

컴파일러가 최적화를 아무리 잘한다고 해도 인간 만큼 최적화를 못하는 부분이 분명 존재합니다. 리얼 타임 시스템의 타이밍을 나노초 이하의 정밀도로 맞춘다거나 하는 그런 영역이요. 컴파일러 박사학위까지 따신 분들도 이런 맹점을 알고 계셔서 컴파일러가 최적화 할 수 없는 부분은  C언어(고급언어) 코드 사이에 어셈블리 코드를 끼워넣을 수 있는 "인라인 어셈블리" 라는 기능을 통해 최적화 한다고 합니다. 물론 어셈블리 코드를 삽입하는거라 성능을 위해 가독성, 호환성을 포기하는 형태가 되겠지만요. 일단은 제가 학부 따리라서 요론 고정밀의 레벨까진 상세하게 설명을 드리긴 힘들거 같군요.

 

 

2. IoT 같은 임베디드 기기

기본적으로 IoT 같은 소형 기기는 CPU, RAM의 크기가 극히 제한되어 있어서 오히려 C언어로 컴파일 하는게 독이될 수도 있습니다. 그리고 또 IoT 기기의 CPU 칩 제조사에서 C언어 컴파일러 같은걸 제공하지 않고, 어셈블러만 제공하는 경우엔 요 어셈블리어로 열심히 코드를 작성해야될 겁니다. 근데 상식적으로 칩 팔 생각이 있으면 C언어 컴파일러는 대부분 준다고 합니다.

 

3. 디스어셈블리(보안)

 

개인적으로 생각했을때 어셈블리어를 배우는 의미 있는 이유가 될 수 있는게 이 보안 분야의 디스어셈블리 입니다. 좋게 말하면 보안이고 나쁘게 말하면 크래킹이죠. 

 

기본적으로 컴퓨터는 기계어(0,1) 밖에 못하고 결국에는 어떤 언어가 되었든 기계어로 번역이 되어야 합니다.

그럼 어떤 프로그램이던 간에 대부분 기계어 (바이너리 파일) 일 것이고, 이걸 열면 그 프로그램이 어떤 기계어로 실행되는지 알 수 있을겁니다.

 

예를 들어서 실행파일(*.exe) 을 열었더니 다음과 같은 기계어가 나왔다고 가정해봅시다.

001000 00001 00000 0000000000001010

이걸 보고선 사람은 아! 이 프로그램을 키면 CPU가 저 001000... 의 기계어를 실행하겠구나 정도는 알 수 있습니다.

문제는 저게 어떤 기계어인지, CPU가 어떤 연산을 수행하는지는 CPU 스펙시트 같은걸 하나하나 찾아봐야겠죠.

 

우선 $2^4 = 16, 2^3 = 8$ 인 수학적 특징을 이용해 16진수의 경우 2진수 4글자를 16진수 1글자로 축약해서 읽을 수 있고, 2진수 3글자를 8진수 1글자로 축약해서 읽을 수 있습니다. (수학적인 이유도 있고 이렇게 하기로 약속했다고 이해하시면 됩니다.)

 

20 20 00 00 00 00 00 0A

2진수 4글자를 16진수 1글자로 묶어서 읽으니 16진수로 묶어서 읽은 기계어가 그냥 2진수로 본 기계어보다는 글자수를 줄여서 조금더 간략하게 볼 수 있게 되었습니다.

 

(참고로 16진수 1글자는 2진수 4자리수를 축약했으므로 , 16진수 1글자는 4비트가 되고, 2글자는 8비트, 즉 1바이트가 됩니다. 16진수 2글자가 1바이트란거 외워두시면 좋으니 참고바랍니다.)

 

아까도 말했듯이 기계어 파일은 텍스트 에디터 같은걸론 읽을 수 없고 HEX에디터 같은걸로 볼 수 있다고 하였는데 영어 이름에서도 유추해볼 수 있듯이 HEX는 16진수를 의미하는것이고 (hexadecimal) HEX 에디터라는 프로그램이 이진수 파일을 위처럼 16진수 코드로 보여주는 프로그램입니다.

 

하지만 이진수 기계어를 간략하게 볼 수 있다는 특징이 생겼을 뿐 그냥 우리가 보기엔 아직도 외계어일 뿐입니다. 인간의 언어와는 전혀 닮아있지 않죠. 그런데, 아까도 말씀드렸듯이 어셈블리어는 기계어와 1:1 대응 할 수 있는 언어입니다. 그래서 기계어 ↔ 어셈블리어 양방향 전환이 자유롭다고 하였습니다. 그러면 지금까지는 어셈블리어로 작성된걸 기계어로 바꾸는 작업만 보았지만 (어셈블러), 이제 기계어를 역으로 어셈블리어로 다시 복구한다면? 

 

addi $0, $1, 10

대충 이런 모습이 됩니다. 이제 알 수 있겠네요. 저 기계어는 1번 레지스터에 있는 값에 10을 더해서 0번 레지스터에 저장하는 명령어라는걸요. 사실 자세히는 몰라도 일단 addi 라는 영어 단어를 봤을때 무언가를 더하는 명령을 수행하고 있다는건 확실히 알 수 있게 됩니다.

 

이렇게 기계어를 다시 어셈블리어로 변환하는 과정을 역어셈블, 또는 디스어셈블리 라고 부릅니다. 또 디스어셈블리를 수행하는 프로그램을 디스어셈블러라고 부릅니다.

 

즉 프로그램을 크래킹한다고 했을때, 보통 그 프로그램에 대한 소스코드는 없고 그냥 exe 파일만 가지고 있을건데, 그 exe 파일을 hex 에디터 같은걸로 열면 어떤 기계어로 실행하는지 보일거고 이 기계어를 조금이라도 알아볼 수 있게 어셈블리어로 복구, 즉 디스어셈블리 과정을 거치면 그 프로그램의 실행코드를 한줄 한줄 분석하면서 프로그램의 패스워드 검사(if문)를 코드를 삽입해 건너뛴다던거 해서 우회할 수 있겠죠.

 

요론 디어셈블러로 유명한게 보통 IDA 나 올리디버거 입니다. 그리고 프로그램을 뜯어서 디스어셈블리 하여 프로그램을 크래킹 하거나 패칭하는 과정을 보통 리버싱 한다고 표현합니다.

 

모든 프로그래밍 과정에서, 종착지는 실행 파일을 만드는 것, 즉 기계어기 때문에 이렇게 자기가 만든 프로그램이 리버싱 당해서 크래킹 당하는걸 사실상 막기가 힘듭니다. 물론 더미다 같은 프로그램을 이용해 난독화, 코드를 매우 읽기 힘들게 만든다던가 패킹을 해서 코드를 통째로 압축하는 보안 솔루션이 있긴한데 뛰어난 리버서한테는 내 프로그램의 보안이 종국에는 박살날겁니다 ㅠㅠ

 

 

 

예를 들어서 아까 C언어로 작성한 sum 함수를 컴파일 과정을 거쳐서 최종적으로 바뀐 기계어를 디스어셈블리 과정을 거쳐서 어셈블리어로 복구하면 오른쪽과 같은 모습이 됩니다.

 

C언어도 어려운데 이렇게 디스어셈블리해서 프로그램의 어셈블리 코드를 분석하는건 매~우 어렵고 힘든 일이며 엄청난 노가다가 동반됩니다. 컴퓨터에 대한 전반적인 지식도 탄탄해야 그나마 읽기라도 가능한 레벨입니다.

 

저도 사실 리버싱 공부를 위해 어셈블리어를 배우고 있는 것이며, 사실 초등학교때 제일 해보고 싶었던것도 이해하기 어려웠던것도 리버싱입니다 ㅋㅋ.. 뭐 지금도 잘 이해되는건 아닌데 어느정도 보는 눈은 생긴거 같습니다. abex crackme 1번은 이제 품

 

앞에서 설명을 계속 드려서 대충은 감이 잡히셨을거지만, 추가로 정보를 드리자면 16진수로 본 기계어나 어셈블리어 코드나 같은겁니다. 어셈블리어 코드는 16진수 코드를 단순히 우리가 보기 쉬운 숫자로 치환했을 뿐입니다. 16진수로 0XAA 라는 코드가 있으면 여기에 PUSH라는 이름을 붙여서 사람이 이해할 수 있고 읽기 편하게 바꾼거 뿐입니다. 16진수 코드와 어셈블리어 코드는 같은것 표현하는데 숫자냐, 문자냐 인 것이죠.

 

 

 

 

 

출처(참고문헌)

Randal E. Bryant , “Computer Systems: A Programmer's Perspective”, Prentice Hall

https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=mumasa&logNo=221049608979 

https://chlalgud8505.tistory.com/8

https://who-is.tistory.com/30

https://yum-history.tistory.com/212

https://namu.wiki/w/%EC%96%B4%EC%85%88%EB%B8%94%EB%A6%AC%EC%96%B4

https://dakuo.tistory.com/37

https://blog.naver.com/suljang2/140195588772

https://freesugar.tistory.com/74

SNS 공유하기
네이버밴드
카카오톡
페이스북
X(트위터)

최근글
인기글
이모티콘창 닫기
울음
안녕
감사
당황
피폐