2P by GN⁺ 11시간전 | ★ favorite | 댓글 1개
  • x86-64 어셈블리 입문을 위한 시리즈의 첫 번째 글 소개임
  • 현대 64비트 시스템 기준으로 도구 설치 및 기본 구조 설명 제공임
  • Flat Assembler (FASM)WinDbg를 주요 개발 및 디버깅 툴로 사용 안내임
  • PE 포맷, DLL 임포트, 윈도우즈 호출 규약 등 실무에서 필요한 핵심 지식 요약 포함임
  • 단순 종료 프로그램 작성 및 디버깅 절차 실습 경험 중심 설명임

소개 및 의의

  • x86 어셈블리를 처음 접할 때 대학에서는 구식 환경(16비트, DOS, 세그먼트 메모리)에 기반한 방식으로 강의 수강 경험임
  • 현대에는 64비트 프로세서가 주류인 만큼, 본 시리즈를 통해 실제로 쓰이는 x86-64 환경만을 다루고 구형 요소는 모두 배제함
  • 본 튜토리얼은 Windows 운영체제 환경에서 동작하는 64비트 프로그램 개발에 집중함
  • 라이브러리를 사용하지 않고, OS에 직접적으로 접근하는 최소한의 코드부터 시작함
  • 이 글은 어셈블리를 처음 배우려는 개발자를 대상으로 하며, 기본적인 C/C++ 지식이 있다고 가정함

개발 도구 준비

어셈블러(Assembler)

  • CPU는 인간이 이해하기 힘든 머신 코드만 해석 가능하며, 이를 사람이 읽을 수 있는 코드로 바꾼 것이 어셈블리 언어임
  • 어셈블리 언어를 머신 코드로 변환해주는 프로그램이 어셈블러
  • x86-64 어셈블리어는 표준이 정해져 있지 않고, 어셈블러마다 문법과 동작 방식이 차별화됨
  • 본 시리즈에서는 Flat Assembler(FASM) 를 사용하며, 작고 사용이 간편하고 강력한 매크로 시스템과 에디터를 제공함

디버거(Debugger)

  • 작성한 어셈블리 코드를 분석 및 실행 흐름 관찰을 위해 디버거를 필수 도구로 활용함
  • WinDbg를 추천하며, 레지스터, 메모리, 어셈블리 코드 등을 독립적으로 확인 및 조작 가능함
  • Windows 10 SDK에서 컴포넌트만 선택하여 설치 가능함
  • 디버거를 통해 프로그램 내부 상태와 메모리 구조, 레지스터 변화를 직접 관찰할 수 있음

어셈블리 프로그래밍의 관점

CPU 구조와 명령어 집합

  • CPU는 특정 명령어 집합에 따라 제한된 동작만 수행 가능함
  • 명령어란 CPU가 수행할 수 있는 기본 단위 작업임
  • 각 명령어는 매개변수와 함께 매우 단순(값 저장, 산술 연산 등)하게 동작하는 구조임
  • 저수준 프로그래밍 및 디버깅에는 이러한 구조가 모든 고수준 개념의 기반임을 이해하는 것이 핵심임

레지스터(Registers)

  • 레지스터는 CPU 내부에 내장된 매우 빠른 전용 메모리 영역임
  • x86-64에는 일반 목적 레지스터가 16개 있으며 모두 64비트 크기임
  • 각 레지스터는 바이트, 워드, 더블워드 단위로 부분 접근이 가능함
레지스터 하위 바이트 하위 워드 하위 더블워드
rax al ax eax
rbx bl bx ebx
rcx cl cx ecx
rdx dl dx edx
rsp spl sp esp
rsi sil si esi
rdi dil di edi
rbp bpl bp ebp
r8~r15 r8b~r15b r8w~r15w r8d~r15d
  • rsp스택 포인터, rsi/rdi는 문자열 처리 인덱스로 동작하는 등 일부 레지스터에는 특수 목적이 할당됨
  • rip명령어 포인터, rflags는 연산 결과 상태 플래그를 담는 특별 레지스터임

메모리와 주소

  • 메모리는 0번 인덱스부터 연속된 바이트 배열처럼 동작함
  • 과거 x86 구조에서는 세그먼트-오프셋 방식이 필수였으나 x86-64에서는 모든 메모리를 플랫(Flat) 주소 공간으로 다룸
  • 실제로는 운영체제와 하드웨어가 각 프로세스 별로 가상 주소 공간을 물리 메모리에 동적으로 매핑하여 제공함
  • 즉, 동일한 가상 주소라 해도 서로 다른 프로세스에서는 다른 물리 메모리에 대응함
  • 명령어와 데이터가 동일 메모리에 존재(폰 노이만 구조)하며, 이는 아두이노에 쓰이는 AVR처럼 데이터를 따로 저장하는 하버드 아키텍처와 구별됨

첫 번째 어셈블리 프로그램 작성

  • FASM을 설치한 후 아래의 간단한 프로그램 코드 작성 및 빌드 실습
format PE64 NX GUI 6.0
entry start

section '.text' code readable executable
start:
        int3
        ret

코드 설명

  • format PE64 NX GUI 6.0 : FASM이 생성할 실행 파일 포맷을 지정, 여기서는 PE(Portable Executable) 64비트 GUI
  • entry start : 프로그램이 진입할 엔트리 포인트를 정의, 해당 레이블(start) 위치에서 실행 시작함
  • section '.text' code readable executable : PE의 코드 구간임을 지정, 실행 가능한 영역임
  • start: : 앞서 지정한 진입점에 이름 붙임
  • int3 : 디버거용 브레이크포인트로 프로그램을 일시정지시켜 상태 점검 목적에 사용
  • ret : 스택의 주소를 꺼내 그 위치로 제어를 전환하는 명령, 이 프로그램에서 바로 종료 응답

디버깅 실습

  • WinDbg에서 위 프로그램의 실행파일(.exe)을 열고, 디스어셈블리·레지스터 등 다양한 창을 준비함

  • F5를 눌러 프로그램이 브레이크포인트에 도달하게 하고, F8을 누를 때마다 한 명령씩 실행(단계별 진행)함

  • 레지스터(rip 등)의 변화를 실시간으로 관찰 가능

  • ret 실행 이후에는 운영체제로 제어가 전달되고, 이후 RtlExitUserThread를 호출하면서 스레드 및 프로세스 종료가 이어짐

  • 주의 : ret 명령만으로 종료 시 스레드 외 추가 백그라운드 실행 여부에 따라 프로세스가 남아 있을 수 있으므로, 정상적인 종료 시에는 반드시 ExitProcess를 호출하는 것이 바람직함

PE 포맷과 DLL 임포트

DLL 함수 임포트 구조 개요

  • ExitProcess와 같은 WinAPI 함수들은 KERNEL32.DLL에 있음
  • 이런 외부 함수를 사용하려면 실행 파일의 임포트 테이블(.idata 섹션)을 구성해야 함
  • idata 섹션의 Import Directory Table(IDT) 에는 DLL명, 함수명, IAT/ILT 등의 주소(RVA) 정보가 담김
  • IAT(Import Address Table)는 실제 함수 주소로 OS 로더에 의해 런타임에 덮어써짐
  • Hint/Name Table은 각 함수의 이름과 힌트 정보로 이루어짐

FASM에서 .idata 섹션 정의 예시

section '.idata' import readable writeable
idt: 
    dd rva kernel32_iat
    dd 0
    dd 0
    dd rva kernel32_name
    dd rva kernel32_iat
    dd 5 dup(0)
name_table: 
    _ExitProcess_Name dw 0
                      db "ExitProcess", 0, 0
kernel32_name: db "KERNEL32.DLL", 0
kernel32_iat: 
    ExitProcess dq rva _ExitProcess_Name
    dq 0 
  • db/dw/dd/dq : 바이트/워드/더블워드/쿼드워드(8바이트) 단위로 값 삽입
  • rva : 심볼의 가상 주소(Relative Virtual Address) 계산
  • IAT와 Name Table을 수작업으로 구성해 DLL 함수 참조 가능

64비트 Windows 호출 규약(MS x64 Calling Convention)

  • 함수 호출 시에 인자 전달 및 스택 사용 방법을 정하는 표준 규약
  • 64비트 Windows에서는 Microsoft x64 Calling Convention을 사용
  • 주요 특징 :
    • 스택 포인터는 항상 16바이트 정렬 되어야 함
    • 첫 4개 정수/포인터 인자는 rcx, rdx, r8, r9 레지스터 사용
    • 첫 4개 부동소수점 인자는 xmm0~xmm3에 넣음
    • 추가 인자는 스택 사용
    • 인자 개수와 무관하게 32바이트 shadow space를 스택에 확보해야 함
    • 스택 정리는 호출자가 담당

ExitProcess 호출 예시

format PE64 NX GUI 6.0
entry start

section '.text' code readable executable
start:
    int3
    sub rsp, 8 * 5
    xor rcx, rcx
    call [ExitProcess]

section '.idata' import readable writeable
idt: 
    dd rva kernel32_iat
    dd 0
    dd 0
    dd rva kernel32_name
    dd rva kernel32_iat
    dd 5 dup(0)
name_table: 
    _ExitProcess_Name dw 0
                      db "ExitProcess", 0, 0
kernel32_name db "KERNEL32.DLL", 0
kernel32_iat: 
    ExitProcess dq rva _ExitProcess_Name
    dq 0 

신규 코드 분석

  • sub rsp, 8 * 5 : 스택 포인터 조정(40바이트 확보), 16바이트 정렬 및 shadow space 확보 한 번에 처리

  • xor rcx, rcx : 첫 번째 인자인 rcx 레지스터에 0을 할당(EXIT 코드로 활용)

  • call [ExitProcess] : import table에 실제로 기록된 ExitProcess의 함수 주소로 점프

  • WinDbg에서 단계별 실행 시, 스택 포인터(rsp)rcx 레지스터의 변화, 그리고 프로세스 종료 흐름을 직접 확인 가능함

마무리

  • 본 글은 기초 도구 세팅부터 PE 포맷, DLL 임포트, x64 호출 규약, 첫 프로그램 작성 및 디버깅까지 실습 중심으로 x86-64 어셈블리의 전반적 흐름을 안내함
  • 다음 파트에서 보다 다양한 기능 구현과 실제 코드를 다룰 예정임
Hacker News 의견
  • 몇 년 동안 개발해온 프로젝트를 공유하고 싶음
    https://asm-editor.specy.app
    M68K, MIPS, RISC-V, X86 등 다양한 어셈블리 언어를 지원하는 온라인 인터랙티브 IDE임
    어셈블리 프로그래밍을 가르치기 위한 다양한 기능이 많음
    다른 웹사이트에도 임베드할 수 있음

  • 포인터 인덱싱 레지스터에 저주소 바이트 직접 접근 기능(예: 16/32비트에서 si/esi가 sil로 접근 가능)이 있다는 걸 몰랐음
    ax/eax에서 al로 접근하는 것과 비슷한 개념임
    x86_64에서 새로 추가된 오퍼코드가 실제로 존재하는지 궁금함
    플랫폼 명세를 다시 확인해봐야겠다는 생각이 들음
    순수한 호기심에서 묻는 것임

  • 직접 작성을 한 어셈블리 입문 자료를 공유함
    https://nayuki.io/page/…

  • 내 CPU 에뮬레이터 디스패치를 C++보다 빠르게 만들 수 있는지 궁금해서 어셈블리로 최적화 시도해봄
    피보나치 프로그램을 실행해봤는데 결과는 전혀 근접하지 못했음
    결국 기본값 비활성화 옵션으로 병합만 했음
    그래도 분명 더 빠르게 할 수 있는 방법이 있다고 믿음
    https://github.com/libriscv/libriscv/…
    메모리 접근 방법을 익히면서 성능을 약간은 개선함
    점프 테이블을 64비트에서 32비트로 줄이고, .text 섹션에 들어가도록 해서 RIP-relative 접근으로 만듦
    피보나치 프로그램에는 많은 바이트코드가 필요 없었음
    더 개선할 점에 대한 팁을 정말 듣고 싶음

    • 본인이 작성한 코드와 C++ 컴파일러가 생성하는 코드를 직접 비교해봤는지 궁금함
      맥락은 잘 모르지만 차이가 디스패치 메커니즘(명령어 페치 방식) 때문이 아니라, 실제 명령 구현 차이 때문일 가능성도 있다고 생각함
      최적화 방안으로, 에뮬레이션하는 레지스터를 x86-64 실제 레지스터에 매핑하고, 메모리로 아예 안 흘리는 방법이 있음
      이렇게 하면 add 같은 연산에서 메모리에서 꺼내지 않고 바로 연산 가능함
      단, 이 방식은 에뮬레이터 작성이 훨씬 번거로움
  • 브라우저에서 실습 가능한 x86 어셈블리 입문 자료임
    별다른 로컬 셋업 없이 예제를 바로 실행해볼 수 있음
    https://shikaan.github.io/assembly/x86/…
    참고로 본인이 작성한 자료임

    • 입력값 검증을 따로 하는지 궁금함
      NASM으로 바로 어셈블 후 바이너리 실행하는 방식 같아서 보안이 궁금해짐
  • 프로필 사진만 보고 junferno인 줄 알았음

  • 어셈블리를 한 번쯤 만져보는 것만으로도 전반적인 이해가 깊어져서 항상 좋은 경험이 됨
    큰 프로젝트를 만들 필요까지 없으니, 용기를 내서 조금이라도 직접 해보는 것을 추천함

  • (2020년) 당시 HN 토론 링크를 공유함
    https://news.ycombinator.com/item?id=24195627

  • 인텔 방식의 어셈블리 문법(구문)이어서 다행이라고 생각함

    • 다른 어셈블리 문법이 뭐가 있는지 궁금해짐
  • 어셈블리로 뭔가 해보고 싶긴 한데 특별히 아이디어가 떠오르지 않음

    • TIS-100이라는 게임을 추천함
      일종의 유사 어셈블리로 퍼즐을 푸는 게임임
      이런 게임이 어셈블리에 대한 갈증을 해소해줄 수 있다고 생각함