어셈블리어 시작하기 – 리눅스 어셈블러 – x86-64 NASM

어셈블리어 시작하기

어셈블리어는 기계어에 1대1로 대응하는 저수준(low level langugae) 언어입니다. 오늘날 대부분의 응용 프로그램은 자바(Java)나 파이썬(Python)같은 고수준(high level language) 언어로 개발해서 어셈블리어의 존재를 잘 모르는 사람도 있을 겁니다. 하지만 여전히 시스템 프로그래밍과 임베디드, IT보안 등 하드웨어 레벨을 다루는 분야에서 사용하고 있고, 근본 언어기 때문에 미래에도 쉽게 없어지지는 않을 겁니다.

대학의 컴퓨터공학부에서는 많지는 않지만 시스템 소프트웨어라는 과목의 수업에서 어셈블리어 기초를 아직도 가르치고 있습니다. 다만 어셈블리어는 아키텍쳐에 의존하기 때문에 write once run everywhere 가 먹히지 않습니다. 대학의 한 학기 강의로는 특정 어셈블리어 하나 정도를 커버할 수 있겠네요. 요즘은 쓸만한 언어들의 크로스 플랫폼에 익숙하다면 어셈블리어를 배운다는 것은 약간 더 시간 투자적으로 고려할 필요도 있습니다. (CPU 종류에 따라 달라지므로)

그렇다고 어셈블리어를 너무 가성비로 보는 것은 좋지 않다는 사람들도 있는데요. 고수준 언어를 더 잘 하기 위해서라도 밑바닥에서 머신 레벨의 방식을 학습해야 한다는 주장도 있습니다. 이러한 논쟁은 자바가 등장하고 모든 대세가 OOP로 쏠려있던 20여년 전부터 지금까지 이어져 오고 있습니다.

약간 구닥다리 느낌적으로 20년 전 어느날 C++과 자바의 초기 시대에 한 프로그래머가 ‘라떼는 말이야~ 어셈블리어로 밤새도록 쏼라쏼라~’ 라며 영웅담을 늘어놓고 있지 않았을까 생각이 듭니다. 지금은 IT사업으로 성공해 있을지도 모르겠네요. (가상의 캐릭터를 상상해 본 겁니다)

일단 어셈블리어는 컴퓨터 아키텍쳐 자체를 모르면 조금 시간이 더 걸리는 언어입니다. 고수준 언어들은 그 안의 로직이 그래도 인간적인데(OOP) 여기서는 레지스터 몇개와 메모리 주소, 스택 같은 지극히 추상적인 개념을 도구로 코드를 작성합니다. 컴퓨터는 흔히 0과1로 동작한다고 말합니다. 어셈블리어를 짜는 것은 CPU에다가 직접 0과1을 나열해서 코딩하는 것과 같습니다. 이렇다 보니 ‘누구나 코딩’ 이런 것은 좀 힘들지 않나 – 라고 볼 수 있습니다.

요즘은 심지어 초등학교 아이들에게도 코딩을 가르치는데 코딩에도 난이도가 있습니다. 누구나 할 수 있는 코딩(노인과 아이도 가능), 좀 배우면 누구나 할 수 있는 코딩(HTML/CSS – HTML is not a programming language?), 전문 교육기관에서 배우는 코딩(JavaScript, Java) 등 세분화가 되는데 이 모든 것을 ‘코딩’이라는 도매급으로 잡기엔 맞지 않는 부분이 있습니다.

어셈블리어는 컴퓨터를 직접 조작해야 하므로 언어로써 가장 어렵고 지루하긴 하지만 근본적인 원리를 이해할 수 있다는 점에서 고수준 언어의 사용자들에게도 도움이 됩니다. 또 운영체제와 커널의 원리에 대해서도 알 수 있습니다. 리눅스 유저는 얻을 수 있는게 많지 않을까 싶습니다.

어셈블리어를 배우는 이유?

이유가 여러가지가 있습니다. 어셈블리어로 GUI를 구현할 것은 아니겠지요.(가능은 하다)

  • 필요에 의해서(시스템/보안 엔지니어 등)
  • 고수준 언어를 더 잘 이해하기 위해서
    (C언어와 함께 배우면 시너지가 난다)
  • 하드웨어 레벨을 이해하기 위해서
  • 도전의식으로 (고난이도에 도전)
  • 순수한 컴퓨팅의 재미로(Nerd?)

NASM (Netwide Assembler)

넷와이드 어셈블러는 인텔 x86 아키텍쳐의 어셈블러 입니다. 64비트 윈도우 환경에서도 사용할 수 있지만 NASM은 리눅스가 자연스럽습니다. 윈도우에는 Visual Studio에서 C++에 사용할 수 있는 어셈블리어인 MASM이 있습니다. 어셈블리어는 CPU 아키텍쳐에 따라 명령어가 달라지는데 또 운영체제에 따라서 달라집니다. 그러니까 처음에 어떤 CPU와 운영체제에서 돌아가는 어셈블러를 사용할지 파악해야 합니다. 여기서는 x86-64 인텔 CPU와 우분투 리눅스(WSL2)에 NASM 어셈블러를 설치하겠습니다.

apt-get으로 nasm 을 설치합니다.

sudo apt-get install nasm

버전체크으로 설치가 되었는지 체크합니다.

nasm --version

Hello World

프로그래밍 전통에 따라 Hello World 를 콘솔에 출력해보겠습니다. 처음에 보면 이 몇 줄을 이해하는데만 하루 종일도 걸릴 수 있습니다. 문법적인 내용을 몰라서 그런 거니까 당황할 필요는 없습니다. C나 자바같이 고수준 언어와는 많이 달라 보입니다. 문법은 시간이 지나면 익숙해질 겁니다.

section .data
    msg: db "Hello World!",10
    len: equ $-msg
    
section .text
    global _start
_start:
    mov rax, 1 ;system call id 1
    mov rdi, 1 ;FD stdout
    mov rsi, msg ;bytes(ADDR) to write
    mov rdx, len ;byte length
    syscall

_last:
    mov rax, 60
    mov rdi, 0
    syscall

먼저 nasm 으로 오브젝트 코드(기계어)로 바꾼 후 ld 로 링크합니다. ./main으로 실행한 결과는 화면에 Hello World!를 출력합니다.

nasm -f elf64 -g main.asm -o main.o

ld -o main main.o

Hello World 가 출력되었다면 이제 어셈블리어를 시작할 수 있습니다. 아쉽지만 여기서 모든 것을 상세히 설명하지는 않겠습니다. 크게 구조를 보면 어셈블리어는 세개의 영역이 있습니다. section .data / section .bss / section .text 입니다. 각각 용도가 다른데 여기서 쓰인 .data는 정적 변수 msg 와 상수인 len을 선언합니다. .text는 프로그램 코드입니다. 위에서 부터 순차적으로 실행합니다. _start: 이것을 레이블이라 하는데 다음 _last: 가 나오기 전까지의 내용이 시스템콜 입니다. rax 의 1번은 stdout 에 쓰는 것 입니다. (mov rax, 1 – 1을 rax 레지스터에 전송) _last: 레이블 내용은 프로그램을 종료합니다.

코드만 봐서는 무슨 내용인지 이해가 잘 안됩니다. 내용적으로만 정리하면 이 코드는 Hello World 라는 문자열을 시스템 콜을 사용해서 표준 출력인 스크린에 출력하고 정상 종료합니다. 이제 하나씩 명령어 문법을 배우면 어느새 머리속에서 CPU 동작을 따라하게 될지도 모릅니다.

요약

먼저 어셈블리어 시작의 개요를 알아봤습니다. 다음은 리눅스 환경에 Nasm 어셈블러를 설치하고 Hello World를 출력했습니다. 간단한 코드의 실행이지만 x86 CPU에 직접 명령어(instruction)를 실행 할 수 있게 되었습니다. 이제부터 하나씩 알아가 보도록 하겠습니다.

참고링크

NASM Documentation

NASM-2.15.05 (linuxfromscratch.org)

NASM Tutorial (lmu.edu)

Leave a Comment