x86-64 어셈블리 배우기
(gpfault.net)- 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 같은 연산에서 메모리에서 꺼내지 않고 바로 연산 가능함
단, 이 방식은 에뮬레이터 작성이 훨씬 번거로움
- 본인이 작성한 코드와 C++ 컴파일러가 생성하는 코드를 직접 비교해봤는지 궁금함
-
브라우저에서 실습 가능한 x86 어셈블리 입문 자료임
별다른 로컬 셋업 없이 예제를 바로 실행해볼 수 있음
https://shikaan.github.io/assembly/x86/…
참고로 본인이 작성한 자료임- 입력값 검증을 따로 하는지 궁금함
NASM으로 바로 어셈블 후 바이너리 실행하는 방식 같아서 보안이 궁금해짐
- 입력값 검증을 따로 하는지 궁금함
-
프로필 사진만 보고 junferno인 줄 알았음
-
어셈블리를 한 번쯤 만져보는 것만으로도 전반적인 이해가 깊어져서 항상 좋은 경험이 됨
큰 프로젝트를 만들 필요까지 없으니, 용기를 내서 조금이라도 직접 해보는 것을 추천함 -
(2020년) 당시 HN 토론 링크를 공유함
https://news.ycombinator.com/item?id=24195627 -
인텔 방식의 어셈블리 문법(구문)이어서 다행이라고 생각함
- 다른 어셈블리 문법이 뭐가 있는지 궁금해짐
-
어셈블리로 뭔가 해보고 싶긴 한데 특별히 아이디어가 떠오르지 않음
- TIS-100이라는 게임을 추천함
일종의 유사 어셈블리로 퍼즐을 푸는 게임임
이런 게임이 어셈블리에 대한 갈증을 해소해줄 수 있다고 생각함
- TIS-100이라는 게임을 추천함