x86 어셈블리어(Nasm)

어셈블리어는... 프로그래밍 계의 끝판왕입니다.

 

어려운 순서로는 기계어에 이어서 Top 2위인데,

요즘 시대에 메뉴얼로 0과1로 기계어를

작성하는 경우는 Intel 같은 CPU제조사를

제외하고 거의 희박하므로 실질적으로

프로그래밍 언어 난이도 1위입니다.

 

해킹 등 리버스 엔지니어링 때문에

어셈블리어에 관심이 있는 학생들이

있을 수 있으나 그런 특수한 케이스를

제외하고는 사실상 이 언어를 배우고자

하는 사람이 거의 없다고 봐야할 겁니다.

 

IT본고장인 미국에서도 어셈블리어를

다루는 사람은 프로그래밍 언어의 컴파일러 제작을

할 정도의 벨연구소의 연구원이나, 운영체제를

개발하는 버클리대의 박사 정도의 레벨에서

사용하는 언어로 알려져 있습니다.

 

물론 누구나 어셈블리어를 배우고 사용가능하죠.

그게 아주 어렵거나 그렇지는 않습니다.

 

하지만 어셈블리어로 세상에 의미있는

결과물을 산출할 수 있는 사람들은

거의 극소수라는 것은 중요한 사실이지요.

 

천재나 엘리트의 영역에 잘 어울리는

언어가 바로 어셈블리어입니다.

 

어셈블리어 다음으로 저수준 언어는 C언어로

C언어로도 의미있는 산출물을 만드는 것은

최고 고수들에게 가능한 일입니다.

리눅스 커널을 개발한 리누즈 토발즈는

지금도 C언어로 개발을 하고 있습니다.

 

그는 인터뷰에서 C언어 코드를 보면 하드웨어가

어떻게 동작하는지 알 수 있다고 합니다.

이는 C언어 코드만 봐도 어셈블리어나 기계어의

동작 과정을 머리속에서 재현할 수 있다는 말입니다.

컴퓨터에서 프로그램을 실행하지 않아도

결과를 알 수 있는 수준에 도달한 것이지요.

이런 사람을 동양에서는 '도사, 장인' 등으로 부릅니다.

 

어셈블리어가 중요한 이유는 기계어와

1대1로 대응하는 니모닉 표현이기 때문입니다.

 

어셈블리어를 배우면 CPU, 하드웨어, 운영체제,

고수준 언어 등 컴퓨터에 대한 거의 모든 것을

이해할 수 있다고 해도 과언이 아닙니다.

 

 

파이썬이나 자바스크립트, C언어를 배우면서

이해가 가지 않았던 수많은 질문들에 대한

답을 할 수 있게 합니다. 어셈블리어의 문제점은

너무 난해한 것 처럼 보여서 대부분의

고등교육기관(대학 등)에서 잘 안가르쳐준다-

는 점입니다. 다른 화려한 언어가 많은 시대에

인기가 없는 분야라서 어셈블리어를 가르칠 수

있는 강사도 많지 않아 보입니다.

 

왜냐하면 인기가 없어서 가르치고 싶지 않습니다.

 

요즘은 자바스크립트 같은 프론트 엔드

동적 타이핑 언어나 백엔드와 범용 언어인

자바, C# 등이 각광을 받습니다.

 

이 언어들만 강의를 잘해도 인정을 받는데

비인기 종목인 어셈블리어를 할 필요가 없지요.

 

해서 필요한 사람만 한다고 하는 어셈블리어에

대해서 약간 알아보겠습니다.

 

Hello World

일단 어셈블리어는 Hello World를 출력하는 것

부터가 난관입니다. CPU와 운영체제에 따라

소스코드가 달라지기 때문에 현재 사용중인

시스템을 잘 파악할 필요가 있습니다.

 

여기서는 윈도우11의 WSL 우분투 리눅스 환경에서

실행하도록 하겠습니다. WSL라고 하지만

우분투와 같은 리눅스 커널을 사용합니다.

 

우분투에서 uname -a로 현재 시스템을

체크합니다.  x86_64 GNU / Linux가 나오면

x86 아키텍처의 64비트 GNU Linux

환경이라는 뜻입니다. x86은 최근 시중에

판매중인 인텔이나 AMD CPU 아키텍처입니다.

 

$uname -a


Linux KAY-MAIN-WIN11 5.10.60.1-microsoft-standard-WSL2 #1 SMP Wed Aug 25 23:20:18 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

 

CPU에 따라 기계어가 달라지기 때문에

어셈블리어는 자바같은 호환성이 없습니다.

이것을 아는게 중요한데 자바나 C#같이

가상머신 런타임을 사용하는 언어는

소스코드를 하나를 쓰면 대충 다른 CPU나

운영체제에서 돌아갑니다. 아주 100% 잘

돌아가지는 않지만 일단은 작동하게 되있습니다.

 

하지만 CPU의 인스트럭션을 직접 사용하는

어셈블리어는 처음부터 돌아가지 않습니다.

이것을 하드웨어 의존적이라고 합니다.

CPU가 x86이냐 ARM이냐에 따라 달라지고

또 OS에 따라 세부적인 차이가 있습니다.

OS가 윈도우냐 리눅스에 따라 CPU의 명령어 집합은

달라지지 않지만 사용방법은 달라지기 때문입니다.

 

빌드 과정

 

어셈블리어의 빌드과정도 C언어의 그것과

큰 차이가 없습니다.

 

1. 소스코드의 작성

2. 오브젝트 코드로 컨파일

- 기계어로 바꾼다

3. 링킹하여 실행 파일 생성

 

예제 코드

예제 코드를 알아보겠습니다.

어셈블리어는 고수준 언어와는 달리

코드를 모르면 이해가 안됩니다.

또 이 코드를 이해하려면 컴퓨터 구조에

대해서 모르면 알 수가 없습니다.

그래서 대부분 x86 어셈블리의 교재들은

코드에 대한 설명보다 컴퓨터 구조, 주로

CPU와 메모리에 대한 설명을 먼저 합니다.

이 포스팅에서는 두꺼운 책의 해설을 하는 것은

어렵기 때문에 튜토리얼(따라하는것) 을 하면서

간략하게 설명하도록 하겠습니다.

 

다음 코드는 리눅스에서 Nasm 컴파일러를

사용하고 Hello World 를 출력합니다.

 

;nasm comments
global _start ;entry symbol

section .data
    text1: db '>> Hello World, Nasm!',10 ;10 is a escape sequence
    text1_L: equ $-text1

section .text
_start:
    mov rax, 1 ;syscall number 1 -> write
    mov rdi, 1 ;file descriptor stdout
    mov rsi, text1 ;string address
    mov rdx, text1_L ;string length in bytes
    syscall ;system call

    mov rax, 60  ;syscall number 60 -> exit
    mov rdi, 0 ;set exit code 0;
    syscall

실행결과는 아래와 같습니다.

>> Hello World, Nasm!

컴파일 명령어가 기니까 아래처럼

적당히 Makefile을 만들면 편합니다.

all:
	nasm -f elf64 hello.asm -o hello.o -l example.lst
	ld -o hello hello.o
	./hello

코드 해설

; 코멘트(주석) 입니다. 주석은 컴파일러가 무시합니다.

 

global _statrt

 

- entry symbol 진입 심볼로 여러개 파일을

컴파일 할 때 시작점입니다. 단일 파일은

_start 없이도 실행되지만 컴파일러가 경고를 출력합니다.

 

global 은 다른 모듈이 사용할 수 있는 지시자입니다. 

 

section .data

 

데이터 섹션입니다. 보통 고수준 언어에서

static 변수라고 부르는 프로그램 시작부터 끝까지

메모리에서 값을 유지하는 변수, 상수를

여기서 지정할 수 있습니다.

 

text1: db '>> Hello World, Nasm!',10

 

text1 변수 이름이고 db 는 define byte로

타입을 지정합니다. 문자의 배열이 text1의

메모리 주소에 순차적으로 들어갑니다.

마지막에 10은 고수준 언어에서 escape sequence라고

부르는 \n 뉴라인 아스키 코드입니다.

db 하고 나오는 문자들은 아스키 코드 테이블에서

대응하는 숫자값이 들어갑니다.

 

text1_L: equ $-text1

 

equ는 상수를 생성합니다. $는 text1_L의

메모리 주소입니다. 바로 전의 코드에서

주소를 빼면? 문자열의 길이가 나옵니다.(byte)

 

어셈블리어는 고수준 언어에서는 당연한 것도

직접 설정해줘야 합니다. C라면 printf 함수가

알아서 문자열의 끝을 만나면 출력을 멈추지만

어셈블리어는 시스템 콜(system call)을 원초적으로

사용하기 때문에 몇글자를 출력할지 알아야 합니다.

이 코드가 없으면 문자가 몇개인지 하나씩 세어야 합니다.

 

section .text
_start:

 

section .text는 코드입니다.

기계어는 0과 1로 채워져 있는데

메모리에 데이터와 코드(명령어)가 물리적으로

분리되지 않았습니다. 그래서 논리적으로

메모리 공간을 분할해서 사용하는 건데

그냥 랜덤하게 메모리를 보면 이게 코드인지

데이터인지 알수가 없습니다.

 

CPU의 레지스터 중에는 프로그램 카운터가

다음 명령어의 주소를 가지고 있습니다. (EIP, RIP)

section .text는 명령어 주소에 대응하는 코드입니다.

 

_start 는 위에서 설정한 엔트리 심볼이지요.

다른 모듈(파일)에서도 접근가능하고

컴파일러는 _start를 시작점으로 인식합니다.

 

    mov rax, 1 ;syscall number 1 -> write
    mov rdi, 1 ;file descriptor stdout
    mov rsi, text1 ;string address
    mov rdx, text1_L ;string length in bytes
    syscall ;system call

 

자, 이제 이 부분이 Hello World 출력방법인데요.

무슨 암호같이 쓰여져 있습니다.

이것은 먼저 CPU 레지스터를 알아야

이해할 수가 있습니다. 유튜브 등에 보면

설명을 잘해놓은 영상들이 있으니 CPU Register라고

검색해서 찾아보면 도움이 될 겁니다.

 

레지스터를 조금이라도 안다는 전제로 설명하면,

 

mov rax, 1

 

뜻은 1의 값을 rax 레지스터에 넣습니다.

시스템 콜(system call)을 하기 위한 밑작업인데요.

어셈블리어가 직접 하드웨어를 제어하는게

아니라 운영체제를 통해서 제어합니다.

모니터, 키보드 같은 장치는 어떤 프로그램 하나가

독점하는게 아니라 시스템이 관리하는 자원입니다.

이 자원들을 사용하려면 운영체제에 맞는

시스템 콜을 호출해야 합니다.

 

시스템콜은 리눅스 커널이 처리합니다.

 

시스템콜의 종류는 Linux System Call Table

로 찾으면 볼 수 있습니다. 이게 또 32비트와

64비트는 시스템콜이 다른데 여기서는

64비트 시스템콜을 사용했습니다.

 

mov rax, 1 은 sys_write 파일에 쓰기 작업입니다.

파일 -> 리눅스는 모든 장치나 스트림 등

파일로 취급합니다. 모니터에 Hello World 로

출력할 때는 스크린을 파일로 봅니다.

그래서 파일에 쓴다 -> 스크린에 출력한다

이렇게 이해하면 됩니다.

mov rdi, 1

 

rdi는 파일 디스크립터(file descriptor)로 1은

표준출력입니다. (Standard output) 지금

이 프로그램에서의 Standard output 이란

콘솔 스크린을 의미합니다.

 

mov rsi, text1

 

rsi는 포인터입니다. (const char *buf)

text1의 주소를 가리킵니다.

text1은 char 가 연속되어 저장되어 있음.

 

mov rdx, text1_L

 

rdx는 size_t count로 쓰는 양입니다.

여기서는 문자를 얼마나 출력할 것인가

문자열의 길이를 의미합니다.

 

이제 준비가 끝났으니 syscall 을 호출하면

Hello World를 출력할 것 입니다.

 

따로따로 해설하면 매우 길고 복잡한데

 

리눅스 시스템콜에서는

 

sys_write 를 호출하여 file descriptor 1인

standard output(콘솔)에 Hello World를

문자 개수만큼 출력하라.

 

는 뜻 입니다. 이것이 우리가 고수준 언어에서

Hello World를 출력한 원리입니다.

(물론 훨씬 고수준 언어의 print 계열 함수는

더 다양한 기능을 가집니다)

 

    mov rax, 60  ;syscall number 60 -> exit
    mov rdi, 0 ;set exit code 0;
    syscall

 

시스템콜을 호출한 후에는 그냥 두는게

아니라 종료해줘야 합니다.

60번이 종료하는 것이고 rdi에 에러코드를

넣습니다. rdi 를 0으로 설정하면 에러가

발생하지 않았다는 뜻 입니다.(return 0 처럼)

 

여기까지가 해석이고 다음 코드에서는

연속적으로 문자열을 출력해보겠습니다.

 

;nasm comments
global _start ;entry symbol

section .data
    text1: db '>> Hello World, Nasm!',10 ;10 is a escape sequence
    text1_L: equ $-text1
    text2: db '>> Nice Weather!', 10
    text2_L: equ $-text2
    text3: db '>> Done!', 10
    text3_L: equ $-text3

section .text
_start:
    mov rax, 1 ;syscall number 1 -> write
    mov rdi, 1 ;file descriptor stdout
    mov rsi, text1 ;string address
    mov rdx, text1_L ;string length in bytes
    syscall ;system call

    mov rax, 1 ;syscall number 1 -> write
    mov rdi, 1 ;file descriptor stdout
    mov rsi, text2 ;string address
    mov rdx, text2_L ;string length in bytes
    syscall ;system call

    mov rax, 1 ;syscall number 1 -> write
    mov rdi, 1 ;file descriptor stdout
    mov rsi, text3 ;string address
    mov rdx, text3_L ;string length in bytes
    syscall ;system call

    mov rax, 60  ;syscall number 60 -> exit
    mov rdi, 0 ;set exit code 0;
    syscall

 

[실행결과]

 

>> Hello World, Nasm!
>> Nice Weather!
>> Done!

 

서브루틴 Hello World

서브루틴으로 만들면 아래와 같습니다.

레이블을 사용해서 call 합니다.

ret 은 호출한 위치로 돌아갑니다.

section .data
    hello: db 'Hello World! NASM',10
    hello_L: equ $-hello

section .text
    global _start

_start:
    call _printHelloWorld
    call _printHelloWorld

    call _exitProgram

_exitProgram:
    mov rax, 60
    mov rdi, 0
    syscall

_printHelloWorld:
    mov rax, 1 ;syscall id 1 ->sys_write
    mov rdi, 1 ;file descriptor stdout
    mov rsi, hello ;hello world text
    mov rdx, hello_L ;byte length
    syscall
    ret ;return to where it called

 

 

요약

어셈블리어 Nasm 으로 Hello World를 출력해봤습니다.

 

보면 알겠지만 컴퓨터공학의 전문 지식이

없이는 거의 알아듣기 어렵습니다.

요새 코딩은 누구나 배울 수 있다는 컨셉으로

대중들에게 친숙해지고 있는데 그것은

언어가 고수준일 때 입니다. 예를 들어

수학 선생님들은 컴퓨터 공학을 배우지 않아도

웬만하면 코딩을 잘합니다. 고수준 언어에서는

수학의 알고리즘을 자유롭게 사용할 수

있기 때문인데요. 하지만 하드웨어 레벨로

내려갈수록 컴퓨터 자체에 대한 이해가 필요합니다.

 

어셈블리어는 컴퓨터 그 자체를 이해하기

위한 언어로써는 C언어 다음으로 좋습니다,

C언어에서 뭔가 부족함을 느낄 때

손댈 수 있는 분야라고 할까... 만족감이 있습니다.

다만 써먹을 수 있는 분야가 제한적이긴 합니다.

 

또 윈도우즈 시스템보다는 리눅스 계열의

사용자에게 더 이해가 쉽습니다.

위에서 본 것 처럼 시스템콜을 사용하니까

이게 C언어와 연결이 됩니다.

리눅스는 윈도우즈와 달리 오픈소스이고

시스템 관련한 대부분 설정을 텍스트파일에

저장하기 때문에 운영체제의 동작에 대해

이해가 빠를 수 밖에 없습니다.

 

오픈소스 시스템 프로그램이나 해킹, 보안에

관심이 있다면 어셈블리어가 필요할 겁니다.

대부분 PC가 x86 이니까 Nasm 부터

시작하는 것도 괜찮은 선택입니다.

 

 

https://syscalls64.paolostivanin.com/

 

Linux Syscall Reference

 

syscalls64.paolostivanin.com

https://www.nasm.us/xdoc/2.15.05/html/nasmdoc1.html

 

NASM - The Netwide Assembler

Chapter 1: Introduction 1.1 What Is NASM? The Netwide Assembler, NASM, is an 80x86 and x86-64 assembler designed for portability and modularity. It supports a range of object file formats, including Linux and *BSD a.out, ELF, Mach-O, 16-bit and 32-bit .obj

www.nasm.us

 

공유하기

facebook twitter kakaoTalk kakaostory naver band